@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
@@ -1,4 +1,4 @@
1
- export declare const AI_PROFILE_SURFACE_TYPES: readonly ["chat_app", "ide", "coding_tool", "agent", "other"];
1
+ export declare const AI_PROFILE_SURFACE_TYPES: readonly ["chat_app", "ide", "cli", "coding_tool", "agent"];
2
2
  export type AiProfileSurfaceType = typeof AI_PROFILE_SURFACE_TYPES[number];
3
3
  export type AiProfileSurfaceTypeSection = AiProfileSurfaceType | 'unclassified';
4
4
  export declare const AI_PROFILE_SURFACE_TYPE_SECTION_ORDER: AiProfileSurfaceTypeSection[];
@@ -1,31 +1,31 @@
1
1
  export const AI_PROFILE_SURFACE_TYPES = [
2
2
  'chat_app',
3
3
  'ide',
4
+ 'cli',
4
5
  'coding_tool',
5
- 'agent',
6
- 'other'
6
+ 'agent'
7
7
  ];
8
8
  export const AI_PROFILE_SURFACE_TYPE_SECTION_ORDER = [
9
9
  'chat_app',
10
10
  'ide',
11
+ 'cli',
11
12
  'coding_tool',
12
13
  'agent',
13
- 'other',
14
14
  'unclassified'
15
15
  ];
16
16
  const AI_PROFILE_SURFACE_TYPE_LABELS = {
17
- chat_app: 'Chat app',
17
+ chat_app: 'Chat App',
18
18
  ide: 'IDE',
19
- coding_tool: 'Coding tool',
20
- agent: 'Agent',
21
- other: 'Other'
19
+ cli: 'CLI',
20
+ coding_tool: 'Coding Tool',
21
+ agent: 'Agent'
22
22
  };
23
23
  const AI_PROFILE_SURFACE_TYPE_SECTION_LABELS = {
24
24
  chat_app: 'Chat Apps',
25
25
  ide: 'IDEs',
26
+ cli: 'CLI',
26
27
  coding_tool: 'Coding Tools',
27
28
  agent: 'Agents',
28
- other: 'Other',
29
29
  unclassified: 'Unclassified'
30
30
  };
