@adhdev/daemon-core 0.9.82-rc.310 → 0.9.82-rc.312
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/commands/router.d.ts +19 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1070 -263
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1069 -270
- package/dist/index.mjs.map +1 -1
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +46 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +31 -0
- package/dist/mesh/mesh-runtime-store.d.ts +18 -0
- package/dist/mesh/mesh-work-queue.d.ts +23 -0
- package/dist/providers/spec/cli-adapter.d.ts +34 -3
- package/dist/providers/spec/types.d.ts +36 -0
- package/dist/repo-mesh-types.d.ts +103 -9
- package/package.json +2 -2
- package/src/commands/chat-commands.ts +10 -2
- package/src/commands/router.ts +323 -6
- package/src/commands/stream-commands.ts +8 -0
- package/src/config/chat-history.ts +9 -0
- package/src/config/mesh-config.ts +17 -1
- package/src/index.ts +16 -2
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +220 -0
- package/src/mesh/coordinator-prompt.ts +1 -0
- package/src/mesh/mesh-events-coordinator.ts +165 -9
- package/src/mesh/mesh-runtime-store.ts +52 -0
- package/src/mesh/mesh-work-queue.ts +105 -1
- package/src/providers/spec/cli-adapter.ts +155 -13
- package/src/providers/spec/fsm-driver.ts +14 -1
- package/src/providers/spec/native-history-executor.ts +114 -22
- package/src/providers/spec/types.ts +37 -0
- package/src/repo-mesh-types.ts +134 -9
package/src/commands/router.ts
CHANGED
|
@@ -40,6 +40,8 @@ import { logCommand } from '../logging/command-log.js';
|
|
|
40
40
|
import type { CommandLogEntry } from '../logging/command-log.js';
|
|
41
41
|
import * as yaml from 'js-yaml';
|
|
42
42
|
import { getRecentLogs, LOG_PATH } from '../logging/logger.js';
|
|
43
|
+
import { readDaemonLogTail, MAX_TAIL_BYTES } from '../logging/log-tail-reader.js';
|
|
44
|
+
import { redactLogLines } from '../logging/log-redactor.js';
|
|
43
45
|
import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
|
|
44
46
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
45
47
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
@@ -513,6 +515,21 @@ function readInlineMeshNodeId(node: any): string {
|
|
|
513
515
|
return normalizeMeshNodeId(node) ?? '';
|
|
514
516
|
}
|
|
515
517
|
|
|
518
|
+
// A local worktree node whose workspace directory has been deleted from disk.
|
|
519
|
+
// The worktree was removed (or the machine pruned it) but the node still lingers
|
|
520
|
+
// in the inline mesh cache. Such a node has no live truth to confirm and must
|
|
521
|
+
// never be probed or counted toward direct-peer-truth — doing so blocks the
|
|
522
|
+
// graph with a permanent `direct_peer_truth_unavailable`. Deliberately narrow:
|
|
523
|
+
// it only fires for `isLocalWorktree === true` nodes with a recorded workspace
|
|
524
|
+
// that does not exist. Remote nodes and nodes whose workspace is present on disk
|
|
525
|
+
// are never matched, so a slow remote peer is still classified unavailable.
|
|
526
|
+
function isDeadLocalWorktreeNode(node: any): boolean {
|
|
527
|
+
if (node?.isLocalWorktree !== true) return false;
|
|
528
|
+
const workspace = readStringValue(node?.workspace);
|
|
529
|
+
if (!workspace) return false;
|
|
530
|
+
return !fs.existsSync(workspace);
|
|
531
|
+
}
|
|
532
|
+
|
|
516
533
|
// Boundary normalization: reconcile a node's identity so `id` and `nodeId` both
|
|
517
534
|
// carry the same canonical value (any incoming form — id / nodeId / node_id — is
|
|
518
535
|
// absorbed by normalizeMeshNodeId, and the SQLite `node_id` leak is dropped).
|
|
@@ -985,6 +1002,73 @@ function readMeshTimeoutEnvMs(name: string, defaultMs: number): number {
|
|
|
985
1002
|
// (still under the P2P REQUEST_TIMEOUT of 30s) and made env-overridable.
|
|
986
1003
|
const MESH_DIRECT_PROBE_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_TIMEOUT_MS', 25_000);
|
|
987
1004
|
const MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS', 25_000);
|
|
1005
|
+
// How long a successful per-peer git_status probe stays fresh enough to be
|
|
1006
|
+
// reused instead of issuing another blocking `refreshUpstream:true` fan-out.
|
|
1007
|
+
// A slow (TURN-relayed) peer's probe can take 9-23s, and the dashboard's
|
|
1008
|
+
// auto-retry loop re-fires every few seconds; without this gate every retry
|
|
1009
|
+
// would start a brand new probe storm to the same peer. Within this window the
|
|
1010
|
+
// last successful result is reused so a refresh quiesces instead of looping.
|
|
1011
|
+
// Min-clamped to 1s by readMeshTimeoutEnvMs; raise via env for very slow peers.
|
|
1012
|
+
const MESH_DIRECT_PROBE_REUSE_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_REUSE_MS', 12_000);
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* De-duplicates and rate-limits per-peer git_status probes so a single mesh
|
|
1016
|
+
* refresh — or a burst of refreshes from the dashboard auto-retry loop — cannot
|
|
1017
|
+
* launch a storm of concurrent/back-to-back `refreshUpstream:true` commands to
|
|
1018
|
+
* the same slow peer.
|
|
1019
|
+
*
|
|
1020
|
+
* Two gates, both keyed by `daemonId::workspace`:
|
|
1021
|
+
* - In-flight dedup: a second probe for a key with a probe already running
|
|
1022
|
+
* shares (awaits) the in-flight promise instead of issuing a second command.
|
|
1023
|
+
* - Recently-probed reuse: a successful probe younger than `reuseMs` is reused
|
|
1024
|
+
* verbatim instead of issuing a fresh probe. Failures are NOT cached (so a
|
|
1025
|
+
* transient timeout doesn't pin a peer to "no truth" for the whole window).
|
|
1026
|
+
*
|
|
1027
|
+
* Lives on the router instance so the gate spans separate mesh_status calls,
|
|
1028
|
+
* which is exactly where the refresh storm happens.
|
|
1029
|
+
*/
|
|
1030
|
+
class MeshGitProbeCache {
|
|
1031
|
+
private inflight = new Map<string, Promise<Record<string, unknown> | null>>();
|
|
1032
|
+
private recent = new Map<string, { at: number; value: Record<string, unknown> }>();
|
|
1033
|
+
|
|
1034
|
+
constructor(private readonly reuseMs: number, private readonly now: () => number = Date.now) {}
|
|
1035
|
+
|
|
1036
|
+
private key(daemonId: string, workspace: string): string {
|
|
1037
|
+
return `${daemonId}::${workspace}`;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Run `probe` for this peer, but reuse a fresh recent result or an in-flight
|
|
1042
|
+
* probe for the same key when one is available. `probe` is only invoked when
|
|
1043
|
+
* neither gate is satisfied.
|
|
1044
|
+
*/
|
|
1045
|
+
async probe(
|
|
1046
|
+
daemonId: string,
|
|
1047
|
+
workspace: string,
|
|
1048
|
+
probe: () => Promise<Record<string, unknown> | null>,
|
|
1049
|
+
): Promise<Record<string, unknown> | null> {
|
|
1050
|
+
const key = this.key(daemonId, workspace);
|
|
1051
|
+
const cached = this.recent.get(key);
|
|
1052
|
+
if (cached && this.now() - cached.at < this.reuseMs) {
|
|
1053
|
+
return cached.value;
|
|
1054
|
+
}
|
|
1055
|
+
const existing = this.inflight.get(key);
|
|
1056
|
+
if (existing) return existing;
|
|
1057
|
+
const pending = (async () => {
|
|
1058
|
+
const result = await probe();
|
|
1059
|
+
if (result) this.recent.set(key, { at: this.now(), value: result });
|
|
1060
|
+
return result;
|
|
1061
|
+
})();
|
|
1062
|
+
this.inflight.set(key, pending);
|
|
1063
|
+
try {
|
|
1064
|
+
return await pending;
|
|
1065
|
+
} finally {
|
|
1066
|
+
// Only clear the slot if it is still ours — a later overlapping call
|
|
1067
|
+
// would have reused this very promise, so it is safe to delete here.
|
|
1068
|
+
if (this.inflight.get(key) === pending) this.inflight.delete(key);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
988
1072
|
|
|
989
1073
|
async function probeRemoteMeshGitStatus(args: {
|
|
990
1074
|
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
@@ -1074,6 +1158,10 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1074
1158
|
// probe was attempted. Only an explicit refresh (probeRemotePeers=true)
|
|
1075
1159
|
// performs the fan-out and classifies an unreachable peer as unavailable.
|
|
1076
1160
|
probeRemotePeers: boolean;
|
|
1161
|
+
// Optional shared probe cache: dedups concurrent probes and reuses a
|
|
1162
|
+
// recently-probed peer's result instead of re-issuing a blocking
|
|
1163
|
+
// refreshUpstream probe within the reuse window.
|
|
1164
|
+
probeCache?: MeshGitProbeCache;
|
|
1077
1165
|
}): Promise<{
|
|
1078
1166
|
directEvidenceCount: number;
|
|
1079
1167
|
localConfirmedCount: number;
|
|
@@ -1081,6 +1169,7 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1081
1169
|
peerConfirmedCount: number;
|
|
1082
1170
|
standingEvidenceCount: number;
|
|
1083
1171
|
unavailableNodeIds: string[];
|
|
1172
|
+
deadNodeIds: string[];
|
|
1084
1173
|
}> {
|
|
1085
1174
|
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
1086
1175
|
if (!nodes.length) {
|
|
@@ -1091,6 +1180,7 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1091
1180
|
peerConfirmedCount: 0,
|
|
1092
1181
|
standingEvidenceCount: 0,
|
|
1093
1182
|
unavailableNodeIds: [],
|
|
1183
|
+
deadNodeIds: [],
|
|
1094
1184
|
};
|
|
1095
1185
|
}
|
|
1096
1186
|
|
|
@@ -1105,6 +1195,7 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1105
1195
|
let peerConfirmedCount = 0;
|
|
1106
1196
|
let standingEvidenceCount = 0;
|
|
1107
1197
|
const unavailableNodeIds: string[] = [];
|
|
1198
|
+
const deadNodeIds: string[] = [];
|
|
1108
1199
|
|
|
1109
1200
|
for (const [nodeIndex, node] of nodes.entries()) {
|
|
1110
1201
|
const nodeId = normalizeMeshNodeId(node) || `node_${nodeIndex}`;
|
|
@@ -1116,6 +1207,22 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1116
1207
|
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
1117
1208
|
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
1118
1209
|
|
|
1210
|
+
// A dead local worktree owned by this coordinator (isLocalWorktree, the
|
|
1211
|
+
// node's daemon is us, workspace path gone) has no live truth and cannot
|
|
1212
|
+
// be probed — the directory it would self-probe no longer exists. Exclude
|
|
1213
|
+
// it entirely from direct-peer-truth accounting: do not probe it, do not
|
|
1214
|
+
// attempt it, do not push it to unavailableNodeIds (which would otherwise
|
|
1215
|
+
// wedge the graph in a permanent direct_peer_truth_unavailable). This is
|
|
1216
|
+
// strictly self + isLocalWorktree + absent-path; remote peers and nodes
|
|
1217
|
+
// whose workspace still exists are unaffected and stay classifiable.
|
|
1218
|
+
const isSelfDaemonNode = Boolean(
|
|
1219
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
1220
|
+
);
|
|
1221
|
+
if ((isSelfNode || isSelfDaemonNode) && isDeadLocalWorktreeNode(node)) {
|
|
1222
|
+
deadNodeIds.push(nodeId);
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1119
1226
|
if (!workspace) {
|
|
1120
1227
|
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
1121
1228
|
continue;
|
|
@@ -1162,8 +1269,10 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1162
1269
|
// Bounded retry, gated on the peer staying `connected`: a slow
|
|
1163
1270
|
// (TURN-relayed) peer that just exceeds one probe window is recovered
|
|
1164
1271
|
// instead of being hard-failed. The connection is re-checked before each
|
|
1165
|
-
// retry so a peer that actually dropped is abandoned promptly.
|
|
1166
|
-
|
|
1272
|
+
// retry so a peer that actually dropped is abandoned promptly. Routed
|
|
1273
|
+
// through the shared probe cache so a refresh burst reuses a recent
|
|
1274
|
+
// result / shares an in-flight probe instead of storming the peer.
|
|
1275
|
+
const runProbe = () => probeRemoteMeshGitStatusWithRetry({
|
|
1167
1276
|
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1168
1277
|
daemonId,
|
|
1169
1278
|
workspace,
|
|
@@ -1171,6 +1280,9 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1171
1280
|
retryTimeoutMs: MESH_DIRECT_PROBE_RETRY_TIMEOUT_MS,
|
|
1172
1281
|
getConnection: args.getMeshPeerConnectionStatus,
|
|
1173
1282
|
});
|
|
1283
|
+
const remoteGit = args.probeCache
|
|
1284
|
+
? await args.probeCache.probe(daemonId, workspace, runProbe)
|
|
1285
|
+
: await runProbe();
|
|
1174
1286
|
if (remoteGit) {
|
|
1175
1287
|
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
1176
1288
|
peerConfirmedCount += 1;
|
|
@@ -1193,6 +1305,7 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1193
1305
|
peerConfirmedCount,
|
|
1194
1306
|
standingEvidenceCount,
|
|
1195
1307
|
unavailableNodeIds,
|
|
1308
|
+
deadNodeIds,
|
|
1196
1309
|
};
|
|
1197
1310
|
}
|
|
1198
1311
|
|
|
@@ -3157,8 +3270,21 @@ export class DaemonCommandRouter {
|
|
|
3157
3270
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
3158
3271
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
3159
3272
|
private inlineMeshCache = new Map<string, any>();
|
|
3273
|
+
/** Tombstones for inline mesh nodes removed via remove_mesh_node, keyed by
|
|
3274
|
+
* meshId → set of removed nodeIds. The dashboard keeps echoing the removed
|
|
3275
|
+
* node in the inlineMesh it attaches to every command; without a tombstone,
|
|
3276
|
+
* reconcileInlineMeshCache MERGEs it straight back (resurrection). A
|
|
3277
|
+
* tombstoned node is skipped during reconcile only while its workspace is
|
|
3278
|
+
* absent from disk — a genuine re-registration (same nodeId, workspace back
|
|
3279
|
+
* on disk) clears the tombstone and merges normally, preserving clone
|
|
3280
|
+
* worktree visibility and legitimate node re-creation. */
|
|
3281
|
+
private removedInlineMeshNodeIds = new Map<string, Set<string>>();
|
|
3160
3282
|
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
3161
3283
|
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
|
|
3284
|
+
/** Shared per-peer git_status probe dedup + recently-probed reuse gate.
|
|
3285
|
+
* Spans separate mesh_status/get_mesh calls so the dashboard auto-retry
|
|
3286
|
+
* loop cannot storm a slow peer with back-to-back refreshUpstream probes. */
|
|
3287
|
+
private meshGitProbeCache = new MeshGitProbeCache(MESH_DIRECT_PROBE_REUSE_MS);
|
|
3162
3288
|
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
3163
3289
|
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
3164
3290
|
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
@@ -3190,10 +3316,33 @@ export class DaemonCommandRouter {
|
|
|
3190
3316
|
const unavailableNodeIds = new Set<string>();
|
|
3191
3317
|
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
3192
3318
|
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
3319
|
+
// Dead local worktree nodes (isLocalWorktree, workspace deleted from disk)
|
|
3320
|
+
// carry no live truth and must never gate the aggregate as unavailable.
|
|
3321
|
+
// A cached snapshot built before the worktree was removed can still list
|
|
3322
|
+
// such a node in unavailableNodeIds, which would wedge the graph in a
|
|
3323
|
+
// permanent direct_peer_truth_unavailable; drop them here so the held
|
|
3324
|
+
// standing-state truth for the surviving nodes satisfies the aggregate.
|
|
3325
|
+
const deadNodeIds = new Set<string>();
|
|
3326
|
+
for (const node of mesh.nodes) {
|
|
3327
|
+
if (!isDeadLocalWorktreeNode(node)) continue;
|
|
3328
|
+
const deadId = readInlineMeshNodeId(node);
|
|
3329
|
+
if (deadId) deadNodeIds.add(deadId);
|
|
3330
|
+
}
|
|
3331
|
+
let droppedDeadUnavailable = false;
|
|
3193
3332
|
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
3194
3333
|
const nodeId = readStringValue(entry);
|
|
3195
|
-
if (nodeId)
|
|
3334
|
+
if (!nodeId) continue;
|
|
3335
|
+
if (deadNodeIds.has(nodeId)) {
|
|
3336
|
+
droppedDeadUnavailable = true;
|
|
3337
|
+
continue;
|
|
3338
|
+
}
|
|
3339
|
+
unavailableNodeIds.add(nodeId);
|
|
3196
3340
|
}
|
|
3341
|
+
// Force a rewrite when a dead worktree was filtered out of a previously
|
|
3342
|
+
// built unavailable set, even if no live git was re-hydrated this pass —
|
|
3343
|
+
// otherwise the early-return below would hand back the stale snapshot that
|
|
3344
|
+
// still says direct_peer_truth_unavailable.
|
|
3345
|
+
if (droppedDeadUnavailable) changed = true;
|
|
3197
3346
|
|
|
3198
3347
|
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
3199
3348
|
const nodeId = normalizeMeshNodeId(statusNode);
|
|
@@ -3327,7 +3476,10 @@ export class DaemonCommandRouter {
|
|
|
3327
3476
|
// Save-boundary node-id normalization: reconcile each node's identity so
|
|
3328
3477
|
// `id` and `nodeId` agree before it enters the cache, so reconcile keys
|
|
3329
3478
|
// and the round-trip through the status serializer stay form-stable.
|
|
3330
|
-
const sanitizedInlineMesh =
|
|
3479
|
+
const sanitizedInlineMesh = this.applyInlineMeshNodeTombstones(
|
|
3480
|
+
meshId,
|
|
3481
|
+
sanitizeInlineMesh(normalizeInlineMeshNodeIdentity(inlineMesh as any)),
|
|
3482
|
+
);
|
|
3331
3483
|
const cached = this.inlineMeshCache.get(meshId);
|
|
3332
3484
|
if (cached) {
|
|
3333
3485
|
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
@@ -3348,7 +3500,10 @@ export class DaemonCommandRouter {
|
|
|
3348
3500
|
const cached = this.getCachedInlineMesh(meshId);
|
|
3349
3501
|
if (cached) {
|
|
3350
3502
|
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
3351
|
-
const merged = reconcileInlineMeshCache(
|
|
3503
|
+
const merged = reconcileInlineMeshCache(
|
|
3504
|
+
cached,
|
|
3505
|
+
this.applyInlineMeshNodeTombstones(meshId, inlineMesh as any),
|
|
3506
|
+
);
|
|
3352
3507
|
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(normalizeInlineMeshNodeIdentity(merged)));
|
|
3353
3508
|
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
3354
3509
|
}
|
|
@@ -3408,13 +3563,56 @@ export class DaemonCommandRouter {
|
|
|
3408
3563
|
if (!mesh || !Array.isArray(mesh.nodes)) return false;
|
|
3409
3564
|
const idx = mesh.nodes.findIndex((entry: any) => meshNodeIdMatches(entry, nodeId));
|
|
3410
3565
|
if (idx === -1) return false;
|
|
3566
|
+
const canonicalNodeId = readInlineMeshNodeId(mesh.nodes[idx]) || nodeId;
|
|
3411
3567
|
mesh.nodes.splice(idx, 1);
|
|
3412
3568
|
mesh.updatedAt = new Date().toISOString();
|
|
3413
3569
|
this.inlineMeshCache.set(meshId, mesh);
|
|
3570
|
+
// Tombstone the removed node so the dashboard's stale inlineMesh echo does
|
|
3571
|
+
// not MERGE it back on the next command (see removedInlineMeshNodeIds).
|
|
3572
|
+
this.tombstoneRemovedInlineMeshNode(meshId, canonicalNodeId);
|
|
3573
|
+
if (canonicalNodeId !== nodeId) this.tombstoneRemovedInlineMeshNode(meshId, nodeId);
|
|
3414
3574
|
this.invalidateAggregateMeshStatus(meshId);
|
|
3415
3575
|
return true;
|
|
3416
3576
|
}
|
|
3417
3577
|
|
|
3578
|
+
private tombstoneRemovedInlineMeshNode(meshId: string, nodeId: string): void {
|
|
3579
|
+
if (!nodeId) return;
|
|
3580
|
+
let set = this.removedInlineMeshNodeIds.get(meshId);
|
|
3581
|
+
if (!set) {
|
|
3582
|
+
set = new Set<string>();
|
|
3583
|
+
this.removedInlineMeshNodeIds.set(meshId, set);
|
|
3584
|
+
}
|
|
3585
|
+
set.add(nodeId);
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
/** Filter an incoming inline mesh against this mesh's tombstones before it is
|
|
3589
|
+
* reconciled into the cache. A tombstoned node is dropped only while its
|
|
3590
|
+
* workspace is still absent from disk; if the workspace is back (genuine
|
|
3591
|
+
* re-registration), the tombstone is cleared and the node merges normally. */
|
|
3592
|
+
private applyInlineMeshNodeTombstones(meshId: string, incoming: any): any {
|
|
3593
|
+
const tombstones = this.removedInlineMeshNodeIds.get(meshId);
|
|
3594
|
+
if (!tombstones?.size || !incoming || typeof incoming !== 'object' || !Array.isArray(incoming.nodes)) {
|
|
3595
|
+
return incoming;
|
|
3596
|
+
}
|
|
3597
|
+
let dropped = false;
|
|
3598
|
+
const nodes = incoming.nodes.filter((node: any) => {
|
|
3599
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
3600
|
+
if (!nodeId || !tombstones.has(nodeId)) return true;
|
|
3601
|
+
const workspace = readStringValue(node?.workspace);
|
|
3602
|
+
// Genuine re-registration: same nodeId, workspace back on disk →
|
|
3603
|
+
// clear the tombstone and let the node merge normally.
|
|
3604
|
+
if (workspace && fs.existsSync(workspace)) {
|
|
3605
|
+
tombstones.delete(nodeId);
|
|
3606
|
+
return true;
|
|
3607
|
+
}
|
|
3608
|
+
dropped = true;
|
|
3609
|
+
return false;
|
|
3610
|
+
});
|
|
3611
|
+
if (tombstones.size === 0) this.removedInlineMeshNodeIds.delete(meshId);
|
|
3612
|
+
if (!dropped) return incoming;
|
|
3613
|
+
return { ...incoming, nodes };
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3418
3616
|
private normalizeMeshSessionCleanupMode(value: unknown): RepoMeshSessionCleanupMode {
|
|
3419
3617
|
return value === 'stop'
|
|
3420
3618
|
|| value === 'delete_stopped'
|
|
@@ -6611,6 +6809,7 @@ export class DaemonCommandRouter {
|
|
|
6611
6809
|
statusInstanceId: this.deps.statusInstanceId,
|
|
6612
6810
|
localMachineId: loadConfig().machineId || '',
|
|
6613
6811
|
probeRemotePeers,
|
|
6812
|
+
probeCache: this.meshGitProbeCache,
|
|
6614
6813
|
});
|
|
6615
6814
|
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
6616
6815
|
const sourceOfTruth = {
|
|
@@ -7289,10 +7488,115 @@ export class DaemonCommandRouter {
|
|
|
7289
7488
|
return result as CommandRouterResult;
|
|
7290
7489
|
}
|
|
7291
7490
|
|
|
7491
|
+
case 'get_mesh_node_logs': {
|
|
7492
|
+
// Coordinator-driven remote log fetch: read a (possibly remote)
|
|
7493
|
+
// daemon's recent log tail over P2P instead of opening a session
|
|
7494
|
+
// and grepping the file by hand. Mirrors fast_forward_mesh_node's
|
|
7495
|
+
// forward pattern — resolve the node, forward to its owning daemon
|
|
7496
|
+
// when remote, otherwise read locally. The reply tail is HARD
|
|
7497
|
+
// byte-bounded and secret-redacted before it leaves the machine.
|
|
7498
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7499
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7500
|
+
let nodeDaemonId: string | undefined;
|
|
7501
|
+
if (meshId && nodeId) {
|
|
7502
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7503
|
+
const node = meshRecord?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7504
|
+
nodeDaemonId = typeof node?.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
7505
|
+
}
|
|
7506
|
+
// _meshDirectDispatch prevents re-forwarding (and P2P self-dial)
|
|
7507
|
+
// once the call lands on the owning daemon — that daemon then reads
|
|
7508
|
+
// its own logs even if the stored daemonId uses a legacy form.
|
|
7509
|
+
const selfDaemonId = this.deps.statusInstanceId;
|
|
7510
|
+
const isRemote = nodeDaemonId && selfDaemonId && nodeDaemonId !== selfDaemonId;
|
|
7511
|
+
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
7512
|
+
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'get_mesh_node_logs', {
|
|
7513
|
+
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7514
|
+
_meshDirectDispatch: true,
|
|
7515
|
+
});
|
|
7516
|
+
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7517
|
+
}
|
|
7518
|
+
|
|
7519
|
+
// Local read on the owning daemon.
|
|
7520
|
+
const rawTailBytes = Number(args?.tailBytes);
|
|
7521
|
+
const tail = readDaemonLogTail({
|
|
7522
|
+
date: typeof args?.date === 'string' ? args.date : undefined,
|
|
7523
|
+
tailBytes: Number.isFinite(rawTailBytes) ? Math.min(rawTailBytes, MAX_TAIL_BYTES) : undefined,
|
|
7524
|
+
grep: typeof args?.grep === 'string' ? args.grep : undefined,
|
|
7525
|
+
sinceMs: Number.isFinite(Number(args?.sinceMs)) ? Number(args?.sinceMs) : undefined,
|
|
7526
|
+
});
|
|
7527
|
+
if (!tail.success) {
|
|
7528
|
+
return {
|
|
7529
|
+
success: false,
|
|
7530
|
+
error: tail.error || 'failed to read daemon log tail',
|
|
7531
|
+
nodeId,
|
|
7532
|
+
logPath: tail.logPath,
|
|
7533
|
+
platform: tail.platform,
|
|
7534
|
+
} as CommandRouterResult;
|
|
7535
|
+
}
|
|
7536
|
+
// SECURITY: redact secrets from every line before returning over P2P.
|
|
7537
|
+
const redactedLines = redactLogLines(tail.lines);
|
|
7538
|
+
return {
|
|
7539
|
+
success: true,
|
|
7540
|
+
nodeId,
|
|
7541
|
+
daemonId: selfDaemonId,
|
|
7542
|
+
logPath: tail.logPath,
|
|
7543
|
+
platform: tail.platform,
|
|
7544
|
+
lines: redactedLines,
|
|
7545
|
+
lineCount: redactedLines.length,
|
|
7546
|
+
truncated: tail.truncated,
|
|
7547
|
+
filtered: tail.filtered,
|
|
7548
|
+
bytesReturned: tail.bytesReturned,
|
|
7549
|
+
...(tail.grep ? { grep: tail.grep } : {}),
|
|
7550
|
+
} as CommandRouterResult;
|
|
7551
|
+
}
|
|
7552
|
+
|
|
7292
7553
|
case 'refine_mesh_node': {
|
|
7293
7554
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7294
7555
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7295
7556
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
7557
|
+
|
|
7558
|
+
// Remote forward: a worktree node lives on its OWN daemon's machine, so the
|
|
7559
|
+
// refine (cd into node.workspace, merge → push → cleanup) must run on THAT
|
|
7560
|
+
// daemon — not the coordinator, whose filesystem has no such path. The sibling
|
|
7561
|
+
// fast_forward_mesh_node / clone_mesh_node handlers already forward to the
|
|
7562
|
+
// node's daemon; refine_mesh_node was the gap (the coordinator would cd into a
|
|
7563
|
+
// non-existent local path and fail), so remote-machine worktrees could not be
|
|
7564
|
+
// converged at all. Forward both dry-run (plan reads the worktree git state)
|
|
7565
|
+
// and execute (async merge job) so the same machine that owns the worktree
|
|
7566
|
+
// resolves it.
|
|
7567
|
+
//
|
|
7568
|
+
// coordinatorDaemonId: refine is ASYNC — the completed/failed event is queued
|
|
7569
|
+
// on the executing daemon's pending-events queue scoped to a coordinator id and
|
|
7570
|
+
// recovered by the coordinator's reconcile loop (pullRemoteNodeQueues →
|
|
7571
|
+
// get_pending_mesh_events). Without stamping our own status id, the remote
|
|
7572
|
+
// daemon would fall back to ITS OWN statusInstanceId as the coordinator
|
|
7573
|
+
// (startMeshRefineJob), scoping the terminal event to the wrong inbox where the
|
|
7574
|
+
// real coordinator never pulls it. Stamp the canonical status id (which is in
|
|
7575
|
+
// the coordinator's self-identity set used to scope the remote drain) so the
|
|
7576
|
+
// event routes back here. Preserve any caller-supplied coordinatorDaemonId.
|
|
7577
|
+
//
|
|
7578
|
+
// _meshDirectDispatch prevents re-forwarding (and P2P self-dial) once the call
|
|
7579
|
+
// has landed on the owning daemon — that daemon then executes locally even if
|
|
7580
|
+
// the stored daemonId uses a legacy form that doesn't match its own identity.
|
|
7581
|
+
{
|
|
7582
|
+
const meshRecordForForward = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7583
|
+
const forwardNode = meshRecordForForward?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7584
|
+
const nodeDaemonId = typeof forwardNode?.daemonId === 'string' ? forwardNode.daemonId.trim() : undefined;
|
|
7585
|
+
const selfDaemonId = this.deps.statusInstanceId;
|
|
7586
|
+
const isRemote = nodeDaemonId && selfDaemonId && nodeDaemonId !== selfDaemonId;
|
|
7587
|
+
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
7588
|
+
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
7589
|
+
? args.coordinatorDaemonId.trim()
|
|
7590
|
+
: undefined;
|
|
7591
|
+
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'refine_mesh_node', {
|
|
7592
|
+
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7593
|
+
coordinatorDaemonId: callerCoordinatorDaemonId || selfDaemonId,
|
|
7594
|
+
_meshDirectDispatch: true,
|
|
7595
|
+
});
|
|
7596
|
+
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7597
|
+
}
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7296
7600
|
// Dry-run (plan-only) is the default and stays synchronous: it does no
|
|
7297
7601
|
// validation/merge/push and returns the plan instantly. Only execute=true
|
|
7298
7602
|
// (and not dry_run) goes through the async refine job that actually
|
|
@@ -8492,6 +8796,11 @@ export class DaemonCommandRouter {
|
|
|
8492
8796
|
|
|
8493
8797
|
const localMachineId = loadConfig().machineId || '';
|
|
8494
8798
|
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
8799
|
+
// Shared probe gate for this mesh_status call: the bootstrap
|
|
8800
|
+
// hydrate below and the per-node render loop further down both
|
|
8801
|
+
// probe the same peers — route both through this cache so they
|
|
8802
|
+
// dedup within the call and reuse recent results across calls.
|
|
8803
|
+
const meshGitProbeCache = this.meshGitProbeCache;
|
|
8495
8804
|
const directTruth = requireDirectPeerTruth
|
|
8496
8805
|
? await hydrateInlineMeshDirectTruth({
|
|
8497
8806
|
mesh,
|
|
@@ -8504,6 +8813,7 @@ export class DaemonCommandRouter {
|
|
|
8504
8813
|
// out a blocking peer git probe. Default loads return
|
|
8505
8814
|
// held truth so one slow peer can't block the graph.
|
|
8506
8815
|
probeRemotePeers: refreshRequested,
|
|
8816
|
+
probeCache: meshGitProbeCache,
|
|
8507
8817
|
})
|
|
8508
8818
|
: {
|
|
8509
8819
|
directEvidenceCount: 0,
|
|
@@ -8512,6 +8822,7 @@ export class DaemonCommandRouter {
|
|
|
8512
8822
|
peerConfirmedCount: 0,
|
|
8513
8823
|
standingEvidenceCount: 0,
|
|
8514
8824
|
unavailableNodeIds: [] as string[],
|
|
8825
|
+
deadNodeIds: [] as string[],
|
|
8515
8826
|
};
|
|
8516
8827
|
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
8517
8828
|
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
@@ -8710,7 +9021,7 @@ export class DaemonCommandRouter {
|
|
|
8710
9021
|
// path), gated on the peer staying connected, so a
|
|
8711
9022
|
// slow TURN-relayed peer is recovered rather than
|
|
8712
9023
|
// dropped after a single timeout.
|
|
8713
|
-
const
|
|
9024
|
+
const runNodeProbe = () => probeRemoteMeshGitStatusWithRetry({
|
|
8714
9025
|
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
8715
9026
|
daemonId,
|
|
8716
9027
|
workspace,
|
|
@@ -8719,6 +9030,12 @@ export class DaemonCommandRouter {
|
|
|
8719
9030
|
getConnection: this.deps.getMeshPeerConnectionStatus,
|
|
8720
9031
|
onConnection: connection => { status.connection = connection; },
|
|
8721
9032
|
});
|
|
9033
|
+
// Same shared cache as the bootstrap hydrate path: within one
|
|
9034
|
+
// mesh_status call this dedups the bootstrap probe against this
|
|
9035
|
+
// per-node probe for the same peer, and across calls it reuses a
|
|
9036
|
+
// recent result so the dashboard auto-retry loop can't restart a
|
|
9037
|
+
// fresh refreshUpstream probe seconds apart.
|
|
9038
|
+
const remoteGit = await meshGitProbeCache.probe(daemonId, workspace, runNodeProbe);
|
|
8722
9039
|
if (remoteGit) {
|
|
8723
9040
|
status.git = remoteGit;
|
|
8724
9041
|
status.health = remoteGit.isGitRepo
|
|
@@ -245,6 +245,14 @@ export function normalizeProviderScriptArgs(args: any, scriptName?: string): Rec
|
|
|
245
245
|
function buildControlScriptResult(scriptName: string, payload: any): Record<string, unknown> {
|
|
246
246
|
if (!payload || typeof payload !== 'object') return {};
|
|
247
247
|
|
|
248
|
+
// The spec-driven control adapter (open_picker LIST/SELECT) already
|
|
249
|
+
// produced a structured controlResult by parsing the live screen. Honour
|
|
250
|
+
// it verbatim instead of re-deriving one from legacy payload shapes —
|
|
251
|
+
// otherwise the screen-parsed options/currentValue get clobbered.
|
|
252
|
+
if (payload.controlResult && typeof payload.controlResult === 'object') {
|
|
253
|
+
return { controlResult: payload.controlResult };
|
|
254
|
+
}
|
|
255
|
+
|
|
248
256
|
const legacyListPayload = (() => {
|
|
249
257
|
if (Array.isArray(payload.options)) return payload;
|
|
250
258
|
if (/^listmodels$/i.test(scriptName) && Array.isArray(payload.models)) {
|
|
@@ -172,6 +172,15 @@ function collapseReplayAssistantTurns(messages: HistoryMessage[], historyBehavio
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
if (message.role === 'assistant') {
|
|
175
|
+
// Tool / activity bubbles are distinct events, not replayed prose —
|
|
176
|
+
// collapsing them would erase every tool call and result after the
|
|
177
|
+
// turn's first assistant message. Only consecutive *prose* assistant
|
|
178
|
+
// turns are the replay-dedup target this collapse exists for.
|
|
179
|
+
const isActivity = message.kind === 'tool' || message.kind === 'terminal' || message.kind === 'thought';
|
|
180
|
+
if (isActivity) {
|
|
181
|
+
collapsed.push(message);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
175
184
|
if (sawAssistantSinceLastUser) continue;
|
|
176
185
|
sawAssistantSinceLastUser = true;
|
|
177
186
|
collapsed.push(message);
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
RepoMeshHostMetadata,
|
|
22
22
|
RepoMeshDaemonRole,
|
|
23
23
|
} from '../repo-mesh-types.js';
|
|
24
|
-
import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
|
|
24
|
+
import { DEFAULT_MESH_POLICY, normalizeMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
25
25
|
import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
|
|
26
26
|
|
|
27
27
|
// ─── Persistence ────────────────────────────────
|
|
@@ -118,6 +118,22 @@ function mergeMeshPolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMe
|
|
|
118
118
|
if (!SPAWNED_SESSION_VISIBILITY_MODES.has(String(policy.spawnedSessionVisibility))) {
|
|
119
119
|
policy.spawnedSessionVisibility = 'visible';
|
|
120
120
|
}
|
|
121
|
+
// Load-balancing: normalize the scheduling strategy so an invalid/blank value
|
|
122
|
+
// falls back to 'first_eligible' (strict no-change). Only persist the field when
|
|
123
|
+
// it is explicitly a non-default value to keep existing meshes.json untouched.
|
|
124
|
+
const normalizedStrategy = normalizeMeshSchedulingStrategy(policy.schedulingStrategy);
|
|
125
|
+
if (normalizedStrategy === 'first_eligible') {
|
|
126
|
+
delete policy.schedulingStrategy;
|
|
127
|
+
} else {
|
|
128
|
+
policy.schedulingStrategy = normalizedStrategy;
|
|
129
|
+
}
|
|
130
|
+
// Convergence routing: strict opt-in (default false). Only persist when explicitly
|
|
131
|
+
// enabled so existing meshes.json stays byte-for-byte untouched.
|
|
132
|
+
if (policy.autoConvergeCodeChange === true) {
|
|
133
|
+
policy.autoConvergeCodeChange = true;
|
|
134
|
+
} else {
|
|
135
|
+
delete policy.autoConvergeCodeChange;
|
|
136
|
+
}
|
|
121
137
|
return policy;
|
|
122
138
|
}
|
|
123
139
|
|
package/src/index.ts
CHANGED
|
@@ -122,6 +122,9 @@ export type {
|
|
|
122
122
|
LocalMeshNodeEntry,
|
|
123
123
|
RepoMeshStatus,
|
|
124
124
|
RepoMeshNodeStatus,
|
|
125
|
+
RepoMeshPeerConnectionStatus,
|
|
126
|
+
RepoMeshPeerConnectionState,
|
|
127
|
+
RepoMeshPeerConnectionTransport,
|
|
125
128
|
RepoMeshSessionStatus,
|
|
126
129
|
RepoMeshQueueTask,
|
|
127
130
|
RepoMeshQueueTaskStatus,
|
|
@@ -131,8 +134,19 @@ export type {
|
|
|
131
134
|
RepoMeshLedgerSummaryStatus,
|
|
132
135
|
RepoMeshLedgerStatus,
|
|
133
136
|
MeshAsyncJobLifecycle,
|
|
137
|
+
RepoMeshSchedulingStrategy,
|
|
138
|
+
} from './repo-mesh-types.js';
|
|
139
|
+
export {
|
|
140
|
+
DEFAULT_MESH_POLICY,
|
|
141
|
+
resolveDelegatedWorkerAutoApprove,
|
|
142
|
+
MESH_SCHEDULING_STRATEGIES,
|
|
143
|
+
DEFAULT_MESH_SCHEDULING_STRATEGY,
|
|
144
|
+
normalizeMeshSchedulingStrategy,
|
|
145
|
+
resolveNodeSchedulingPriority,
|
|
146
|
+
MESH_CONVERGE_REFINE_TAG,
|
|
147
|
+
MESH_CONVERGE_FAST_FORWARD_TAG,
|
|
148
|
+
resolveAutoConvergeCodeChange,
|
|
134
149
|
} from './repo-mesh-types.js';
|
|
135
|
-
export { DEFAULT_MESH_POLICY, resolveDelegatedWorkerAutoApprove } from './repo-mesh-types.js';
|
|
136
150
|
|
|
137
151
|
// ── Git Surface ──
|
|
138
152
|
export * from './git/index.js';
|
|
@@ -217,7 +231,7 @@ export { buildMeshLedgerReconciliationEvidence, buildMeshLedgerReplicaEvidence }
|
|
|
217
231
|
export type { AnyLedgerSlice, MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshLedgerReplicaStatus } from './mesh/mesh-ledger-reconciliation.js';
|
|
218
232
|
|
|
219
233
|
// ── Mesh Work Queue (GUPP) ──
|
|
220
|
-
export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, deleteDirectDispatchesByTaskId, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState } from './mesh/mesh-work-queue.js';
|
|
234
|
+
export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, resolveConvergeRequiredTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, deleteDirectDispatchesByTaskId, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState } from './mesh/mesh-work-queue.js';
|
|
221
235
|
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult, DirectDispatchRecord, MeshToolCallRateResult } from './mesh/mesh-work-queue.js';
|
|
222
236
|
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary, classifyStaleDirectForPrune, PRUNABLE_ORPHAN_STALE_REASONS } from './mesh/mesh-active-work.js';
|
|
223
237
|
export type { StaleDirectPruneClassification } from './mesh/mesh-active-work.js';
|