@parhelia/core 0.1.12776 → 0.1.12777
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/components/ui/button.d.ts +1 -1
- package/dist/editor/ai/AgentTerminal.d.ts +5 -1
- package/dist/editor/ai/AgentTerminal.js +117 -246
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/agentDiagnostics.d.ts +24 -51
- package/dist/editor/ai/agentDiagnostics.js +22 -199
- package/dist/editor/ai/agentDiagnostics.js.map +1 -1
- package/dist/editor/ai/agentDiagnostics.test.js +93 -322
- package/dist/editor/ai/agentDiagnostics.test.js.map +1 -1
- package/dist/editor/client/EditorShell.js +8 -7
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/splash-screen/ParheliaAssistantChat.js +37 -16
- package/dist/splash-screen/ParheliaAssistantChat.js.map +1 -1
- package/dist/splash-screen/SplashScreenAgentContext.d.ts +26 -0
- package/dist/splash-screen/SplashScreenAgentContext.js +40 -0
- package/dist/splash-screen/SplashScreenAgentContext.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
declare const buttonVariants: (props?: ({
|
|
3
|
-
variant?: "
|
|
3
|
+
variant?: "link" | "select" | "outline" | "default" | "defaultInverted" | "destructive" | "secondary" | "ghost" | null | undefined;
|
|
4
4
|
size?: "select" | "default" | "icon" | "sm" | "xs" | "xxs" | "lg" | "iconSm" | "iconXs" | null | undefined;
|
|
5
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
6
|
type BaseButtonProps = React.ComponentProps<"button"> & {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { AgentChatMessage, Agent, AgentMetadata } from "../services/agentService";
|
|
3
3
|
import { AiProfile } from "../services/aiService";
|
|
4
|
-
export declare function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadProfiles, isActive, isVisible, isFocused, compact, displayMode, showSummaryInput, hideContext, hideBottomControls, hideGreeting, defaultCollapseJson, simpleMode, className, initialPrompt, onAgentUpdate, onMessage, onInteractionSubmitted, onQuestionnaireOpenChange, questionnaireFooterActions, hideSummaryMessages, summaryPlaceholderActions, summaryPlaceholderMessage, hideSummaryWaitingPlaceholder, }: {
|
|
4
|
+
export declare function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadProfiles, isActive, isVisible, isFocused, compact, displayMode, showSummaryInput, hideContext, hideBottomControls, showStatusBar, showAgentConfigControls, hideGreeting, defaultCollapseJson, simpleMode, className, initialPrompt, onAgentUpdate, onMessage, onInteractionSubmitted, onQuestionnaireOpenChange, questionnaireFooterActions, hideSummaryMessages, summaryPlaceholderActions, summaryPlaceholderMessage, hideSummaryWaitingPlaceholder, }: {
|
|
5
5
|
agentStub: Agent;
|
|
6
6
|
initialMetadata?: AgentMetadata;
|
|
7
7
|
profiles: AiProfile[];
|
|
@@ -17,6 +17,10 @@ export declare function AgentTerminal({ agentStub, initialMetadata, profiles, on
|
|
|
17
17
|
showSummaryInput?: boolean;
|
|
18
18
|
hideContext?: boolean;
|
|
19
19
|
hideBottomControls?: boolean;
|
|
20
|
+
/** Force the bottom status bar (model/context/cost) to render even when bottom controls are hidden. Defaults to `!hideBottomControls`. */
|
|
21
|
+
showStatusBar?: boolean;
|
|
22
|
+
/** Force the agent configuration row (mode selector, Agent settings popover) to render even when bottom controls are hidden. Defaults to `!hideBottomControls`. */
|
|
23
|
+
showAgentConfigControls?: boolean;
|
|
20
24
|
hideGreeting?: boolean;
|
|
21
25
|
defaultCollapseJson?: boolean;
|
|
22
26
|
simpleMode?: boolean;
|
|
@@ -21,7 +21,7 @@ import { QueuedPromptsPanel } from "./QueuedPromptsPanel";
|
|
|
21
21
|
import { AgentCapacityBanner, AgentCostLimitBanner, AgentErrorBanner, } from "./AgentBanners";
|
|
22
22
|
import { InitialThinkingSplash } from "./InitialThinkingSplash";
|
|
23
23
|
import { AgentInlineDialogContent } from "./AgentInlineDialogContent";
|
|
24
|
-
import { AGENT_HISTORY_LIMIT, MACHINE_CAPACITY_REASON, buildPlaceholderAgentDetails, formatAllowanceLabel, formatAllowanceSource, getAgentRunMessageAgentId, getAgentRunMessageDetail, getAgentRunMessageSeq,
|
|
24
|
+
import { AGENT_HISTORY_LIMIT, MACHINE_CAPACITY_REASON, buildPlaceholderAgentDetails, formatAllowanceLabel, formatAllowanceSource, getAgentRunMessageAgentId, getAgentRunMessageDetail, getAgentRunMessageSeq, isAgentErrorStatusValue, isHeartbeatRunEventMessage, mergeAgentOperationHistory, normalizeDialogAgentId, normalizeProfileAllowanceOperations, normalizeServerExecutionStatus, } from "./agentMessageHelpers";
|
|
25
25
|
import { AgentDocumentList, } from "./AgentDocumentList";
|
|
26
26
|
import { AgentEditOperationsPanel } from "./EditOperationsPanel";
|
|
27
27
|
import { SpawnedAgentsPanel } from "./SpawnedAgentsPanel";
|
|
@@ -40,7 +40,7 @@ import { cn } from "../../lib/utils";
|
|
|
40
40
|
import { sanitizeSvg } from "../../lib/sanitize";
|
|
41
41
|
import { Select } from "../../components/ui/select";
|
|
42
42
|
import { AgentTerminalStatusBar } from "./AgentTerminalStatusBar";
|
|
43
|
-
import {
|
|
43
|
+
import { isStreamStalled, shouldReloadAfterDiagnostics, } from "./agentDiagnostics";
|
|
44
44
|
import { forceEditorSocketReconnect } from "../client/hooks/useEditorWebSocket";
|
|
45
45
|
import { SimpleTabs } from "../ui/SimpleTabs";
|
|
46
46
|
import { Splitter } from "../ui/Splitter";
|
|
@@ -80,7 +80,7 @@ function hasStaleRunningLoadSuppression(agentId) {
|
|
|
80
80
|
// interface AgentTerminalProps {
|
|
81
81
|
// agentStub: Agent;
|
|
82
82
|
// }
|
|
83
|
-
export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadProfiles, isActive, isVisible, isFocused, compact = false, displayMode = "full", showSummaryInput = false, hideContext = false, hideBottomControls = false, hideGreeting = false, defaultCollapseJson = false, simpleMode = false, className, initialPrompt, onAgentUpdate, onMessage, onInteractionSubmitted, onQuestionnaireOpenChange, questionnaireFooterActions, hideSummaryMessages = false, summaryPlaceholderActions, summaryPlaceholderMessage, hideSummaryWaitingPlaceholder = false, }) {
|
|
83
|
+
export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadProfiles, isActive, isVisible, isFocused, compact = false, displayMode = "full", showSummaryInput = false, hideContext = false, hideBottomControls = false, showStatusBar, showAgentConfigControls, hideGreeting = false, defaultCollapseJson = false, simpleMode = false, className, initialPrompt, onAgentUpdate, onMessage, onInteractionSubmitted, onQuestionnaireOpenChange, questionnaireFooterActions, hideSummaryMessages = false, summaryPlaceholderActions, summaryPlaceholderMessage, hideSummaryWaitingPlaceholder = false, }) {
|
|
84
84
|
// Derived from props. `isVisible` controls "is this terminal mounted and
|
|
85
85
|
// streaming" (subscriptions, polling). `isFocused` controls "does this
|
|
86
86
|
// terminal own keyboard focus and dialogs". Both fall back to legacy
|
|
@@ -1249,16 +1249,14 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
1249
1249
|
const lastSeqRef = useRef(0);
|
|
1250
1250
|
const subscribedAgentIdRef = useRef(null);
|
|
1251
1251
|
const reconcileServerStateInFlightRef = useRef(false);
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
//
|
|
1258
|
-
|
|
1259
|
-
const
|
|
1260
|
-
const lastDiagnosticsResponseRef = useRef(null);
|
|
1261
|
-
const [showStaleAgentBanner, setShowStaleAgentBanner] = useState(false);
|
|
1252
|
+
// Stream-stall watchdog timestamps. `lastNonHeartbeatUpdateAtRef` is set every time
|
|
1253
|
+
// something non-heartbeat advances the run (prompt submit, isExecuting transition,
|
|
1254
|
+
// status/delta/complete/error event, socket reconnect, or a stall reload).
|
|
1255
|
+
// `lastStallReloadAtRef` enforces a minimum gap between automatic reloads so a
|
|
1256
|
+
// backend that keeps advancing while our socket keeps bouncing can't trigger reloads
|
|
1257
|
+
// every 15 seconds.
|
|
1258
|
+
const lastNonHeartbeatUpdateAtRef = useRef(0);
|
|
1259
|
+
const lastStallReloadAtRef = useRef(0);
|
|
1262
1260
|
const toolCallFirstSeenAtRef = useRef({});
|
|
1263
1261
|
const pendingToolCompletionTimersRef = useRef({});
|
|
1264
1262
|
// Cache mode/model/profile changes made while the agent is still "new" (not yet persisted)
|
|
@@ -3019,6 +3017,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3019
3017
|
if (seq) {
|
|
3020
3018
|
lastSeqRef.current = seq;
|
|
3021
3019
|
}
|
|
3020
|
+
if (!isHeartbeat) {
|
|
3021
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
3022
|
+
}
|
|
3022
3023
|
// Route based on delta type
|
|
3023
3024
|
const agentStreamMessage = {
|
|
3024
3025
|
type,
|
|
@@ -3092,6 +3093,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3092
3093
|
if (seq) {
|
|
3093
3094
|
lastSeqRef.current = seq;
|
|
3094
3095
|
}
|
|
3096
|
+
// Status events never carry a "Heartbeat" payload — heartbeats are deltas. Any
|
|
3097
|
+
// status update is meaningful progress for the watchdog.
|
|
3098
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
3095
3099
|
// Route based on statusData.state
|
|
3096
3100
|
try {
|
|
3097
3101
|
// Normalize various status shapes and handle Cancelled uniformly
|
|
@@ -3425,12 +3429,14 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
3425
3429
|
}
|
|
3426
3430
|
// Lifecycle: agent:run:complete
|
|
3427
3431
|
if (messageType === "agent:run:complete") {
|
|
3432
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
3428
3433
|
const finalStatus = normalizeServerExecutionStatus(message.payload?.finalStatus);
|
|
3429
3434
|
settleCompletedRun(finalStatus === "cancelled" ? "cancelled" : "completed");
|
|
3430
3435
|
return;
|
|
3431
3436
|
}
|
|
3432
3437
|
// Lifecycle: agent:run:error
|
|
3433
3438
|
if (messageType === "agent:run:error") {
|
|
3439
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
3434
3440
|
const errorMsg = toUserFacingAgentErrorMessage(message.payload?.error) ||
|
|
3435
3441
|
"AI could not complete this request.";
|
|
3436
3442
|
clearHeartbeatMessages();
|
|
@@ -4105,6 +4111,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
4105
4111
|
context: canonicalizeAgentMetadata(effectiveContext), // Use fresh live context when in live mode
|
|
4106
4112
|
};
|
|
4107
4113
|
console.log("[AgentTerminal] Calling startAgent API for agent:", agentId);
|
|
4114
|
+
// Reset the stall watchdog so its 15s window starts at submit, not at the last
|
|
4115
|
+
// delta of the previous run (which could be hours ago for a reused chat).
|
|
4116
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
4108
4117
|
const response = await startAgent(request);
|
|
4109
4118
|
console.log("[AgentTerminal] startAgent response:", response);
|
|
4110
4119
|
const isQueuedForCapacity = response.reason === MACHINE_CAPACITY_REASON ||
|
|
@@ -4999,7 +5008,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
4999
5008
|
const lastEvent = recentAgentRunEvents[recentAgentRunEvents.length - 1];
|
|
5000
5009
|
const { receivedSeqs, missingSeqs, totalCount } = currentRunDiagnostics;
|
|
5001
5010
|
const observedMaxSeq = receivedSeqs[receivedSeqs.length - 1] ?? 0;
|
|
5002
|
-
const inlineCallback = activeInlineDialogRef.current?.request.callbackId?.trim() || null;
|
|
5003
5011
|
return {
|
|
5004
5012
|
agentId: currentAgentId,
|
|
5005
5013
|
isSubmitting,
|
|
@@ -5026,8 +5034,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5026
5034
|
totalToolCallCount,
|
|
5027
5035
|
incompleteToolCallCount,
|
|
5028
5036
|
recentToolUiEvents,
|
|
5029
|
-
activeInlineDialogCallbackId: inlineCallback,
|
|
5030
|
-
pendingDialogReplayCallbackIds: Array.from(pendingDialogReplayCallbackIdsRef.current),
|
|
5031
5037
|
};
|
|
5032
5038
|
}, [
|
|
5033
5039
|
assistantGroupCount,
|
|
@@ -5048,239 +5054,130 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5048
5054
|
recentToolUiEvents,
|
|
5049
5055
|
totalToolCallCount,
|
|
5050
5056
|
agent?.statusMessage,
|
|
5051
|
-
activeInlineDialog,
|
|
5052
5057
|
]);
|
|
5053
|
-
//
|
|
5054
|
-
//
|
|
5058
|
+
// Bump the watchdog timestamp when isExecuting goes false→true. Covers resumed and
|
|
5059
|
+
// foreign starts (where another browser kicks off a run we're observing) on top of
|
|
5060
|
+
// the explicit submit-time bump in handleSubmit.
|
|
5061
|
+
const previousIsExecutingRef = useRef(false);
|
|
5055
5062
|
useEffect(() => {
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5063
|
+
if (isExecuting && !previousIsExecutingRef.current) {
|
|
5064
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
5065
|
+
}
|
|
5066
|
+
previousIsExecutingRef.current = isExecuting;
|
|
5067
|
+
}, [isExecuting]);
|
|
5068
|
+
// When the editor socket reconnects, the subscription registry resubscribes for us.
|
|
5069
|
+
// Give it a fresh window before the watchdog judges silence — otherwise a slow
|
|
5070
|
+
// resubscribe handshake would look like a stall.
|
|
5059
5071
|
useEffect(() => {
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
clearTimeout(replayVerifyTimeoutRef.current);
|
|
5063
|
-
replayVerifyTimeoutRef.current = null;
|
|
5072
|
+
if (isExecuting) {
|
|
5073
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
5064
5074
|
}
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
//
|
|
5068
|
-
// banner — the registry has just re-issued subscriptions, so any "stuck" state should
|
|
5069
|
-
// resolve on the next poll. If it doesn't, the banner will reappear.
|
|
5075
|
+
}, [editContext?.socketConnectionVersion, isExecuting]);
|
|
5076
|
+
// Reset watchdog cooldown timestamps when the active agent changes — otherwise a
|
|
5077
|
+
// recent reload on agent A would suppress the first reload for agent B.
|
|
5070
5078
|
useEffect(() => {
|
|
5071
|
-
|
|
5072
|
-
|
|
5079
|
+
lastNonHeartbeatUpdateAtRef.current = 0;
|
|
5080
|
+
lastStallReloadAtRef.current = 0;
|
|
5081
|
+
}, [currentAgentId]);
|
|
5082
|
+
// Stream-stall watchdog. While the run is active and the terminal is visible, the
|
|
5083
|
+
// watchdog ticks every 5s and checks whether the gap since the last non-heartbeat
|
|
5084
|
+
// update exceeds 15s. If it does, it asks the backend whether it has produced
|
|
5085
|
+
// sequence numbers we have not applied — and only if so, falls back to what a user
|
|
5086
|
+
// would do: reload the agent and reconnect the socket.
|
|
5073
5087
|
useEffect(() => {
|
|
5074
5088
|
if (!effectiveIsVisible || !isExecuting || !currentAgentId) {
|
|
5075
5089
|
return;
|
|
5076
5090
|
}
|
|
5077
5091
|
let disposed = false;
|
|
5078
5092
|
const agentIdAtMount = currentAgentId;
|
|
5079
|
-
|
|
5080
|
-
let state = recoveryStateByAgentRef.current.get(agentIdAtMount);
|
|
5081
|
-
if (!state) {
|
|
5082
|
-
state = createRecoveryStateSlice();
|
|
5083
|
-
recoveryStateByAgentRef.current.set(agentIdAtMount, state);
|
|
5084
|
-
}
|
|
5085
|
-
return state;
|
|
5086
|
-
};
|
|
5093
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
5087
5094
|
const logRecoveryEvent = (kind, payload) => {
|
|
5088
5095
|
try {
|
|
5089
5096
|
appendToolUiEvent(`recovery:${kind}`, JSON.stringify({ agentId: agentIdAtMount, ...payload }));
|
|
5090
5097
|
}
|
|
5091
5098
|
catch {
|
|
5092
|
-
// Telemetry must never throw out of the
|
|
5099
|
+
// Telemetry must never throw out of the watchdog path.
|
|
5093
5100
|
}
|
|
5094
5101
|
};
|
|
5095
|
-
const
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
return false;
|
|
5102
|
+
const sessionId = editContext?.sessionId;
|
|
5103
|
+
const tick = async () => {
|
|
5104
|
+
if (!isStreamStalled({
|
|
5105
|
+
isExecuting: true,
|
|
5106
|
+
isVisible: true,
|
|
5107
|
+
isStopping: isStoppingRef.current,
|
|
5108
|
+
isWaitingForCapacity: lastRunStatusReason === MACHINE_CAPACITY_REASON,
|
|
5109
|
+
lastUpdateAt: lastNonHeartbeatUpdateAtRef.current,
|
|
5110
|
+
now: Date.now(),
|
|
5111
|
+
})) {
|
|
5112
|
+
return;
|
|
5107
5113
|
}
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
const checkServerCompletion = async () => {
|
|
5114
|
+
logRecoveryEvent("stall-check", {
|
|
5115
|
+
silentMs: Date.now() - lastNonHeartbeatUpdateAtRef.current,
|
|
5116
|
+
localSeq: lastSeqRef.current,
|
|
5117
|
+
});
|
|
5118
|
+
let diagnostics;
|
|
5114
5119
|
try {
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
if (isInactiveServerExecutionStatus(serverStatus)) {
|
|
5122
|
-
setShowStaleAgentBanner(false);
|
|
5123
|
-
const lastRunRequestedAt = Date.parse(diagnostics.lastRunRequest?.requestedAt || "");
|
|
5124
|
-
const cancelRequestedAt = Date.parse(diagnostics.lastCancellation?.requestedAt || "");
|
|
5125
|
-
const cancellationAppliesToCurrentRun = Number.isFinite(cancelRequestedAt) &&
|
|
5126
|
-
(!Number.isFinite(lastRunRequestedAt) ||
|
|
5127
|
-
cancelRequestedAt >= lastRunRequestedAt);
|
|
5128
|
-
settleCompletedRun(normalizeServerExecutionStatus(serverStatus) === "cancelled" ||
|
|
5129
|
-
cancellationAppliesToCurrentRun
|
|
5130
|
-
? "cancelled"
|
|
5131
|
-
: "completed");
|
|
5132
|
-
return;
|
|
5133
|
-
}
|
|
5134
|
-
// Backstop: if the backend reports an error but we never received the
|
|
5135
|
-
// agent:run:error lifecycle event (e.g. it was missed or the broadcast
|
|
5136
|
-
// path failed to fire), surface the error locally instead of waiting
|
|
5137
|
-
// for the user to reload.
|
|
5138
|
-
if (normalizeServerExecutionStatus(serverStatus) === "error") {
|
|
5139
|
-
const rawError = diagnostics.execution?.error ?? null;
|
|
5140
|
-
const errorMsg = toUserFacingAgentErrorMessage(rawError) ||
|
|
5141
|
-
rawError ||
|
|
5142
|
-
"AI could not complete this request.";
|
|
5143
|
-
clearHeartbeatMessages();
|
|
5144
|
-
setLastRunStatusReason(null);
|
|
5145
|
-
setError(errorMsg);
|
|
5146
|
-
setAgent((prev) => prev ? { ...prev, status: "error", statusMessage: errorMsg } : prev);
|
|
5147
|
-
setIsWaitingForResponse(false);
|
|
5148
|
-
isWaitingRef.current = false;
|
|
5149
|
-
setIsConnecting(false);
|
|
5150
|
-
setIsAgentThinking(false);
|
|
5151
|
-
setShowStaleAgentBanner(false);
|
|
5152
|
-
return;
|
|
5153
|
-
}
|
|
5154
|
-
const snapshot = runDiagnosticsSnapshot;
|
|
5155
|
-
if (!editContext?.socketDiagnostics) {
|
|
5156
|
-
// No edit context to read socket diagnostics from — recovery decisions need it,
|
|
5157
|
-
// and without it we'd misclassify the stream. Wait for the next tick.
|
|
5158
|
-
return;
|
|
5159
|
-
}
|
|
5160
|
-
const summary = interpretAgentRunDiagnostics({
|
|
5161
|
-
socketDiagnostics: editContext.socketDiagnostics,
|
|
5162
|
-
localSnapshot: snapshot,
|
|
5163
|
-
serverDiagnostics: diagnostics,
|
|
5164
|
-
});
|
|
5165
|
-
const state = getRecoveryState();
|
|
5166
|
-
const now = Date.now();
|
|
5167
|
-
reconcilePendingDialogTracker(state, diagnostics.pendingDialogs, now);
|
|
5168
|
-
// --- Verify-window resolution. Structured to avoid the "expired branch
|
|
5169
|
-
// unreachable" pitfall: first check whether a verify is in flight, then branch on
|
|
5170
|
-
// whether it's still inside the window or just expired.
|
|
5171
|
-
if (state.lastReplayVerifyDeadlineAt > 0) {
|
|
5172
|
-
if (now < state.lastReplayVerifyDeadlineAt) {
|
|
5173
|
-
// Still inside the 4s window — leave verification to the one-shot timer.
|
|
5174
|
-
return;
|
|
5175
|
-
}
|
|
5176
|
-
const verified = checkReplayVerified(state, snapshot, diagnostics);
|
|
5177
|
-
state.lastReplayVerifyDeadlineAt = 0;
|
|
5178
|
-
state.lastReplayVerifySnapshot = null;
|
|
5179
|
-
logRecoveryEvent(verified ? "replay-succeeded" : "replay-failed", { kind: "poll", summaryCode: summary.code });
|
|
5180
|
-
if (verified) {
|
|
5181
|
-
setShowStaleAgentBanner(false);
|
|
5182
|
-
return;
|
|
5183
|
-
}
|
|
5184
|
-
// Fall through — decideRecoveryAction will likely return "reconnect" now.
|
|
5185
|
-
}
|
|
5186
|
-
const decision = decideRecoveryAction({
|
|
5187
|
-
summary,
|
|
5188
|
-
localSnapshot: snapshot,
|
|
5189
|
-
serverDiagnostics: diagnostics,
|
|
5190
|
-
recoveryState: state,
|
|
5191
|
-
now,
|
|
5192
|
-
});
|
|
5193
|
-
state.lastDecidedAction = decision.action;
|
|
5194
|
-
logRecoveryEvent("decided", {
|
|
5195
|
-
action: decision.action,
|
|
5196
|
-
summaryCode: summary.code,
|
|
5197
|
-
reasonCode: decision.reasonCode,
|
|
5198
|
-
replayKey: decision.replayKey,
|
|
5120
|
+
diagnostics = await getAgentDiagnostics(agentIdAtMount, sessionId);
|
|
5121
|
+
}
|
|
5122
|
+
catch (error) {
|
|
5123
|
+
console.warn("[AgentTerminal] Stall watchdog failed to fetch diagnostics", error);
|
|
5124
|
+
logRecoveryEvent("diagnostics-failed", {
|
|
5125
|
+
message: error instanceof Error ? error.message : String(error),
|
|
5199
5126
|
});
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
logRecoveryEvent(verified ? "replay-succeeded" : "replay-failed", { kind: "one-shot" });
|
|
5230
|
-
if (verified) {
|
|
5231
|
-
setShowStaleAgentBanner(false);
|
|
5232
|
-
}
|
|
5233
|
-
// Do not auto-escalate from here; the next poll will see the failed verify
|
|
5234
|
-
// and ask decideRecoveryAction for the next step (typically reconnect).
|
|
5235
|
-
}, 4_000);
|
|
5236
|
-
logRecoveryEvent("replay-started", {
|
|
5237
|
-
replayKey: decision.replayKey,
|
|
5238
|
-
summaryCode: summary.code,
|
|
5239
|
-
serverLastSeq,
|
|
5240
|
-
localSeq: snapshot.lastSeq,
|
|
5241
|
-
});
|
|
5242
|
-
break;
|
|
5243
|
-
}
|
|
5244
|
-
case "reconnect": {
|
|
5245
|
-
tryReconnect(state, "agent-recovery-escalation");
|
|
5246
|
-
break;
|
|
5247
|
-
}
|
|
5248
|
-
case "stale": {
|
|
5249
|
-
setShowStaleAgentBanner(true);
|
|
5250
|
-
logRecoveryEvent("stale", {
|
|
5251
|
-
summaryCode: summary.code,
|
|
5252
|
-
reasonCode: decision.reasonCode,
|
|
5253
|
-
});
|
|
5254
|
-
break;
|
|
5255
|
-
}
|
|
5256
|
-
case "none":
|
|
5257
|
-
break;
|
|
5258
|
-
}
|
|
5127
|
+
// Reset so we don't refire diagnostics every 5s while the call keeps failing.
|
|
5128
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
5129
|
+
return;
|
|
5130
|
+
}
|
|
5131
|
+
if (disposed || agentIdAtMount !== currentAgentId) {
|
|
5132
|
+
return;
|
|
5133
|
+
}
|
|
5134
|
+
const backendSeq = diagnostics.currentSession?.lastDelivery?.lastSeq ??
|
|
5135
|
+
diagnostics.transport?.lastSeq ??
|
|
5136
|
+
null;
|
|
5137
|
+
const localSeq = lastSeqRef.current;
|
|
5138
|
+
if (!shouldReloadAfterDiagnostics({ localSeq, backendSeq })) {
|
|
5139
|
+
// Backend is at or behind us — silence is something else (idle, server lag).
|
|
5140
|
+
// Suppress the watchdog for another full window instead of refiring every 5s.
|
|
5141
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
5142
|
+
return;
|
|
5143
|
+
}
|
|
5144
|
+
const sinceLastReload = Date.now() - lastStallReloadAtRef.current;
|
|
5145
|
+
if (lastStallReloadAtRef.current > 0 && sinceLastReload < 30_000) {
|
|
5146
|
+
logRecoveryEvent("reload-cooldown", { sinceLastReload });
|
|
5147
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
5148
|
+
return;
|
|
5149
|
+
}
|
|
5150
|
+
lastStallReloadAtRef.current = Date.now();
|
|
5151
|
+
logRecoveryEvent("reload-fired", { backendSeq, localSeq });
|
|
5152
|
+
lastSeqRef.current = 0;
|
|
5153
|
+
seenMessageIdsRef.current.clear();
|
|
5154
|
+
try {
|
|
5155
|
+
await loadAgent();
|
|
5259
5156
|
}
|
|
5260
5157
|
catch (error) {
|
|
5261
|
-
console.warn("[AgentTerminal]
|
|
5158
|
+
console.warn("[AgentTerminal] Stall reload loadAgent() failed", error);
|
|
5159
|
+
}
|
|
5160
|
+
if (disposed || agentIdAtMount !== currentAgentId) {
|
|
5161
|
+
return;
|
|
5262
5162
|
}
|
|
5163
|
+
forceEditorSocketReconnect("agent-stream-stalled");
|
|
5164
|
+
lastNonHeartbeatUpdateAtRef.current = Date.now();
|
|
5263
5165
|
};
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5166
|
+
const intervalId = window.setInterval(() => {
|
|
5167
|
+
void tick();
|
|
5168
|
+
}, 5_000);
|
|
5267
5169
|
return () => {
|
|
5268
5170
|
disposed = true;
|
|
5269
|
-
window.clearTimeout(timeoutId);
|
|
5270
5171
|
window.clearInterval(intervalId);
|
|
5271
|
-
clearVerifyTimer();
|
|
5272
5172
|
};
|
|
5273
5173
|
}, [
|
|
5274
5174
|
currentAgentId,
|
|
5275
5175
|
editContext?.sessionId,
|
|
5276
|
-
editContext?.socketConnectionVersion,
|
|
5277
|
-
editContext?.socketDiagnostics,
|
|
5278
5176
|
effectiveIsVisible,
|
|
5279
5177
|
isExecuting,
|
|
5178
|
+
lastRunStatusReason,
|
|
5280
5179
|
appendToolUiEvent,
|
|
5281
|
-
|
|
5282
|
-
runDiagnosticsSnapshot,
|
|
5283
|
-
settleCompletedRun,
|
|
5180
|
+
loadAgent,
|
|
5284
5181
|
]);
|
|
5285
5182
|
const showInitialThinkingSplash = messages.length === 0 &&
|
|
5286
5183
|
!error &&
|
|
@@ -5439,33 +5336,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5439
5336
|
"Waiting for capacity. The agent will start automatically when a slot becomes available.";
|
|
5440
5337
|
return _jsx(AgentCapacityBanner, { message: message });
|
|
5441
5338
|
};
|
|
5442
|
-
const renderStaleAgentBanner = () => {
|
|
5443
|
-
if (!showStaleAgentBanner || !currentAgentId)
|
|
5444
|
-
return null;
|
|
5445
|
-
const handleRefreshStream = () => {
|
|
5446
|
-
const state = recoveryStateByAgentRef.current.get(currentAgentId);
|
|
5447
|
-
if (state) {
|
|
5448
|
-
state.lastReplayKey = null;
|
|
5449
|
-
state.lastReplayAt = 0;
|
|
5450
|
-
}
|
|
5451
|
-
requestAgentSubscriptionReplay(currentAgentId);
|
|
5452
|
-
void loadAgent();
|
|
5453
|
-
setShowStaleAgentBanner(false);
|
|
5454
|
-
};
|
|
5455
|
-
const handleReconnect = () => {
|
|
5456
|
-
const state = recoveryStateByAgentRef.current.get(currentAgentId);
|
|
5457
|
-
if (state) {
|
|
5458
|
-
state.lastReconnectAt = 0;
|
|
5459
|
-
}
|
|
5460
|
-
forceEditorSocketReconnect("user-stale-agent-action");
|
|
5461
|
-
setShowStaleAgentBanner(false);
|
|
5462
|
-
};
|
|
5463
|
-
const handleCancel = () => {
|
|
5464
|
-
void handleStop();
|
|
5465
|
-
setShowStaleAgentBanner(false);
|
|
5466
|
-
};
|
|
5467
|
-
return (_jsxs("div", { role: "status", className: "m-3 rounded border border-amber-300 bg-amber-50 p-3 text-[12px] text-amber-900", children: [_jsx("p", { className: "font-medium", children: "Agent run looks stuck" }), _jsx("p", { className: "mt-1 text-[11px] text-amber-800", children: "The backend reports this run is still active, but recent recovery attempts did not resolve the stream. Pick an action below \u2014 automatic retries are paused until then." }), _jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [_jsx(Button, { size: "sm", variant: "outline", onClick: handleRefreshStream, children: "Refresh stream" }), _jsx(Button, { size: "sm", variant: "outline", onClick: handleReconnect, children: "Reconnect socket" }), _jsx(Button, { size: "sm", variant: "outline", onClick: handleCancel, children: "Cancel agent" })] })] }));
|
|
5468
|
-
};
|
|
5469
5339
|
const renderBrowserClaimBanner = (variant = "inline") => {
|
|
5470
5340
|
if (!agent?.id || !editContext?.sessionId)
|
|
5471
5341
|
return null;
|
|
@@ -5589,7 +5459,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5589
5459
|
? getOperationsForMessageGroup(summaryMessages, agentOperations)
|
|
5590
5460
|
: [];
|
|
5591
5461
|
return (_jsxs("div", { className: `flex h-full min-h-0 flex-col ${className || ""}`, children: [fixedBrowserClaimBanner, readOnlyAccessNotice, error &&
|
|
5592
|
-
!isAgentErrorStatusValue((agent || agentStub)?.status) && (_jsx("div", { className: "m-4 rounded-lg border-l-4 border-red-500 bg-red-50 p-3 select-text", children: _jsxs("div", { className: "flex items-start", children: [_jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 text-red-400", strokeWidth: 1 }), _jsxs("div", { className: "ml-3", children: [_jsx("p", { className: "text-[11px] font-medium text-red-800", children: "Error" }), _jsx("p", { className: "mt-1 text-[11px] text-red-700", children: error })] })] }) })), renderCapacityBanner(), renderErrorBanner(),
|
|
5462
|
+
!isAgentErrorStatusValue((agent || agentStub)?.status) && (_jsx("div", { className: "m-4 rounded-lg border-l-4 border-red-500 bg-red-50 p-3 select-text", children: _jsxs("div", { className: "flex items-start", children: [_jsx(AlertCircle, { className: "mt-0.5 h-5 w-5 text-red-400", strokeWidth: 1 }), _jsxs("div", { className: "ml-3", children: [_jsx("p", { className: "text-[11px] font-medium text-red-800", children: "Error" }), _jsx("p", { className: "mt-1 text-[11px] text-red-700", children: error })] })] }) })), renderCapacityBanner(), renderErrorBanner(), _jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto", onScroll: handleScroll, children: [showInitialThinkingSplash && (_jsx(InitialThinkingSplash, { svgIcon: activeProfile?.svgIcon })), inlineBrowserClaimBanner, inlineDialog ? (inlineDialog) : latestSummaryAssistantGroup ? (_jsx("div", { className: "space-y-0 divide-y divide-gray-100 select-text", children: _jsx(AiResponseMessage, { messages: summaryMessages, finished: !latestSummaryAssistantGroup.isLastGroup || !isExecuting, editOperations: summaryOperations, defaultCollapseJson: defaultCollapseJson, profileSvgIcon: activeProfile?.svgIcon, agentId: agent?.id || agentStub.id, agentName: activeProfile?.agentName ||
|
|
5593
5463
|
activeProfile?.displayTitle ||
|
|
5594
5464
|
activeProfile?.name, allPendingApprovals: allPendingApprovals, onSwitchToAutonomous: handleSwitchToAutonomous, browserCaptureInlinePrompt: browserCaptureInlinePrompt, readOnly: readOnly, onQuickAction: (action) => {
|
|
5595
5465
|
const text = (action.prompt ||
|
|
@@ -5951,9 +5821,10 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
5951
5821
|
const placeholderShowsOwnButtons = hideBottomControls && isInPlaceholderMode;
|
|
5952
5822
|
if (placeholderShowsOwnButtons)
|
|
5953
5823
|
return null;
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5824
|
+
const showConfigControls = (showAgentConfigControls ?? !hideBottomControls) &&
|
|
5825
|
+
!simpleMode &&
|
|
5826
|
+
!isInPlaceholderMode;
|
|
5827
|
+
return (_jsxs("div", { className: cn("mt-2 flex items-stretch gap-2", showConfigControls ? "justify-between" : "justify-end"), children: [showConfigControls && (_jsxs("div", { className: "flex flex-wrap items-center justify-start gap-2", children: [readOnly && (_jsx("span", { className: "shrink-0 rounded bg-gray-100 px-1 py-0.5 text-[10px] text-gray-600", title: "You have View-only access to this agent.", "data-testid": "agent-read-only-badge", children: "Read-only" })), _jsx(Select, { "data-testid": "agent-mode-selector", size: "xs", maxWidth: 240, disabled: readOnly, className: cn("h-5 w-auto min-w-[95px] rounded border px-1.5 text-[11px] font-normal", mode === "read-only"
|
|
5957
5828
|
? "border-green-300 bg-green-50! text-green-700 hover:bg-green-100!"
|
|
5958
5829
|
: mode === "supervised"
|
|
5959
5830
|
? "border-amber-300 bg-amber-50! text-amber-700 hover:bg-amber-100!"
|
|
@@ -6212,7 +6083,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, onReloadPr
|
|
|
6212
6083
|
: allPendingApprovals.length > 0
|
|
6213
6084
|
? "Approve or reject pending tool calls first"
|
|
6214
6085
|
: "Send", "aria-label": isExecuting ? "Stop" : "Send", "data-testid": "agent-send-stop-button", "data-executing": isExecuting ? "true" : "false", children: isExecuting ? (_jsx(Square, { className: "size-3", strokeWidth: 1 })) : (_jsx(Send, { className: "size-3", strokeWidth: 1 })) })] })] }));
|
|
6215
|
-
})(), !hideBottomControls &&
|
|
6086
|
+
})(), (showStatusBar ?? !hideBottomControls) &&
|
|
6216
6087
|
!simpleMode &&
|
|
6217
6088
|
editContext &&
|
|
6218
6089
|
!editContext.isMobile && (_jsx(AgentTerminalStatusBar, { agent: agent, contextWindowStatus: contextWindowStatus, effectiveModelName: effectiveModelName, socketDiagnostics: editContext.socketDiagnostics, runDiagnosticsSnapshot: runDiagnosticsSnapshot, liveTotals: liveTotals, totalTokens: liveTotals
|