@adhdev/mesh-shared 1.0.60-rc.60 → 1.0.60-rc.62

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.
@@ -333,6 +333,16 @@ export interface MissionListQueryRequest {
333
333
  withStats?: boolean;
334
334
  limit?: number;
335
335
  historyIdLimit?: number;
336
+ /**
337
+ * The mesh_status mission projection, computed in the daemon (the MCP no longer
338
+ * opens the daemon's store for it): 'compact' = `getMeshStatusMissionsCompact`
339
+ * (live missions goal-elided + folded history), 'verbose' =
340
+ * `getMeshStatusMissionSummaries({verbose: true})` (live + capped history, full
341
+ * goal, no stats — stats ride one batched `task_stats_query`). Rows then also
342
+ * carry `createdAt`/`updatedAt`/`closeCandidateEmittedAt`. When set, every other
343
+ * option except `historyIdLimit` is ignored. An older daemon rejects the key.
344
+ */
345
+ meshStatusView?: 'compact' | 'verbose';
336
346
  }
337
347
  export interface MeshMissionTaskAggregateWire {
338
348
  total: number;
@@ -366,6 +376,10 @@ export interface MissionListSummaryVerboseWire {
366
376
  stats?: MeshMissionStatsWire;
367
377
  /** H2: absent = no brief attached. */
368
378
  brief?: MissionBriefWire;
379
+ /** `meshStatusView` rows only. */
380
+ createdAt?: string;
381
+ updatedAt?: string;
382
+ closeCandidateEmittedAt?: string | null;
369
383
  }
370
384
  /** Compact (default) mission row. `goalPreview`/`goalTruncated` present, `goal` absent. */
371
385
  export interface MissionListSummarySlimWire {
@@ -380,6 +394,10 @@ export interface MissionListSummarySlimWire {
380
394
  stats?: MeshMissionStatsWire;
381
395
  /** H2: absent = no brief attached. */
382
396
  brief?: MissionBriefWire;
397
+ /** `meshStatusView` rows only. */
398
+ createdAt?: string;
399
+ updatedAt?: string;
400
+ closeCandidateEmittedAt?: string | null;
383
401
  }
384
402
  export type MissionListSummaryWire = MissionListSummaryVerboseWire | MissionListSummarySlimWire;
385
403
  export interface MeshMissionHistoryFoldWire {
@@ -545,9 +563,36 @@ export interface QueueQueryRequest {
545
563
  taskId?: string;
546
564
  /** Project for a VIEW surface: the persisted input envelope → `inputSummary` (summarizeQueueEntryInputForView). */
547
565
  view?: boolean;
566
+ /**
567
+ * Also return the per-status row counts of the WHOLE mesh queue (columns only,
568
+ * no payload parse) — lets a view read only the rows it shows yet report
569
+ * mesh-wide counts. An older daemon rejects the key.
570
+ */
571
+ withCounts?: boolean;
572
+ /** With `withCounts`: also count terminal rows whose `updated_at` is older than this age. */
573
+ historicalOlderThanMs?: number;
574
+ /**
575
+ * Also return `dependencyHeads`: id/status/blockedReason/cancelReason of every
576
+ * row the returned entries list in `dependsOn` that is not itself returned —
577
+ * enough to annotate dependency state without reading the whole queue.
578
+ */
579
+ withDependencyHeads?: boolean;
580
+ }
581
+ /** A dependency row reduced to the fields dependency-state annotation reads. */
582
+ export interface QueueDependencyHeadWire {
583
+ id: string;
584
+ status: string;
585
+ blockedReason?: string;
586
+ cancelReason?: string;
548
587
  }
549
588
  export interface QueueQueryResponse {
550
589
  entries: readonly QueueEntryWire[];
590
+ /** `withCounts`: row count per status over the whole mesh queue. */
591
+ counts?: Record<string, number>;
592
+ /** `withCounts` + `historicalOlderThanMs`: terminal rows older than that age. */
593
+ oldHistoricalCount?: number;
594
+ /** `withDependencyHeads`. */
595
+ dependencyHeads?: readonly QueueDependencyHeadWire[];
551
596
  }
552
597
  export declare function isQueueQueryRequest(value: unknown): value is QueueQueryRequest;
553
598
  export declare function decodeQueueQueryRequest(value: unknown): QueueQueryRequest | null;
@@ -675,7 +720,12 @@ export interface ActiveWorkQueryRequest {
675
720
  includeInputs?: boolean;
676
721
  includeSummary?: boolean;
677
722
  includeSchedulingRuntime?: boolean;
678
- /** The caller's mesh snapshot (required for the scheduling runtime), JSON passthrough. */
723
+ /**
724
+ * The caller's mesh snapshot for the scheduling runtime, JSON passthrough.
725
+ * Optional since the daemon resolves its own mesh record when it is absent;
726
+ * an older daemon rejects `includeSchedulingRuntime` without it (the caller's
727
+ * feature-detect signal to resend with the mesh).
728
+ */
679
729
  mesh?: Record<string, unknown>;
680
730
  }
681
731
  export interface ActiveWorkQueryResponse {
@@ -697,9 +747,17 @@ export interface RecoveryContextQueryRequest {
697
747
  nodeId?: string;
698
748
  sessionId?: string;
699
749
  maxRetries?: number;
750
+ /**
751
+ * Batch: one context per node id, computed over ONE record read (answers
752
+ * `contexts`). Exclusive with `nodeId`/`sessionId`. An older daemon rejects it.
753
+ */
754
+ nodeIds?: readonly string[];
700
755
  }
701
756
  export interface RecoveryContextQueryResponse {
702
- context: Record<string, unknown>;
757
+ /** Single mode (nodeId/sessionId). */
758
+ context?: Record<string, unknown>;
759
+ /** Batch mode (`nodeIds`): context per node id. */
760
+ contexts?: Record<string, Record<string, unknown>>;
703
761
  }
704
762
  export declare function isRecoveryContextQueryRequest(value: unknown): value is RecoveryContextQueryRequest;
705
763
  export declare function decodeRecoveryContextQueryRequest(value: unknown): RecoveryContextQueryRequest | null;
@@ -844,12 +902,20 @@ export interface TaskStatsQueryRequest {
844
902
  tail?: number;
845
903
  /** Also compute `computeMeshMissionStats` — requires `missionId`. */
846
904
  rollup?: boolean;
905
+ /**
906
+ * Batch rollups: `computeMeshMissionStats` for every id, over ONE queue read and
907
+ * ONE record read (answers `missions`, `tasks` empty). Exclusive with
908
+ * `taskIds`/`missionId`. An older daemon rejects the key.
909
+ */
910
+ missionIds?: readonly string[];
847
911
  }
848
912
  export interface TaskStatsQueryResponse {
849
913
  /** `MeshTaskStats[]`, JSON passthrough. */
850
914
  tasks: readonly Record<string, unknown>[];
851
915
  /** `MeshMissionStats`, JSON passthrough; present only when `rollup: true` was requested and satisfiable. */
852
916
  mission?: Record<string, unknown>;
917
+ /** Batch mode: `MeshMissionStats` per requested mission id, JSON passthrough. */
918
+ missions?: Record<string, Record<string, unknown>>;
853
919
  }
854
920
  export declare function isTaskStatsQueryRequest(value: unknown): value is TaskStatsQueryRequest;
855
921
  export declare function decodeTaskStatsQueryRequest(value: unknown): TaskStatsQueryRequest | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/mesh-shared",
3
- "version": "1.0.60-rc.60",
3
+ "version": "1.0.60-rc.62",
4
4
  "description": "ADHDev mesh-shared — pure mesh/git status normalizers shared by daemon-core and web-core",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/turn-ipc.ts CHANGED
@@ -748,6 +748,16 @@ export interface MissionListQueryRequest {
748
748
  withStats?: boolean
749
749
  limit?: number
750
750
  historyIdLimit?: number
751
+ /**
752
+ * The mesh_status mission projection, computed in the daemon (the MCP no longer
753
+ * opens the daemon's store for it): 'compact' = `getMeshStatusMissionsCompact`
754
+ * (live missions goal-elided + folded history), 'verbose' =
755
+ * `getMeshStatusMissionSummaries({verbose: true})` (live + capped history, full
756
+ * goal, no stats — stats ride one batched `task_stats_query`). Rows then also
757
+ * carry `createdAt`/`updatedAt`/`closeCandidateEmittedAt`. When set, every other
758
+ * option except `historyIdLimit` is ignored. An older daemon rejects the key.
759
+ */
760
+ meshStatusView?: 'compact' | 'verbose'
751
761
  }
752
762
 
753
763
  export interface MeshMissionTaskAggregateWire {
@@ -784,6 +794,10 @@ export interface MissionListSummaryVerboseWire {
784
794
  stats?: MeshMissionStatsWire
785
795
  /** H2: absent = no brief attached. */
786
796
  brief?: MissionBriefWire
797
+ /** `meshStatusView` rows only. */
798
+ createdAt?: string
799
+ updatedAt?: string
800
+ closeCandidateEmittedAt?: string | null
787
801
  }
788
802
 
789
803
  /** Compact (default) mission row. `goalPreview`/`goalTruncated` present, `goal` absent. */
@@ -799,6 +813,10 @@ export interface MissionListSummarySlimWire {
799
813
  stats?: MeshMissionStatsWire
800
814
  /** H2: absent = no brief attached. */
801
815
  brief?: MissionBriefWire
816
+ /** `meshStatusView` rows only. */
817
+ createdAt?: string
818
+ updatedAt?: string
819
+ closeCandidateEmittedAt?: string | null
802
820
  }
803
821
 
804
822
  export type MissionListSummaryWire = MissionListSummaryVerboseWire | MissionListSummarySlimWire
@@ -819,7 +837,8 @@ export interface MissionListQueryResponse {
819
837
  }
820
838
 
821
839
  export function isMissionListQueryRequest(value: unknown): value is MissionListQueryRequest {
822
- if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'verbose', 'includeMagi', 'withStats', 'limit', 'historyIdLimit'])) return false
840
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'verbose', 'includeMagi', 'withStats', 'limit', 'historyIdLimit', 'meshStatusView'])) return false
841
+ if (value.meshStatusView !== undefined && value.meshStatusView !== 'compact' && value.meshStatusView !== 'verbose') return false
823
842
  if (value.v !== TURN_IPC_PROTOCOL_VERSION) return false
