@taskforcehq/taskforce 0.3.302 → 0.3.303

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 (77) hide show
  1. package/dist/Taskforce.module.css +11 -0
  2. package/dist/TaskforceCore.js +59 -9
  3. package/dist/TaskforceCore.test.js +717 -14
  4. package/dist/components/task/TaskKanban.test.js +17 -0
  5. package/dist/components/views/PlansPage.d.ts +3 -1
  6. package/dist/components/views/PlansPage.js +110 -51
  7. package/dist/components/views/PlansPage.test.d.ts +1 -0
  8. package/dist/components/views/PlansPage.test.js +100 -0
  9. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  10. package/dist/components/views/StandaloneLayout.js +106 -151
  11. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  12. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  13. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  14. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  15. package/dist/core/EntitlementsPolicy.test.js +2 -2
  16. package/dist/core/GlobalSettingsService.js +78 -18
  17. package/dist/core/PlanEntitlementService.d.ts +29 -5
  18. package/dist/core/PlanEntitlementService.js +297 -219
  19. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  20. package/dist/core/SystemAdmin.test.js +186 -96
  21. package/dist/core/Taskforce.d.ts +51 -10
  22. package/dist/core/Taskforce.js +218 -49
  23. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  24. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  25. package/dist/core/types.d.ts +11 -12
  26. package/dist/hooks/useSyncOrchestrator.js +17 -3
  27. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  28. package/dist/hooks/useTaskforce.d.ts +3 -0
  29. package/dist/hooks/useTaskforce.js +9 -4
  30. package/dist/mcp/runtime.js +90 -29
  31. package/dist/mcp/runtime.test.js +272 -81
  32. package/dist/migrations/billingSchemaParity.test.js +3 -0
  33. package/dist/migrations/taskSchemaMigrations.js +15 -0
  34. package/dist/server/index.cookieProxy.test.js +1 -0
  35. package/dist/server/index.js +2 -0
  36. package/dist/server/index.test.js +2 -1
  37. package/dist/server/mockStripe.d.ts +1 -0
  38. package/dist/server/mockStripe.js +2 -0
  39. package/dist/server/routes/admin.js +80 -23
  40. package/dist/server/routes/auth.d.ts +1 -0
  41. package/dist/server/routes/auth.js +194 -53
  42. package/dist/server/routes/billing.js +506 -23
  43. package/dist/server/routes/billing.test.js +937 -45
  44. package/dist/server/routes/shared.js +5 -3
  45. package/dist/server/routes/sync.integration.test.js +160 -0
  46. package/dist/server/routes/sync.js +15 -5
  47. package/dist/server/routes.js +3 -3
  48. package/dist/server/routes.test.js +287 -76
  49. package/dist/sync/cloudSyncApi.d.ts +1 -0
  50. package/dist/sync/contentSyncState.d.ts +10 -0
  51. package/dist/sync/contentSyncState.js +277 -16
  52. package/dist/sync/syncService.d.ts +1 -0
  53. package/dist/sync/syncService.js +21 -8
  54. package/dist/sync/workspacePullFeed.d.ts +10 -0
  55. package/dist/sync/workspacePullFeed.js +111 -63
  56. package/dist/sync/workspacePullFeed.test.js +112 -0
  57. package/dist/ui/assets/{AgentsModule-JG5jc3he.js → AgentsModule-CpsTmrIW.js} +1 -1
  58. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  59. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-C3GyzrWm.js} +1 -1
  60. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  61. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  62. package/dist/ui/assets/{TaskSettings-Bqd4cCzW.js → TaskSettings-BOC5F6Ag.js} +1 -1
  63. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  64. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  65. package/dist/ui/assets/index-DneETxcu.css +1 -0
  66. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-pWocEipT.js} +1 -1
  67. package/dist/ui/index.html +3 -3
  68. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  69. package/dist/utils/accountProfileSummaryCache.js +97 -0
  70. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  71. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  72. package/package.json +3 -3
  73. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  74. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  75. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  76. package/dist/utils/billingStatusCache.d.ts +0 -21
  77. package/dist/utils/billingStatusCache.js +0 -70
@@ -21,6 +21,8 @@ interface ContentSyncStateDeps {
21
21
  recordSyncDiagnostic: RecordSyncDiagnostic;
22
22
  workspaceDocumentSnapshotCache: Map<string, Map<string, CachedWorkspaceSyncDocument>>;
23
23
  }
24
+ export type DocumentSyncIndexEntry = Omit<DocumentSyncPayload, 'content'>;
25
+ export type BinaryAssetSyncIndexEntry = Omit<BinaryAssetSyncPayload, 'contentBase64'>;
24
26
  export declare function resolveCanonicalAssetSyncLinkState(deps: ContentSyncStateDeps, asset: WorkspaceAssetEntry, workspaceId: string): {
25
27
  primaryLink: WorkspaceAssetLinkEntry | null;
26
28
  effectiveUpdatedAt: string;
@@ -42,7 +44,11 @@ export declare function reconcileWorkspaceSyncLinks(deps: ContentSyncStateDeps,
42
44
  }): void;
