@taskforcehq/taskforce 0.3.327 → 0.3.328

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/Taskforce.module.css +24 -53
  2. package/dist/components/activity/EntityActivityTimeline.js +15 -5
  3. package/dist/components/context/AttachmentPreviewHost.js +1 -1
  4. package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
  5. package/dist/components/context/ContextAttachmentManager.js +116 -20
  6. package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
  7. package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
  8. package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
  9. package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
  10. package/dist/components/features/planning/PlanningModule.js +26 -9
  11. package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
  12. package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
  13. package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
  14. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  15. package/dist/components/task/TaskActionHeader.js +2 -2
  16. package/dist/components/task/TaskCard.js +37 -4
  17. package/dist/components/task/TaskContextUpload.d.ts +3 -0
  18. package/dist/components/task/TaskContextUpload.js +2 -2
  19. package/dist/components/task/TaskForm.d.ts +4 -0
  20. package/dist/components/task/TaskForm.js +32 -7
  21. package/dist/components/task/TaskImageReviews.js +16 -16
  22. package/dist/components/task/TaskStatusActions.js +11 -2
  23. package/dist/components/task/TaskWorkstreamPicker.js +2 -1
  24. package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
  25. package/dist/components/ui/DocumentTypeIcon.js +8 -0
  26. package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
  27. package/dist/components/ui/TaxonomyDropdown.js +30 -3
  28. package/dist/components/views/StandaloneLayout.js +62 -9
  29. package/dist/components/views/WidgetView.js +8 -5
  30. package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
  31. package/dist/components/views/panels/PlanningDrawer.js +48 -44
  32. package/dist/config/envSchema.js +1 -0
  33. package/dist/config/localServiceCloudTarget.d.ts +10 -0
  34. package/dist/config/localServiceCloudTarget.js +76 -0
  35. package/dist/core/AiProfileService.d.ts +1 -0
  36. package/dist/core/AiProfileService.js +2 -1
  37. package/dist/core/AttachmentLinkService.js +7 -4
  38. package/dist/core/DeletedTaskLifecycleService.d.ts +4 -0
  39. package/dist/core/DeletedTaskLifecycleService.js +21 -0
  40. package/dist/core/McpTokenService.d.ts +4 -0
  41. package/dist/core/McpTokenService.js +52 -0
  42. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +5 -0
  43. package/dist/core/TaskAttachmentLinksDurableMutationService.js +20 -11
  44. package/dist/core/Taskforce.d.ts +63 -1
  45. package/dist/core/Taskforce.js +558 -73
  46. package/dist/core/types.d.ts +2 -1
  47. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
  48. package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
  49. package/dist/documentReviews/service.d.ts +4 -0
  50. package/dist/documentReviews/service.js +141 -46
  51. package/dist/hooks/tasks/useTaskFormActions.d.ts +3 -0
  52. package/dist/hooks/tasks/useTaskFormActions.js +33 -7
  53. package/dist/hooks/useTaskforce.d.ts +59 -50
  54. package/dist/hooks/useTaskforce.js +103 -1
  55. package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
  56. package/dist/mcp/canonicalAssetHelpers.js +39 -5
  57. package/dist/mcp/collaborationRegistrar.js +15 -13
  58. package/dist/mcp/documentAssetRegistrar.js +16 -9
  59. package/dist/mcp/localServiceProxy.js +4 -1
  60. package/dist/mcp/runtime.d.ts +16 -0
  61. package/dist/mcp/runtime.js +566 -171
  62. package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
  63. package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
  64. package/dist/mcp/taskPlanningRegistrar.js +42 -34
  65. package/dist/mcp/toolRegistry.js +2 -2
  66. package/dist/migrations/taskSchemaMigrations.js +35 -0
  67. package/dist/runtime/appGateDefinitions.d.ts +2 -2
  68. package/dist/runtime/appGateDefinitions.js +2 -2
  69. package/dist/server/appAccess.d.ts +12 -0
  70. package/dist/server/appAccess.js +21 -0
  71. package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
  72. package/dist/server/authorizedWorkspaceAccess.js +40 -0
  73. package/dist/server/cloudCollectionRead.d.ts +25 -0
  74. package/dist/server/cloudCollectionRead.js +120 -0
  75. package/dist/server/cloudCollectionReadPool.d.ts +52 -0
  76. package/dist/server/cloudCollectionReadPool.js +140 -0
  77. package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
  78. package/dist/server/cloudCollectionReadWorker.js +36 -0
  79. package/dist/server/index.d.ts +3 -11
  80. package/dist/server/index.js +53 -40
  81. package/dist/server/localServiceLease.d.ts +3 -0
  82. package/dist/server/localServiceLease.js +9 -1
  83. package/dist/server/routes/documents.d.ts +5 -2
  84. package/dist/server/routes/documents.js +160 -13
  85. package/dist/server/routes/durableTaskHttpRouters.d.ts +2 -0
  86. package/dist/server/routes/durableTaskHttpRouters.js +2 -2
  87. package/dist/server/routes/localService.js +2 -0
  88. package/dist/server/routes/primitives.d.ts +2 -0
  89. package/dist/server/routes/shared.d.ts +24 -0
  90. package/dist/server/routes/shared.js +114 -18
  91. package/dist/server/routes/sync.js +22 -0
  92. package/dist/server/routes/syncV3FeedRoutes.js +4 -2
  93. package/dist/server/routes/syncV3LocalCaptureRoutes.js +20 -2
  94. package/dist/server/routes/syncV3OperationRoutes.js +17 -2
  95. package/dist/server/routes/tasks.js +199 -89
  96. package/dist/server/routes.js +35 -7
  97. package/dist/service/localServiceCli.js +36 -25
  98. package/dist/service/localServiceClientLifecycle.js +67 -6
  99. package/dist/shared/planningIdentity.d.ts +20 -0
  100. package/dist/shared/planningIdentity.js +79 -0
  101. package/dist/storage/postgresAdapter.js +4 -0
  102. package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
  103. package/dist/storage/postgresRequestDiagnostics.js +24 -0
  104. package/dist/storage/workspaceAssetStore.d.ts +32 -0
  105. package/dist/storage/workspaceAssetStore.js +70 -0
  106. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +16 -0
  107. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +70 -22
  108. package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
  109. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +5 -0
  110. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
  111. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +15 -0
  112. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +26 -3
  113. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
  114. package/dist/sync/syncApplyHandlers.d.ts +3 -1
  115. package/dist/sync/syncApplyHandlers.js +29 -0
  116. package/dist/sync/syncService.js +9 -0
  117. package/dist/sync/taskSyncPayload.d.ts +1 -0
  118. package/dist/sync/taskSyncPayload.js +6 -0
  119. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +1 -0
  120. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +21 -3
  121. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +17 -4
  122. package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
  123. package/dist/sync/v3/localOutboxDispatcher.js +5 -1
  124. package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
  125. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +1 -0
  126. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +15 -3
  127. package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
  128. package/dist/sync/v3/syncDurabilityStore.d.ts +8 -0
  129. package/dist/sync/v3/syncDurabilityStore.js +171 -33
  130. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +1 -0
  131. package/dist/sync/v3/taskAttachmentLinksMutationService.js +18 -2
  132. package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
  133. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +1 -9
  134. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +1 -0
  135. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +7 -1
  136. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +15 -3
  137. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +4 -0
  138. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +29 -6
  139. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +1 -0
  140. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +29 -7
  141. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +62 -7
  142. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +1 -0
  143. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
  144. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
  145. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
  146. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
  147. package/dist/sync/workspacePullFeed.d.ts +6 -1
  148. package/dist/sync/workspacePullFeed.js +172 -161
  149. package/dist/sync/workspaceSyncModel.d.ts +11 -0
  150. package/dist/sync/workspaceSyncModel.js +27 -2
  151. package/dist/types.d.ts +8 -0
  152. package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-MZs7lVED.js} +1 -1
  153. package/dist/ui/assets/{AiProfilesModule-DPxQeF9N.js → AiProfilesModule-C3i0LOxe.js} +1 -1
  154. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DHNYaNg6.js → AnnotatedAttachmentWorkspace-DVZk5Tln.js} +1 -1
  155. package/dist/ui/assets/AssetTraySortControl-Kf1pelSy.js +1 -0
  156. package/dist/ui/assets/ContextAttachmentManager-B8boOs6Q.js +3 -0
  157. package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-DXH4XUsB.js} +4 -4
  158. package/dist/ui/assets/EntityActivityTimeline-BtSTq0ON.js +1 -0
  159. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +1 -0
  160. package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
  161. package/dist/ui/assets/{PlansPage-BVH_NfgT.js → PlansPage-ChqgxALc.js} +1 -1
  162. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +1 -0
  163. package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
  164. package/dist/ui/assets/{TaskSettings-xVsxSa0g.js → TaskSettings-g4ECKNOz.js} +1 -1
  165. package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BazdUwxZ.js} +1 -1
  166. package/dist/ui/assets/WorkflowManagerModule-DWFspC-o.js +1 -0
  167. package/dist/ui/assets/index-BUplSsv_.js +7 -0
  168. package/dist/ui/assets/index-gz2EXuoK.css +1 -0
  169. package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-BkFLXavV.js} +1 -1
  170. package/dist/ui/index.html +3 -3
  171. package/dist/utils/constants.d.ts +2 -2
  172. package/dist/utils/constants.js +1 -1
  173. package/dist/utils/contextAttachmentUpload.d.ts +2 -0
  174. package/dist/utils/contextAttachmentUpload.js +7 -0
  175. package/dist/utils/contextFiles.d.ts +1 -1
  176. package/dist/utils/contextFiles.js +4 -4
  177. package/package.json +6 -1
  178. package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
  179. package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
  180. package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
  181. package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
  182. package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
  183. package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
  184. package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
  185. package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
  186. package/dist/ui/assets/index-BzJlVnNS.css +0 -1
  187. package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