824
843
  if (!isEvidenceIdentifier(value.meshId)) return false
825
844
  if (value.statuses !== undefined) {
@@ -864,7 +883,7 @@ function isMissionListSummaryWire(value: unknown): value is MissionListSummaryWi
864
883
  const hasGoal = 'goal' in value
865
884
  const hasPreview = 'goalPreview' in value && 'goalTruncated' in value
866
885
  if (hasGoal === hasPreview) return false // exactly one of the two shapes
867
- const baseKeys = ['id', 'meshId', 'title', 'status', 'source', 'tasks', 'stats', 'brief'] as const
886
+ const baseKeys = ['id', 'meshId', 'title', 'status', 'source', 'tasks', 'stats', 'brief', 'createdAt', 'updatedAt', 'closeCandidateEmittedAt'] as const
868
887
  const allowed = hasGoal ? [...baseKeys, 'goal'] : [...baseKeys, 'goalPreview', 'goalTruncated']
869
888
  if (!hasOnlyKeys(value, allowed)) return false
870
889
  if (!isEvidenceIdentifier(value.id) || !isEvidenceIdentifier(value.meshId)) return false
@@ -876,6 +895,9 @@ function isMissionListSummaryWire(value: unknown): value is MissionListSummaryWi
876
895
  if (value.brief !== undefined && !isMissionBriefWire(value.brief)) return false
877
896
  if (hasGoal && typeof value.goal !== 'string') return false
878
897
  if (hasPreview && (typeof value.goalPreview !== 'string' || typeof value.goalTruncated !== 'boolean')) return false
898
+ if (value.createdAt !== undefined && typeof value.createdAt !== 'string') return false
899
+ if (value.updatedAt !== undefined && typeof value.updatedAt !== 'string') return false
900
+ if (value.closeCandidateEmittedAt !== undefined && value.closeCandidateEmittedAt !== null && typeof value.closeCandidateEmittedAt !== 'string') return false
879
901
  return true
880
902
  }