31
31
  export function isAiProfileSurfaceType(value) {
@@ -36,6 +36,8 @@ export function parseAiProfileSurfaceType(value) {
36
36
  if (typeof value !== 'string')
37
37
  return null;
38
38
  const normalized = value.trim();
39
+ if (normalized === 'other')
40
+ return 'agent';
39
41
  return isAiProfileSurfaceType(normalized) ? normalized : null;
40
42
  }
41
43
  export function formatAiProfileSurfaceTypeLabel(surfaceType) {
@@ -8,6 +8,7 @@ export interface R2ObjectStorageConfig {
8
8
  keyPrefix: string;
9
9
  signedUploadTtlSeconds: number;
10
10
  signedDownloadTtlSeconds: number;
11
+ requestTimeoutMs?: number;
11
12
  }
12
13
  export interface ResolvedObjectStorageConfig {
13
14
  provider: ObjectStorageProvider;
@@ -9,7 +9,8 @@ export function toPublicObjectStorageConfig(config) {
9
9
  endpoint: config.r2.endpoint,
10
10
  keyPrefix: config.r2.keyPrefix,
11
11
  signedUploadTtlSeconds: config.r2.signedUploadTtlSeconds,
12
- signedDownloadTtlSeconds: config.r2.signedDownloadTtlSeconds
12
+ signedDownloadTtlSeconds: config.r2.signedDownloadTtlSeconds,
13
+ ...(typeof config.r2.requestTimeoutMs === 'number' ? { requestTimeoutMs: config.r2.requestTimeoutMs } : {})
13
14
  }
14
15
  };
15
16
  }
@@ -8,6 +8,7 @@ export declare class ObjectStorageClient {
8
8
  constructor(config: ResolvedObjectStorageConfig);
9
9
  provider(): ObjectStorageProvider;
10
10
  keyForRelativePath(relativePath: string): string;
11
+ private requestTimeoutMs;
11
12
  putObject(relativePath: string, body: Buffer, contentType: string): Promise<void>;
12
13
  getObject(relativePath: string): Promise<StoredObject | null>;
13
14
  objectExists(relativePath: string): Promise<boolean>;
@@ -4,6 +4,7 @@ const AWS_SERVICE = 's3';
4
4
  const AWS_REGION = 'auto';
5
5
  const TRANSIENT_GET_OBJECT_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
6
6
  const MAX_GET_OBJECT_ATTEMPTS = 3;
7
+ const DEFAULT_REQUEST_TIMEOUT_MS = 15000;
7
8
  function sha256Hex(value) {
8
9
  return createHash('sha256').update(value).digest('hex');
9
10
  }
@@ -77,6 +78,33 @@ function shouldRetryGetObjectError(error) {
77
78
  || message.includes('ecanceled')
78
79
  || message.includes('und_err');
79
80
  }
81
+ async function fetchWithTimeout(url, init, timeoutMsRaw) {
82
+ const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0
83
+ ? Math.floor(timeoutMsRaw)
84
+ : DEFAULT_REQUEST_TIMEOUT_MS;
85
+ const controller = new AbortController();
86
+ const timeout = setTimeout(() => {
87
+ controller.abort(new Error(`R2 request timed out after ${timeoutMs}ms`));
88
+ }, timeoutMs);
89
+ try {
90
+ return await fetch(url, {
91
+ ...init,
92
+ signal: controller.signal
93
+ });
94
+ }
95
+ catch (error) {
96
+ if (controller.signal.aborted) {
97
+ const reason = controller.signal.reason;
98
+ if (reason instanceof Error)
99
+ throw reason;
100
+ throw new Error(`R2 request timed out after ${timeoutMs}ms`);
101
+ }
102
+ throw error;
103
+ }
104
+ finally {
105
+ clearTimeout(timeout);
106
+ }
107
+ }
80
108
  function signRequest(params) {
81
109
  const amzDate = createAmzDate(params.now);
82
110
  const dateStamp = toDateStamp(amzDate);
@@ -117,6 +145,11 @@ export class ObjectStorageClient {
117
145
  keyForRelativePath(relativePath) {
118
146
  return buildObjectKey(this.config, relativePath);
119
147
  }
148
+ requestTimeoutMs() {
149
+ return this.config.provider === 'r2'
150
+ ? Number(this.config.r2?.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS)
151
+ : DEFAULT_REQUEST_TIMEOUT_MS;
152
+ }
120
153
  async putObject(relativePath, body, contentType) {
121
154
  if (this.config.provider !== 'r2' || !this.config.r2) {
122
155
  throw new Error('R2 object storage is not configured.');
@@ -131,7 +164,7 @@ export class ObjectStorageClient {
131
164
  secretAccessKey: this.config.r2.secretAccessKey,
132
165
  payloadHash
133
166
  });
134
- const response = await fetch(url, {
167
+ const response = await fetchWithTimeout(url, {
135
168
  method: 'PUT',
136
169
  headers: {
137
170
  'authorization': signed.authorization,
@@ -140,7 +173,7 @@ export class ObjectStorageClient {
140
173
  'content-type': contentType
141
174
  },
142
175
  body: new Uint8Array(body)
143
- });
176
+ }, this.requestTimeoutMs());
144
177
  if (!response.ok) {
145
178
  const text = await response.text().catch(() => '');
146
179
  throw new Error(`Failed to upload object (${response.status}): ${text || response.statusText}`);
@@ -163,14 +196,14 @@ export class ObjectStorageClient {
163
196
  payloadHash
164
197
  });
165
198
  try {
166
- const response = await fetch(url, {
199
+ const response = await fetchWithTimeout(url, {
167
200
  method: 'GET',
168
201
  headers: {
169
202
  'authorization': signed.authorization,
170
203
  'x-amz-date': signed.amzDate,
171
204
  'x-amz-content-sha256': payloadHash
172
205
  }
173
- });
206
+ }, this.requestTimeoutMs());
174
207
  if (response.status === 404)
175
208
  return null;
176
209
  if (!response.ok) {
@@ -211,14 +244,14 @@ export class ObjectStorageClient {
211
244
  secretAccessKey: this.config.r2.secretAccessKey,
212
245
  payloadHash
213
246
  });
214
- const response = await fetch(url, {
247
+ const response = await fetchWithTimeout(url, {
215
248
  method: 'HEAD',
216
249
  headers: {
217
250
  'authorization': signed.authorization,
218
251
  'x-amz-date': signed.amzDate,
219
252
  'x-amz-content-sha256': payloadHash
220
253
  }
221
- });
254
+ }, this.requestTimeoutMs());
222
255
  if (response.status === 404)
223
256
  return false;
224
257
  if (!response.ok) {
@@ -241,14 +274,14 @@ export class ObjectStorageClient {
241
274
  secretAccessKey: this.config.r2.secretAccessKey,
242
275
  payloadHash
243
276
  });
244
- const response = await fetch(url, {
277
+ const response = await fetchWithTimeout(url, {
245
278
  method: 'DELETE',
246
279
  headers: {
247
280
  'authorization': signed.authorization,
248
281
  'x-amz-date': signed.amzDate,
249
282
  'x-amz-content-sha256': payloadHash
250
283
  }
251
- });
284
+ }, this.requestTimeoutMs());
252
285
  if (response.status === 404)
253
286
  return false;
254
287
  if (!response.ok) {
@@ -10,9 +10,34 @@ const TEST_CONFIG = {
10
10
  secretAccessKey: 'secret-key-1',
11
11
  keyPrefix: '',
12
12
  signedUploadTtlSeconds: 600,
13
- signedDownloadTtlSeconds: 300
13
+ signedDownloadTtlSeconds: 300,
14
+ requestTimeoutMs: 15000
14
15
  }
15
16
  };
17
+ describe('ObjectStorageClient.putObject', () => {
18
+ afterEach(() => {
19
+ vi.unstubAllGlobals();
20
+ vi.restoreAllMocks();
21
+ });
22
+ it('aborts stalled uploads before MCP tool calls can time out', async () => {
23
+ const fetchMock = vi.fn((_input, init) => (new Promise((_resolve, reject) => {
24
+ const signal = init?.signal;
25
+ signal?.addEventListener('abort', () => {
26
+ reject(signal.reason);
27
+ }, { once: true });
28
+ })));
29
+ vi.stubGlobal('fetch', fetchMock);
30
+ const client = new ObjectStorageClient({
31
+ ...TEST_CONFIG,
32
+ r2: {
33
+ ...TEST_CONFIG.r2,
34
+ requestTimeoutMs: 1
35
+ }
36
+ });
37
+ await expect(client.putObject('workspaces/workspace-1/assets/documents/doc-1.md', Buffer.from('# plan', 'utf8'), 'text/markdown')).rejects.toThrow('R2 request timed out after 1ms');
38
+ expect(fetchMock).toHaveBeenCalledTimes(1);
39
+ });
40
+ });
16
41
  describe('ObjectStorageClient.getObject', () => {
17
42
  afterEach(() => {
18
43
  vi.unstubAllGlobals();
@@ -31,6 +31,7 @@ export function resolveObjectStorageConfigFromEnv(env = process.env) {
31
31
  : '';
32
32
  const signedUploadTtlSeconds = readPositiveInteger(env.R2_SIGNED_UPLOAD_TTL_SECONDS, 600);
33
33
  const signedDownloadTtlSeconds = readPositiveInteger(env.R2_SIGNED_DOWNLOAD_TTL_SECONDS, 300);
34
+ const requestTimeoutMs = readPositiveInteger(env.R2_REQUEST_TIMEOUT_MS, 15000);
34
35
  return {
35
36
  provider: 'r2',
36
37
  r2: {
@@ -41,7 +42,8 @@ export function resolveObjectStorageConfigFromEnv(env = process.env) {
41
42
  secretAccessKey,
42
43
  keyPrefix,
43
44
  signedUploadTtlSeconds,
44
- signedDownloadTtlSeconds
45
+ signedDownloadTtlSeconds,
46
+ requestTimeoutMs
45
47
  }
46
48
  };
47
49
  }
@@ -20,7 +20,8 @@ describe('resolveObjectStorageConfigFromEnv', () => {
20
20
  secretAccessKey: 'secret-key-1',
21
21
  keyPrefix: '',
22
22
  signedUploadTtlSeconds: 600,
23
- signedDownloadTtlSeconds: 300
23
+ signedDownloadTtlSeconds: 300,
24
+ requestTimeoutMs: 15000
24
25
  }
25
26
  });
26
27
  });
@@ -35,4 +36,15 @@ describe('resolveObjectStorageConfigFromEnv', () => {
35
36
  });
36
37
  expect(config.r2?.keyPrefix).toBe('taskforce-assets');
37
38
  });
39
+ it('reads a bounded R2 request timeout', () => {
40
+ const config = resolveObjectStorageConfigFromEnv({
41
+ TASKFORCE_OBJECT_STORAGE_PROVIDER: 'r2',
42
+ R2_ACCOUNT_ID: 'acct-1',
43
+ R2_BUCKET: 'taskforce-dev-assets',
44
+ R2_ACCESS_KEY_ID: 'access-key-1',
45
+ R2_SECRET_ACCESS_KEY: 'secret-key-1',
46
+ R2_REQUEST_TIMEOUT_MS: '2500'
47
+ });
48
+ expect(config.r2?.requestTimeoutMs).toBe(2500);
49
+ });
38
50
  });
@@ -39,6 +39,8 @@ export function normalizeAiProfileSyncPayload(raw, workspaceId, normalizeSurface
39
39
  color: String(profile.color || '#8b5cf6').trim() || '#8b5cf6',
40
40
  ...(hasOwnSyncField(profile, 'avatarUrl') ? { avatarUrl: readSyncSameOriginAssetUrlField(profile, 'avatarUrl') ?? null } : {}),
41
41
  ...(hasOwnSyncField(profile, 'avatarSourceUrl') ? { avatarSourceUrl: readSyncSameOriginAssetUrlField(profile, 'avatarSourceUrl') ?? null } : {}),
42
+ ...(hasOwnSyncField(profile, 'avatarRevision') ? { avatarRevision: toOptionalNonNegativeInteger(profile.avatarRevision) } : {}),
43
+ avatarUpdatedAt: readSyncNullableIsoStringField(profile, 'avatarUpdatedAt') ?? null,
42
44
  description: readSyncNullableStringField(profile, 'description') ?? null,
43
45
  role: readSyncNullableStringField(profile, 'role') ?? null,
44
46
  provider: readSyncNullableStringField(profile, 'provider') ?? null,
@@ -39,6 +39,7 @@ describe('collaborationSyncPayload', () => {
39
39
  username: 'agent',
40
40
  icon: 'Bot',
41
41
  color: '#111111',
42
+ avatarUpdatedAt: null,
42
43
  description: 'Product coding agent',
43
44
  role: 'Implementer',
44
45
  provider: 'OpenAI',
@@ -1,5 +1,5 @@
1
1
  import type { TaskforceCore, TaskforceSyncCapable } from '../core/Taskforce.js';
2
- import type { WorkspaceSyncAnnotatedAttachmentSessionSnapshot, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncTaskEventSnapshot } from './workspaceSyncModel.js';
2
+ import type { WorkspaceSyncAnnotatedAttachmentSessionSnapshot, WorkspaceSyncDocumentReviewCommentSnapshot, WorkspaceSyncDocumentReviewSessionSnapshot, WorkspaceSyncEntityEventSnapshot } from './workspaceSyncModel.js';
3
3
  type SyncCore = TaskforceCore & TaskforceSyncCapable;
4
4
  interface CollaborationSyncStateDeps {
5
5
  core: SyncCore;
@@ -7,11 +7,11 @@ interface CollaborationSyncStateDeps {
7
7
  export declare function listWorkspaceSyncDocumentReviewSessionsSnapshot(deps: CollaborationSyncStateDeps, workspaceId?: string): WorkspaceSyncDocumentReviewSessionSnapshot[];
8
8
  export declare function listWorkspaceSyncDocumentReviewCommentsSnapshot(deps: CollaborationSyncStateDeps, workspaceId?: string): WorkspaceSyncDocumentReviewCommentSnapshot[];
9
9
  export declare function listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(deps: CollaborationSyncStateDeps, workspaceId?: string): WorkspaceSyncAnnotatedAttachmentSessionSnapshot[];
10
- export declare function listWorkspaceSyncTaskEventsSnapshot(deps: CollaborationSyncStateDeps, workspaceId?: string): WorkspaceSyncTaskEventSnapshot[];
10
+ export declare function listWorkspaceSyncTaskEventsSnapshot(deps: CollaborationSyncStateDeps, workspaceId?: string): WorkspaceSyncEntityEventSnapshot[];
11
11
  export declare function createCollaborationSyncState(deps: CollaborationSyncStateDeps): {
12
12
  listWorkspaceSyncDocumentReviewSessionsSnapshot: (workspaceId?: string) => WorkspaceSyncDocumentReviewSessionSnapshot[];
13
13
  listWorkspaceSyncDocumentReviewCommentsSnapshot: (workspaceId?: string) => WorkspaceSyncDocumentReviewCommentSnapshot[];
14
14
  listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot: (workspaceId?: string) => WorkspaceSyncAnnotatedAttachmentSessionSnapshot[];
15
- listWorkspaceSyncTaskEventsSnapshot: (workspaceId?: string) => WorkspaceSyncTaskEventSnapshot[];
15
+ listWorkspaceSyncTaskEventsSnapshot: (workspaceId?: string) => WorkspaceSyncEntityEventSnapshot[];
16
16
  };
17
17
  export {};
@@ -52,19 +52,26 @@ export function listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(deps, works
52
52
  && session.baseImageAssetId.length > 0));
53
53
  }
54
54
  export function listWorkspaceSyncTaskEventsSnapshot(deps, workspaceId = 'default') {
55
- return deps.core.listTaskEventsForSync({ workspaceId })
56
- .map((event) => ({
57
- id: event.id,
58
- taskId: event.taskId,
59
- workspaceId: event.workspaceId,
60
- action: event.action,
61
- actor: event.actor,
62
- actorType: event.actorType,
63
- details: event.details,
64
- createdAt: event.createdAt,
65
- }))
55
+ return deps.core.listEntityEventsForSync({ workspaceId })
56
+ .map((event) => {
57
+ const entityType = event.entityType === 'initiative' || event.entityType === 'workstream'
58
+ ? event.entityType
59
+ : 'task';
60
+ return {
61
+ id: event.id,
62
+ workspaceId: event.workspaceId,
63
+ entityType,
64
+ entityId: String(event.entityId || ('taskId' in event ? event.taskId : '') || '').trim(),
65
+ taskId: entityType === 'task' && 'taskId' in event ? event.taskId : undefined,
66
+ action: event.action,
67
+ actor: event.actor,
68
+ actorType: event.actorType,
69
+ details: event.details,
70
+ createdAt: event.createdAt,
71
+ };
72
+ })
66
73
  .filter((event) => (event.id.length > 0
67
- && event.taskId.length > 0
74
+ && event.entityId.length > 0
68
75
  && event.workspaceId.length > 0
69
76
  && event.action.length > 0
70
77
  && event.createdAt.length > 0));
@@ -1,4 +1,4 @@
1
- import type { WorkspaceAssetLinkRole } from '../storage/workspaceAssetStore.js';
1
+ import type { WorkspaceAssetLinkRole, WorkspaceAssetLinkTargetType } from '../storage/workspaceAssetStore.js';
2
2
  export interface DocumentSyncPayload {
3
3
  path: string;
4
4
  updatedAt: string;
@@ -8,6 +8,8 @@ export interface DocumentSyncPayload {
8
8
  referenceNumber?: number | null;
9
9
  version?: number | null;
10
10
  taskId?: string | null;
11
+ linkTargetType?: WorkspaceAssetLinkTargetType | null;
12
+ linkTargetId?: string | null;
11
13
  logicalName?: string | null;
12
14
  caption?: string | null;
13
15
  originalFilename?: string | null;
@@ -27,6 +29,8 @@ export interface BinaryAssetSyncPayload {
27
29
  mimeType: string;
28
30
  referenceNumber?: number | null;
29
31
  taskId?: string | null;
32
+ linkTargetType?: WorkspaceAssetLinkTargetType | null;
33
+ linkTargetId?: string | null;
30
34
  logicalName?: string | null;
31
35
  caption?: string | null;
32
36
  originalFilename?: string | null;
@@ -1,3 +1,8 @@
1
+ function toOptionalLinkTargetType(value) {
2
+ return value === 'initiative' || value === 'workstream' || value === 'task'
3
+ ? value
4
+ : undefined;
5
+ }
1
6
  function toOptionalTrimmedString(value) {
2
7
  if (typeof value !== 'string')
3
8
  return undefined;
@@ -32,6 +37,8 @@ export function normalizeDocumentSyncPayload(raw, normalizePath) {
32
37
  referenceNumber: toOptionalPositiveInteger(source.referenceNumber),
33
38
  version: toOptionalPositiveInteger(source.version),
34
39
  taskId: toNullableTrimmedString(source.taskId),
40
+ linkTargetType: toOptionalLinkTargetType(source.linkTargetType) ?? null,
41
+ linkTargetId: toNullableTrimmedString(source.linkTargetId),
35
42
  logicalName: toNullableTrimmedString(source.logicalName),
36
43
  caption: toNullableTrimmedString(source.caption),
37
44
  originalFilename: toNullableTrimmedString(source.originalFilename),
@@ -67,6 +74,8 @@ export function normalizeBinaryAssetSyncPayload(raw, normalizePath) {
67
74
  mimeType: toOptionalTrimmedString(source.mimeType) || 'application/octet-stream',
68
75
  referenceNumber: toOptionalPositiveInteger(source.referenceNumber),
69
76
  taskId: toNullableTrimmedString(source.taskId),
77
+ linkTargetType: toOptionalLinkTargetType(source.linkTargetType) ?? null,
78
+ linkTargetId: toNullableTrimmedString(source.linkTargetId),
70
79
  logicalName: toNullableTrimmedString(source.logicalName),
71
80
  caption: toNullableTrimmedString(source.caption),
72
81
  originalFilename: toNullableTrimmedString(source.originalFilename),
@@ -18,6 +18,8 @@ describe('contentSyncPayload', () => {
18
18
  referenceNumber: '7',
19
19
  version: '2',
20
20
  taskId: ' task-1 ',
21
+ linkTargetType: 'task',
22
+ linkTargetId: ' task-1 ',
21
23
  logicalName: ' Spec ',
22
24
  caption: ' Draft ',
23
25
  originalFilename: ' spec.md ',
@@ -32,6 +34,8 @@ describe('contentSyncPayload', () => {
32
34
  referenceNumber: 7,
33
35
  version: 2,
34
36
  taskId: 'task-1',
37
+ linkTargetType: 'task',
38
+ linkTargetId: 'task-1',
35
39
  logicalName: 'Spec',
36
40
  caption: 'Draft',
37
41
  originalFilename: 'spec.md',
@@ -47,6 +51,8 @@ describe('contentSyncPayload', () => {
47
51
  mimeType: ' image/png ',
48
52
  referenceNumber: '3',
49
53
  taskId: ' task-2 ',
54
+ linkTargetType: 'task',
55
+ linkTargetId: ' task-2 ',
50
56
  logicalName: ' Hero ',
51
57
  caption: ' Screenshot ',
52
58
  originalFilename: ' hero.png ',
@@ -61,6 +67,8 @@ describe('contentSyncPayload', () => {
61
67
  mimeType: 'image/png',
62
68
  referenceNumber: 3,
63
69
  taskId: 'task-2',
70
+ linkTargetType: 'task',
71
+ linkTargetId: 'task-2',
64
72
  logicalName: 'Hero',
65
73
  caption: 'Screenshot',
66
74
  originalFilename: 'hero.png',
@@ -72,6 +80,37 @@ describe('contentSyncPayload', () => {
72
80
  assetId: undefined,
73
81
  });
74
82
  });
83
+ it('normalizes planning link targets for document and asset payloads', () => {
84
+ expect(normalizeDocumentSyncPayload({
85
+ path: 'docs/plan.md',
86
+ updatedAt: '2026-04-10T17:00:00.000Z',
87
+ content: 'hello',
88
+ linkTargetType: 'initiative',
89
+ linkTargetId: ' initiative-1 ',
90
+ caption: ' Plan ',
91
+ }, normalizeDocumentPath)).toEqual(expect.objectContaining({
92
+ path: 'docs/plan.md',
93
+ linkTargetType: 'initiative',
94
+ linkTargetId: 'initiative-1',
95
+ taskId: null,
96
+ caption: 'Plan',
97
+ }));
98
+ expect(normalizeBinaryAssetSyncPayload({
99
+ path: 'workspaces/a/assets/files/file.bin',
100
+ updatedAt: '2026-04-10T17:00:00.000Z',
101
+ contentBase64: 'YWJj',
102
+ kind: 'file',
103
+ mimeType: 'application/octet-stream',
104
+ linkTargetType: 'workstream',
105
+ linkTargetId: ' workstream-1 ',
106
+ }, normalizeAssetPath)).toEqual(expect.objectContaining({
107
+ path: 'workspaces/a/assets/files/file.bin',
108
+ kind: 'file',
109
+ linkTargetType: 'workstream',
110
+ linkTargetId: 'workstream-1',
111
+ taskId: null,
112
+ }));
113
+ });
75
114
  it('serializes push-side document and asset payloads without route-local parsing', () => {
76
115
  expect(serializeDocumentSyncPayload({
77
116
  path: 'docs/plan.md',
@@ -1,6 +1,6 @@
1
1
  import type { TaskforceCore, TaskforceSyncCapable } from '../core/Taskforce.js';
2
2
  import type { ObjectStorageClient } from '../storage/objectStorageClient.js';
3
- import type { WorkspaceAssetEntry, WorkspaceAssetLinkEntry, WorkspaceAssetLinkRole, WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
3
+ import type { WorkspaceAssetEntry, WorkspaceAssetLinkEntry, WorkspaceAssetLinkRole, WorkspaceAssetLinkTargetType, WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
4
4
  import type { BinaryAssetDeleteSyncPayload, BinaryAssetSyncPayload, DocumentDeleteSyncPayload, DocumentSyncPayload } from './contentSyncPayload.js';
5
5
  type SyncCore = TaskforceCore & TaskforceSyncCapable;
6
6
  type RecordSyncDiagnostic = (workspaceId: string, eventType: 'pull' | 'push' | 'apply', errorMessage: string, details?: Record<string, unknown>) => void;
@@ -34,6 +34,8 @@ export declare function reconcileWorkspaceSyncLinks(deps: ContentSyncStateDeps,
34
34
  workspaceId: string;
35
35
  assetId: string;
36
36
  taskId?: string | null;
37
+ linkTargetType?: WorkspaceAssetLinkTargetType | null;
38
+ linkTargetId?: string | null;
37
39
  role?: WorkspaceAssetLinkRole | null;
38
40
  displayName?: string | null;
39
41
  updatedAt?: string | null;
@@ -70,6 +72,8 @@ export declare function createContentSyncState(deps: ContentSyncStateDeps): {
70
72
  workspaceId: string;
71
73
  assetId: string;
72
74
  taskId?: string | null;
75
+ linkTargetType?: WorkspaceAssetLinkTargetType | null;
76
+ linkTargetId?: string | null;
73
77
  role?: WorkspaceAssetLinkRole | null;
74
78
  displayName?: string | null;
75
79
  updatedAt?: string | null;