@adhdev/daemon-core 0.9.82-rc.305 → 0.9.82-rc.307
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.
- package/dist/index.js +174 -164
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +174 -164
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-missions.d.ts +9 -0
- package/dist/repo-mesh-types.d.ts +7 -2
- package/package.json +2 -2
- package/src/commands/router.ts +111 -65
- package/src/mesh/mesh-missions.ts +18 -2
- package/src/repo-mesh-types.ts +7 -2
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* never stored — it is derived from queue task statuses (mission_id) at
|
|
12
12
|
* query time.
|
|
13
13
|
*/
|
|
14
|
+
import { type MeshMissionStats } from './mesh-task-stats.js';
|
|
14
15
|
export type MeshMissionStatus = 'active' | 'paused' | 'completed' | 'abandoned';
|
|
15
16
|
export declare const MESH_MISSION_STATUSES: MeshMissionStatus[];
|
|
16
17
|
export interface MeshMissionRecord {
|
|
@@ -36,6 +37,13 @@ export interface MeshMissionTaskAggregate {
|
|
|
36
37
|
}
|
|
37
38
|
export interface MeshMissionSummary extends MeshMissionRecord {
|
|
38
39
|
tasks: MeshMissionTaskAggregate;
|
|
40
|
+
/**
|
|
41
|
+
* Operational rollup (durations / attempts) derived from the ledger via
|
|
42
|
+
* computeMeshMissionStats. Optional: only populated by surfaces that opt in
|
|
43
|
+
* (e.g. mesh_status), since the rollup scans a bounded ledger tail per
|
|
44
|
+
* mission. Absent on the lightweight task-aggregate-only summaries.
|
|
45
|
+
*/
|
|
46
|
+
stats?: MeshMissionStats;
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
49
|
* Slim mission summary for the mesh_status compact (default) surface. Drops the
|
|
@@ -83,6 +91,7 @@ export declare function getActiveMeshMissionSummaries(meshId: string): MeshMissi
|
|
|
83
91
|
export declare function getMeshStatusMissionSummaries(meshId: string, options?: {
|
|
84
92
|
historyLimit?: number;
|
|
85
93
|
verbose?: boolean;
|
|
94
|
+
withStats?: boolean;
|
|
86
95
|
}): MeshMissionSummary[] | MeshMissionSlimSummary[];
|
|
87
96
|
/**
|
|
88
97
|
* Read-only mission listing for the mesh_mission_list tool. Returns summaries
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* IMPORTANT: This file must remain runtime-free (types only).
|
|
12
12
|
*/
|
|
13
13
|
import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
|
|
14
|
-
import type { MeshMissionSummary } from './mesh/mesh-missions.js';
|
|
14
|
+
import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
|
|
15
15
|
export interface RepoMesh {
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
@@ -393,8 +393,13 @@ export interface RepoMeshStatus {
|
|
|
393
393
|
* capped, newest-first slice of completed/abandoned history. Omitted by older
|
|
394
394
|
* daemons — the dashboard must treat this as optional and render an empty
|
|
395
395
|
* state when absent. Split on each entry's `status` for live vs. history.
|
|
396
|
+
*
|
|
397
|
+
* Compact (the default) status calls send the slim shape — `goalPreview` +
|
|
398
|
+
* `goalTruncated` instead of the full `goal` — while verbose sends the full
|
|
399
|
+
* `goal`. Consumers must read `goal ?? goalPreview`. Each entry may also carry
|
|
400
|
+
* an optional `stats` operational rollup (durations / retries).
|
|
396
401
|
*/
|
|
397
|
-
missions?: MeshMissionSummary[];
|
|
402
|
+
missions?: (MeshMissionSummary | MeshMissionSlimSummary)[];
|
|
398
403
|
}
|
|
399
404
|
export type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
|
|
400
405
|
import type { RepoMeshSessionStatus } from '@adhdev/mesh-shared';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.307",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.307",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
package/src/commands/router.ts
CHANGED
|
@@ -1003,10 +1003,66 @@ async function probeRemoteMeshGitStatus(args: {
|
|
|
1003
1003
|
: null;
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
|
+
/** Number of bounded retries after the initial direct-peer git probe attempt. */
|
|
1007
|
+
const MESH_DIRECT_PROBE_MAX_RETRIES = 2;
|
|
1008
|
+
|
|
1009
|
+
function readMeshConnectionState(connection: Record<string, unknown> | null | undefined): string | undefined {
|
|
1010
|
+
return readStringValue((connection as any)?.state);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Probe a remote peer's git_status with a bounded retry budget, but only while
|
|
1015
|
+
* the peer is reported `connected`. A single slow (often TURN-relayed) peer can
|
|
1016
|
+
* exceed one probe window; retrying — with the connection re-checked before each
|
|
1017
|
+
* attempt so we abandon a peer that dropped — recovers it without blocking the
|
|
1018
|
+
* mesh forever. Shared by the bootstrap hydrate path and the per-node render
|
|
1019
|
+
* path so both treat a connected-but-slow peer identically.
|
|
1020
|
+
*
|
|
1021
|
+
* Returns the git status on success, or null if every attempt failed/timed out
|
|
1022
|
+
* (caller decides how to classify). `getConnection` is consulted before each
|
|
1023
|
+
* attempt; a non-`connected` state short-circuits the retry loop (the very first
|
|
1024
|
+
* attempt always runs so a missing connection getter still gets one try).
|
|
1025
|
+
*/
|
|
1026
|
+
async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
1027
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
1028
|
+
daemonId: string;
|
|
1029
|
+
workspace: string;
|
|
1030
|
+
timeoutMs: number;
|
|
1031
|
+
/** Per-attempt timeout for retries (attempts > 0); defaults to timeoutMs. */
|
|
1032
|
+
retryTimeoutMs?: number;
|
|
1033
|
+
getConnection?: (daemonId: string) => Record<string, unknown> | null;
|
|
1034
|
+
onConnection?: (connection: Record<string, unknown>) => void;
|
|
1035
|
+
}): Promise<Record<string, unknown> | null> {
|
|
1036
|
+
for (let attempt = 0; attempt <= MESH_DIRECT_PROBE_MAX_RETRIES; attempt += 1) {
|
|
1037
|
+
if (attempt > 0) {
|
|
1038
|
+
// Re-check liveness before spending another probe window; a peer that
|
|
1039
|
+
// dropped between attempts is not worth retrying.
|
|
1040
|
+
const connection = args.getConnection?.(args.daemonId);
|
|
1041
|
+
if (args.getConnection && readMeshConnectionState(connection) !== 'connected') break;
|
|
1042
|
+
if (connection) args.onConnection?.(connection);
|
|
1043
|
+
// Exponential backoff: 250ms, 500ms before attempts 1 and 2.
|
|
1044
|
+
await new Promise(resolve => setTimeout(resolve, 250 * 2 ** (attempt - 1)));
|
|
1045
|
+
}
|
|
1046
|
+
try {
|
|
1047
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
1048
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1049
|
+
daemonId: args.daemonId,
|
|
1050
|
+
workspace: args.workspace,
|
|
1051
|
+
timeoutMs: attempt === 0 ? args.timeoutMs : (args.retryTimeoutMs ?? args.timeoutMs),
|
|
1052
|
+
});
|
|
1053
|
+
if (remoteGit) return remoteGit;
|
|
1054
|
+
} catch {
|
|
1055
|
+
// Timed out or P2P error — fall through to the next bounded attempt.
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1006
1061
|
async function hydrateInlineMeshDirectTruth(args: {
|
|
1007
1062
|
mesh: any;
|
|
1008
1063
|
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
1009
1064
|
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
1065
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
1010
1066
|
statusInstanceId?: string;
|
|
1011
1067
|
localMachineId?: string;
|
|
1012
1068
|
// Standing-state model: the default (non-refresh) bootstrap load must NOT
|
|
@@ -1103,22 +1159,30 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1103
1159
|
}
|
|
1104
1160
|
|
|
1105
1161
|
peerAttemptedCount += 1;
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1162
|
+
// Bounded retry, gated on the peer staying `connected`: a slow
|
|
1163
|
+
// (TURN-relayed) peer that just exceeds one probe window is recovered
|
|
1164
|
+
// instead of being hard-failed. The connection is re-checked before each
|
|
1165
|
+
// retry so a peer that actually dropped is abandoned promptly.
|
|
1166
|
+
const remoteGit = await probeRemoteMeshGitStatusWithRetry({
|
|
1167
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1168
|
+
daemonId,
|
|
1169
|
+
workspace,
|
|
1170
|
+
timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
|
|
1171
|
+
retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
1172
|
+
getConnection: args.getMeshPeerConnectionStatus,
|
|
1173
|
+
});
|
|
1174
|
+
if (remoteGit) {
|
|
1175
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
1176
|
+
peerConfirmedCount += 1;
|
|
1177
|
+
continue;
|
|
1120
1178
|
}
|
|
1121
1179
|
|
|
1180
|
+
// Invariant: a connected peer that still holds standing git truth is
|
|
1181
|
+
// never classified unavailable (standingGit short-circuited above, so by
|
|
1182
|
+
// here there is no held truth). Only push to unavailable when the peer is
|
|
1183
|
+
// not currently connected, or it is connected but every bounded probe
|
|
1184
|
+
// failed — that is the genuine "connected, no truth, retries exhausted"
|
|
1185
|
+
// case that drives the explicit-refresh hard-fail.
|
|
1122
1186
|
unavailableNodeIds.push(nodeId);
|
|
1123
1187
|
}
|
|
1124
1188
|
|
|
@@ -6543,6 +6607,7 @@ export class DaemonCommandRouter {
|
|
|
6543
6607
|
mesh: meshRecord.mesh,
|
|
6544
6608
|
meshSource: meshRecord.source,
|
|
6545
6609
|
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6610
|
+
getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
|
|
6546
6611
|
statusInstanceId: this.deps.statusInstanceId,
|
|
6547
6612
|
localMachineId: loadConfig().machineId || '',
|
|
6548
6613
|
probeRemotePeers,
|
|
@@ -8432,6 +8497,7 @@ export class DaemonCommandRouter {
|
|
|
8432
8497
|
mesh,
|
|
8433
8498
|
meshSource: meshRecord.source,
|
|
8434
8499
|
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
8500
|
+
getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
|
|
8435
8501
|
statusInstanceId: this.deps.statusInstanceId,
|
|
8436
8502
|
localMachineId,
|
|
8437
8503
|
// Standing-state model: only an explicit refresh fans
|
|
@@ -8640,57 +8706,32 @@ export class DaemonCommandRouter {
|
|
|
8640
8706
|
// per-node git probe. On the default load a peer
|
|
8641
8707
|
// with no held truth falls through to
|
|
8642
8708
|
// gitProbePending below — the graph still renders.
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
8667
|
-
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
8668
|
-
status.connection = refreshedConnection;
|
|
8669
|
-
try {
|
|
8670
|
-
const remoteGit = await probeRemoteMeshGitStatus({
|
|
8671
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
8672
|
-
daemonId,
|
|
8673
|
-
workspace,
|
|
8674
|
-
timeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
8675
|
-
});
|
|
8676
|
-
if (remoteGit) {
|
|
8677
|
-
status.git = remoteGit;
|
|
8678
|
-
status.health = remoteGit.isGitRepo
|
|
8679
|
-
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
8680
|
-
: 'degraded';
|
|
8681
|
-
const connection = readObjectRecord(status.connection);
|
|
8682
|
-
const connectionState = readStringValue(connection.state);
|
|
8683
|
-
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
8684
|
-
if (!connectionReported || connectionState === 'unknown') {
|
|
8685
|
-
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
8686
|
-
}
|
|
8687
|
-
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
8688
|
-
remoteProbeApplied = true;
|
|
8689
|
-
}
|
|
8690
|
-
} catch {
|
|
8691
|
-
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
8692
|
-
}
|
|
8709
|
+
// Bounded retry (shared with the bootstrap hydrate
|
|
8710
|
+
// path), gated on the peer staying connected, so a
|
|
8711
|
+
// slow TURN-relayed peer is recovered rather than
|
|
8712
|
+
// dropped after a single timeout.
|
|
8713
|
+
const remoteGit = await probeRemoteMeshGitStatusWithRetry({
|
|
8714
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
8715
|
+
daemonId,
|
|
8716
|
+
workspace,
|
|
8717
|
+
timeoutMs: MESH_DIRECT_PROBE_TIMEOUT_MS,
|
|
8718
|
+
retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
8719
|
+
getConnection: this.deps.getMeshPeerConnectionStatus,
|
|
8720
|
+
onConnection: connection => { status.connection = connection; },
|
|
8721
|
+
});
|
|
8722
|
+
if (remoteGit) {
|
|
8723
|
+
status.git = remoteGit;
|
|
8724
|
+
status.health = remoteGit.isGitRepo
|
|
8725
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
8726
|
+
: 'degraded';
|
|
8727
|
+
const connection = readObjectRecord(status.connection);
|
|
8728
|
+
const connectionState = readStringValue(connection.state);
|
|
8729
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
8730
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
8731
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
8693
8732
|
}
|
|
8733
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
8734
|
+
remoteProbeApplied = true;
|
|
8694
8735
|
}
|
|
8695
8736
|
}
|
|
8696
8737
|
if (!remoteProbeApplied) {
|
|
@@ -8781,7 +8822,12 @@ export class DaemonCommandRouter {
|
|
|
8781
8822
|
liveSessionRecords: liveMeshSessions,
|
|
8782
8823
|
});
|
|
8783
8824
|
const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
|
|
8784
|
-
|
|
8825
|
+
// withStats opts in to per-mission operational rollups (durations /
|
|
8826
|
+
// retries) for the dashboard mission detail. The rollup scans a
|
|
8827
|
+
// bounded ledger tail per mission, but only over the bounded set
|
|
8828
|
+
// returned here (live + capped history), so the cost stays linear
|
|
8829
|
+
// in visible missions rather than the whole mesh history.
|
|
8830
|
+
const missions = getMeshStatusMissionSummaries(meshId, { verbose: verboseMissions, withStats: true });
|
|
8785
8831
|
const statusResult = {
|
|
8786
8832
|
success: true,
|
|
8787
8833
|
meshId: mesh.id,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { randomUUID } from 'crypto';
|
|
16
16
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
17
17
|
import { getQueue } from './mesh-work-queue.js';
|
|
18
|
+
import { computeMeshMissionStats, type MeshMissionStats } from './mesh-task-stats.js';
|
|
18
19
|
|
|
19
20
|
export type MeshMissionStatus = 'active' | 'paused' | 'completed' | 'abandoned';
|
|
20
21
|
|
|
@@ -45,6 +46,13 @@ export interface MeshMissionTaskAggregate {
|
|
|
45
46
|
|
|
46
47
|
export interface MeshMissionSummary extends MeshMissionRecord {
|
|
47
48
|
tasks: MeshMissionTaskAggregate;
|
|
49
|
+
/**
|
|
50
|
+
* Operational rollup (durations / attempts) derived from the ledger via
|
|
51
|
+
* computeMeshMissionStats. Optional: only populated by surfaces that opt in
|
|
52
|
+
* (e.g. mesh_status), since the rollup scans a bounded ledger tail per
|
|
53
|
+
* mission. Absent on the lightweight task-aggregate-only summaries.
|
|
54
|
+
*/
|
|
55
|
+
stats?: MeshMissionStats;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
/**
|
|
@@ -171,7 +179,7 @@ function slimMissionSummary(summary: MeshMissionSummary): MeshMissionSlimSummary
|
|
|
171
179
|
*/
|
|
172
180
|
export function getMeshStatusMissionSummaries(
|
|
173
181
|
meshId: string,
|
|
174
|
-
options?: { historyLimit?: number; verbose?: boolean },
|
|
182
|
+
options?: { historyLimit?: number; verbose?: boolean; withStats?: boolean },
|
|
175
183
|
): MeshMissionSummary[] | MeshMissionSlimSummary[] {
|
|
176
184
|
const historyLimit = Math.max(0, options?.historyLimit ?? 10);
|
|
177
185
|
const all = getMeshMissions(meshId);
|
|
@@ -180,7 +188,15 @@ export function getMeshStatusMissionSummaries(
|
|
|
180
188
|
.filter(m => m.status === 'completed' || m.status === 'abandoned')
|
|
181
189
|
.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))
|
|
182
190
|
.slice(0, historyLimit);
|
|
183
|
-
|
|
191
|
+
let full = [...live, ...history].map(mission => summarizeMeshMission(meshId, mission));
|
|
192
|
+
// Operational stats (durations / attempts) are an opt-in projection: each
|
|
193
|
+
// mission's rollup scans a bounded ledger tail, so we only compute it for
|
|
194
|
+
// the bounded set we are about to return (live + capped history), not for
|
|
195
|
+
// every mission in the mesh. The dashboard graph opts in so mission detail
|
|
196
|
+
// can show wall-clock / retries without a second round trip.
|
|
197
|
+
if (options?.withStats) {
|
|
198
|
+
full = full.map(summary => ({ ...summary, stats: computeMeshMissionStats(meshId, summary.id) }));
|
|
199
|
+
}
|
|
184
200
|
return options?.verbose ? full : full.map(slimMissionSummary);
|
|
185
201
|
}
|
|
186
202
|
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
|
|
15
|
-
import type { MeshMissionSummary } from './mesh/mesh-missions.js';
|
|
15
|
+
import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
|
|
16
16
|
|
|
17
17
|
// ─── Core Mesh Types ────────────────────────────
|
|
18
18
|
|
|
@@ -486,8 +486,13 @@ export interface RepoMeshStatus {
|
|
|
486
486
|
* capped, newest-first slice of completed/abandoned history. Omitted by older
|
|
487
487
|
* daemons — the dashboard must treat this as optional and render an empty
|
|
488
488
|
* state when absent. Split on each entry's `status` for live vs. history.
|
|
489
|
+
*
|
|
490
|
+
* Compact (the default) status calls send the slim shape — `goalPreview` +
|
|
491
|
+
* `goalTruncated` instead of the full `goal` — while verbose sends the full
|
|
492
|
+
* `goal`. Consumers must read `goal ?? goalPreview`. Each entry may also carry
|
|
493
|
+
* an optional `stats` operational rollup (durations / retries).
|
|
489
494
|
*/
|
|
490
|
-
missions?: MeshMissionSummary[];
|
|
495
|
+
missions?: (MeshMissionSummary | MeshMissionSlimSummary)[];
|
|
491
496
|
}
|
|
492
497
|
|
|
493
498
|
// RepoMeshSessionStatus shape now lives in @adhdev/mesh-shared (shared with
|