881
903
 
@@ -1288,25 +1310,67 @@ export interface QueueQueryRequest {
1288
1310
  taskId?: string
1289
1311
  /** Project for a VIEW surface: the persisted input envelope → `inputSummary` (summarizeQueueEntryInputForView). */
1290
1312
  view?: boolean
1313
+ /**
1314
+ * Also return the per-status row counts of the WHOLE mesh queue (columns only,
1315
+ * no payload parse) — lets a view read only the rows it shows yet report
1316
+ * mesh-wide counts. An older daemon rejects the key.
1317
+ */
1318
+ withCounts?: boolean
1319
+ /** With `withCounts`: also count terminal rows whose `updated_at` is older than this age. */
1320
+ historicalOlderThanMs?: number
1321
+ /**
1322
+ * Also return `dependencyHeads`: id/status/blockedReason/cancelReason of every
1323
+ * row the returned entries list in `dependsOn` that is not itself returned —
1324
+ * enough to annotate dependency state without reading the whole queue.
1325
+ */
1326
+ withDependencyHeads?: boolean
1327
+ }
1328
+
1329
+ /** A dependency row reduced to the fields dependency-state annotation reads. */
1330
+ export interface QueueDependencyHeadWire {
1331
+ id: string
1332
+ status: string
1333
+ blockedReason?: string
1334
+ cancelReason?: string
1291
1335
  }
