adhdev 0.9.82-rc.4 → 0.9.82-rc.40
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/cli/index.js +2095 -739
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1614 -349
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/vendor/mcp-server/index.js +331 -32
- package/vendor/mcp-server/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adhdev",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.40",
|
|
4
4
|
"description": "ADHDev — Agent Dashboard Hub for Dev. Remote-control AI coding agents from anywhere.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"node": ">=18"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@adhdev/daemon-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/daemon-core": "0.9.82-rc.40",
|
|
51
51
|
"@adhdev/ghostty-vt-node": "*",
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
53
53
|
"@xterm/addon-serialize": "^0.14.0",
|
|
@@ -143,6 +143,10 @@ function isLocalTransport(transport) {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// src/tools/chat-compact.ts
|
|
146
|
+
function isAssistantLike(message) {
|
|
147
|
+
const role = String(message?.role ?? "").toLowerCase();
|
|
148
|
+
return role === "assistant" || role === "agent";
|
|
149
|
+
}
|
|
146
150
|
function messageContent(message) {
|
|
147
151
|
const content = message?.content;
|
|
148
152
|
if (typeof content === "string") return content;
|
|
@@ -161,16 +165,22 @@ function isCoordinatorVisibleMessage(message) {
|
|
|
161
165
|
if (meta?.internal === true || meta?.debug === true || meta?.control === true || meta?.userVisible === false || meta?.user_visible === false) return false;
|
|
162
166
|
return role === "user" || role === "assistant" || role === "agent";
|
|
163
167
|
}
|
|
168
|
+
function buildCompactMessageTail(visibleMessages, opts) {
|
|
169
|
+
const summary = typeof opts.summary === "string" ? opts.summary.trim() : "";
|
|
170
|
+
const shouldOmitSummaryMessage = !!summary && !!opts.finalAssistant && isAssistantLike(opts.finalAssistant) && messageContent(opts.finalAssistant).trim() === summary;
|
|
171
|
+
const sourceMessages = shouldOmitSummaryMessage ? visibleMessages.filter((message) => message !== opts.finalAssistant) : visibleMessages;
|
|
172
|
+
return sourceMessages.slice(-opts.limit);
|
|
173
|
+
}
|
|
164
174
|
function compactChatPayload(payload, opts = {}) {
|
|
165
175
|
const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
166
176
|
const visible = rawMessages.filter(isCoordinatorVisibleMessage);
|
|
167
177
|
const limit = Math.max(1, Math.min(opts.limit ?? 10, 10));
|
|
168
|
-
const messages = visible.slice(-limit);
|
|
169
178
|
const finalAssistant = [...visible].reverse().find((message) => {
|
|
170
179
|
const role = String(message?.role ?? "").toLowerCase();
|
|
171
180
|
return (role === "assistant" || role === "agent") && messageContent(message).trim();
|
|
172
181
|
});
|
|
173
182
|
const summary = typeof payload?.summary === "string" && payload.summary.trim() ? payload.summary.trim() : messageContent(finalAssistant).trim();
|
|
183
|
+
const messages = buildCompactMessageTail(visible, { summary, finalAssistant, limit });
|
|
174
184
|
return {
|
|
175
185
|
success: payload?.success !== false,
|
|
176
186
|
compact: true,
|
|
@@ -246,15 +256,24 @@ async function refreshMeshFromDaemon(ctx) {
|
|
|
246
256
|
const result = await ctx.transport.command("get_mesh", { meshId: ctx.mesh.id });
|
|
247
257
|
if (!result?.success || !Array.isArray(result.mesh?.nodes)) return;
|
|
248
258
|
const refreshedNodes = result.mesh.nodes.filter((n) => n?.id).map((n) => n);
|
|
249
|
-
if (!refreshedNodes.length) return;
|
|
250
259
|
ctx.mesh.nodes.splice(0, ctx.mesh.nodes.length, ...refreshedNodes);
|
|
251
260
|
ctx.mesh.updatedAt = result.mesh.updatedAt ?? ctx.mesh.updatedAt;
|
|
252
261
|
} catch {
|
|
253
262
|
}
|
|
254
263
|
}
|
|
264
|
+
async function syncCoordinatorDaemonMeshCache(ctx) {
|
|
265
|
+
if (!(ctx.transport instanceof IpcTransport)) return;
|
|
266
|
+
try {
|
|
267
|
+
await ctx.transport.command("get_mesh", {
|
|
268
|
+
meshId: ctx.mesh.id,
|
|
269
|
+
inlineMesh: ctx.mesh
|
|
270
|
+
});
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
255
274
|
async function findNodeWithRefresh(ctx, nodeId) {
|
|
256
275
|
const hit = ctx.mesh.nodes.find((n) => n.id === nodeId);
|
|
257
|
-
if (hit) return hit;
|
|
276
|
+
if (hit && !hit.isLocalWorktree) return hit;
|
|
258
277
|
await refreshMeshFromDaemon(ctx);
|
|
259
278
|
const refreshed = ctx.mesh.nodes.find((n) => n.id === nodeId);
|
|
260
279
|
if (!refreshed) throw new Error(`Node '${nodeId}' is not a member of mesh '${ctx.mesh.name}'`);
|
|
@@ -262,7 +281,7 @@ async function findNodeWithRefresh(ctx, nodeId) {
|
|
|
262
281
|
}
|
|
263
282
|
async function findOptionalNodeWithRefresh(ctx, nodeId) {
|
|
264
283
|
const hit = ctx.mesh.nodes.find((n) => n.id === nodeId);
|
|
265
|
-
if (hit) return hit;
|
|
284
|
+
if (hit && !hit.isLocalWorktree) return hit;
|
|
266
285
|
await refreshMeshFromDaemon(ctx);
|
|
267
286
|
return ctx.mesh.nodes.find((n) => n.id === nodeId) ?? null;
|
|
268
287
|
}
|
|
@@ -314,9 +333,26 @@ function buildMissingNodeReadChatRecovery(ctx, args) {
|
|
|
314
333
|
readDebugLocator: readString(lastTerminal?.payload?.readDebugLocator) || readString(lastTerminal?.payload?.debugBundlePath)
|
|
315
334
|
};
|
|
316
335
|
if (finalSummary) {
|
|
336
|
+
if (args.compact === true) {
|
|
337
|
+
return {
|
|
338
|
+
...compactChatPayload({
|
|
339
|
+
success: true,
|
|
340
|
+
status: "idle",
|
|
341
|
+
providerSessionId,
|
|
342
|
+
summary: finalSummary,
|
|
343
|
+
messages: [{ role: "assistant", content: finalSummary, isHistorical: true }]
|
|
344
|
+
}, {
|
|
345
|
+
nodeId: args.node_id,
|
|
346
|
+
sessionId: args.session_id,
|
|
347
|
+
limit: args.tail ?? 10
|
|
348
|
+
}),
|
|
349
|
+
recoveredFromLedger: true,
|
|
350
|
+
ledger
|
|
351
|
+
};
|
|
352
|
+
}
|
|
317
353
|
return {
|
|
318
354
|
success: true,
|
|
319
|
-
compact:
|
|
355
|
+
compact: false,
|
|
320
356
|
recoveredFromLedger: true,
|
|
321
357
|
nodeId: args.node_id,
|
|
322
358
|
sessionId: args.session_id,
|
|
@@ -368,6 +404,14 @@ function buildMissingNodeReadChatRecovery(ctx, args) {
|
|
|
368
404
|
function readSessionRecordId(session) {
|
|
369
405
|
return readString(session?.id) || readString(session?.sessionId) || readString(session?.session_id) || readString(session?.runtimeSessionId) || readString(session?.runtime_session_id) || readString(session?.instanceId) || readString(session?.instance_id);
|
|
370
406
|
}
|
|
407
|
+
function extractStatusMetadataSessions(value) {
|
|
408
|
+
const payload = unwrapCommandPayload(value);
|
|
409
|
+
const status = payload?.status && typeof payload.status === "object" ? payload.status : payload;
|
|
410
|
+
return Array.isArray(status?.sessions) ? status.sessions : [];
|
|
411
|
+
}
|
|
412
|
+
function resolveSessionProviderType(session) {
|
|
413
|
+
return readString(session?.providerType) || readString(session?.cliType) || readString(session?.agentType) || "";
|
|
414
|
+
}
|
|
371
415
|
function addSessionRecord(target, session) {
|
|
372
416
|
if (!session || typeof session !== "object" || isTerminalSessionRecord(session)) return;
|
|
373
417
|
const sessionId = readSessionRecordId(session);
|
|
@@ -580,13 +624,58 @@ function isIdleSessionRecord(session) {
|
|
|
580
624
|
const chatStatus = typeof session?.activeChat?.status === "string" ? session.activeChat.status.toLowerCase() : "";
|
|
581
625
|
return status === "idle" || chatStatus === "waiting_input";
|
|
582
626
|
}
|
|
627
|
+
function isMeshOwnedDelegateSession(session, meshId, nodeId) {
|
|
628
|
+
const settings = session?.settings;
|
|
629
|
+
const sessionMeshId = typeof settings?.meshNodeFor === "string" ? settings.meshNodeFor.trim() : "";
|
|
630
|
+
const coordinatorDaemonId = typeof settings?.meshCoordinatorDaemonId === "string" ? settings.meshCoordinatorDaemonId.trim() : "";
|
|
631
|
+
const sessionNodeId = typeof settings?.meshNodeId === "string" ? settings.meshNodeId.trim() : "";
|
|
632
|
+
if (sessionMeshId !== meshId || !coordinatorDaemonId) return false;
|
|
633
|
+
return !sessionNodeId || sessionNodeId === nodeId;
|
|
634
|
+
}
|
|
583
635
|
function chooseDispatchableSession(sessions, providerType, meshId, nodeId) {
|
|
584
636
|
const live = sessions.filter((session) => !isTerminalSessionRecord(session));
|
|
585
637
|
const matchingProvider = (session) => !providerType || session?.providerType === providerType || session?.cliType === providerType;
|
|
586
638
|
const meshSessions = live.filter(
|
|
587
|
-
(session) => session
|
|
639
|
+
(session) => isMeshOwnedDelegateSession(session, meshId, nodeId)
|
|
588
640
|
);
|
|
589
|
-
return meshSessions.find((session) => isIdleSessionRecord(session) && matchingProvider(session)) || meshSessions.find(matchingProvider) ||
|
|
641
|
+
return meshSessions.find((session) => isIdleSessionRecord(session) && matchingProvider(session)) || meshSessions.find(matchingProvider) || void 0;
|
|
642
|
+
}
|
|
643
|
+
function buildRelayUnsafeRemoteSessionFailure(ctx, node, sessionId, providerType) {
|
|
644
|
+
return {
|
|
645
|
+
success: false,
|
|
646
|
+
recoverable: true,
|
|
647
|
+
code: "mesh_delegate_session_missing_relay_metadata",
|
|
648
|
+
reason: "mesh_delegate_session_missing_relay_metadata",
|
|
649
|
+
transport: "mesh_transport",
|
|
650
|
+
retryRecommended: true,
|
|
651
|
+
meshId: ctx.mesh.id,
|
|
652
|
+
nodeId: node.id,
|
|
653
|
+
daemonId: node.daemonId,
|
|
654
|
+
workspace: node.workspace,
|
|
655
|
+
sessionId,
|
|
656
|
+
...providerType ? { resolvedProviderType: providerType } : {},
|
|
657
|
+
error: `Remote session '${sessionId}' is not relay-safe for mesh '${ctx.mesh.id}': missing meshNodeFor/meshCoordinatorDaemonId metadata, so completion events would not reach the coordinator ledger.`,
|
|
658
|
+
nextAction: `Launch a fresh relay-safe session with mesh_launch_session(node_id: '${node.id}'${providerType ? `, type: '${providerType}'` : ""}) or dispatch without session_id so Repo Mesh can choose a valid delegate session.`,
|
|
659
|
+
noFallbackReason: "Blindly reusing a remote session without mesh relay metadata would silently drop task_completed / generating_completed events."
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function buildMissingCoordinatorDaemonIdFailure(ctx, node, providerType) {
|
|
663
|
+
return {
|
|
664
|
+
success: false,
|
|
665
|
+
recoverable: true,
|
|
666
|
+
code: "mesh_coordinator_daemon_unknown",
|
|
667
|
+
reason: "mesh_coordinator_daemon_unknown",
|
|
668
|
+
transport: "mesh_transport",
|
|
669
|
+
retryRecommended: true,
|
|
670
|
+
meshId: ctx.mesh.id,
|
|
671
|
+
nodeId: node.id,
|
|
672
|
+
daemonId: node.daemonId,
|
|
673
|
+
workspace: node.workspace,
|
|
674
|
+
...providerType ? { resolvedProviderType: providerType } : {},
|
|
675
|
+
error: `Cannot launch a remote mesh delegate for node '${node.id}': coordinator daemon identity is unavailable, so the worker would be unable to relay completion events back to the coordinator.`,
|
|
676
|
+
nextAction: "Retry after the coordinator daemon identity is available (for example from an attached daemon-backed MCP session) so meshCoordinatorDaemonId can be stamped on the worker session.",
|
|
677
|
+
noFallbackReason: "Launching without meshCoordinatorDaemonId would create a worker session that can finish work but cannot emit task_completed / generating_completed back to the coordinator."
|
|
678
|
+
};
|
|
590
679
|
}
|
|
591
680
|
function findNestedPayload(value, predicate) {
|
|
592
681
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -615,12 +704,16 @@ function extractGitDiff(value) {
|
|
|
615
704
|
}
|
|
616
705
|
function extractSubmodules(value, ignorePaths) {
|
|
617
706
|
const payload = unwrapCommandPayload(value);
|
|
618
|
-
const subs = payload?.submodules ?? value?.submodules;
|
|
707
|
+
const subs = payload?.status?.submodules ?? payload?.submodules ?? value?.status?.submodules ?? value?.submodules;
|
|
619
708
|
if (!Array.isArray(subs)) return void 0;
|
|
620
709
|
if (ignorePaths.length === 0) return subs;
|
|
621
710
|
const ignoreSet = new Set(ignorePaths);
|
|
622
711
|
return subs.filter((s) => s?.path && !ignoreSet.has(s.path));
|
|
623
712
|
}
|
|
713
|
+
function assignFullGitSnapshot(entry, status) {
|
|
714
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) return;
|
|
715
|
+
entry.git = status;
|
|
716
|
+
}
|
|
624
717
|
function extractLaunchPayload(value) {
|
|
625
718
|
return findNestedPayload(value, (payload) => Boolean(payload?.sessionId || payload?.id || payload?.runtimeSessionId));
|
|
626
719
|
}
|
|
@@ -745,20 +838,63 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
745
838
|
let sessionId = args.session_id?.trim() || "";
|
|
746
839
|
const providerPriorityList = Array.isArray(node.policy?.providerPriority) ? node.policy.providerPriority : [];
|
|
747
840
|
let resolvedProviderType = args.providerType?.trim() || providerPriorityList[0] || "";
|
|
748
|
-
if (!sessionId) {
|
|
841
|
+
if (!sessionId || args.session_id) {
|
|
749
842
|
try {
|
|
750
843
|
const relayResult = await transport.meshCommand(daemonId, "get_status_metadata", {});
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
844
|
+
const sessions = extractStatusMetadataSessions(relayResult);
|
|
845
|
+
if (sessionId) {
|
|
846
|
+
const explicitSession = sessions.find((session) => readSessionRecordId(session) === sessionId);
|
|
847
|
+
if (!explicitSession) {
|
|
848
|
+
return {
|
|
849
|
+
success: false,
|
|
850
|
+
recoverable: true,
|
|
851
|
+
code: "mesh_target_session_not_found",
|
|
852
|
+
reason: "mesh_target_session_not_found",
|
|
853
|
+
transport: "mesh_transport",
|
|
854
|
+
retryRecommended: true,
|
|
855
|
+
meshId: ctx.mesh.id,
|
|
856
|
+
nodeId: node.id,
|
|
857
|
+
daemonId,
|
|
858
|
+
workspace: node.workspace,
|
|
859
|
+
sessionId,
|
|
860
|
+
...resolvedProviderType ? { resolvedProviderType } : {},
|
|
861
|
+
error: `Remote session '${sessionId}' is not present in the live status for node '${node.id}'.`,
|
|
862
|
+
nextAction: `Launch a fresh session with mesh_launch_session(node_id: '${node.id}'${resolvedProviderType ? `, type: '${resolvedProviderType}'` : ""}) or retry without session_id so Repo Mesh can target a live delegate session.`
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
if (!isMeshOwnedDelegateSession(explicitSession, ctx.mesh.id, node.id)) {
|
|
866
|
+
return buildRelayUnsafeRemoteSessionFailure(
|
|
867
|
+
ctx,
|
|
868
|
+
node,
|
|
869
|
+
sessionId,
|
|
870
|
+
resolvedProviderType || resolveSessionProviderType(explicitSession) || void 0
|
|
871
|
+
);
|
|
872
|
+
}
|
|
757
873
|
if (!resolvedProviderType) {
|
|
758
|
-
resolvedProviderType =
|
|
874
|
+
resolvedProviderType = resolveSessionProviderType(explicitSession);
|
|
875
|
+
}
|
|
876
|
+
} else {
|
|
877
|
+
const targetSession = chooseDispatchableSession(sessions, resolvedProviderType, ctx.mesh.id, node.id);
|
|
878
|
+
if (targetSession?.id || targetSession?.sessionId) {
|
|
879
|
+
sessionId = targetSession.id || targetSession.sessionId;
|
|
880
|
+
if (!resolvedProviderType) {
|
|
881
|
+
resolvedProviderType = resolveSessionProviderType(targetSession);
|
|
882
|
+
}
|
|
759
883
|
}
|
|
760
884
|
}
|
|
761
885
|
} catch (e) {
|
|
886
|
+
if (sessionId) {
|
|
887
|
+
return {
|
|
888
|
+
...buildCoordinatorP2pRelayFailure(e, {
|
|
889
|
+
command: "get_status_metadata",
|
|
890
|
+
targetDaemonId: daemonId,
|
|
891
|
+
nodeId: node.id,
|
|
892
|
+
sessionId
|
|
893
|
+
}),
|
|
894
|
+
success: false,
|
|
895
|
+
error: `Cannot verify remote session '${sessionId}' before dispatch: ${e?.message || String(e)}`
|
|
896
|
+
};
|
|
897
|
+
}
|
|
762
898
|
}
|
|
763
899
|
}
|
|
764
900
|
if (!resolvedProviderType) {
|
|
@@ -872,6 +1008,10 @@ function summarizeRelatedRepoStatus(repo, status) {
|
|
|
872
1008
|
workspace: repo.workspace,
|
|
873
1009
|
isGitRepo: status?.isGitRepo === true,
|
|
874
1010
|
branch: status?.branch ?? null,
|
|
1011
|
+
upstream: status?.upstream ?? null,
|
|
1012
|
+
upstreamStatus: typeof status?.upstreamStatus === "string" ? status.upstreamStatus : status?.upstream ? "unchecked" : "no_upstream",
|
|
1013
|
+
upstreamFetchedAt: Number.isFinite(Number(status?.upstreamFetchedAt)) ? Number(status.upstreamFetchedAt) : null,
|
|
1014
|
+
upstreamFetchError: typeof status?.upstreamFetchError === "string" ? status.upstreamFetchError : null,
|
|
875
1015
|
ahead: Number.isFinite(Number(status?.ahead)) ? Number(status.ahead) : 0,
|
|
876
1016
|
behind: Number.isFinite(Number(status?.behind)) ? Number(status.behind) : 0,
|
|
877
1017
|
dirty,
|
|
@@ -888,7 +1028,7 @@ async function collectRelatedRepoStatuses(ctx, node) {
|
|
|
888
1028
|
const results = [];
|
|
889
1029
|
for (const repo of relatedRepos) {
|
|
890
1030
|
try {
|
|
891
|
-
const statusResult = !isLocalTransport(ctx.transport) && node.daemonId ? await ctx.transport.gitStatus(node.daemonId, repo.workspace, false) : await commandForNode(ctx, node, "git_status", { workspace: repo.workspace });
|
|
1031
|
+
const statusResult = !isLocalTransport(ctx.transport) && node.daemonId ? await ctx.transport.gitStatus(node.daemonId, repo.workspace, false, true) : await commandForNode(ctx, node, "git_status", { workspace: repo.workspace, refreshUpstream: true });
|
|
892
1032
|
const status = extractGitStatus(statusResult);
|
|
893
1033
|
results.push(summarizeRelatedRepoStatus(repo, status));
|
|
894
1034
|
} catch (e) {
|
|
@@ -936,11 +1076,13 @@ function buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges) {
|
|
|
936
1076
|
const ahead = readNumeric(status?.ahead);
|
|
937
1077
|
const behind = readNumeric(status?.behind);
|
|
938
1078
|
const upstream = readString(status?.upstream) ?? null;
|
|
1079
|
+
const upstreamStatus = readString(status?.upstreamStatus) ?? (upstream ? "unchecked" : "no_upstream");
|
|
939
1080
|
const hasConflicts = status?.hasConflicts === true || Array.isArray(status?.conflictFiles) && status.conflictFiles.length > 0;
|
|
940
1081
|
const base = {
|
|
941
1082
|
defaultBranch,
|
|
942
1083
|
branch,
|
|
943
1084
|
upstream,
|
|
1085
|
+
upstreamStatus,
|
|
944
1086
|
ahead,
|
|
945
1087
|
behind,
|
|
946
1088
|
isWorktree: node.isLocalWorktree === true,
|
|
@@ -974,6 +1116,15 @@ function buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges) {
|
|
|
974
1116
|
};
|
|
975
1117
|
}
|
|
976
1118
|
if (branch === defaultBranch) {
|
|
1119
|
+
if (upstream && upstreamStatus !== "fresh") {
|
|
1120
|
+
return {
|
|
1121
|
+
...base,
|
|
1122
|
+
status: "blocked_review",
|
|
1123
|
+
needsConvergence: true,
|
|
1124
|
+
reason: "default_branch_upstream_unverified",
|
|
1125
|
+
nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${node.id}'.`
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
977
1128
|
if (ahead > 0 || behind > 0) {
|
|
978
1129
|
return {
|
|
979
1130
|
...base,
|
|
@@ -1000,6 +1151,15 @@ function buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges) {
|
|
|
1000
1151
|
nextStep: `Run mesh_refine_node(node_id: "${node.id}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`
|
|
1001
1152
|
};
|
|
1002
1153
|
}
|
|
1154
|
+
if (upstream && upstreamStatus !== "fresh") {
|
|
1155
|
+
return {
|
|
1156
|
+
...base,
|
|
1157
|
+
status: "blocked_review",
|
|
1158
|
+
needsConvergence: true,
|
|
1159
|
+
reason: "feature_branch_upstream_unverified",
|
|
1160
|
+
nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1003
1163
|
if (!upstream || ahead > 0 || behind > 0) {
|
|
1004
1164
|
return {
|
|
1005
1165
|
...base,
|
|
@@ -1043,6 +1203,71 @@ async function commandForNode(ctx, node, command, args = {}) {
|
|
|
1043
1203
|
}
|
|
1044
1204
|
throw new Error(`Command '${command}' requires daemon IPC/local transport for node '${node.id}'`);
|
|
1045
1205
|
}
|
|
1206
|
+
function normalizePendingMeshCoordinatorEvents(value) {
|
|
1207
|
+
const payload = unwrapCommandPayload(value);
|
|
1208
|
+
const events = Array.isArray(payload?.events) ? payload.events : Array.isArray(value?.events) ? value.events : [];
|
|
1209
|
+
return events.filter((event) => event && typeof event === "object");
|
|
1210
|
+
}
|
|
1211
|
+
function buildMeshForwardPayloadFromPendingEvent(event) {
|
|
1212
|
+
const metadataEvent = event?.metadataEvent && typeof event.metadataEvent === "object" ? event.metadataEvent : {};
|
|
1213
|
+
return {
|
|
1214
|
+
event: readString(event?.event),
|
|
1215
|
+
meshId: readString(event?.meshId),
|
|
1216
|
+
nodeId: readString(event?.nodeId) || readString(metadataEvent.meshNodeId),
|
|
1217
|
+
workspace: readString(event?.workspace) || readString(metadataEvent.workspace),
|
|
1218
|
+
targetSessionId: readString(metadataEvent.targetSessionId) || readString(metadataEvent.sessionId) || readString(metadataEvent.instanceId),
|
|
1219
|
+
providerType: readString(metadataEvent.providerType),
|
|
1220
|
+
providerSessionId: readString(metadataEvent.providerSessionId),
|
|
1221
|
+
finalSummary: readString(metadataEvent.finalSummary) || readString(metadataEvent.summary),
|
|
1222
|
+
...metadataEvent.intentional === true ? { intentional: true } : {},
|
|
1223
|
+
...metadataEvent.intentionalStop === true ? { intentionalStop: true } : {},
|
|
1224
|
+
...metadataEvent.operatorCleanup === true ? { operatorCleanup: true } : {},
|
|
1225
|
+
...readString(metadataEvent.reason) ? { reason: readString(metadataEvent.reason) } : {},
|
|
1226
|
+
...readString(metadataEvent.stopReason) ? { stopReason: readString(metadataEvent.stopReason) } : {},
|
|
1227
|
+
...readString(metadataEvent.cleanupReason) ? { cleanupReason: readString(metadataEvent.cleanupReason) } : {},
|
|
1228
|
+
...readString(metadataEvent.source) ? { source: readString(metadataEvent.source) } : {}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
async function drainCoordinatorPendingEvents(ctx, opts) {
|
|
1232
|
+
const requestedNodeIds = opts?.nodeIds?.length ? new Set(opts.nodeIds) : null;
|
|
1233
|
+
const matchesCurrentMesh = (event) => readString(event?.meshId) === ctx.mesh.id;
|
|
1234
|
+
if (ctx.transport instanceof IpcTransport) {
|
|
1235
|
+
const surfacedEvents = [];
|
|
1236
|
+
try {
|
|
1237
|
+
surfacedEvents.push(
|
|
1238
|
+
...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", { meshId: ctx.mesh.id })).filter(matchesCurrentMesh)
|
|
1239
|
+
);
|
|
1240
|
+
} catch {
|
|
1241
|
+
}
|
|
1242
|
+
for (const node of ctx.mesh.nodes) {
|
|
1243
|
+
if (!node.daemonId || isLocalControlPlaneNode(ctx, node)) continue;
|
|
1244
|
+
if (requestedNodeIds && !requestedNodeIds.has(node.id)) continue;
|
|
1245
|
+
try {
|
|
1246
|
+
const remoteEvents = normalizePendingMeshCoordinatorEvents(
|
|
1247
|
+
await ctx.transport.meshCommand(node.daemonId, "get_pending_mesh_events", { meshId: ctx.mesh.id })
|
|
1248
|
+
).filter(matchesCurrentMesh);
|
|
1249
|
+
if (remoteEvents.length === 0) continue;
|
|
1250
|
+
for (const event of remoteEvents) {
|
|
1251
|
+
const payload = buildMeshForwardPayloadFromPendingEvent(event);
|
|
1252
|
+
if (!payload.event || !payload.meshId) continue;
|
|
1253
|
+
await ctx.transport.command("mesh_forward_event", payload);
|
|
1254
|
+
}
|
|
1255
|
+
} catch {
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
try {
|
|
1259
|
+
surfacedEvents.push(
|
|
1260
|
+
...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", { meshId: ctx.mesh.id })).filter(matchesCurrentMesh)
|
|
1261
|
+
);
|
|
1262
|
+
} catch {
|
|
1263
|
+
}
|
|
1264
|
+
return surfacedEvents;
|
|
1265
|
+
}
|
|
1266
|
+
if (isLocalTransport(ctx.transport)) {
|
|
1267
|
+
return (0, import_daemon_core.drainPendingMeshCoordinatorEvents)(ctx.mesh.id).filter(matchesCurrentMesh);
|
|
1268
|
+
}
|
|
1269
|
+
return [];
|
|
1270
|
+
}
|
|
1046
1271
|
function isP2pTransportUnavailableError(error) {
|
|
1047
1272
|
return (0, import_daemon_core.isP2pRelayTransportFailure)(error);
|
|
1048
1273
|
}
|
|
@@ -1344,11 +1569,12 @@ async function meshStatus(ctx) {
|
|
|
1344
1569
|
};
|
|
1345
1570
|
try {
|
|
1346
1571
|
if (!isLocalTransport(transport) && node.daemonId) {
|
|
1347
|
-
const result = await transport.gitStatus(node.daemonId, node.workspace, false);
|
|
1572
|
+
const result = await transport.gitStatus(node.daemonId, node.workspace, false, true);
|
|
1348
1573
|
const status = extractGitStatus(result);
|
|
1349
1574
|
const uncommittedChanges = countUncommittedChanges(status);
|
|
1350
1575
|
const dirty = isGitStatusDirty(status);
|
|
1351
1576
|
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
1577
|
+
assignFullGitSnapshot(entry, status);
|
|
1352
1578
|
entry.branch = status?.branch;
|
|
1353
1579
|
entry.isDirty = dirty;
|
|
1354
1580
|
entry.uncommittedChanges = uncommittedChanges;
|
|
@@ -1362,6 +1588,7 @@ async function meshStatus(ctx) {
|
|
|
1362
1588
|
const autoDiscover = node.policy?.autoDiscoverSubmodules !== false;
|
|
1363
1589
|
const statusResult = await commandForNode(ctx, node, "git_status", {
|
|
1364
1590
|
workspace: node.workspace,
|
|
1591
|
+
refreshUpstream: true,
|
|
1365
1592
|
includeSubmodules: autoDiscover,
|
|
1366
1593
|
submoduleIgnorePaths: node.policy?.submoduleIgnorePaths || void 0
|
|
1367
1594
|
});
|
|
@@ -1369,6 +1596,7 @@ async function meshStatus(ctx) {
|
|
|
1369
1596
|
const uncommittedChanges = countUncommittedChanges(status);
|
|
1370
1597
|
const dirty = isGitStatusDirty(status);
|
|
1371
1598
|
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
1599
|
+
assignFullGitSnapshot(entry, status);
|
|
1372
1600
|
entry.branch = status?.branch;
|
|
1373
1601
|
entry.isDirty = dirty;
|
|
1374
1602
|
entry.uncommittedChanges = uncommittedChanges;
|
|
@@ -1452,6 +1680,11 @@ async function meshStatus(ctx) {
|
|
|
1452
1680
|
repoIdentity: mesh.repoIdentity,
|
|
1453
1681
|
policy: mesh.policy,
|
|
1454
1682
|
refreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1683
|
+
sourceOfTruth: {
|
|
1684
|
+
membership: "coordinator_daemon_live_mesh",
|
|
1685
|
+
currentStatus: "live_git_and_session_probes",
|
|
1686
|
+
historicalEvidenceOnly: ["recoveryHints", "ledgerSummary"]
|
|
1687
|
+
},
|
|
1455
1688
|
nodes: results,
|
|
1456
1689
|
branchConvergenceSummary: summarizeBranchConvergence(results)
|
|
1457
1690
|
};
|
|
@@ -1460,13 +1693,7 @@ async function meshStatus(ctx) {
|
|
|
1460
1693
|
} catch {
|
|
1461
1694
|
}
|
|
1462
1695
|
try {
|
|
1463
|
-
|
|
1464
|
-
if (ctx.transport instanceof IpcTransport) {
|
|
1465
|
-
const eventsResult = await ctx.transport.command("get_pending_mesh_events", {});
|
|
1466
|
-
pendingEvents = Array.isArray(eventsResult?.events) ? eventsResult.events : [];
|
|
1467
|
-
} else if (isLocalTransport(ctx.transport)) {
|
|
1468
|
-
pendingEvents = (0, import_daemon_core.drainPendingMeshCoordinatorEvents)();
|
|
1469
|
-
}
|
|
1696
|
+
const pendingEvents = await drainCoordinatorPendingEvents(ctx);
|
|
1470
1697
|
if (pendingEvents.length > 0) {
|
|
1471
1698
|
response.pendingCoordinatorEvents = pendingEvents;
|
|
1472
1699
|
}
|
|
@@ -1476,6 +1703,7 @@ async function meshStatus(ctx) {
|
|
|
1476
1703
|
}
|
|
1477
1704
|
async function meshTaskHistory(ctx, args) {
|
|
1478
1705
|
const { mesh } = ctx;
|
|
1706
|
+
await drainCoordinatorPendingEvents(ctx);
|
|
1479
1707
|
const tail = typeof args.tail === "number" && args.tail > 0 ? args.tail : 20;
|
|
1480
1708
|
const kind = typeof args.kind === "string" && args.kind.trim() ? [args.kind.trim()] : void 0;
|
|
1481
1709
|
const entries = (0, import_daemon_core.readLedgerEntries)(mesh.id, { tail, kind });
|
|
@@ -1767,9 +1995,52 @@ async function meshSendTask(ctx, args) {
|
|
|
1767
1995
|
}
|
|
1768
1996
|
if (args.session_id && isLocalTransport(ctx.transport)) {
|
|
1769
1997
|
const cached = meshSessionProviderMetadata.get(meshSessionCacheKey(args.node_id, args.session_id));
|
|
1998
|
+
let resolvedProviderType = cached?.providerType || "";
|
|
1999
|
+
if (!resolvedProviderType) {
|
|
2000
|
+
const statusResult = await commandForNode(ctx, node, "get_status_metadata", {});
|
|
2001
|
+
const sessions = extractStatusMetadataSessions(statusResult);
|
|
2002
|
+
const explicitSession = sessions.find((session) => readSessionRecordId(session) === args.session_id);
|
|
2003
|
+
if (!explicitSession) {
|
|
2004
|
+
return JSON.stringify({
|
|
2005
|
+
success: false,
|
|
2006
|
+
recoverable: true,
|
|
2007
|
+
code: "mesh_target_session_not_found",
|
|
2008
|
+
reason: "mesh_target_session_not_found",
|
|
2009
|
+
transport: "local_ipc",
|
|
2010
|
+
retryRecommended: true,
|
|
2011
|
+
nodeId: args.node_id,
|
|
2012
|
+
sessionId: args.session_id,
|
|
2013
|
+
error: `Local session '${args.session_id}' is not present in live status for node '${args.node_id}'.`,
|
|
2014
|
+
nextAction: `Launch a fresh session with mesh_launch_session(node_id: '${args.node_id}') or retry without session_id so Repo Mesh can target a live delegate session.`
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
resolvedProviderType = resolveSessionProviderType(explicitSession);
|
|
2018
|
+
if (resolvedProviderType) {
|
|
2019
|
+
meshSessionProviderMetadata.set(meshSessionCacheKey(args.node_id, args.session_id), {
|
|
2020
|
+
providerType: resolvedProviderType,
|
|
2021
|
+
providerSessionId: readString(explicitSession?.providerSessionId) || void 0
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
if (!resolvedProviderType) {
|
|
2026
|
+
return JSON.stringify({
|
|
2027
|
+
success: false,
|
|
2028
|
+
recoverable: true,
|
|
2029
|
+
code: "mesh_target_session_provider_unknown",
|
|
2030
|
+
reason: "mesh_target_session_provider_unknown",
|
|
2031
|
+
transport: "local_ipc",
|
|
2032
|
+
retryRecommended: false,
|
|
2033
|
+
nodeId: args.node_id,
|
|
2034
|
+
sessionId: args.session_id,
|
|
2035
|
+
error: `Local session '${args.session_id}' is live but does not expose providerType/cliType, so agent_command cannot be routed safely.`,
|
|
2036
|
+
nextAction: `Relaunch the target session on node '${args.node_id}' or retry without session_id so Repo Mesh can pick a session with provider metadata.`
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
1770
2039
|
const dispatchResult = await commandForNode(ctx, node, "agent_command", {
|
|
1771
2040
|
targetSessionId: args.session_id,
|
|
1772
|
-
|
|
2041
|
+
agentType: resolvedProviderType,
|
|
2042
|
+
cliType: resolvedProviderType,
|
|
2043
|
+
providerType: resolvedProviderType,
|
|
1773
2044
|
action: "send_chat",
|
|
1774
2045
|
message: args.message
|
|
1775
2046
|
});
|
|
@@ -1787,7 +2058,7 @@ async function meshSendTask(ctx, args) {
|
|
|
1787
2058
|
kind: "task_dispatched",
|
|
1788
2059
|
nodeId: args.node_id,
|
|
1789
2060
|
sessionId: args.session_id,
|
|
1790
|
-
providerType:
|
|
2061
|
+
providerType: resolvedProviderType,
|
|
1791
2062
|
payload: { message: args.message, via: "local_direct" }
|
|
1792
2063
|
});
|
|
1793
2064
|
} catch {
|
|
@@ -1802,7 +2073,7 @@ async function meshSendTask(ctx, args) {
|
|
|
1802
2073
|
ctx.transport.command("trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
1803
2074
|
});
|
|
1804
2075
|
}
|
|
1805
|
-
const pendingEvents = isLocalTransport(ctx.transport) ? (0, import_daemon_core.drainPendingMeshCoordinatorEvents)() : [];
|
|
2076
|
+
const pendingEvents = isLocalTransport(ctx.transport) ? (0, import_daemon_core.drainPendingMeshCoordinatorEvents)(ctx.mesh.id) : [];
|
|
1806
2077
|
const result = { success: true, nodeId: args.node_id, taskId: task.id, status: task.status };
|
|
1807
2078
|
if (pendingEvents.length > 0) {
|
|
1808
2079
|
result.pendingCoordinatorEvents = pendingEvents;
|
|
@@ -1823,6 +2094,9 @@ async function meshReadChat(ctx, args) {
|
|
|
1823
2094
|
if (!node) {
|
|
1824
2095
|
return JSON.stringify(buildMissingNodeReadChatRecovery(ctx, args), null, 2);
|
|
1825
2096
|
}
|
|
2097
|
+
if (ctx.transport instanceof IpcTransport || isLocalTransport(ctx.transport)) {
|
|
2098
|
+
await drainCoordinatorPendingEvents(ctx, { nodeIds: [args.node_id] });
|
|
2099
|
+
}
|
|
1826
2100
|
if (isLocalTransport(ctx.transport)) {
|
|
1827
2101
|
const cached = meshSessionProviderMetadata.get(meshSessionCacheKey(args.node_id, args.session_id));
|
|
1828
2102
|
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
@@ -1925,6 +2199,10 @@ async function meshLaunchSession(ctx, args) {
|
|
|
1925
2199
|
const coordinatorNode = resolveCoordinatorNode(ctx);
|
|
1926
2200
|
const coordinatorDaemonId = coordinatorNode?.daemonId || ctx.localDaemonId;
|
|
1927
2201
|
const spawnedSessionVisibility = readSpawnedSessionVisibility(ctx.mesh.policy);
|
|
2202
|
+
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
2203
|
+
if (node.daemonId && !isLocalNode && !coordinatorDaemonId) {
|
|
2204
|
+
return JSON.stringify(buildMissingCoordinatorDaemonIdFailure(ctx, node, resolvedProviderType), null, 2);
|
|
2205
|
+
}
|
|
1928
2206
|
let result;
|
|
1929
2207
|
try {
|
|
1930
2208
|
result = await commandForNode(ctx, node, "launch_cli", {
|
|
@@ -1965,7 +2243,6 @@ async function meshLaunchSession(ctx, args) {
|
|
|
1965
2243
|
});
|
|
1966
2244
|
} catch {
|
|
1967
2245
|
}
|
|
1968
|
-
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
1969
2246
|
if (ctx.transport instanceof IpcTransport && node.daemonId && !isLocalNode) {
|
|
1970
2247
|
ctx.transport.meshCommand(node.daemonId, "trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
1971
2248
|
});
|
|
@@ -1990,6 +2267,9 @@ async function meshLaunchSession(ctx, args) {
|
|
|
1990
2267
|
const coordinatorNode = resolveCoordinatorNode(ctx);
|
|
1991
2268
|
const coordinatorDaemonId = coordinatorNode?.daemonId || ctx.localDaemonId;
|
|
1992
2269
|
const spawnedSessionVisibility = readSpawnedSessionVisibility(ctx.mesh.policy);
|
|
2270
|
+
if (!coordinatorDaemonId) {
|
|
2271
|
+
return JSON.stringify(buildMissingCoordinatorDaemonIdFailure(ctx, node, resolvedProviderType), null, 2);
|
|
2272
|
+
}
|
|
1993
2273
|
try {
|
|
1994
2274
|
const res = await ctx.transport.launch(node.daemonId, {
|
|
1995
2275
|
type: resolvedProviderType,
|
|
@@ -2028,7 +2308,7 @@ async function meshGitStatus(ctx, args) {
|
|
|
2028
2308
|
const submoduleIgnorePaths = node.policy?.submoduleIgnorePaths || [];
|
|
2029
2309
|
try {
|
|
2030
2310
|
if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
2031
|
-
const result = await ctx.transport.gitStatus(node.daemonId, node.workspace, true);
|
|
2311
|
+
const result = await ctx.transport.gitStatus(node.daemonId, node.workspace, true, true);
|
|
2032
2312
|
return JSON.stringify({
|
|
2033
2313
|
nodeId: args.node_id,
|
|
2034
2314
|
workspace: node.workspace,
|
|
@@ -2040,6 +2320,7 @@ async function meshGitStatus(ctx, args) {
|
|
|
2040
2320
|
} else if (isLocalTransport(ctx.transport)) {
|
|
2041
2321
|
const statusResult = await commandForNode(ctx, node, "git_status", {
|
|
2042
2322
|
workspace: node.workspace,
|
|
2323
|
+
refreshUpstream: true,
|
|
2043
2324
|
includeSubmodules: autoDiscoverSubmodules,
|
|
2044
2325
|
submoduleIgnorePaths: submoduleIgnorePaths.length > 0 ? submoduleIgnorePaths : void 0
|
|
2045
2326
|
});
|
|
@@ -2154,6 +2435,7 @@ async function meshCloneNode(ctx, args) {
|
|
|
2154
2435
|
if (existingIndex >= 0) ctx.mesh.nodes[existingIndex] = clonePayload.node;
|
|
2155
2436
|
else ctx.mesh.nodes.push(clonePayload.node);
|
|
2156
2437
|
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2438
|
+
await syncCoordinatorDaemonMeshCache(ctx);
|
|
2157
2439
|
}
|
|
2158
2440
|
return JSON.stringify(result, null, 2);
|
|
2159
2441
|
} else if (!isLocalTransport(ctx.transport) && sourceNode.daemonId) {
|
|
@@ -2171,6 +2453,7 @@ async function meshCloneNode(ctx, args) {
|
|
|
2171
2453
|
if (existingIndex >= 0) ctx.mesh.nodes[existingIndex] = clonePayload.node;
|
|
2172
2454
|
else ctx.mesh.nodes.push(clonePayload.node);
|
|
2173
2455
|
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2456
|
+
await syncCoordinatorDaemonMeshCache(ctx);
|
|
2174
2457
|
}
|
|
2175
2458
|
return JSON.stringify(res, null, 2);
|
|
2176
2459
|
} catch (e) {
|
|
@@ -2511,8 +2794,8 @@ var CloudTransport = class {
|
|
|
2511
2794
|
if (!res.ok) throw new Error(`Approve failed: ${res.status}`);
|
|
2512
2795
|
return res.json();
|
|
2513
2796
|
}
|
|
2514
|
-
async gitStatus(daemonId, workspace, includeDiff = true) {
|
|
2515
|
-
const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff) });
|
|
2797
|
+
async gitStatus(daemonId, workspace, includeDiff = true, refreshUpstream = false) {
|
|
2798
|
+
const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff), refreshUpstream: String(refreshUpstream) });
|
|
2516
2799
|
const res = await fetch(
|
|
2517
2800
|
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-status?${params}`,
|
|
2518
2801
|
{ headers: this.headers() }
|
|
@@ -2907,6 +3190,22 @@ function formatChatResult(result, sessionId, format, limit = 50, compact = false
|
|
|
2907
3190
|
}))
|
|
2908
3191
|
}, null, 2);
|
|
2909
3192
|
}
|
|
3193
|
+
if ((format === "text" || format === void 0) && compact && compactPayload) {
|
|
3194
|
+
const lines2 = outputMessages.slice(-limit).map((m) => {
|
|
3195
|
+
const role = m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role;
|
|
3196
|
+
const content = messageContent(m);
|
|
3197
|
+
const truncated = content.length > 500 ? `${content.slice(0, 500)}\u2026` : content;
|
|
3198
|
+
return `[${role}] ${truncated}`;
|
|
3199
|
+
});
|
|
3200
|
+
if (compactPayload.summary) {
|
|
3201
|
+
const truncatedSummary = compactPayload.summary.length > 500 ? `${compactPayload.summary.slice(0, 500)}\u2026` : compactPayload.summary;
|
|
3202
|
+
lines2.push(`[Summary] ${truncatedSummary}`);
|
|
3203
|
+
}
|
|
3204
|
+
if (result?.pollingAdvisory) {
|
|
3205
|
+
lines2.push(`Advisory: ${result.pollingAdvisory.message}`);
|
|
3206
|
+
}
|
|
3207
|
+
return lines2.length > 0 ? lines2.join("\n\n") : "No messages in chat.";
|
|
3208
|
+
}
|
|
2910
3209
|
if (outputMessages.length === 0) {
|
|
2911
3210
|
return result?.pollingAdvisory ? `No messages in chat.
|
|
2912
3211
|
|