@taskforcehq/taskforce 0.3.306 → 0.3.308

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 (232) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +317 -27
  3. package/dist/TaskforceCore.js +1 -0
  4. package/dist/TaskforceCore.test.js +178 -0
  5. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  6. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  7. package/dist/components/context/ContextAttachmentManager.js +15 -3
  8. package/dist/components/features/AgentsModule.d.ts +5 -1
  9. package/dist/components/features/AgentsModule.js +142 -39
  10. package/dist/components/features/AgentsModule.test.js +260 -11
  11. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  12. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  13. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  14. package/dist/components/features/DocumentIndex.d.ts +2 -2
  15. package/dist/components/features/DocumentIndex.js +3 -4
  16. package/dist/components/features/DocumentViewer.d.ts +3 -1
  17. package/dist/components/features/DocumentViewer.js +6 -5
  18. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  19. package/dist/components/features/DocumentWorkspace.js +15 -4
  20. package/dist/components/features/DocumentWorkspace.test.js +20 -3
  21. package/dist/components/features/TaskSettings.d.ts +2 -0
  22. package/dist/components/features/TaskSettings.js +83 -78
  23. package/dist/components/features/TaskSettings.test.js +171 -82
  24. package/dist/components/task/TaskActionHeader.js +11 -5
  25. package/dist/components/task/TaskActionHeader.test.js +10 -0
  26. package/dist/components/task/TaskCard.js +6 -2
  27. package/dist/components/task/TaskContextUpload.test.js +3 -3
  28. package/dist/components/task/TaskForm.js +14 -64
  29. package/dist/components/task/TaskKanban.js +0 -8
  30. package/dist/components/task/TaskList.js +5 -1
  31. package/dist/components/task/TaskList.test.js +4 -0
  32. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  33. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  34. package/dist/components/views/AppShellHeader.d.ts +3 -1
  35. package/dist/components/views/AppShellHeader.js +27 -11
  36. package/dist/components/views/AppShellHeader.test.js +18 -1
  37. package/dist/components/views/PlanComparisonPage.d.ts +20 -15
  38. package/dist/components/views/PlanComparisonPage.js +221 -82
  39. package/dist/components/views/PlanComparisonPage.test.js +268 -14
  40. package/dist/components/views/PlansPage.js +16 -10
  41. package/dist/components/views/PlansPage.test.js +4 -2
  42. package/dist/components/views/StandaloneLayout.js +256 -69
  43. package/dist/components/views/WidgetView.js +10 -8
  44. package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
  45. package/dist/components/views/panels/PlanningDrawer.js +259 -51
  46. package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
  47. package/dist/config/envSchema.js +1 -0
  48. package/dist/core/AiProfileService.d.ts +7 -0
  49. package/dist/core/AiProfileService.js +460 -35
  50. package/dist/core/AiProfiles.test.js +648 -2
  51. package/dist/core/AttachmentLinkService.d.ts +3 -0
  52. package/dist/core/AttachmentLinkService.js +58 -13
  53. package/dist/core/GlobalSettingsService.js +30 -0
  54. package/dist/core/GlobalSettingsService.test.d.ts +1 -0
  55. package/dist/core/GlobalSettingsService.test.js +45 -0
  56. package/dist/core/McpTokenService.d.ts +12 -0
  57. package/dist/core/McpTokenService.js +44 -1
  58. package/dist/core/PlanEntitlementService.d.ts +20 -0
  59. package/dist/core/PlanEntitlementService.js +215 -19
  60. package/dist/core/PlanFeatureCatalog.test.js +55 -0
  61. package/dist/core/PlanningEntities.test.js +366 -4
  62. package/dist/core/TaskActivityService.d.ts +28 -1
  63. package/dist/core/TaskActivityService.js +227 -46
  64. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  65. package/dist/core/TaskActivitySync.test.js +90 -0
  66. package/dist/core/TaskAuditTimeline.test.js +96 -0
  67. package/dist/core/Taskforce.d.ts +204 -26
  68. package/dist/core/Taskforce.ids.test.js +58 -0
  69. package/dist/core/Taskforce.js +781 -71
  70. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  71. package/dist/core/types.d.ts +10 -0
  72. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
  73. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  74. package/dist/hooks/bootstrapTimeouts.js +8 -0
  75. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  76. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  77. package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
  78. package/dist/hooks/sync/orchestratorShared.js +61 -1
  79. package/dist/hooks/sync/recovery.js +8 -0
  80. package/dist/hooks/sync/recovery.test.js +5 -5
  81. package/dist/hooks/sync/transfers.d.ts +2 -1
  82. package/dist/hooks/sync/transfers.js +36 -9
  83. package/dist/hooks/sync/transfers.test.js +534 -2
  84. package/dist/hooks/useFilterSort.js +13 -13
  85. package/dist/hooks/useFilterSort.test.js +87 -1
  86. package/dist/hooks/useSyncOrchestrator.js +50 -5
  87. package/dist/hooks/useTaskforce.d.ts +85 -50
  88. package/dist/hooks/useTaskforce.js +44 -14
  89. package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
  90. package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
  91. package/dist/hooks/useWorkspaceSyncController.js +7 -1
  92. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  93. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  95. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  96. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  97. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  98. package/dist/mcp/clientIntegrations.d.ts +1 -0
  99. package/dist/mcp/clientIntegrations.js +25 -0
  100. package/dist/mcp/clientIntegrations.test.js +14 -3
  101. package/dist/mcp/clientProfileDefaults.js +3 -3
  102. package/dist/mcp/collaborationRegistrar.js +7 -7
  103. package/dist/mcp/documentAssetRegistrar.js +113 -7
  104. package/dist/mcp/runtime.d.ts +1 -0
  105. package/dist/mcp/runtime.js +969 -122
  106. package/dist/mcp/runtime.test.js +1083 -11
  107. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  108. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  109. package/dist/mcp/toolRegistry.d.ts +56 -0
  110. package/dist/mcp/toolRegistry.js +8 -0
  111. package/dist/mcp/toolRegistry.test.js +4 -0
  112. package/dist/migrations/taskSchemaMigrations.js +58 -15
  113. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  114. package/dist/server/aiProfileAvatarAssets.js +319 -0
  115. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  116. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  117. package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
  118. package/dist/server/index.js +4 -0
  119. package/dist/server/index.test.js +6 -1
  120. package/dist/server/routes/admin.js +328 -9
  121. package/dist/server/routes/annotatedAttachments.js +51 -0
  122. package/dist/server/routes/auth.d.ts +11 -0
  123. package/dist/server/routes/auth.js +189 -27
  124. package/dist/server/routes/authSupport.d.ts +5 -0
  125. package/dist/server/routes/authSupport.js +13 -1
  126. package/dist/server/routes/billing.d.ts +1 -0
  127. package/dist/server/routes/billing.js +116 -63
  128. package/dist/server/routes/billing.test.js +162 -11
  129. package/dist/server/routes/documents.js +10 -0
  130. package/dist/server/routes/shared.js +0 -10
  131. package/dist/server/routes/sync.integration.test.js +221 -0
  132. package/dist/server/routes/sync.js +13 -3
  133. package/dist/server/routes/syncSnapshotRoutes.js +2 -0
  134. package/dist/server/routes/tasks.js +12 -9
  135. package/dist/server/routes.js +87 -8
  136. package/dist/server/routes.test.js +972 -23
  137. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  138. package/dist/shared/aiProfileSurfaceType.js +10 -8
  139. package/dist/storage/objectStorage.d.ts +1 -0
  140. package/dist/storage/objectStorage.js +2 -1
  141. package/dist/storage/objectStorageClient.d.ts +1 -0
  142. package/dist/storage/objectStorageClient.js +41 -8
  143. package/dist/storage/objectStorageClient.test.js +26 -1
  144. package/dist/storage/objectStorageConfig.js +3 -1
  145. package/dist/storage/objectStorageConfig.test.js +13 -1
  146. package/dist/sync/collaborationSyncPayload.js +2 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +1 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -3
  149. package/dist/sync/collaborationSyncState.js +19 -12
  150. package/dist/sync/contentSyncPayload.d.ts +5 -1
  151. package/dist/sync/contentSyncPayload.js +9 -0
  152. package/dist/sync/contentSyncPayload.test.js +39 -0
  153. package/dist/sync/contentSyncState.d.ts +5 -1
  154. package/dist/sync/contentSyncState.js +231 -37
  155. package/dist/sync/planningSyncPayload.js +5 -0
  156. package/dist/sync/planningSyncPayload.test.js +24 -0
  157. package/dist/sync/syncApplyHandlers.d.ts +4 -4
  158. package/dist/sync/syncApplyHandlers.js +89 -31
  159. package/dist/sync/syncApplyHandlers.test.js +184 -4
  160. package/dist/sync/syncResourceAdapters.d.ts +2 -0
  161. package/dist/sync/syncResourceAdapters.js +2 -0
  162. package/dist/sync/syncService.d.ts +1 -0
  163. package/dist/sync/syncService.js +13 -1
  164. package/dist/sync/syncService.test.js +39 -0
  165. package/dist/sync/workspacePullFeed.d.ts +4 -0
  166. package/dist/sync/workspacePullFeed.js +129 -70
  167. package/dist/sync/workspacePullFeed.test.js +49 -0
  168. package/dist/sync/workspaceSyncModel.d.ts +28 -6
  169. package/dist/sync/workspaceSyncModel.js +159 -45
  170. package/dist/sync/workspaceSyncModel.test.js +269 -3
  171. package/dist/types.d.ts +33 -2
  172. package/dist/ui/agent-logos/ChatGPT.png +0 -0
  173. package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
  174. package/dist/ui/agent-logos/antigravity.jpeg +0 -0
  175. package/dist/ui/agent-logos/claude-code.jpeg +0 -0
  176. package/dist/ui/agent-logos/codex.jpeg +0 -0
  177. package/dist/ui/agent-logos/cursor.png +0 -0
  178. package/dist/ui/agent-logos/openclaw.jpeg +0 -0
  179. package/dist/ui/agent-logos/windsurf.png +0 -0
  180. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
  181. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  182. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
  183. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
  184. package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
  185. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  186. package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
  187. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  188. package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
  189. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
  190. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
  191. package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
  192. package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
  193. package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
  194. package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
  195. package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
  196. package/dist/ui/assets/index-MAHKvFqp.js +4 -0
  197. package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
  198. package/dist/ui/index.html +3 -3
  199. package/dist/ui/og-image.png +0 -0
  200. package/dist/ui/product/agent manager.png +0 -0
  201. package/dist/ui/product/agent_manager_02.png +0 -0
  202. package/dist/utils/aiProfileEvents.d.ts +7 -0
  203. package/dist/utils/aiProfileEvents.js +6 -0
  204. package/dist/utils/assignees.d.ts +6 -0
  205. package/dist/utils/assignees.js +8 -1
  206. package/dist/utils/assignees.test.d.ts +1 -0
  207. package/dist/utils/assignees.test.js +25 -0
  208. package/dist/utils/avatarDisplay.d.ts +1 -0
  209. package/dist/utils/avatarDisplay.js +15 -0
  210. package/dist/utils/contextFiles.d.ts +3 -1
  211. package/dist/utils/contextFiles.js +2 -1
  212. package/dist/utils/taskActivity.d.ts +7 -1
  213. package/dist/utils/taskActivity.js +63 -1
  214. package/dist/utils/taskActivity.test.js +23 -1
  215. package/dist/utils/taskSearch.d.ts +22 -0
  216. package/dist/utils/taskSearch.js +87 -0
  217. package/dist/utils/taskSearch.test.d.ts +1 -0
  218. package/dist/utils/taskSearch.test.js +69 -0
  219. package/dist/utils/taskUtils.js +10 -3
  220. package/dist/utils/taskUtils.test.d.ts +1 -0
  221. package/dist/utils/taskUtils.test.js +19 -0
  222. package/package.json +1 -1
  223. package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
  224. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  225. package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
  226. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
  227. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
  228. package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
  229. package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
  230. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
  231. package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
  232. package/dist/ui/assets/index-C6k75jr8.js +0 -6