1292
1336
 
1293
1337
  export interface QueueQueryResponse {
1294
1338
  entries: readonly QueueEntryWire[]
1339
+ /** `withCounts`: row count per status over the whole mesh queue. */
1340
+ counts?: Record<string, number>
1341
+ /** `withCounts` + `historicalOlderThanMs`: terminal rows older than that age. */
1342
+ oldHistoricalCount?: number
1343
+ /** `withDependencyHeads`. */
1344
+ dependencyHeads?: readonly QueueDependencyHeadWire[]
1295
1345
  }
1296
1346
 
1297
1347
  export function isQueueQueryRequest(value: unknown): value is QueueQueryRequest {
1298
- if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'taskId', 'view'])) return false
1348
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'statuses', 'taskId', 'view', 'withCounts', 'historicalOlderThanMs', 'withDependencyHeads'])) return false
1299
1349
  if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
1300
1350
  if (value.statuses !== undefined && (!Array.isArray(value.statuses) || !value.statuses.every(isEvidenceIdentifier))) return false
1351
+ if (value.historicalOlderThanMs !== undefined && !isNonNegativeInt(value.historicalOlderThanMs)) return false
1301
1352
  return isOptionalId(value.taskId) && isOptionalBoolean(value.view)
1353
+ && isOptionalBoolean(value.withCounts) && isOptionalBoolean(value.withDependencyHeads)
1302
1354
  }
1303
1355
 
1304
1356
  export function decodeQueueQueryRequest(value: unknown): QueueQueryRequest | null {
1305
1357
  return isQueueQueryRequest(value) ? value : null
1306
1358
  }
1307
1359
 
1360
+ function isQueueDependencyHeadWire(value: unknown): value is QueueDependencyHeadWire {
1361
+ return isRecord(value) && hasOnlyKeys(value, ['id', 'status', 'blockedReason', 'cancelReason'])
1362
+ && typeof value.id === 'string' && typeof value.status === 'string'
1363
+ && (value.blockedReason === undefined || typeof value.blockedReason === 'string')
1364
+ && (value.cancelReason === undefined || typeof value.cancelReason === 'string')
1365
+ }
1366
+
1308
1367
  export function isQueueQueryResponse(value: unknown): value is QueueQueryResponse {
1309
- return isRecord(value) && hasOnlyKeys(value, ['entries']) && Array.isArray(value.entries) && value.entries.every(isQueueEntryWire)
1368
+ if (!isRecord(value) || !hasOnlyKeys(value, ['entries', 'counts', 'oldHistoricalCount', 'dependencyHeads'])) return false
1369
+ if (!Array.isArray(value.entries) || !value.entries.every(isQueueEntryWire)) return false
1370
+ if (value.counts !== undefined && (!isRecord(value.counts) || !Object.values(value.counts).every(isNonNegativeInt))) return false
1371
+ if (value.oldHistoricalCount !== undefined && !isNonNegativeInt(value.oldHistoricalCount)) return false
1372
+ if (value.dependencyHeads !== undefined && (!Array.isArray(value.dependencyHeads) || !value.dependencyHeads.every(isQueueDependencyHeadWire))) return false
1373
+ return true
1310
1374
  }
1311
1375
 
