mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix(ui): send parent messages while only subagents run (#125428)
This commit is contained in:
@@ -44,7 +44,7 @@ import {
|
||||
QUEUED_MESSAGE_RETRY_CONFLICT_ERROR,
|
||||
QUEUED_MESSAGE_REORDER_CONFLICT_ERROR,
|
||||
} from "./queued-message-edit.ts";
|
||||
import { hasAbortableSessionRun } from "./run-lifecycle.ts";
|
||||
import { hasDirectSessionRun } from "./run-lifecycle.ts";
|
||||
import {
|
||||
OFFLINE_QUEUE_STORAGE_ERROR,
|
||||
steerQueuedChatMessage as steerQueuedChatMessageLifecycle,
|
||||
@@ -240,7 +240,7 @@ export async function retryQueuedChatMessage(host: ChatHost, id: string) {
|
||||
setChatError(host, t("chat.sendErrors.steerRunNoLongerActive"));
|
||||
return;
|
||||
}
|
||||
if (hasAbortableSessionRun(host)) {
|
||||
if (hasDirectSessionRun(host)) {
|
||||
const retry = updateQueuedMessage(host, id, (entry) => ({
|
||||
...entry,
|
||||
sendAttempts: 0,
|
||||
|
||||
@@ -61,7 +61,7 @@ import { formatConnectError } from "./connect-error.ts";
|
||||
import { readChatSessionProjectionScope, reduceChatSessionProjection } from "./history-merge.ts";
|
||||
import { resetChatInputHistoryNavigation } from "./input-history.ts";
|
||||
import { controlUiNowMs, roundedControlUiDurationMs } from "./performance.ts";
|
||||
import { hasAbortableSessionRun, isChatBusy, reconcileChatRunLifecycle } from "./run-lifecycle.ts";
|
||||
import { hasDirectSessionRun, isChatBusy, reconcileChatRunLifecycle } from "./run-lifecycle.ts";
|
||||
import { resetChatScroll, scheduleChatScroll } from "./scroll.ts";
|
||||
import {
|
||||
formatTerminalChatSendAckError,
|
||||
@@ -614,7 +614,7 @@ export async function deliverChatQueueItem(
|
||||
if (
|
||||
drainResult === undefined &&
|
||||
routeVisible &&
|
||||
(isChatBusy(host) || hasAbortableSessionRun(host))
|
||||
(isChatBusy(host) || hasDirectSessionRun(host))
|
||||
) {
|
||||
const parked = finishChatDeliveryAdmission(
|
||||
host,
|
||||
|
||||
@@ -19,7 +19,7 @@ import { recordChatSendTiming, schedulePendingSendPaintTiming } from "./chat-sen
|
||||
import { getPendingChatPickerPatch } from "./chat-session.ts";
|
||||
import { storedChatOutboxScopeKey, type StoredChatOutboxScope } from "./composer-persistence.ts";
|
||||
import { controlUiNowMs } from "./performance.ts";
|
||||
import { hasAbortableSessionRun, isChatBusy } from "./run-lifecycle.ts";
|
||||
import { hasDirectSessionRun, isChatBusy } from "./run-lifecycle.ts";
|
||||
import { scheduleChatScroll } from "./scroll.ts";
|
||||
import { OFFLINE_QUEUE_STORAGE_ERROR, surfaceChatDeliveryFailure } from "./steer-lifecycle.ts";
|
||||
|
||||
@@ -192,7 +192,7 @@ export function finishChatDeliveryAdmission(
|
||||
}
|
||||
return "pending";
|
||||
}
|
||||
if (routeVisible(current.agentId) && (isChatBusy(host) || hasAbortableSessionRun(host))) {
|
||||
if (routeVisible(current.agentId) && (isChatBusy(host) || hasDirectSessionRun(host))) {
|
||||
const parked = setState(host.connected && host.client ? "waiting-idle" : "waiting-reconnect");
|
||||
if (!parked) {
|
||||
setChatError(host, OFFLINE_QUEUE_STORAGE_ERROR);
|
||||
|
||||
@@ -61,6 +61,7 @@ import { activeQueuedMessageEdit, retireEditedQueuedMessageSource } from "./queu
|
||||
import {
|
||||
handleAbortChat,
|
||||
hasAbortableSessionRun,
|
||||
hasDirectSessionRun,
|
||||
isChatBusy,
|
||||
isChatStopCommand,
|
||||
} from "./run-lifecycle.ts";
|
||||
@@ -584,7 +585,7 @@ export async function handleSendChat(
|
||||
pending?.sendState === "waiting-idle" &&
|
||||
host.sessionKey === submittedSessionKey &&
|
||||
visibleSessionMatches(host, submittedSessionKey, pending.agentId) &&
|
||||
(isChatBusy(host) || hasAbortableSessionRun(host));
|
||||
(isChatBusy(host) || hasDirectSessionRun(host));
|
||||
if (pendingBusySend) {
|
||||
recordChatSendTiming(host, pending, "queued-busy", submittedAtMs);
|
||||
// Only an explicit browser override replaces inherited Gateway policy.
|
||||
@@ -596,7 +597,7 @@ export async function handleSendChat(
|
||||
!skillWorkshopRevision &&
|
||||
followUpMode !== "queue" &&
|
||||
host.connected &&
|
||||
hasAbortableSessionRun(host)
|
||||
hasDirectSessionRun(host)
|
||||
) {
|
||||
void sendQueuedChatMessageWithQueueModeLifecycle(
|
||||
host,
|
||||
|
||||
@@ -3873,6 +3873,34 @@ describe("handleSendChat", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sends normally when only a descendant run is active", async () => {
|
||||
const host = makeChatHost({
|
||||
requestHandlers: {
|
||||
"chat.send": { status: "started", runId: "new-parent-run" },
|
||||
},
|
||||
chatMessage: "start another parent turn",
|
||||
chatRunId: null,
|
||||
sessionKey: "agent:main:main",
|
||||
sessionsResult: createSessionsResult([
|
||||
row("agent:main:main", {
|
||||
hasActiveRun: false,
|
||||
hasActiveSubagentRun: true,
|
||||
status: "done",
|
||||
}),
|
||||
]),
|
||||
settings: { chatFollowUpMode: "steer" },
|
||||
});
|
||||
|
||||
await handleSendChat(host);
|
||||
|
||||
await waitForFast(() => expect(host.request).toHaveBeenCalled());
|
||||
const payload = findRequestPayload(host.request, "chat.send", "chat send payload");
|
||||
expect(payload.message).toBe("start another parent turn");
|
||||
expect(payload).not.toHaveProperty("queueMode");
|
||||
expect(payload).not.toHaveProperty("expectedRunId");
|
||||
expect(host.chatError).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps a steered message visible when only the session row reports an active run", async () => {
|
||||
let wireRunId: unknown;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CHAT_RUN_STATUS_TOAST_DURATION_MS,
|
||||
handleAbortChat,
|
||||
hasAbortableSessionRun,
|
||||
hasDirectSessionRun,
|
||||
reconcileChatRunFromCurrentSessionRow,
|
||||
reconcileChatRunFromSessionRow,
|
||||
reconcileChatRunLifecycle,
|
||||
@@ -80,6 +81,7 @@ describe("handleAbortChat", () => {
|
||||
]),
|
||||
});
|
||||
|
||||
expect(hasDirectSessionRun(host)).toBe(false);
|
||||
expect(hasAbortableSessionRun(host)).toBe(true);
|
||||
await handleAbortChat(host);
|
||||
|
||||
|
||||
@@ -140,23 +140,35 @@ export function isChatBusy(host: { chatSending?: boolean; chatRunId?: string | n
|
||||
return Boolean(host.chatSending || host.chatRunId);
|
||||
}
|
||||
|
||||
export function hasAbortableSessionRun(host: {
|
||||
type SessionRunHost = {
|
||||
chatRunId?: string | null;
|
||||
sessionKey: string;
|
||||
sessionsResult?: SessionsListResult | null;
|
||||
}): boolean {
|
||||
if (host.chatRunId) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export function hasDirectSessionRun(host: SessionRunHost): boolean {
|
||||
return Boolean(
|
||||
host.chatRunId ||
|
||||
host.sessionsResult?.sessions.some(
|
||||
(session) =>
|
||||
areUiSessionKeysEquivalent(session.key, host.sessionKey) &&
|
||||
(isSessionRunActive(session) || session.hasActiveSubagentRun === true),
|
||||
areUiSessionKeysEquivalent(session.key, host.sessionKey) && isSessionRunActive(session),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function hasAbortableSessionRun(host: SessionRunHost): boolean {
|
||||
return (
|
||||
hasDirectSessionRun(host) ||
|
||||
Boolean(
|
||||
host.sessionsResult?.sessions.some(
|
||||
(session) =>
|
||||
areUiSessionKeysEquivalent(session.key, host.sessionKey) &&
|
||||
session.hasActiveSubagentRun === true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isChatStopCommand(text: string) {
|
||||
return CHAT_STOP_COMMANDS.has(normalizeLowercaseStringOrEmpty(text.trim()));
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
isQueuedMessageBeingEdited,
|
||||
QUEUED_MESSAGE_STEER_CONFLICT_ERROR,
|
||||
} from "./queued-message-edit.ts";
|
||||
import { hasAbortableSessionRun } from "./run-lifecycle.ts";
|
||||
import { hasDirectSessionRun } from "./run-lifecycle.ts";
|
||||
import { scheduleChatScroll, type ChatScrollHost } from "./scroll.ts";
|
||||
import { appendChatMessageToCache, readChatMessagesFromCache } from "./session-message-cache.ts";
|
||||
import {
|
||||
@@ -368,7 +368,7 @@ export async function sendQueuedChatMessageWithQueueMode(
|
||||
queueMode: QueueMode | undefined,
|
||||
dependencies: SteerSendDependencies,
|
||||
): Promise<void> {
|
||||
if (!host.connected || !hasAbortableSessionRun(host)) {
|
||||
if (!host.connected || !hasDirectSessionRun(host)) {
|
||||
return;
|
||||
}
|
||||
const isSteer = queueMode === "steer";
|
||||
|
||||
Reference in New Issue
Block a user