@@ -289,6 +289,82 @@ describe('TaskforceCore MCP auth invalidation', () => {
289
289
  cleanup();
290
290
  }
291
291
  });
292
+ it('does not delete active MCP access tokens', () => {
293
+ const { core, cleanup } = makeCore();
294
+ try {
295
+ setupWorkspace(core, 'owner-1', 'workspace-1');
296
+ const issued = core.createMcpAccessToken({
297
+ userId: 'owner-1',
298
+ workspaceId: 'workspace-1',
299
+ name: 'Active Token',
300
+ scopes: ['tasks:read']
301
+ });
302
+ expect(() => core.deleteMcpAccessToken({
303
+ actorUserId: 'owner-1',
304
+ workspaceId: 'workspace-1',
305
+ tokenId: issued.record.id
306
+ })).toThrow('Active MCP tokens must be revoked before deletion');
307
+ expect(core.listMcpAccessTokens({ userId: 'owner-1', workspaceId: 'workspace-1' }).items).toHaveLength(1);
308
+ }
309
+ finally {
310
+ cleanup();
311
+ }
312
+ });
313
+ it('deletes revoked MCP access token records', () => {
314
+ const { core, cleanup } = makeCore();
315
+ try {
316
+ setupWorkspace(core, 'owner-1', 'workspace-1');
317
+ const issued = core.createMcpAccessToken({
318
+ userId: 'owner-1',
319
+ workspaceId: 'workspace-1',
320
+ name: 'Revoked Token',
321
+ scopes: ['tasks:read']
322
+ });
323
+ core.revokeMcpAccessToken({
324
+ actorUserId: 'owner-1',
325
+ workspaceId: 'workspace-1',
326
+ tokenId: issued.record.id
327
+ });
328
+ expect(core.deleteMcpAccessToken({
329
+ actorUserId: 'owner-1',
330
+ workspaceId: 'workspace-1',
331
+ tokenId: issued.record.id
332
+ })).toBe(true);
333
+ expect(core.listMcpAccessTokens({ userId: 'owner-1', workspaceId: 'workspace-1' }).items).toHaveLength(0);
334
+ expect(core.authenticateMcpAccessToken(issued.token)).toBeNull();
335
+ }
336
+ finally {
337
+ cleanup();
338
+ }
339
+ });
340
+ it('deletes expired MCP access token records', () => {
341
+ const { core, cleanup } = makeCore();
342
+ try {
343
+ setupWorkspace(core, 'owner-1', 'workspace-1');
344
+ const issued = core.createMcpAccessToken({
345
+ userId: 'owner-1',
346
+ workspaceId: 'workspace-1',
347
+ name: 'Expired Token',
348
+ scopes: ['tasks:read'],
349
+ expiresAt: new Date(Date.now() + 60_000).toISOString()
350
+ });
351
+ core.db.prepare(`
352
+ UPDATE mcp_access_tokens
353
+ SET expires_at = ?
354
+ WHERE tenant_id = ? AND id = ?
355
+ `).run(new Date(Date.now() - 60_000).toISOString(), 'default', issued.record.id);
356
+ expect(core.listMcpAccessTokens({ userId: 'owner-1', workspaceId: 'workspace-1' }).items[0]?.status).toBe('expired');
357
+ expect(core.deleteMcpAccessToken({
358
+ actorUserId: 'owner-1',
359
+ workspaceId: 'workspace-1',
360
+ tokenId: issued.record.id
361
+ })).toBe(true);
362
+ expect(core.listMcpAccessTokens({ userId: 'owner-1', workspaceId: 'workspace-1' }).items).toHaveLength(0);
363
+ }
364
+ finally {
365
+ cleanup();
366
+ }
367
+ });
292
368
  it('denies OAuth authentication after the authorized member is disabled', () => {
293
369
  const { core, cleanup } = makeCore();
294
370
  try {
@@ -308,6 +384,39 @@ describe('TaskforceCore MCP auth invalidation', () => {
308
384
  cleanup();
309
385
  }
310
386
  });
387
+ it('returns persisted AI profile binding on OAuth connector authentication', () => {
388
+ const { core, cleanup } = makeCore();
389
+ try {
390
+ setupWorkspace(core, 'owner-1', 'workspace-1');
391
+ const accessToken = createOauthGrantToken(core, 'owner-1', 'workspace-1');
392
+ const auth = core.authenticateMcpOAuthAccessToken(accessToken);
393
+ expect(auth?.aiProfileToken).toBeFalsy();
394
+ const resolved = core.resolveAiProfile({
395
+ workspaceId: 'workspace-1',
396
+ name: 'Codex Connector',
397
+ icon: 'Code',
398
+ color: '#0ea5e9',
399
+ }, {
400
+ trustedSeatScope: 'cloud_metered',
401
+ });
402
+ expect(core.bindMcpOAuthConnectorGrantAiProfile({
403
+ grantId: auth.grantId,
404
+ workspaceId: 'workspace-1',
405
+ profileId: resolved.profile.id,
406
+ profileToken: resolved.profile.profileToken,
407
+ profileName: resolved.profile.name,
408
+ })).toBe(true);
409
+ expect(core.authenticateMcpOAuthAccessToken(accessToken)).toEqual(expect.objectContaining({
410
+ grantId: auth.grantId,
411
+ aiProfileId: resolved.profile.id,
412
+ aiProfileToken: resolved.profile.profileToken,
413
+ aiProfileName: 'Codex Connector',
414
+ }));
415
+ }
416
+ finally {
417
+ cleanup();
418
+ }
419
+ });
311
420
  it('denies OAuth authentication after account access is suspended', () => {
312
421
  const { core, cleanup } = makeCore();
313
422
  try {
@@ -275,6 +275,11 @@ export interface GlobalTaskforceSettings {
275
275
  site?: {
276
276
  showRunTaskforceLocally?: boolean;
277
277
  pricingPageEnabled?: boolean;
278
+ animatedBackgroundEnabled?: boolean;
279
+ siteTheme?: 'dark' | 'light';
280
+ pricingPageTitle?: string;
281
+ pricingPageDescription?: string;
282
+ pricingPageShowPublicDescriptions?: boolean;
278
283
  };
279
284
  setup?: {
280
285
  mode?: 'core' | 'operations';
@@ -505,8 +510,10 @@ export interface PlanFeatureCatalogItem {
505
510
  featureKey: string;
506
511
  label: string;
507
512
  description: string;
513
+ isCustom?: boolean;
508
514
  publicLabel?: string | null;
509
515
  publicDescription?: string | null;
516
+ publicDescriptionVisible?: boolean | null;
510
517
  publicDisplayOrder?: number | null;
511
518
  allowedAccessModes: PlanFeatureAccess[];
512
519
  configTemplate: Record<string, unknown>;
@@ -678,4 +685,7 @@ export interface McpOAuthAccessTokenAuthResult {
678
685
  provider: McpOAuthProvider;
679
686
  scopes: McpAccessTokenScope[];
680
687
  status: McpOAuthGrantStatus;
688
+ aiProfileId?: string | null;
689
+ aiProfileToken?: string | null;
690
+ aiProfileName?: string | null;
681
691
  }
@@ -2,6 +2,7 @@ import { useCallback, useEffect } from 'react';
2
2
  import { createDefaultAssigneeOptions } from '../../utils/assignees';
3
3
  import { logBootstrapDebug } from '../workspace/workspaceLocalState';
4
4
  import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../../utils/taskforceApiClient';
5
+ import { AUTH_SESSION_REQUEST_TIMEOUT_MS } from '../bootstrapTimeouts';
5
6
  function getBootstrapTimestamp() {
6
7
  return typeof performance !== 'undefined' ? performance.now() : Date.now();
7
8
  }
@@ -48,7 +49,7 @@ export function useTaskforceAuthBootstrap({ runtimeConfigReady, shouldProbeCloud
48
49
  const startedAt = getBootstrapTimestamp();
49
50
  const abortController = new AbortController();
50
51
  const timeoutId = typeof window !== 'undefined'
51
- ? window.setTimeout(() => abortController.abort(), 8000)
52
+ ? window.setTimeout(() => abortController.abort(), AUTH_SESSION_REQUEST_TIMEOUT_MS)
52
53
  : null;
53
54
  const isStaleRequest = () => authSessionEpochRef.current !== requestEpoch;
54
55
  const logResult = (event, details) => {
@@ -0,0 +1,4 @@
1
+ export declare const BOOTSTRAP_REQUEST_TIMEOUT_MS = 12000;
2
+ export declare const CLOUD_BOOTSTRAP_REQUEST_TIMEOUT_MS = 30000;
3
+ export declare const AUTH_SESSION_REQUEST_TIMEOUT_MS = 20000;
4
+ export declare function resolveBootstrapRequestTimeoutMs(runtimeMode: 'local' | 'cloud'): number;
@@ -0,0 +1,8 @@
1
+ export const BOOTSTRAP_REQUEST_TIMEOUT_MS = 12_000;
2
+ export const CLOUD_BOOTSTRAP_REQUEST_TIMEOUT_MS = 30_000;
3
+ export const AUTH_SESSION_REQUEST_TIMEOUT_MS = 20_000;
4
+ export function resolveBootstrapRequestTimeoutMs(runtimeMode) {
5
+ return runtimeMode === 'cloud'
6
+ ? CLOUD_BOOTSTRAP_REQUEST_TIMEOUT_MS
7
+ : BOOTSTRAP_REQUEST_TIMEOUT_MS;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AUTH_SESSION_REQUEST_TIMEOUT_MS, BOOTSTRAP_REQUEST_TIMEOUT_MS, CLOUD_BOOTSTRAP_REQUEST_TIMEOUT_MS, resolveBootstrapRequestTimeoutMs } from './bootstrapTimeouts';
3
+ describe('bootstrap timeout budgets', () => {
4
+ it('keeps local bootstrap checks short', () => {
5
+ expect(resolveBootstrapRequestTimeoutMs('local')).toBe(BOOTSTRAP_REQUEST_TIMEOUT_MS);
6
+ });
7
+ it('gives cloud bootstrap checks enough room for edge delays', () => {
8
+ expect(resolveBootstrapRequestTimeoutMs('cloud')).toBe(CLOUD_BOOTSTRAP_REQUEST_TIMEOUT_MS);
9
+ expect(CLOUD_BOOTSTRAP_REQUEST_TIMEOUT_MS).toBeGreaterThan(BOOTSTRAP_REQUEST_TIMEOUT_MS);
10
+ expect(AUTH_SESSION_REQUEST_TIMEOUT_MS).toBeGreaterThan(BOOTSTRAP_REQUEST_TIMEOUT_MS);
11
+ });
12
+ });
@@ -1,7 +1,7 @@
1
1
  import type { WorkspaceTaxonomyState } from '../../shared/taxonomyState.js';
2
- import type { WorkspaceSyncAiProfileSnapshot, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncTaskEventSnapshot } from '../../sync/workspaceSyncModel';
2
+ import type { WorkspaceSyncAiProfileSnapshot, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncEntityEventSnapshot } from '../../sync/workspaceSyncModel';
3
3
  import type { WorkspaceSyncPushBaselineSnapshot } from '../../sync/workspaceSyncState.js';
4
- import type { TaskItem } from '../../types';
4
+ import type { InitiativeItem, TaskItem, WorkstreamItem } from '../../types';
5
5
  export declare const USER_GLOBAL_SYNC_META_KEY = "taskforce.userGlobalSyncMeta.v1";
6
6
  export declare const SYNC_DEBUG_LOG_KEY = "taskforce.syncDebug.v1";
7
7
  export declare const WORKSPACE_BOOTSTRAP_TIMEOUT_MS = 120000;
@@ -19,7 +19,7 @@ export declare function writeUserGlobalSyncMeta(next: Record<string, {
19
19
  }>): void;
20
20
  export declare function buildTaxonomyStateFingerprint(taxonomyState?: WorkspaceTaxonomyState): string;
21
21
  export declare function flattenDocumentReviewComments(sessions: WorkspaceSyncDocumentReviewSessionSnapshot[]): WorkspaceSyncDocumentReviewCommentSnapshot[];
22
- export declare function flattenTaskEvents(tasks: TaskItem[], archivedTasks: TaskItem[]): WorkspaceSyncTaskEventSnapshot[];
22
+ export declare function flattenTaskEvents(tasks: TaskItem[], archivedTasks: TaskItem[], initiatives?: InitiativeItem[], workstreams?: WorkstreamItem[]): WorkspaceSyncEntityEventSnapshot[];
23
23
  export interface PersistedSyncWatermarks {
24
24
  taskIds: Set<string>;
25
25
  taskWatermarks: Map<string, string>;
@@ -40,6 +40,8 @@ export function normalizeWorkspaceSyncAiProfileSnapshotEntry(entry) {
40
40
  color: String(entry?.color || '#8b5cf6').trim() || '#8b5cf6',
41
41
  avatarUrl: normalizeSyncSameOriginAssetUrl(entry?.avatarUrl ?? entry?.avatar_url),
42
42
  avatarSourceUrl: normalizeSyncSameOriginAssetUrl(entry?.avatarSourceUrl ?? entry?.avatar_source_url),
43
+ avatarRevision: Number.isFinite(Number(entry?.avatarRevision ?? entry?.avatar_revision)) ? Math.max(0, Math.floor(Number(entry?.avatarRevision ?? entry?.avatar_revision))) : 0,
44
+ avatarUpdatedAt: normalizeSyncNullableIsoString(entry?.avatarUpdatedAt ?? entry?.avatar_updated_at),
43
45
  description: normalizeSyncNullableString(entry?.description),
44
46
  role: normalizeSyncNullableString(entry?.role),
45
47
  provider: normalizeSyncNullableString(entry?.provider),
@@ -113,7 +115,7 @@ export function flattenDocumentReviewComments(sessions) {
113
115
  .filter((comment) => comment.id.length > 0 && comment.sessionId.length > 0);
114
116
  });
115
117
  }
116
- export function flattenTaskEvents(tasks, archivedTasks) {
118
+ export function flattenTaskEvents(tasks, archivedTasks, initiatives = [], workstreams = []) {
117
119
  const deduped = new Map();
118
120
  for (const task of [...(tasks || []), ...(archivedTasks || [])]) {
119
121
  const taskEvents = Array.isArray(task?.taskEvents) ? task.taskEvents : [];
@@ -126,8 +128,66 @@ export function flattenTaskEvents(tasks, archivedTasks) {
126
128
  continue;
127
129
  const nextEvent = {
128
130
  id,
131
+ workspaceId,
132
+ entityType: 'task',
133
+ entityId: taskId,
129
134
  taskId,
135
+ action: String(event?.action || '').trim(),
136
+ actor: String(event?.actor || 'system').trim() || 'system',
137
+ actorType: event?.actorType === 'ai' || event?.actorType === 'human' || event?.actorType === 'system'
138
+ ? event.actorType
139
+ : 'system',
140
+ details: event?.details,
141
+ createdAt,
142
+ };
143
+ const existing = deduped.get(id);
144
+ if (!existing || nextEvent.createdAt > existing.createdAt) {
145
+ deduped.set(id, nextEvent);
146
+ }
147
+ }
148
+ }
149
+ for (const initiative of initiatives || []) {
150
+ const entityEvents = Array.isArray(initiative?.entityEvents) ? initiative.entityEvents : [];
151
+ for (const event of entityEvents) {
152
+ const id = String(event?.id || '').trim();
153
+ const entityId = String(event?.entityId || initiative?.id || '').trim();
154
+ const workspaceId = String(event?.workspaceId || '').trim();
155
+ const createdAt = String(event?.createdAt || '').trim();
156
+ if (!id || !entityId || !workspaceId || !createdAt)
157
+ continue;
158
+ const nextEvent = {
159
+ id,
160
+ workspaceId,
161
+ entityType: 'initiative',
162
+ entityId,
163
+ action: String(event?.action || '').trim(),
164
+ actor: String(event?.actor || 'system').trim() || 'system',
165
+ actorType: event?.actorType === 'ai' || event?.actorType === 'human' || event?.actorType === 'system'
166
+ ? event.actorType
167
+ : 'system',
168
+ details: event?.details,
169
+ createdAt,
170
+ };
171
+ const existing = deduped.get(id);
172
+ if (!existing || nextEvent.createdAt > existing.createdAt) {
173
+ deduped.set(id, nextEvent);
174
+ }
175
+ }
176
+ }
177
+ for (const workstream of workstreams || []) {
178
+ const entityEvents = Array.isArray(workstream?.entityEvents) ? workstream.entityEvents : [];
179
+ for (const event of entityEvents) {
180
+ const id = String(event?.id || '').trim();
181
+ const entityId = String(event?.entityId || workstream?.id || '').trim();
182
+ const workspaceId = String(event?.workspaceId || '').trim();
183
+ const createdAt = String(event?.createdAt || '').trim();
184
+ if (!id || !entityId || !workspaceId || !createdAt)
185
+ continue;
186
+ const nextEvent = {
187
+ id,
130
188
  workspaceId,
189
+ entityType: 'workstream',
190
+ entityId,
131
191
  action: String(event?.action || '').trim(),
132
192
  actor: String(event?.actor || 'system').trim() || 'system',
133
193
  actorType: event?.actorType === 'ai' || event?.actorType === 'human' || event?.actorType === 'system'
@@ -10,6 +10,10 @@ function resetWorkspaceSyncStatePushBaselines(args) {
10
10
  args.workspaceLastPushedAiProfileIdsRef.current = new Set();
11
11
  args.workspaceLastPushedAiProfileWatermarksRef.current = new Map();
12
12
  args.workspaceLastPushedAiProfileChangeKeysRef.current = new Map();
13
+ args.workspaceLastPushedDocumentPathsRef.current = new Set();
14
+ args.workspaceLastPushedDocumentWatermarksRef.current = new Map();
15
+ args.workspaceLastPushedAssetPathsRef.current = new Set();
16
+ args.workspaceLastPushedAssetWatermarksRef.current = new Map();
13
17
  args.workspaceBaselineResetReasonRef.current = 'repair-reset';
14
18
  }
15
19
  function resetAllWorkspacePushBaselines(args) {
@@ -187,6 +191,10 @@ export function createSyncRecoveryCoordinator({ currentWorkspaceId, workspaceClo
187
191
  workspaceLastPushedAiProfileIdsRef,
188
192
  workspaceLastPushedAiProfileWatermarksRef,
189
193
  workspaceLastPushedAiProfileChangeKeysRef,
194
+ workspaceLastPushedDocumentPathsRef,
195
+ workspaceLastPushedDocumentWatermarksRef,
196
+ workspaceLastPushedAssetPathsRef,
197
+ workspaceLastPushedAssetWatermarksRef,
190
198
  workspaceBaselineResetReasonRef,
191
199
  });
192
200
  }
@@ -168,7 +168,7 @@ describe('createSyncRecoveryCoordinator', () => {
168
168
  expect(args.workspaceBaselineResetReasonRef.current).toBeNull();
169
169
  expect(args.workspaceRepairModeRef.current).toBe(false);
170
170
  });
171
- it('resets sync-state push baselines during active repair and preserves content baselines', async () => {
171
+ it('resets sync-state and content push baselines during active repair', async () => {
172
172
  const taskWatermarks = new Map([['task-1', '2026-03-15T17:00:00.000Z']]);
173
173
  const taskChangeKeys = new Map([['task-1', 'task-change-key-1']]);
174
174
  const initiativeWatermarks = new Map([['initiative-1', '2026-03-15T17:00:00.000Z']]);
@@ -213,8 +213,8 @@ describe('createSyncRecoveryCoordinator', () => {
213
213
  expect(args.workspaceLastPushedWorkstreamWatermarksRef.current.size).toBe(0);
214
214
  expect(args.workspaceLastPushedAiProfileWatermarksRef.current.size).toBe(0);
215
215
  expect(args.workspaceLastPushedAiProfileChangeKeysRef.current.size).toBe(0);
216
- expect(args.workspaceLastPushedDocumentWatermarksRef.current).toBe(documentWatermarks);
217
- expect(args.workspaceLastPushedAssetWatermarksRef.current).toBe(assetWatermarks);
216
+ expect(args.workspaceLastPushedDocumentWatermarksRef.current.size).toBe(0);
217
+ expect(args.workspaceLastPushedAssetWatermarksRef.current.size).toBe(0);
218
218
  expect(pullWorkspaceChangesFromCloud).toHaveBeenCalledWith({ repairMode: true });
219
219
  expect(pushWorkspaceChangesToCloud).toHaveBeenCalledWith('signature-1', {
220
220
  forceAllAiProfiles: true,
@@ -227,8 +227,8 @@ describe('createSyncRecoveryCoordinator', () => {
227
227
  expect(persisted?.workstreamWatermarks.size).toBe(0);
228
228
  expect(persisted?.aiProfileWatermarks.size).toBe(0);
229
229
  expect(persisted?.aiProfileChangeKeys?.size).toBe(0);
230
- expect(persisted?.documentWatermarks.get('docs/spec.md')).toBe('2026-03-15T17:00:00.000Z');
231
- expect(persisted?.assetWatermarks.get('assets/image.png')).toBe('2026-03-15T17:00:00.000Z');
230
+ expect(persisted?.documentWatermarks.size).toBe(0);
231
+ expect(persisted?.assetWatermarks.size).toBe(0);
232
232
  expect(persisted?.taskEventWatermarks.get('event-1')).toBe('2026-03-15T17:00:00.000Z');
233
233
  });
234
234
  it('does not push during attach-cloud repair', async () => {
@@ -60,6 +60,7 @@ interface CreateWorkspaceTransferCoordinatorArgs {
60
60
  refreshWorkspaceSyncMarkdownDocumentsSnapshot: () => Promise<void>;
61
61
  refreshWorkspaceSyncAiProfilesSnapshot: () => Promise<void>;
62
62
  refreshWorkspaceSyncAssetsSnapshot: () => Promise<void>;
63
+ refreshWorkspaceSyncPlanningEntitiesSnapshot: () => Promise<void>;
63
64
  refreshWorkspaceSyncDocumentReviewSessionsSnapshot: () => Promise<void>;
64
65
  refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot: () => Promise<void>;
65
66
  refreshLocalWorkspaceTaskCollections: () => Promise<void>;
@@ -122,7 +123,7 @@ interface CreateWorkspaceTransferCoordinatorArgs {
122
123
  setTasks: Dispatch<SetStateAction<any[]>>;
123
124
  setArchivedTasks: Dispatch<SetStateAction<any[]>>;
124
125
  }
125
- export declare function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, currentWorkspaceId, workspaceSyncPhase, isAuthenticated, checkAuthSession, resolveCloudAuthUrl, ensureCloudWorkspaceReadyForSync, ensureWorkspaceSyncReady, handleSyncAuthFailure, clearWorkspaceRetry, clearWorkspaceIssue, isWorkspaceRetryPending, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely, persistWorkspaceSyncPatchBestEffort, persistWorkspaceSyncWatermarksSnapshotBestEffort, reportSyncEvent, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, reportWorkspaceWindowContention, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot, refreshLocalWorkspaceTaskCollections, buildWorkspaceSyncPayload, buildWorkspaceSyncSignature, seedWorkspaceSnapshotPushBaseline, shouldSuppressAttachBootstrapPush, recordSuppressedEventIds, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspacePushInFlightRef, workspacePullInFlightRef, workspacePendingPushReplayRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, workspaceLastPushedWatermarksRef, workspaceLastPushedTaskChangeKeysRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedAiProfileChangeKeysRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, workspaceBaselineResetReasonRef, workspaceCaptureAttachBootstrapBaselineRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, bootstrapLastProgressAtRef, realtimeSuppressedEventQueueRef, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceSyncPendingChanges, setTasks, setArchivedTasks }: CreateWorkspaceTransferCoordinatorArgs): {
126
+ export declare function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, currentWorkspaceId, workspaceSyncPhase, isAuthenticated, checkAuthSession, resolveCloudAuthUrl, ensureCloudWorkspaceReadyForSync, ensureWorkspaceSyncReady, handleSyncAuthFailure, clearWorkspaceRetry, clearWorkspaceIssue, isWorkspaceRetryPending, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely, persistWorkspaceSyncPatchBestEffort, persistWorkspaceSyncWatermarksSnapshotBestEffort, reportSyncEvent, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, reportWorkspaceWindowContention, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncPlanningEntitiesSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot, refreshLocalWorkspaceTaskCollections, buildWorkspaceSyncPayload, buildWorkspaceSyncSignature, seedWorkspaceSnapshotPushBaseline, shouldSuppressAttachBootstrapPush, recordSuppressedEventIds, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspacePushInFlightRef, workspacePullInFlightRef, workspacePendingPushReplayRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, workspaceLastPushedWatermarksRef, workspaceLastPushedTaskChangeKeysRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedAiProfileChangeKeysRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, workspaceBaselineResetReasonRef, workspaceCaptureAttachBootstrapBaselineRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, bootstrapLastProgressAtRef, realtimeSuppressedEventQueueRef, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceSyncPendingChanges, setTasks, setArchivedTasks }: CreateWorkspaceTransferCoordinatorArgs): {
126
127
  pushWorkspaceChangesToCloud: (signature: string, options?: {
127
128
  forceAllAiProfiles?: boolean;
128
129
  repairMode?: boolean;
@@ -3,7 +3,7 @@ import { isTransientWorkspaceSyncStatus, runWorkspacePullSyncService, runWorkspa
3
3
  import { createWorkspaceSyncLeaseHeldDecision, resolvePullSyncReadiness, resolvePushSyncReadiness } from './controlPlane';
4
4
  import { resolveWorkspacePullRequestPlan, resolveWorkspacePullSuccessPlan } from './pullLifecycle';
5
5
  import { logSyncDebug } from './orchestratorShared';
6
- export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, currentWorkspaceId, workspaceSyncPhase, isAuthenticated, checkAuthSession, resolveCloudAuthUrl, ensureCloudWorkspaceReadyForSync, ensureWorkspaceSyncReady, handleSyncAuthFailure, clearWorkspaceRetry, clearWorkspaceIssue, isWorkspaceRetryPending, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely, persistWorkspaceSyncPatchBestEffort, persistWorkspaceSyncWatermarksSnapshotBestEffort, reportSyncEvent, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, reportWorkspaceWindowContention, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot, refreshLocalWorkspaceTaskCollections, buildWorkspaceSyncPayload, buildWorkspaceSyncSignature, seedWorkspaceSnapshotPushBaseline, shouldSuppressAttachBootstrapPush, recordSuppressedEventIds, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspacePushInFlightRef, workspacePullInFlightRef, workspacePendingPushReplayRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, workspaceLastPushedWatermarksRef, workspaceLastPushedTaskChangeKeysRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedAiProfileChangeKeysRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, workspaceBaselineResetReasonRef, workspaceCaptureAttachBootstrapBaselineRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, bootstrapLastProgressAtRef, realtimeSuppressedEventQueueRef, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceSyncPendingChanges, setTasks, setArchivedTasks }) {
6
+ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, currentWorkspaceId, workspaceSyncPhase, isAuthenticated, checkAuthSession, resolveCloudAuthUrl, ensureCloudWorkspaceReadyForSync, ensureWorkspaceSyncReady, handleSyncAuthFailure, clearWorkspaceRetry, clearWorkspaceIssue, isWorkspaceRetryPending, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely, persistWorkspaceSyncPatchBestEffort, persistWorkspaceSyncWatermarksSnapshotBestEffort, reportSyncEvent, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, reportWorkspaceWindowContention, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncPlanningEntitiesSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot, refreshLocalWorkspaceTaskCollections, buildWorkspaceSyncPayload, buildWorkspaceSyncSignature, seedWorkspaceSnapshotPushBaseline, shouldSuppressAttachBootstrapPush, recordSuppressedEventIds, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspacePushInFlightRef, workspacePullInFlightRef, workspacePendingPushReplayRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, workspaceLastPushedWatermarksRef, workspaceLastPushedTaskChangeKeysRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedAiProfileChangeKeysRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, workspaceBaselineResetReasonRef, workspaceCaptureAttachBootstrapBaselineRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, bootstrapLastProgressAtRef, realtimeSuppressedEventQueueRef, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceSyncPendingChanges, setTasks, setArchivedTasks }) {
7
7
  const pushWorkspaceChangesToCloud = async (signature, options) => {
8
8
  let pushReadiness = resolvePushSyncReadiness({
9
9
  cloudAuthConfigured,
@@ -61,16 +61,29 @@ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtim
61
61
  const repairMode = options?.repairMode === true || workspaceRepairModeRef.current === true;
62
62
  let payloadForAttempt = null;
63
63
  let replayedPayload = false;
64
+ let attemptSignature = signature;
64
65
  try {
65
66
  await refreshWorkspaceSyncMarkdownDocumentsSnapshot();
66
67
  await refreshWorkspaceSyncAiProfilesSnapshot();
67
68
  await refreshWorkspaceSyncAssetsSnapshot();
69
+ await refreshWorkspaceSyncPlanningEntitiesSnapshot();
68
70
  await refreshWorkspaceSyncDocumentReviewSessionsSnapshot();
69
71
  const ready = await ensureWorkspaceSyncReady();
70
72
  if (!ready)
71
73
  return false;
74
+ const refreshedSignature = buildWorkspaceSyncSignature();
75
+ if (refreshedSignature) {
76
+ attemptSignature = refreshedSignature;
77
+ }
78
+ if (shouldSuppressAttachBootstrapPush(attemptSignature)) {
79
+ return false;
80
+ }
72
81
  const pendingReplay = workspacePendingPushReplayRef.current;
73
- const replayPayload = pendingReplay && pendingReplay.signature === signature
82
+ const canReplayPayload = (pendingReplay
83
+ && pendingReplay.signature === attemptSignature
84
+ && (!forceAllAiProfiles || pendingReplay.forceAllAiProfiles === true)
85
+ && (!repairMode || pendingReplay.repairMode === true));
86
+ const replayPayload = canReplayPayload
74
87
  ? pendingReplay
75
88
  : null;
76
89
  const payload = replayPayload?.payload || buildWorkspaceSyncPayload({ forceAllAiProfiles });
@@ -111,10 +124,10 @@ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtim
111
124
  replayedPayload
112
125
  }
113
126
  });
114
- workspacePendingSignatureRef.current = signature;
127
+ workspacePendingSignatureRef.current = attemptSignature;
115
128
  if (pushResult.transient || isTransientWorkspaceSyncStatus(pushResult.status)) {
116
129
  workspacePendingPushReplayRef.current = {
117
- signature,
130
+ signature: attemptSignature,
118
131
  payload,
119
132
  forceAllAiProfiles: replayPayload?.forceAllAiProfiles ?? forceAllAiProfiles,
120
133
  repairMode: replayPayload?.repairMode ?? repairMode
@@ -130,7 +143,7 @@ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtim
130
143
  return false;
131
144
  }
132
145
  workspacePendingPushReplayRef.current = null;
133
- workspaceLastPushedSignatureRef.current = signature;
146
+ workspaceLastPushedSignatureRef.current = attemptSignature;
134
147
  workspaceLastPushedTaskIdsRef.current = pushResult.currentTaskIds;
135
148
  workspaceLastPushedWatermarksRef.current = pushResult.currentTaskWatermarks
136
149
  ? new Map(pushResult.currentTaskWatermarks)
@@ -247,10 +260,10 @@ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtim
247
260
  setUserGlobalSyncError(message);
248
261
  setWorkspaceLastErrorMessage(message);
249
262
  setWorkspaceLastErrorAt(new Date().toISOString());
250
- workspacePendingSignatureRef.current = signature;
263
+ workspacePendingSignatureRef.current = attemptSignature;
251
264
  if (payloadForAttempt) {
252
265
  workspacePendingPushReplayRef.current = {
253
- signature,
266
+ signature: attemptSignature,
254
267
  payload: payloadForAttempt,
255
268
  forceAllAiProfiles,
256
269
  repairMode
@@ -435,7 +448,8 @@ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtim
435
448
  if (pullResult.appliedChanges > 0
436
449
  && (pullResult.pulledActiveUpserts
437
450
  || pullResult.pulledArchivedUpserts
438
- || pullResult.pulledTaskEventUpserts)) {
451
+ || pullResult.pulledTaskEventUpserts
452
+ || pullResult.pulledPlanningUpserts)) {
439
453
  try {
440
454
  await refreshLocalWorkspaceTaskCollections();
441
455
  }
@@ -508,7 +522,20 @@ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtim
508
522
  });
509
523
  }, 250);
510
524
  }
511
- if (pullSuccessPlan.deferredPushSignature) {
525
+ const shouldReassertAiProfilesAfterFullReconcile = (options?.forceCursorNull === true
526
+ && options?.repairMode !== true
527
+ && workspaceSyncPhase === 'active');
528
+ if (shouldReassertAiProfilesAfterFullReconcile) {
529
+ const reassertSignature = buildWorkspaceSyncSignature();
530
+ if (reassertSignature) {
531
+ workspaceForceFullAiProfilePushRef.current = true;
532
+ workspacePendingSignatureRef.current = reassertSignature;
533
+ window.setTimeout(() => {
534
+ void pushWorkspaceChangesToCloud(reassertSignature, { forceAllAiProfiles: true });
535
+ }, 120);
536
+ }
537
+ }
538
+ else if (pullSuccessPlan.deferredPushSignature) {
512
539
  const deferredPushSignature = pullSuccessPlan.deferredPushSignature;
513
540
  window.setTimeout(() => {
514
541
  void pushWorkspaceChangesToCloud(deferredPushSignature);