1312
1376
  export function decodeQueueQueryResponse(value: unknown): QueueQueryResponse | null {
@@ -1592,7 +1656,12 @@ export interface ActiveWorkQueryRequest {
1592
1656
  includeInputs?: boolean
1593
1657
  includeSummary?: boolean
1594
1658
  includeSchedulingRuntime?: boolean
1595
- /** The caller's mesh snapshot (required for the scheduling runtime), JSON passthrough. */
1659
+ /**
1660
+ * The caller's mesh snapshot for the scheduling runtime, JSON passthrough.
1661
+ * Optional since the daemon resolves its own mesh record when it is absent;
1662
+ * an older daemon rejects `includeSchedulingRuntime` without it (the caller's
1663
+ * feature-detect signal to resend with the mesh).
1664
+ */
1596
1665
  mesh?: Record<string, unknown>
1597
1666
  }
1598
1667
 
@@ -1612,7 +1681,6 @@ export function isActiveWorkQueryRequest(value: unknown): value is ActiveWorkQue
1612
1681
  if (value.nodes !== undefined && !isRecordArray(value.nodes)) return false
1613
1682
  if (value.queue !== undefined && !isRecordArray(value.queue)) return false
1614
1683
  if (value.recordTail !== undefined && !isNonNegativeInt(value.recordTail)) return false
1615
- if (value.includeSchedulingRuntime === true && !isRecord(value.mesh)) return false
1616
1684
  return isOptionalBoolean(value.includeTerminalDirect) && isOptionalBoolean(value.compute) && isOptionalBoolean(value.includeInputs)
1617
1685
  && isOptionalBoolean(value.includeSummary) && isOptionalBoolean(value.includeSchedulingRuntime) && isOptionalRecord(value.mesh)
1618
1686
  }
@@ -1643,27 +1711,46 @@ export interface RecoveryContextQueryRequest {
1643
1711
  nodeId?: string
1644
1712
  sessionId?: string
1645
1713
  maxRetries?: number
1714
+ /**
1715
+ * Batch: one context per node id, computed over ONE record read (answers
1716
+ * `contexts`). Exclusive with `nodeId`/`sessionId`. An older daemon rejects it.
1717
+ */
1718
+ nodeIds?: readonly string[]
1646
1719
  }
1647
1720
 
1648
1721
  export interface RecoveryContextQueryResponse {
1649
- context: Record<string, unknown>
1722
+ /** Single mode (nodeId/sessionId). */
1723
+ context?: Record<string, unknown>
1724
+ /** Batch mode (`nodeIds`): context per node id. */
1725
+ contexts?: Record<string, Record<string, unknown>>
1650
1726
  }
1651
1727
 
1652
1728
  export function isRecoveryContextQueryRequest(value: unknown): value is RecoveryContextQueryRequest {
1653
- return isRecord(value) && hasOnlyKeys(value, ['v', 'meshId', 'nodeId', 'sessionId', 'maxRetries'])
1654
- && value.v === TURN_IPC_PROTOCOL_VERSION && isEvidenceIdentifier(value.meshId)
1655
- && isOptionalId(value.nodeId) && isOptionalId(value.sessionId)
1656
- && (value.maxRetries === undefined || isNonNegativeInt(value.maxRetries))
1657
- && (value.nodeId !== undefined || value.sessionId !== undefined)
1729
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'nodeId', 'sessionId', 'maxRetries', 'nodeIds'])) return false
1730
+ if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
1731
+ if (!isOptionalId(value.nodeId) || !isOptionalId(value.sessionId)) return false
1732
+ if (value.maxRetries !== undefined && !isNonNegativeInt(value.maxRetries)) return false
1733
+ if (value.nodeIds !== undefined) {
1734
+ if (!Array.isArray(value.nodeIds) || value.nodeIds.length === 0 || !value.nodeIds.every(isEvidenceIdentifier)) return false
1735
+ return value.nodeId === undefined && value.sessionId === undefined
1736
+ }
1737
+ return value.nodeId !== undefined || value.sessionId !== undefined
1658
1738
  }
1659
1739
 
1660
1740
  export function decodeRecoveryContextQueryRequest(value: unknown): RecoveryContextQueryRequest | null {
1661
1741
  return isRecoveryContextQueryRequest(value) ? value : null
1662
1742
  }
1663
1743
 