43
45
  export declare function listWorkspaceSyncDocumentDeletesSnapshot(deps: ContentSyncStateDeps, workspaceId?: string): DocumentDeleteSyncPayload[];
44
46
  export declare function listWorkspaceSyncDocumentsSnapshot(deps: ContentSyncStateDeps, basePath: string, workspaceId?: string): Promise<DocumentSyncPayload[]>;
47
+ export declare function listWorkspaceSyncDocumentIndex(deps: ContentSyncStateDeps, basePath: string, workspaceId?: string): Promise<DocumentSyncIndexEntry[]>;
45
48
  export declare function listWorkspaceSyncBinaryAssetsSnapshot(deps: ContentSyncStateDeps, basePath: string, workspaceId?: string): Promise<BinaryAssetSyncPayload[]>;
49
+ export declare function listWorkspaceSyncBinaryAssetIndex(deps: ContentSyncStateDeps, workspaceId?: string): Promise<BinaryAssetSyncIndexEntry[]>;
50
+ export declare function loadWorkspaceSyncDocumentPayload(deps: ContentSyncStateDeps, basePath: string, workspaceId: string, relativePath: string): Promise<DocumentSyncPayload | null>;
51
+ export declare function loadWorkspaceSyncBinaryAssetPayload(deps: ContentSyncStateDeps, basePath: string, workspaceId: string, assetPath: string): Promise<BinaryAssetSyncPayload | null>;
46
52
  export declare function listWorkspaceSyncBinaryAssetDeletesSnapshot(deps: ContentSyncStateDeps, workspaceId?: string): BinaryAssetDeleteSyncPayload[];