@@ -1,9 +1,10 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import { applyLocalServiceCloudTarget, localServiceCloudTargetsEqual, parseLocalServiceCloudTarget, resolveLocalServiceCloudTarget, } from '../config/localServiceCloudTarget.js';
3
4
  import { createStandaloneServer } from '../server/index.js';
4
5
  import { inspectLocalServiceDescriptor, redactLocalServiceDescriptor, } from '../server/localServiceLease.js';
5
6
  import { acquireLocalServiceOwnershipForRuntime, } from '../server/localServiceOwnershipBootstrap.js';
6
- import { LOCAL_SERVICE_API_BASE_PATH, LOCAL_SERVICE_PROTOCOL_VERSION, resolveLocalServiceLeaseInput, spawnDetachedLocalService, waitForLocalServiceDescriptor, } from './localServiceClientLifecycle.js';
7
+ import { discoverOrStartLocalService, LOCAL_SERVICE_API_BASE_PATH, LOCAL_SERVICE_PROTOCOL_VERSION, resolveLocalServiceLeaseInput, spawnDetachedLocalService, waitForLocalServiceDescriptor, } from './localServiceClientLifecycle.js';
7
8
  import { listSqliteWalHolders } from '../storage/sqliteWalHealth.js';
8
9
  const STARTUP_TIMEOUT_MS = 15_000;