1744
+ function isRecoveryContextWire(value: unknown): boolean {
1745
+ return isRecord(value) && typeof value.consecutiveNodeFailures === 'number'
1746
+ }
1747
+
1664
1748
  export function isRecoveryContextQueryResponse(value: unknown): value is RecoveryContextQueryResponse {
1665
- return isRecord(value) && hasOnlyKeys(value, ['context']) && isRecord(value.context)
1666
- && typeof value.context.consecutiveNodeFailures === 'number'
1749
+ if (!isRecord(value) || !hasOnlyKeys(value, ['context', 'contexts'])) return false
1750
+ if (value.context === undefined && value.contexts === undefined) return false
1751
+ if (value.context !== undefined && !isRecoveryContextWire(value.context)) return false
1752
+ if (value.contexts !== undefined && (!isRecord(value.contexts) || !Object.values(value.contexts).every(isRecoveryContextWire))) return false
1753
+ return true
1667
1754
  }
1668
1755
 
1669
1756
  export function decodeRecoveryContextQueryResponse(value: unknown): RecoveryContextQueryResponse | null {
@@ -1967,6 +2054,12 @@ export interface TaskStatsQueryRequest {
1967
2054
  tail?: number
1968
2055
  /** Also compute `computeMeshMissionStats` — requires `missionId`. */
1969
2056
  rollup?: boolean
2057
+ /**
2058
+ * Batch rollups: `computeMeshMissionStats` for every id, over ONE queue read and
2059
+ * ONE record read (answers `missions`, `tasks` empty). Exclusive with
2060
+ * `taskIds`/`missionId`. An older daemon rejects the key.
2061
+ */
2062
+ missionIds?: readonly string[]
1970
2063
  }
1971
2064
 
1972
2065
  export interface TaskStatsQueryResponse {
@@ -1974,16 +2067,22 @@ export interface TaskStatsQueryResponse {
1974
2067
  tasks: readonly Record<string, unknown>[]
1975
2068
  /** `MeshMissionStats`, JSON passthrough; present only when `rollup: true` was requested and satisfiable. */
1976
2069
  mission?: Record<string, unknown>
2070
+ /** Batch mode: `MeshMissionStats` per requested mission id, JSON passthrough. */
2071
+ missions?: Record<string, Record<string, unknown>>
1977
2072
  }
1978
2073
 
1979
2074
  export function isTaskStatsQueryRequest(value: unknown): value is TaskStatsQueryRequest {
1980
- if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'taskIds', 'missionId', 'tail', 'rollup'])) return false
2075
+ if (!isRecord(value) || !hasOnlyKeys(value, ['v', 'meshId', 'taskIds', 'missionId', 'tail', 'rollup', 'missionIds'])) return false
1981
2076
  if (value.v !== TURN_IPC_PROTOCOL_VERSION || !isEvidenceIdentifier(value.meshId)) return false
1982
2077
  if (value.taskIds !== undefined) {
1983
2078
  if (!Array.isArray(value.taskIds) || value.taskIds.length === 0 || !value.taskIds.every(isEvidenceIdentifier)) return false
1984
2079
  }
1985
2080
  if (value.missionId !== undefined && !isEvidenceIdentifier(value.missionId)) return false
1986
2081
  if (value.tail !== undefined && !isNonNegativeInt(value.tail)) return false
2082
+ if (value.missionIds !== undefined) {
2083
+ if (!Array.isArray(value.missionIds) || value.missionIds.length === 0 || !value.missionIds.every(isEvidenceIdentifier)) return false
2084
+ if (value.taskIds !== undefined || value.missionId !== undefined) return false
2085
+ }
1987
2086
  if (value.rollup === true && value.missionId === undefined) return false
1988
2087
  return isOptionalBoolean(value.rollup)
1989
2088
  }
@@ -1993,7 +2092,8 @@ export function decodeTaskStatsQueryRequest(value: unknown): TaskStatsQueryReque
1993
2092
  }
1994
2093
 
1995
2094
  export function isTaskStatsQueryResponse(value: unknown): value is TaskStatsQueryResponse {
1996
- return isRecord(value) && hasOnlyKeys(value, ['tasks', 'mission']) && isRecordArray(value.tasks) && isOptionalRecord(value.mission)
2095
+ return isRecord(value) && hasOnlyKeys(value, ['tasks', 'mission', 'missions']) && isRecordArray(value.tasks) && isOptionalRecord(value.mission)
2096
+ && (value.missions === undefined || (isRecord(value.missions) && Object.values(value.missions).every(isRecord)))
1997
2097
  }
1998
2098
 
1999
2099
  export function decodeTaskStatsQueryResponse(value: unknown): TaskStatsQueryResponse | null {