47
53
  export declare function writeWorkspaceSyncDocument(deps: ContentSyncStateDeps, basePath: string, doc: DocumentSyncPayload, workspaceId?: string, options?: {
48
54
  allowReferenceNumberReassignment?: boolean;
@@ -73,8 +79,12 @@ export declare function createContentSyncState(deps: ContentSyncStateDeps): {
73
79
  updatedAt?: string | null;
74
80
  }) => void;
75
81
  listWorkspaceSyncDocumentDeletesSnapshot: (workspaceId?: string) => DocumentDeleteSyncPayload[];
82
+ listWorkspaceSyncDocumentIndex: (basePath: string, workspaceId?: string) => Promise<DocumentSyncIndexEntry[]>;
76
83
  listWorkspaceSyncDocumentsSnapshot: (basePath: string, workspaceId?: string) => Promise<DocumentSyncPayload[]>;
84
+ loadWorkspaceSyncDocumentPayload: (basePath: string, workspaceId: string, relativePath: string) => Promise<DocumentSyncPayload | null>;
85
+ listWorkspaceSyncBinaryAssetIndex: (workspaceId?: string) => Promise<BinaryAssetSyncIndexEntry[]>;
77
86
  listWorkspaceSyncBinaryAssetsSnapshot: (basePath: string, workspaceId?: string) => Promise<BinaryAssetSyncPayload[]>;
87
+ loadWorkspaceSyncBinaryAssetPayload: (basePath: string, workspaceId: string, assetPath: string) => Promise<BinaryAssetSyncPayload | null>;
78
88
  listWorkspaceSyncBinaryAssetDeletesSnapshot: (workspaceId?: string) => BinaryAssetDeleteSyncPayload[];
79
89
  writeWorkspaceSyncDocument: (basePath: string, doc: DocumentSyncPayload, workspaceId?: string, options?: {
80
90
  allowReferenceNumberReassignment?: boolean;
@@ -68,10 +68,10 @@ export function hasDocumentSyncMetadataDrift(deps, workspaceId, doc, options) {
68
68
  : null;
69
69
  const normalizedIncomingLinkRole = doc.linkRole === 'reference' ? 'reference' : 'attachment';
70
70
  const allowReferenceNumberReassignment = options?.allowReferenceNumberReassignment === true;
71
- if (!allowReferenceNumberReassignment
72
- && existingAsset.referenceNumber
71
+ const referenceNumberMismatch = Boolean(existingAsset.referenceNumber
73
72
  && normalizedIncomingReference
74
- && existingAsset.referenceNumber !== normalizedIncomingReference) {
73
+ && existingAsset.referenceNumber !== normalizedIncomingReference);
74
+ if (referenceNumberMismatch) {
75
75
  deps.recordSyncDiagnostic(workspaceId, 'apply', 'Detected document reference number mismatch during normal sync.', {
76
76
  source: 'documents.referenceNumber.mismatch',
77
77
  path: doc.path,
@@ -80,7 +80,8 @@ export function hasDocumentSyncMetadataDrift(deps, workspaceId, doc, options) {
80
80
  incomingReferenceNumber: normalizedIncomingReference
81
81
  });
82
82
  }
83
- return (allowReferenceNumberReassignment && existingAsset.referenceNumber !== normalizedIncomingReference)
83
+ return referenceNumberMismatch
84
+ || (allowReferenceNumberReassignment && existingAsset.referenceNumber !== normalizedIncomingReference)
84
85
  || existingAsset.version !== normalizedIncomingVersion
85
86
  || (existingAsset.documentId || null) !== normalizedIncomingDocumentId
86
87
  || (existingAsset.logicalName || null) !== normalizedIncomingLogicalName
@@ -117,10 +118,10 @@ export function hasBinaryAssetSyncMetadataDrift(deps, workspaceId, asset, option
117
118
  ? 'image'
118
119
  : 'attachment';
119
120
  const allowReferenceNumberReassignment = options?.allowReferenceNumberReassignment === true;
120
- if (!allowReferenceNumberReassignment
121
- && existingAsset.referenceNumber
121
+ const referenceNumberMismatch = Boolean(existingAsset.referenceNumber
122
122
  && normalizedIncomingReference
123
- && existingAsset.referenceNumber !== normalizedIncomingReference) {
123
+ && existingAsset.referenceNumber !== normalizedIncomingReference);
124
+ if (referenceNumberMismatch) {
124
125
  deps.recordSyncDiagnostic(workspaceId, 'apply', 'Detected asset reference number mismatch during normal sync.', {
125
126
  source: 'assets.referenceNumber.mismatch',
126
127
  path: asset.path,
@@ -129,7 +130,8 @@ export function hasBinaryAssetSyncMetadataDrift(deps, workspaceId, asset, option
129
130
  incomingReferenceNumber: normalizedIncomingReference
130
131
  });
131
132
  }
132
- return (allowReferenceNumberReassignment && existingAsset.referenceNumber !== normalizedIncomingReference)
133
+ return referenceNumberMismatch
134
+ || (allowReferenceNumberReassignment && existingAsset.referenceNumber !== normalizedIncomingReference)
133
135
  || (existingAsset.logicalName || null) !== normalizedIncomingLogicalName
134
136
  || (existingAsset.originalFilename || null) !== normalizedIncomingFilename
135
137
  || (primaryLink?.taskId || null) !== normalizedIncomingTaskId
@@ -344,6 +346,80 @@ export async function listWorkspaceSyncDocumentsSnapshot(deps, basePath, workspa
344
346
  }
345
347
  return docs;
346
348
  }
349
+ export async function listWorkspaceSyncDocumentIndex(deps, basePath, workspaceId = 'default') {
350
+ const root = path.resolve(basePath);
351
+ const candidatePaths = new Set();
352
+ const docsRoot = path.join(root, 'docs');
353
+ const collectMarkdownPaths = (directory) => {
354
+ if (!fs.existsSync(directory))
355
+ return;
356
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
357
+ for (const entry of entries) {
358
+ const absolutePath = path.join(directory, entry.name);
359
+ if (entry.isDirectory()) {
360
+ collectMarkdownPaths(absolutePath);
361
+ continue;
362
+ }
363
+ if (!entry.isFile())
364
+ continue;
365
+ const relativeFromRoot = path.relative(root, absolutePath).replace(/\\/g, '/');
366
+ const normalized = deps.normalizeWorkspaceSyncDocumentPath(relativeFromRoot);
367
+ if (normalized)
368
+ candidatePaths.add(normalized);
369
+ }
370
+ };
371
+ collectMarkdownPaths(docsRoot);
372
+ if (deps.workspaceAssetStore) {
373
+ const canonicalAssets = deps.workspaceAssetStore.listAllByWorkspace(workspaceId, {
374
+ includeDeleted: false,
375
+ kind: 'document'
376
+ });
377
+ for (const asset of canonicalAssets) {
378
+ const normalized = deps.normalizeWorkspaceSyncDocumentPath(asset.storageKey);
379
+ if (normalized)
380
+ candidatePaths.add(normalized);
381
+ }
382
+ }
383
+ const docs = [];
384
+ for (const relativePath of Array.from(candidatePaths).sort((a, b) => a.localeCompare(b))) {
385
+ try {
386
+ const canonicalAsset = deps.workspaceAssetStore?.getByStorageKey(relativePath, workspaceId);
387
+ let updatedAt = '';
388
+ if (canonicalAsset?.kind === 'document') {
389
+ updatedAt = resolveCanonicalAssetSyncLinkState(deps, canonicalAsset, workspaceId).effectiveUpdatedAt;
390
+ }
391
+ else {
392
+ const absPath = path.resolve(path.join(basePath, relativePath));
393
+ const safeRoot = path.resolve(basePath);
394
+ if (absPath !== safeRoot && !absPath.startsWith(`${safeRoot}${path.sep}`))
395
+ continue;
396
+ if (!fs.existsSync(absPath))
397
+ continue;
398
+ const stat = fs.statSync(absPath);
399
+ if (!stat.isFile())
400
+ continue;
401
+ const existing = deps.core.getDocumentWatermark(workspaceId, relativePath);
402
+ updatedAt = existing?.updatedAt
403
+ || (Number.isFinite(stat.mtimeMs) ? new Date(stat.mtimeMs).toISOString() : new Date().toISOString());
404
+ }
405
+ if (!Number.isFinite(Date.parse(String(updatedAt || '').trim())))
406
+ continue;
407
+ docs.push({
408
+ path: relativePath,
409
+ updatedAt: String(updatedAt || '').trim(),
410
+ ...getCanonicalSyncDocumentMetadata(deps, relativePath, workspaceId)
411
+ });
412
+ }
413
+ catch (error) {
414
+ deps.recordSyncDiagnostic(workspaceId, 'pull', 'Failed to index sync document snapshot entry.', {
415
+ source: 'documents.snapshot.indexEntry',
416
+ path: relativePath,
417
+ error: String(error?.message || error || '')
418
+ });
419
+ }
420
+ }
421
+ return docs;
422
+ }
347
423
  export async function listWorkspaceSyncBinaryAssetsSnapshot(deps, basePath, workspaceId = 'default') {
348
424
  if (!deps.workspaceAssetStore)
349
425
  return [];
@@ -418,6 +494,181 @@ export async function listWorkspaceSyncBinaryAssetsSnapshot(deps, basePath, work
418
494
  }
419
495
  return snapshots;
420
496
  }
497
+ export async function listWorkspaceSyncBinaryAssetIndex(deps, workspaceId = 'default') {
498
+ if (!deps.workspaceAssetStore)
499
+ return [];
500
+ const assets = deps.workspaceAssetStore.listAllByWorkspace(workspaceId, {
501
+ includeDeleted: false
502
+ });
503
+ return assets
504
+ .filter((asset) => asset.kind === 'image' || asset.kind === 'file')
505
+ .filter((asset) => asset.storageProvider !== 'external')
506
+ .filter((asset) => {
507
+ const storageKey = String(asset.storageKey || '').trim();
508
+ return Boolean(storageKey && deps.normalizeTaskDataRelativePath(storageKey));
509
+ })
510
+ .map((asset) => {
511
+ const storageKey = String(asset.storageKey || '').trim();
512
+ const { primaryLink, effectiveUpdatedAt } = resolveCanonicalAssetSyncLinkState(deps, asset, workspaceId);
513
+ const assetKind = asset.kind === 'image' ? 'image' : 'file';
514
+ return {
515
+ path: storageKey,
516
+ updatedAt: effectiveUpdatedAt,
517
+ assetId: asset.assetId,
518
+ kind: assetKind,
519
+ mimeType: asset.mimeType || 'application/octet-stream',
520
+ referenceNumber: asset.referenceNumber,
521
+ taskId: primaryLink?.taskId || null,
522
+ logicalName: asset.logicalName || null,
523
+ caption: primaryLink?.displayName || null,
524
+ originalFilename: asset.originalFilename || path.basename(storageKey),
525
+ linkRole: primaryLink?.role || (asset.kind === 'image' ? 'image' : 'attachment')
526
+ };
527
+ })
528
+ .filter((entry) => Number.isFinite(Date.parse(entry.updatedAt)) && entry.path.length > 0);
529
+ }
530
+ export async function loadWorkspaceSyncDocumentPayload(deps, basePath, workspaceId, relativePath) {
531
+ const normalizedPath = deps.normalizeWorkspaceSyncDocumentPath(relativePath);
532
+ if (!normalizedPath)
533
+ return null;
534
+ const root = path.resolve(basePath);
535
+ const absolutePath = path.resolve(path.join(root, normalizedPath));
536
+ if (absolutePath !== root && !absolutePath.startsWith(`${root}${path.sep}`))
537
+ return null;
538
+ let content = '';
539
+ let stat = null;
540
+ try {
541
+ if (!fs.existsSync(absolutePath))
542
+ return null;
543
+ stat = fs.statSync(absolutePath);
544
+ if (!stat.isFile())
545
+ return null;
546
+ content = fs.readFileSync(absolutePath, 'utf8');
547
+ }
548
+ catch (error) {
549
+ deps.recordSyncDiagnostic(workspaceId, 'pull', 'Failed to load targeted sync document payload.', {
550
+ source: 'documents.snapshot.loadPayload',
551
+ path: normalizedPath,
552
+ error: String(error?.message || error || '')
553
+ });
554
+ return null;
555
+ }
556
+ const workspaceCache = deps.workspaceDocumentSnapshotCache.get(workspaceId)
557
+ ?? (() => {
558
+ const next = new Map();
559
+ deps.workspaceDocumentSnapshotCache.set(workspaceId, next);
560
+ return next;
561
+ })();
562
+ const cached = workspaceCache.get(normalizedPath);
563
+ const canonicalAsset = deps.workspaceAssetStore?.getByStorageKey(normalizedPath, workspaceId);
564
+ let updatedAt = '';
565
+ if (canonicalAsset?.kind === 'document') {
566
+ updatedAt = resolveCanonicalAssetSyncLinkState(deps, canonicalAsset, workspaceId).effectiveUpdatedAt;
567
+ }
568
+ else {
569
+ const contentHash = createHash('sha256').update(content).digest('hex');
570
+ const existing = deps.core.getDocumentWatermark(workspaceId, normalizedPath);
571
+ if (existing && existing.contentHash === contentHash) {
572
+ updatedAt = existing.updatedAt;
573
+ }
574
+ else {
575
+ updatedAt = stat && Number.isFinite(stat.mtimeMs)
576
+ ? new Date(stat.mtimeMs).toISOString()
577
+ : new Date().toISOString();
578
+ try {
579
+ deps.core.upsertDocumentWatermark(workspaceId, normalizedPath, contentHash, updatedAt);
580
+ }
581
+ catch (error) {
582
+ deps.recordSyncDiagnostic(workspaceId, 'pull', 'Failed to persist document watermark during targeted payload load.', {
583
+ source: 'documents.snapshot.loadPayload.upsertWatermark',
584
+ path: normalizedPath,
585
+ error: String(error?.message || error || '')
586
+ });
587
+ }
588
+ }
589
+ }
590
+ if (cached
591
+ && stat
592
+ && cached.size === Buffer.byteLength(content, 'utf8')
593
+ && (canonicalAsset
594
+ ? cached.updatedAt === updatedAt
595
+ : cached.mtimeMs === stat.mtimeMs && cached.ctimeMs === stat.ctimeMs)) {
596
+ return {
597
+ path: normalizedPath,
598
+ updatedAt: cached.updatedAt,
599
+ content: cached.content,
600
+ ...getCanonicalSyncDocumentMetadata(deps, normalizedPath, workspaceId)
601
+ };
602
+ }
603
+ workspaceCache.set(normalizedPath, {
604
+ path: normalizedPath,
605
+ mtimeMs: stat?.mtimeMs ?? 0,
606
+ ctimeMs: stat?.ctimeMs ?? 0,
607
+ size: Buffer.byteLength(content, 'utf8'),
608
+ updatedAt,
609
+ content
610
+ });
611
+ return {
612
+ path: normalizedPath,
613
+ updatedAt,
614
+ content,
615
+ ...getCanonicalSyncDocumentMetadata(deps, normalizedPath, workspaceId)
616
+ };
617
+ }
618
+ export async function loadWorkspaceSyncBinaryAssetPayload(deps, basePath, workspaceId, assetPath) {
619
+ const normalizedPath = deps.normalizeTaskDataRelativePath(assetPath);
620
+ if (!normalizedPath)
621
+ return null;
622
+ if (!deps.workspaceAssetStore)
623
+ return null;
624
+ const asset = deps.workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
625
+ if (!asset || (asset.kind !== 'image' && asset.kind !== 'file') || asset.storageProvider === 'external') {
626
+ return null;
627
+ }
628
+ const objectStorageClient = deps.getObjectStorageClient();
629
+ let buffer = null;
630
+ try {
631
+ if (asset.storageProvider === 'r2' && objectStorageClient) {
632
+ const object = await objectStorageClient.getObject(normalizedPath);
633
+ buffer = object?.body || null;
634
+ }
635
+ else {
636
+ const root = path.resolve(basePath);
637
+ const absolutePath = path.resolve(root, normalizedPath);
638
+ if (absolutePath !== root && !absolutePath.startsWith(`${root}${path.sep}`))
639
+ return null;
640
+ if (!fs.existsSync(absolutePath))
641
+ return null;
642
+ buffer = fs.readFileSync(absolutePath);
643
+ }
644
+ }
645
+ catch (error) {
646
+ deps.recordSyncDiagnostic(workspaceId, 'pull', 'Failed to load targeted binary asset payload.', {
647
+ source: 'assets.snapshot.loadPayload',
648
+ path: normalizedPath,
649
+ assetId: asset.assetId,
650
+ error: String(error?.message || error || '')
651
+ });
652
+ return null;
653
+ }
654
+ if (!buffer)
655
+ return null;
656
+ const { primaryLink, effectiveUpdatedAt } = resolveCanonicalAssetSyncLinkState(deps, asset, workspaceId);
657
+ return {
658
+ path: normalizedPath,
659
+ updatedAt: effectiveUpdatedAt,
660
+ contentBase64: buffer.toString('base64'),
661
+ assetId: asset.assetId,
662
+ kind: asset.kind,
663
+ mimeType: asset.mimeType || 'application/octet-stream',
664
+ referenceNumber: asset.referenceNumber,
665
+ taskId: primaryLink?.taskId || null,
666
+ logicalName: asset.logicalName || null,
667
+ caption: primaryLink?.displayName || null,
668
+ originalFilename: asset.originalFilename || path.basename(normalizedPath),
669
+ linkRole: primaryLink?.role || (asset.kind === 'image' ? 'image' : 'attachment')
670
+ };
671
+ }
421
672
  export function listWorkspaceSyncBinaryAssetDeletesSnapshot(deps, workspaceId = 'default') {
422
673
  if (!deps.workspaceAssetStore)
423
674
  return [];
@@ -479,6 +730,11 @@ export async function writeWorkspaceSyncDocument(deps, basePath, doc, workspaceI
479
730
  if (deps.workspaceAssetStore) {
480
731
  const existing = deps.workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
481
732
  const effectiveUpdatedAt = updatedAt || new Date().toISOString();
733
+ const incomingReferenceNumber = Number.isFinite(Number(doc.referenceNumber))
734
+ ? Math.max(1, Math.floor(Number(doc.referenceNumber)))
735
+ : null;
736
+ const allowReferenceNumberReassignment = options?.allowReferenceNumberReassignment === true
737
+ || Boolean(existing?.referenceNumber && incomingReferenceNumber && existing.referenceNumber !== incomingReferenceNumber);
482
738
  const originalFilename = String(doc.originalFilename || existing?.originalFilename || path.basename(normalizedPath) || 'document.md').trim() || 'document.md';
483
739
  const asset = deps.workspaceAssetStore.upsertAsset({
484
740
  assetId: String(doc.assetId || existing?.assetId || `asset-${randomUUID().replace(/-/g, '')}`),
@@ -494,9 +750,7 @@ export async function writeWorkspaceSyncDocument(deps, basePath, doc, workspaceI
494
750
  contentSha256: contentHash,
495
751
  sizeBytes: Buffer.byteLength(content, 'utf8'),
496
752
  documentId: String(doc.documentId || existing?.documentId || `doc-${randomUUID().replace(/-/g, '')}`),
497
- referenceNumber: Number.isFinite(Number(doc.referenceNumber))
498
- ? Math.max(1, Math.floor(Number(doc.referenceNumber)))
499
- : existing?.referenceNumber || null,
753
+ referenceNumber: incomingReferenceNumber ?? existing?.referenceNumber ?? null,
500
754
  version: Number.isFinite(Number(doc.version))
501
755
  ? Math.max(1, Math.floor(Number(doc.version)))
502
756
  : Math.max(1, Number(existing?.version || 0) + 1),
@@ -509,7 +763,7 @@ export async function writeWorkspaceSyncDocument(deps, basePath, doc, workspaceI
509
763
  updatedAt: effectiveUpdatedAt,
510
764
  deletedAt: null,
511
765
  purgeAfter: null,
512
- allowReferenceNumberReassignment: options?.allowReferenceNumberReassignment === true
766
+ allowReferenceNumberReassignment
513
767
  });
514
768
  const taskId = String(doc.taskId || '').trim();
515
769
  reconcileWorkspaceSyncLinks(deps, {
@@ -569,6 +823,11 @@ export async function writeWorkspaceSyncBinaryAsset(deps, basePath, asset, works
569
823
  try {
570
824
  const existing = deps.workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
571
825
  const effectiveUpdatedAt = updatedAt || new Date().toISOString();
826
+ const incomingReferenceNumber = Number.isFinite(Number(asset.referenceNumber))
827
+ ? Math.max(1, Math.floor(Number(asset.referenceNumber)))
828
+ : null;
829
+ const allowReferenceNumberReassignment = options?.allowReferenceNumberReassignment === true
830
+ || Boolean(existing?.referenceNumber && incomingReferenceNumber && existing.referenceNumber !== incomingReferenceNumber);
572
831
  const assetRecord = deps.workspaceAssetStore.upsertAsset({
573
832
  assetId: String(asset.assetId || existing?.assetId || `asset-${randomUUID().replace(/-/g, '')}`),
574
833
  workspaceId,
@@ -583,9 +842,7 @@ export async function writeWorkspaceSyncBinaryAsset(deps, basePath, asset, works
583
842
  contentSha256: createHash('sha256').update(buffer).digest('hex'),
584
843
  sizeBytes: buffer.length,
585
844
  documentId: null,
586
- referenceNumber: Number.isFinite(Number(asset.referenceNumber))
587
- ? Math.max(1, Math.floor(Number(asset.referenceNumber)))
588
- : existing?.referenceNumber || null,
845
+ referenceNumber: incomingReferenceNumber ?? existing?.referenceNumber ?? null,
589
846
  version: null,
590
847
  isLatest: true,
591
848
  scanStatus: existing?.scanStatus || 'clean',
@@ -596,7 +853,7 @@ export async function writeWorkspaceSyncBinaryAsset(deps, basePath, asset, works
596
853
  updatedAt: effectiveUpdatedAt,
597
854
  deletedAt: null,
598
855
  purgeAfter: null,
599
- allowReferenceNumberReassignment: options?.allowReferenceNumberReassignment === true
856
+ allowReferenceNumberReassignment
600
857
  });
601
858
  const taskId = String(asset.taskId || '').trim();
602
859
  reconcileWorkspaceSyncLinks(deps, {
@@ -732,8 +989,12 @@ export function createContentSyncState(deps) {
732
989
  hasBinaryAssetSyncMetadataDrift: (workspaceId, asset, options) => hasBinaryAssetSyncMetadataDrift(deps, workspaceId, asset, options),
733
990
  reconcileWorkspaceSyncLinks: (input) => reconcileWorkspaceSyncLinks(deps, input),
734
991
  listWorkspaceSyncDocumentDeletesSnapshot: (workspaceId = 'default') => listWorkspaceSyncDocumentDeletesSnapshot(deps, workspaceId),
992
+ listWorkspaceSyncDocumentIndex: (basePath, workspaceId = 'default') => listWorkspaceSyncDocumentIndex(deps, basePath, workspaceId),
735
993
  listWorkspaceSyncDocumentsSnapshot: (basePath, workspaceId = 'default') => listWorkspaceSyncDocumentsSnapshot(deps, basePath, workspaceId),
994
+ loadWorkspaceSyncDocumentPayload: (basePath, workspaceId, relativePath) => loadWorkspaceSyncDocumentPayload(deps, basePath, workspaceId, relativePath),
995
+ listWorkspaceSyncBinaryAssetIndex: (workspaceId = 'default') => listWorkspaceSyncBinaryAssetIndex(deps, workspaceId),
736
996
  listWorkspaceSyncBinaryAssetsSnapshot: (basePath, workspaceId = 'default') => listWorkspaceSyncBinaryAssetsSnapshot(deps, basePath, workspaceId),
997
+ loadWorkspaceSyncBinaryAssetPayload: (basePath, workspaceId, assetPath) => loadWorkspaceSyncBinaryAssetPayload(deps, basePath, workspaceId, assetPath),
737
998
  listWorkspaceSyncBinaryAssetDeletesSnapshot: (workspaceId = 'default') => listWorkspaceSyncBinaryAssetDeletesSnapshot(deps, workspaceId),
738
999
  writeWorkspaceSyncDocument: (basePath, doc, workspaceId = 'default', options) => writeWorkspaceSyncDocument(deps, basePath, doc, workspaceId, options),
739
1000
  writeWorkspaceSyncBinaryAsset: (basePath, asset, workspaceId = 'default', options) => writeWorkspaceSyncBinaryAsset(deps, basePath, asset, workspaceId, options),
@@ -115,6 +115,7 @@ export interface WorkspacePullSyncResult {
115
115
  pulledArchivedUpserts: boolean;
116
116
  documentDecryptFailures?: string[];
117
117
  emittedEventIds?: string[];
118
+ serverDiagnostics?: Record<string, unknown> | null;
118
119
  }
119
120
  export declare function runWorkspacePullSyncService(input: WorkspacePullSyncInput): Promise<WorkspacePullSyncResult>;
120
121
  export declare function syncUserGlobalSettingsService(input: UserSettingsSyncInput): Promise<UserSettingsSyncResult>;
@@ -243,6 +243,7 @@ export async function runWorkspacePullSyncService(input) {
243
243
  const documentDecryptFailures = new Set();
244
244
  const emittedEventIds = new Set();
245
245
  const pulledAnnotatedAttachmentSessionIds = new Set();
246
+ let lastServerDiagnostics = null;
246
247
  const maxPages = Number.isFinite(Number(input.maxPages)) ? Math.max(1, Math.floor(Number(input.maxPages))) : 20;
247
248
  try {
248
249
  while (hasMore && pages < maxPages) {
@@ -279,11 +280,16 @@ export async function runWorkspacePullSyncService(input) {
279
280
  pulledDeleteTaskIds,
280
281
  pulledActiveUpserts,
281
282
  pulledArchivedUpserts,
282
- documentDecryptFailures: Array.from(documentDecryptFailures)
283
+ documentDecryptFailures: Array.from(documentDecryptFailures),
284
+ serverDiagnostics: null
283
285
  };
284
286
  }
285
287
  const pullData = pullRes.data || {};
286
288
  const changes = Array.isArray(pullData?.changes) ? pullData.changes : [];
289
+ const serverDiagnostics = pullData?.diagnostics && typeof pullData.diagnostics === 'object'
290
+ ? pullData.diagnostics
291
+ : null;
292
+ lastServerDiagnostics = serverDiagnostics;
287
293
  const hasWorkspaceMembersSnapshot = Array.isArray(pullData?.workspaceMembers);
288
294
  const workspaceMembers = hasWorkspaceMembersSnapshot
289
295
  ? pullData.workspaceMembers
@@ -365,7 +371,8 @@ export async function runWorkspacePullSyncService(input) {
365
371
  pulledDeleteTaskIds,
366
372
  pulledActiveUpserts,
367
373
  pulledArchivedUpserts,
368
- documentDecryptFailures: Array.from(documentDecryptFailures)
374
+ documentDecryptFailures: Array.from(documentDecryptFailures),
375
+ serverDiagnostics
369
376
  };
370
377
  }
371
378
  const hasTransientApplyFailure = applyFailures.some((entry) => {
@@ -392,7 +399,8 @@ export async function runWorkspacePullSyncService(input) {
392
399
  pulledActiveUpserts,
393
400
  pulledArchivedUpserts,
394
401
  documentDecryptFailures: Array.from(documentDecryptFailures),
395
- emittedEventIds: Array.from(emittedEventIds)
402
+ emittedEventIds: Array.from(emittedEventIds),
403
+ serverDiagnostics: lastServerDiagnostics
396
404
  };
397
405
  }
398
406
  const applyPayloadData = asObjectRecord(applyResult.data);
@@ -412,7 +420,8 @@ export async function runWorkspacePullSyncService(input) {
412
420
  pulledActiveUpserts,
413
421
  pulledArchivedUpserts,
414
422
  documentDecryptFailures: Array.from(documentDecryptFailures),
415
- emittedEventIds: Array.from(emittedEventIds)
423
+ emittedEventIds: Array.from(emittedEventIds),
424
+ serverDiagnostics: lastServerDiagnostics
416
425
  };
417
426
  }
418
427
  applyMs += (Date.now() - applyStartedAtMs);
@@ -461,7 +470,8 @@ export async function runWorkspacePullSyncService(input) {
461
470
  pulledActiveUpserts,
462
471
  pulledArchivedUpserts,
463
472
  documentDecryptFailures: Array.from(documentDecryptFailures),
464
- emittedEventIds: Array.from(emittedEventIds)
473
+ emittedEventIds: Array.from(emittedEventIds),
474
+ serverDiagnostics: lastServerDiagnostics
465
475
  };
466
476
  }
467
477
  const hasTransientApplyFailure = bootstrapApplyFailures.some((entry) => {
@@ -488,7 +498,8 @@ export async function runWorkspacePullSyncService(input) {
488
498
  pulledActiveUpserts,
489
499
  pulledArchivedUpserts,
490
500
  documentDecryptFailures: Array.from(documentDecryptFailures),
491
- emittedEventIds: Array.from(emittedEventIds)
501
+ emittedEventIds: Array.from(emittedEventIds),
502
+ serverDiagnostics: lastServerDiagnostics
492
503
  };
493
504
  }
494
505
  applyMs += (Date.now() - bootstrapApplyStartedAtMs);
@@ -505,7 +516,8 @@ export async function runWorkspacePullSyncService(input) {
505
516
  pulledActiveUpserts,
506
517
  pulledArchivedUpserts,
507
518
  documentDecryptFailures: Array.from(documentDecryptFailures),
508
- emittedEventIds: Array.from(emittedEventIds)
519
+ emittedEventIds: Array.from(emittedEventIds),
520
+ serverDiagnostics: lastServerDiagnostics
509
521
  };
510
522
  }
511
523
  catch (error) {
@@ -524,7 +536,8 @@ export async function runWorkspacePullSyncService(input) {
524
536
  pulledActiveUpserts,
525
537
  pulledArchivedUpserts,
526
538
  documentDecryptFailures: Array.from(documentDecryptFailures),
527
- emittedEventIds: Array.from(emittedEventIds)
539
+ emittedEventIds: Array.from(emittedEventIds),
540
+ serverDiagnostics: null
528
541
  };
529
542
  }
530
543
  }
@@ -15,6 +15,16 @@ export interface WorkspacePullFeedResult {
15
15
  changes: EncodedWorkspaceSyncChange[];
16
16
  nextCursor: string | null;
17
17
  hasMore: boolean;
18
+ diagnostics: {
19
+ totalEntries: number;
20
+ pageEntries: number;
21
+ documentIndexCount: number;
22
+ assetIndexCount: number;
23
+ indexMs: number;
24
+ hydrationMs: number;
25
+ encodeMs: number;
26
+ buildMs: number;
27
+ };
18
28
  }
19
29
  export declare function buildWorkspacePullFeed(input: {
20
30
  core: SyncCore;