9
10
  export { LOCAL_SERVICE_PROTOCOL_VERSION } from './localServiceClientLifecycle.js';
@@ -58,6 +59,13 @@ function reportIncompatibleDescriptor(inspection, error) {
58
59
  error(`Taskforce service protocol ${inspection.descriptor.protocolVersion} is incompatible with client protocol ${LOCAL_SERVICE_PROTOCOL_VERSION}.`);
59
60
  return true;
60
61
  }
62
+ function reportCloudTargetMismatch(descriptorTarget, requestedTarget, error) {
63
+ if (localServiceCloudTargetsEqual(descriptorTarget, requestedTarget))
64
+ return false;
65
+ error('Taskforce service is running for a different or unverified cloud target. '
66
+ + 'Stop it with `taskforce service stop --drain`, then retry.');
67
+ return true;
68
+ }
61
69
  function defaultRegisterShutdownSignals(handler) {
62
70
  process.once('SIGINT', handler);
63
71
  process.once('SIGTERM', handler);
@@ -75,6 +83,8 @@ async function waitForDescriptor(input, expected, options) {
75
83
  });
76
84
  }
77
85
  async function runForeground(command, options) {
86
+ const requestedCloudTarget = resolveLocalServiceCloudTarget(options.env);
87
+ applyLocalServiceCloudTarget(options.env, requestedCloudTarget);
78
88
  const ownership = await options.acquireOwnership(command.projectRoot, options.env);
79
89
  if (!ownership)
80
90
  throw new Error('The shared local service requires local runtime mode.');
@@ -84,6 +94,8 @@ async function runForeground(command, options) {
84
94
  if (inspection.status === 'live' && inspection.descriptor) {
85
95
  if (reportIncompatibleDescriptor(inspection, options.error))
86
96
  return 2;
97
+ if (reportCloudTargetMismatch(inspection.descriptor.cloudTarget, requestedCloudTarget, options.error))
98
+ return 2;
87
99
  options.log(`Taskforce service is already running at ${inspection.descriptor.endpoint}.`);
88
100
  return 0;
89
101
  }
@@ -116,6 +128,7 @@ async function runForeground(command, options) {
116
128
  ownerPid: process.pid,
117
129
  bootstrapCapability: ownership.lease.bootstrapCapability,
118
130
  ownershipKey: ownership.lease.identity.ownershipKey,
131
+ cloudTarget: requestedCloudTarget,
119
132
  },
120
133
  afterShutdown: async () => {
121
134
  removeSignals();
@@ -138,6 +151,7 @@ async function runForeground(command, options) {
138
151
  endpoint: `http://127.0.0.1:${address.port}`,
139
152
  protocolVersion: LOCAL_SERVICE_PROTOCOL_VERSION,
140
153
  appVersion: options.appVersion,
154
+ cloudTarget: requestedCloudTarget,
141
155
  });
142
156
  removeSignals = options.registerShutdownSignals(shutdown);
143
157
  server.once('error', (error) => {
@@ -164,34 +178,28 @@ async function runForeground(command, options) {
164
178
  }
165
179
  }
166
180
  async function runStart(command, options) {
167
- const leaseInput = resolveLocalServiceLeaseInput(command.projectRoot, options.env);
168
- const current = await options.inspectDescriptor(leaseInput);
169
- if (current.status === 'live' && current.descriptor) {
170
- if (reportIncompatibleDescriptor(current, options.error))
171
- return 2;
172
- options.log(`Taskforce service is already running at ${current.descriptor.endpoint}.`);
181
+ const requestedCloudTarget = resolveLocalServiceCloudTarget(options.env);
182
+ applyLocalServiceCloudTarget(options.env, requestedCloudTarget);
183
+ try {
184
+ const descriptor = await discoverOrStartLocalService({
185
+ projectRoot: command.projectRoot,
186
+ env: options.env,
187
+ executablePath: options.executablePath,
188
+ cliEntryPath: options.cliEntryPath,
189
+ execArgv: options.execArgv,
190
+ inspectDescriptor: options.inspectDescriptor,
191
+ spawnDetached: options.spawnDetached,
192
+ fetch: options.fetch,
193
+ sleep: options.sleep,
194
+ nowMs: options.nowMs,
195
+ });
196
+ options.log(`Taskforce service is ready at ${descriptor.endpoint}.`);
173
197
  return 0;
174
198
  }
175
- if (current.status === 'indeterminate') {
176
- options.error('Taskforce service state is indeterminate; refusing to start a competing owner.');
177
- return 2;
178
- }
179
- options.spawnDetached(options.executablePath, [
180
- ...options.execArgv,
181
- options.cliEntryPath,
182
- 'service',
183
- command.projectRoot,
184
- '--detached-child',
185
- ], options.env);
186
- const inspection = await waitForDescriptor(leaseInput, 'live', options);
187
- if (inspection.status !== 'live' || !inspection.descriptor) {
188
- options.error('Taskforce service did not become ready before the startup deadline.');
199
+ catch (startError) {
200
+ options.error(String(startError?.message || startError));
189
201
  return 2;
190
202
  }
191
- if (reportIncompatibleDescriptor(inspection, options.error))
192
- return 2;
193
- options.log(`Taskforce service started at ${inspection.descriptor.endpoint}.`);
194
- return 0;
195
203
  }
196
204
  async function runStatus(command, options) {
197
205
  const leaseInput = resolveLocalServiceLeaseInput(command.projectRoot, options.env);
@@ -222,9 +230,12 @@ async function runStatus(command, options) {
222
230
  throw new Error(`HTTP ${response.status}`);
223
231
  const payload = await response.json();
224
232
  const coordinatorPayload = payload.coordinator;
233
+ const diagnosticCloudTarget = parseLocalServiceCloudTarget(payload.cloudTarget);
225
234
  if (payload.ownerPid !== inspection.descriptor.pid
226
235
  || payload.generation !== inspection.descriptor.generation
227
236
  || payload.protocolVersion !== inspection.descriptor.protocolVersion
237
+ || (inspection.descriptor.cloudTarget
238
+ && !localServiceCloudTargetsEqual(diagnosticCloudTarget, inspection.descriptor.cloudTarget))
228
239
  || !Array.isArray(payload.sessions)
229
240
  || !coordinatorPayload
230
241
  || typeof coordinatorPayload !== 'object'
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
+ import { applyLocalServiceCloudTarget, localServiceCloudTargetsEqual, parseLocalServiceCloudTarget, resolveLocalServiceCloudTarget, } from '../config/localServiceCloudTarget.js';
4
5
  import { inspectLocalServiceDescriptor, } from '../server/localServiceLease.js';
5
6
  import { resolveLocalDatabaseIdentity } from '../server/localDatabaseIdentity.js';
6
7
  export const LOCAL_SERVICE_PROTOCOL_VERSION = '1';
@@ -39,10 +40,11 @@ export function spawnDetachedLocalService(executablePath, args, env) {
39
40
  });
40
41
  child.unref();
41
42
  }
42
- function buildLocalServiceProcessEnv(env) {
43
+ function buildLocalServiceProcessEnv(env, cloudTarget) {
43
44
  const serviceEnv = { ...env };
44
45
  for (const key of LOCAL_MCP_CLIENT_ENV_KEYS)
45
46
  delete serviceEnv[key];
47
+ applyLocalServiceCloudTarget(serviceEnv, cloudTarget);
46
48
  return serviceEnv;
47
49
  }
48
50
  export async function waitForLocalServiceDescriptor(input, expected, options = {}) {
@@ -78,15 +80,59 @@ async function validateLocalServiceReadiness(descriptor, fetchFn) {
78
80
  catch {
79
81
  throw new Error('Taskforce shared local service readiness returned invalid JSON.');
80
82
  }
83
+ const readinessHasCloudTarget = Object.prototype.hasOwnProperty.call(readiness, 'cloudTarget');
84
+ const readinessCloudTarget = readinessHasCloudTarget
85
+ ? parseLocalServiceCloudTarget(readiness.cloudTarget)
86
+ : null;
87
+ const descriptorCloudTarget = descriptor.cloudTarget || null;
88
+ const cloudTargetsMatch = descriptorCloudTarget || readinessCloudTarget
89
+ ? localServiceCloudTargetsEqual(descriptorCloudTarget, readinessCloudTarget)
90
+ : true;
81
91
  if (readiness.ready !== true
82
92
  || readiness.protocolVersion !== LOCAL_SERVICE_PROTOCOL_VERSION
83
93
  || readiness.protocolVersion !== descriptor.protocolVersion
84
94
  || readiness.generation !== descriptor.generation
85
95
  || readiness.pid !== descriptor.pid
86
96
  || readiness.endpoint !== descriptor.endpoint
87
- || readiness.dataIdentity !== descriptor.ownershipKey) {
97
+ || readiness.dataIdentity !== descriptor.ownershipKey
98
+ || (readinessHasCloudTarget && !readinessCloudTarget)
99
+ || !cloudTargetsMatch) {
88
100
  throw new Error('Taskforce shared local service readiness does not match its descriptor.');
89
101
  }
102
+ return descriptorCloudTarget;
103
+ }
104
+ async function requestIdleLocalServiceShutdown(descriptor, fetchFn) {
105
+ let response;
106
+ try {
107
+ response = await fetchFn(`${descriptor.endpoint}${LOCAL_SERVICE_API_BASE_PATH}/shutdown`, {
108
+ method: 'POST',
109
+ headers: {
110
+ 'content-type': 'application/json',
111
+ 'x-taskforce-local-service-capability': descriptor.bootstrapCapability,
112
+ },
113
+ body: JSON.stringify({
114
+ intent: 'administrative-stop',
115
+ mode: 'refuse',
116
+ pid: descriptor.pid,
117
+ generation: descriptor.generation,
118
+ }),
119
+ redirect: 'error',
120
+ signal: AbortSignal.timeout(LOCAL_SERVICE_STARTUP_TIMEOUT_MS),
121
+ });
122
+ }
123
+ catch (error) {
124
+ throw new Error('Taskforce service uses a different cloud target and could not be stopped safely. '
125
+ + `No replacement was started. ${String(error?.message || error)}`);
126
+ }
127
+ if (!response.ok) {
128
+ const detail = (await response.text()).slice(0, 500);
129
+ const activeClientHint = response.status === 409
130
+ ? ' It may have active MCP clients; close them or run `taskforce service stop --drain`, then retry.'
131
+ : '';
132
+ throw new Error(`Taskforce service uses a different cloud target and refused safe idle replacement (${response.status}).`
133
+ + activeClientHint
134
+ + (detail ? ` ${detail}` : ''));
135
+ }
90
136
  }
91
137
  export async function discoverOrStartLocalService(input) {
92
138
  const requestedRoot = path.resolve(input.projectRoot);
@@ -94,13 +140,25 @@ export async function discoverOrStartLocalService(input) {
94
140
  const leaseInput = resolveLocalServiceLeaseInput(projectRoot, input.env);
95
141
  const inspectDescriptor = input.inspectDescriptor || inspectLocalServiceDescriptor;
96
142
  const fetchFn = input.fetch || fetch;
143
+ const requestedCloudTarget = resolveLocalServiceCloudTarget(input.env);
97
144
  const current = await inspectDescriptor(leaseInput);
98
145
  if (current.status === 'live' && current.descriptor) {
99
146
  if (current.descriptor.protocolVersion !== LOCAL_SERVICE_PROTOCOL_VERSION) {
100
147
  throw new Error(`Taskforce service protocol ${current.descriptor.protocolVersion} is incompatible with client protocol ${LOCAL_SERVICE_PROTOCOL_VERSION}.`);
101
148
  }
102
- await validateLocalServiceReadiness(current.descriptor, fetchFn);
103
- return current.descriptor;
149
+ const runningCloudTarget = await validateLocalServiceReadiness(current.descriptor, fetchFn);
150
+ if (localServiceCloudTargetsEqual(runningCloudTarget, requestedCloudTarget)) {
151
+ return current.descriptor;
152
+ }
153
+ await requestIdleLocalServiceShutdown(current.descriptor, fetchFn);
154
+ const stopped = await waitForLocalServiceDescriptor(leaseInput, 'stale', {
155
+ inspectDescriptor,
156
+ sleep: input.sleep,
157
+ nowMs: input.nowMs,
158
+ });
159
+ if (stopped.status !== 'stale') {
160
+ throw new Error('Taskforce service accepted cloud-target replacement but did not release its owner lease. No replacement was started.');
161
+ }
104
162
  }
105
163
  if (current.status === 'indeterminate') {
106
164
  throw new Error('Taskforce service state is indeterminate; refusing to start a competing owner.');
@@ -111,7 +169,7 @@ export async function discoverOrStartLocalService(input) {
111
169
  'service',
112
170
  projectRoot,
113
171
  '--detached-child',
114
- ], buildLocalServiceProcessEnv(input.env));
172
+ ], buildLocalServiceProcessEnv(input.env, requestedCloudTarget));
115
173
  const inspection = await waitForLocalServiceDescriptor(leaseInput, 'live', {
116
174
  inspectDescriptor,
117
175
  sleep: input.sleep,
@@ -123,6 +181,9 @@ export async function discoverOrStartLocalService(input) {
123
181
  if (inspection.descriptor.protocolVersion !== LOCAL_SERVICE_PROTOCOL_VERSION) {
124
182
  throw new Error(`Taskforce service protocol ${inspection.descriptor.protocolVersion} is incompatible with client protocol ${LOCAL_SERVICE_PROTOCOL_VERSION}.`);
125
183
  }
126
- await validateLocalServiceReadiness(inspection.descriptor, fetchFn);
184
+ const startedCloudTarget = await validateLocalServiceReadiness(inspection.descriptor, fetchFn);
185
+ if (!localServiceCloudTargetsEqual(startedCloudTarget, requestedCloudTarget)) {
186
+ throw new Error('Taskforce service started with a different or unverified cloud target; refusing to attach.');
187
+ }
127
188
  return inspection.descriptor;
128
189
  }
@@ -0,0 +1,20 @@
1
+ export type PlanningIdentityEntityType = 'initiative' | 'workstream';
2
+ export declare const PLANNING_IDENTITY_ICONS: readonly ["Target", "Route", "Flag", "Rocket", "Briefcase", "Folder", "Layers", "Map", "Compass", "Landmark", "BookOpen", "Lightbulb", "Wrench", "Shield", "Bug", "Palette", "Camera", "Music", "Heart", "Globe", "Users", "Sparkles", "FlaskConical", "Code"];
3
+ export type PlanningIdentityIcon = typeof PLANNING_IDENTITY_ICONS[number];
4
+ export declare const PLANNING_IDENTITY_COLORS: readonly ["red", "orange", "amber", "green", "teal", "sky", "blue", "indigo", "violet"];
5
+ export type PlanningIdentityColor = typeof PLANNING_IDENTITY_COLORS[number];
6
+ export declare function normalizePlanningIdentityIcon(value: unknown): PlanningIdentityIcon | null;
7
+ export declare function normalizePlanningIdentityColor(value: unknown): PlanningIdentityColor | null;
8
+ export declare function assertPlanningIdentityIcon(value: unknown): PlanningIdentityIcon | null;
9
+ export declare function assertPlanningIdentityColor(value: unknown): PlanningIdentityColor | null;
10
+ export declare function resolvePlanningIdentity(input: {
11
+ entityType: PlanningIdentityEntityType;
12
+ entityId: string;
13
+ icon?: unknown;
14
+ color?: unknown;
15
+ }): {
16
+ icon: PlanningIdentityIcon;
17
+ color: PlanningIdentityColor | null;
18
+ customized: boolean;
19
+ };
20
+ export declare function planningIdentityColorToken(color?: PlanningIdentityColor | null): string;
@@ -0,0 +1,79 @@
1
+ export const PLANNING_IDENTITY_ICONS = Object.freeze([
2
+ 'Target',
3
+ 'Route',
4
+ 'Flag',
5
+ 'Rocket',
6
+ 'Briefcase',
7
+ 'Folder',
8
+ 'Layers',
9
+ 'Map',
10
+ 'Compass',
11
+ 'Landmark',
12
+ 'BookOpen',
13
+ 'Lightbulb',
14
+ 'Wrench',
15
+ 'Shield',
16
+ 'Bug',
17
+ 'Palette',
18
+ 'Camera',
19
+ 'Music',
20
+ 'Heart',
21
+ 'Globe',
22
+ 'Users',
23
+ 'Sparkles',
24
+ 'FlaskConical',
25
+ 'Code',
26
+ ]);
27
+ export const PLANNING_IDENTITY_COLORS = Object.freeze([
28
+ 'red',
29
+ 'orange',
30
+ 'amber',
31
+ 'green',
32
+ 'teal',
33
+ 'sky',
34
+ 'blue',
35
+ 'indigo',
36
+ 'violet',
37
+ ]);
38
+ const ICON_SET = new Set(PLANNING_IDENTITY_ICONS);
39
+ const COLOR_SET = new Set(PLANNING_IDENTITY_COLORS);
40
+ export function normalizePlanningIdentityIcon(value) {
41
+ if (value === null || value === undefined || value === '')
42
+ return null;
43
+ const normalized = typeof value === 'string' ? value.trim() : '';
44
+ return ICON_SET.has(normalized) ? normalized : null;
45
+ }
46
+ export function normalizePlanningIdentityColor(value) {
47
+ if (value === null || value === undefined || value === '')
48
+ return null;
49
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
50
+ return COLOR_SET.has(normalized) ? normalized : null;
51
+ }
52
+ export function assertPlanningIdentityIcon(value) {
53
+ const normalized = normalizePlanningIdentityIcon(value);
54
+ if (value !== null && value !== undefined && value !== '' && !normalized) {
55
+ throw new Error('Planning identity icon is not supported.');
56
+ }
57
+ return normalized;
58
+ }
59
+ export function assertPlanningIdentityColor(value) {
60
+ const normalized = normalizePlanningIdentityColor(value);
61
+ if (value !== null && value !== undefined && value !== '' && !normalized) {
62
+ throw new Error('Planning identity color is not supported.');
63
+ }
64
+ return normalized;
65
+ }
66
+ export function resolvePlanningIdentity(input) {
67
+ const explicitIcon = normalizePlanningIdentityIcon(input.icon);
68
+ const explicitColor = normalizePlanningIdentityColor(input.color);
69
+ return {
70
+ icon: explicitIcon || (input.entityType === 'initiative' ? 'Target' : 'Route'),
71
+ color: explicitColor,
72
+ customized: Boolean(explicitIcon || explicitColor),
73
+ };
74
+ }
75
+ export function planningIdentityColorToken(color) {
76
+ return color
77
+ ? `var(--planning-identity-${color})`
78
+ : 'var(--planning-progress-empty)';
79
+ }
@@ -1,6 +1,8 @@
1
1
  import { TextDecoder, TextEncoder } from 'util';
2
2
  import { Worker } from 'worker_threads';
3
+ import { performance } from 'node:perf_hooks';
3
4
  import { assertSynchronousTransactionResult, } from './databaseAdapter.js';
5
+ import { recordPostgresWorkerWait } from './postgresRequestDiagnostics.js';
4
6
  const RESPONSE_STATUS_INDEX = 0;
5
7
  const RESPONSE_LENGTH_INDEX = 1;
6
8
  const RESPONSE_READY = 1;
@@ -88,7 +90,9 @@ export class PostgresAdapter {
88
90
  statusBuffer,
89
91
  responseBuffer
90
92
  });
93
+ const waitStartedAt = performance.now();
91
94
  const waitResult = Atomics.wait(status, RESPONSE_STATUS_INDEX, RESPONSE_INITIAL, RESPONSE_TIMEOUT_MS);
95
+ recordPostgresWorkerWait(payload.op, performance.now() - waitStartedAt);
92
96
  if (waitResult === 'timed-out') {
93
97
  throw new Error('[Taskforce] Postgres worker timed out.');
94
98
  }
@@ -0,0 +1,8 @@
1
+ export type RequestPerformanceDiagnostics = {
2
+ postgresQueryCount: number;
3
+ postgresWorkerCallCount: number;
4
+ postgresWorkerWaitMs: number;
5
+ };
6
+ export declare function runWithRequestPerformanceDiagnostics<T>(enabled: boolean, callback: () => T): T;
7
+ export declare function recordPostgresWorkerWait(operation: string, durationMs: number): void;
8
+ export declare function getRequestPerformanceDiagnostics(): RequestPerformanceDiagnostics | null;
@@ -0,0 +1,24 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const requestPerformanceStorage = new AsyncLocalStorage();
3
+ export function runWithRequestPerformanceDiagnostics(enabled, callback) {
4
+ if (!enabled)
5
+ return callback();
6
+ return requestPerformanceStorage.run({
7
+ postgresQueryCount: 0,
8
+ postgresWorkerCallCount: 0,
9
+ postgresWorkerWaitMs: 0,
10
+ }, callback);
11
+ }
12
+ export function recordPostgresWorkerWait(operation, durationMs) {
13
+ const diagnostics = requestPerformanceStorage.getStore();
14
+ if (!diagnostics)
15
+ return;
16
+ diagnostics.postgresWorkerCallCount += 1;
17
+ if (operation === 'query')
18
+ diagnostics.postgresQueryCount += 1;
19
+ diagnostics.postgresWorkerWaitMs += Math.max(0, durationMs);
20
+ }
21
+ export function getRequestPerformanceDiagnostics() {
22
+ const diagnostics = requestPerformanceStorage.getStore();
23
+ return diagnostics ? { ...diagnostics } : null;
24
+ }
@@ -67,11 +67,43 @@ export interface WorkspaceAssetLinkEntry {
67
67
  updatedAt: string;
68
68
  deletedAt: string | null;
69
69
  }
70
+ export interface WorkspaceAssetUploadDraftEntry {
71
+ workspaceId: string;
72
+ draftId: string;
73
+ assetId: string;
74
+ storageKey: string;
75
+ purpose: string;
76
+ createdByUserId: string;
77
+ createdAt: string;
78
+ retiredAt: string | null;
79
+ }
70
80
  export declare class WorkspaceAssetStore {
71
81
  private readonly db;
72
82
  private readonly tenantId;
73
83
  private supportsTargetTypeCache;
74
84
  constructor(db: DatabaseAdapter, tenantId: string);
85
+ recordUploadDraft(input: {
86
+ workspaceId: string;
87
+ draftId: string;
88
+ assetId: string;
89
+ storageKey: string;
90
+ purpose: string;
91
+ createdByUserId: string;
92
+ createdAt?: string;
93
+ }): WorkspaceAssetUploadDraftEntry;
94
+ listUploadDrafts(input: {
95
+ workspaceId: string;
96
+ draftId?: string;
97
+ createdByUserId?: string;
98
+ createdBefore?: string;
99
+ includeRetired?: boolean;
100
+ }): WorkspaceAssetUploadDraftEntry[];
101
+ retireUploadDraft(input: {
102
+ workspaceId: string;
103
+ draftId: string;
104
+ assetId: string;
105
+ retiredAt?: string;
106
+ }): boolean;
75
107
  private isPlanningTargetDurablyOwned;
76
108
  assertAssetDeletionAllowed(assetIdRaw: string, workspaceIdRaw: string): void;
77
109
  private runDurableAssetMutation;
@@ -114,6 +114,76 @@ export class WorkspaceAssetStore {
114
114
  this.db = db;
115
115
  this.tenantId = tenantId;
116
116
  }
117
+ recordUploadDraft(input) {
118
+ const workspaceId = normalizeWorkspaceId(input.workspaceId);
119
+ const draftId = normalizeId(input.draftId);
120
+ const assetId = normalizeId(input.assetId);
121
+ const storageKey = normalizeId(input.storageKey);
122
+ const purpose = normalizeId(input.purpose);
123
+ const createdByUserId = normalizeId(input.createdByUserId, 'anonymous');
124
+ const createdAt = normalizeId(input.createdAt, new Date().toISOString());
125
+ if (!draftId || !assetId || !storageKey || !purpose) {
126
+ throw new Error('Draft upload identity, asset, storage key, and purpose are required.');
127
+ }
128
+ this.db.prepare(`
129
+ INSERT INTO workspace_asset_upload_drafts (
130
+ tenant_id, workspace_id, draft_id, asset_id, storage_key,
131
+ purpose, created_by_user_id, created_at, retired_at
132
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)
133
+ ON CONFLICT (tenant_id, workspace_id, draft_id, asset_id) DO UPDATE SET
134
+ storage_key = EXCLUDED.storage_key,
135
+ purpose = EXCLUDED.purpose,
136
+ created_by_user_id = EXCLUDED.created_by_user_id
137
+ `).run(this.tenantId, workspaceId, draftId, assetId, storageKey, purpose, createdByUserId, createdAt);
138
+ return {
139
+ workspaceId, draftId, assetId, storageKey, purpose,
140
+ createdByUserId, createdAt, retiredAt: null,
141
+ };
142
+ }
143
+ listUploadDrafts(input) {
144
+ const workspaceId = normalizeWorkspaceId(input.workspaceId);
145
+ const clauses = ['tenant_id = ?', 'workspace_id = ?'];
146
+ const values = [this.tenantId, workspaceId];
147
+ if (input.draftId) {
148
+ clauses.push('draft_id = ?');
149
+ values.push(normalizeId(input.draftId));
150
+ }
151
+ if (input.createdByUserId) {
152
+ clauses.push('created_by_user_id = ?');
153
+ values.push(normalizeId(input.createdByUserId));
154
+ }
155
+ if (input.createdBefore) {
156
+ clauses.push('created_at <= ?');
157
+ values.push(normalizeId(input.createdBefore));
158
+ }
159
+ if (!input.includeRetired)
160
+ clauses.push('retired_at IS NULL');
161
+ const rows = this.db.prepare(`
162
+ SELECT workspace_id, draft_id, asset_id, storage_key, purpose,
163
+ created_by_user_id, created_at, retired_at
164
+ FROM workspace_asset_upload_drafts
165
+ WHERE ${clauses.join(' AND ')}
166
+ ORDER BY created_at ASC, asset_id ASC
167
+ `).all(...values);
168
+ return rows.map((row) => ({
169
+ workspaceId: String(row.workspace_id || ''),
170
+ draftId: String(row.draft_id || ''),
171
+ assetId: String(row.asset_id || ''),
172
+ storageKey: String(row.storage_key || ''),
173
+ purpose: String(row.purpose || ''),
174
+ createdByUserId: String(row.created_by_user_id || ''),
175
+ createdAt: String(row.created_at || ''),
176
+ retiredAt: row.retired_at == null ? null : String(row.retired_at),
177
+ }));
178
+ }
179
+ retireUploadDraft(input) {
180
+ const result = this.db.prepare(`
181
+ UPDATE workspace_asset_upload_drafts
182
+ SET retired_at = COALESCE(retired_at, ?)
183
+ WHERE tenant_id = ? AND workspace_id = ? AND draft_id = ? AND asset_id = ?
184
+ `).run(normalizeId(input.retiredAt, new Date().toISOString()), this.tenantId, normalizeWorkspaceId(input.workspaceId), normalizeId(input.draftId), normalizeId(input.assetId));
185
+ return Number(result.changes || 0) > 0;
186
+ }
117
187
  isPlanningTargetDurablyOwned(workspaceId, targetType, targetId) {
118
188
  if (targetType !== 'initiative' && targetType !== 'workstream')
119
189
  return false;
@@ -4,6 +4,8 @@ import type { WorkspaceSyncEngineV2PushBoundary } from '../engine/workspaceSyncE
4
4
  import { type LocalSyncExecutionPermitCleanAbandonResult, type LocalSyncExecutionPermitDrainSnapshot, type LocalSyncExecutionPermit, type LocalSyncExecutionPermitOwnerMode, type LocalSyncExecutionPermitState, type LocalSyncGatewayOperationClass, type LocalSyncRecoveryObligation, type LocalSyncUpstreamDisposition } from './localSyncExecutionPermitTypes.js';
5
5
  export declare const LOCAL_SYNC_PERMIT_RETENTION_MIN_AGE_MS: number;
6
6
  export declare const LOCAL_SYNC_PERMIT_RETENTION_SOFT_LIMIT = 1000;
7
+ export declare const LOCAL_SYNC_PERMIT_RETENTION_HARD_LIMIT = 10000;
8
+ export declare const LOCAL_SYNC_OBLIGATION_RETENTION_HARD_LIMIT = 2000;
7
9
  export declare const LOCAL_SYNC_PERMIT_RETENTION_DELETE_BATCH = 250;
8
10
  export declare const LOCAL_SYNC_PERMIT_RETENTION_SWEEP_LIMIT = 5000;
9
11
  type LocalSyncPermitCompletionGroupFence = {
@@ -60,6 +62,12 @@ export declare class LocalSyncExecutionPermitStore {
60
62
  byState: Partial<Record<LocalSyncExecutionPermitState, number>>;
61
63
  recoveryObligations: number;
62
64
  localCommitInvariantViolation: boolean;
65
+ history: {
66
+ terminalPermits: number;
67
+ oldestTerminalPermitAt: string | null;
68
+ resolvedRecoveryObligations: number;
69
+ oldestResolvedRecoveryObligationAt: string | null;
70
+ };
63
71
  };
64
72
  inspectGenerationDrain(input: {
65
73
  workspaceId: string;
@@ -110,6 +118,14 @@ export declare class LocalSyncExecutionPermitStore {
110
118
  ownerMode: LocalSyncExecutionPermitOwnerMode;
111
119
  serverInstanceId: string;
112
120
  }): LocalSyncExecutionPermit;
121
+ renewAcceptedApplyCompletion(input: {
122
+ permitId: string;
123
+ workspaceId: string;
124
+ coordinatorGeneration: number;
125
+ serverInstanceId: string;
126
+ ownerMode: LocalSyncExecutionPermitOwnerMode;
127
+ deadlineAt: string;
128
+ }): LocalSyncExecutionPermit;
113
129
  completeAcceptedV3Dispatch<T>(input: {
114
130
  permitId: string;
115
131
  workspaceId: string;