feat: improve agent stream view UI, fix history replay timestamps, and enhance planner/worker loop

This commit is contained in:
Mohamed Boudra
2026-01-15 15:10:08 +07:00
parent 5289be9c06
commit ef853914d5
12 changed files with 179 additions and 107 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
"packages/website" "packages/website"
], ],
"scripts": { "scripts": {
"dev": "npm run dev --workspace=@paseo/server", "dev": "NODE_ENV=development tsx packages/server/scripts/dev-runner.ts",
"dev:app": "npm run start --workspace=@paseo/app", "dev:app": "npm run start --workspace=@paseo/app",
"dev:website": "npm run dev --workspace=@paseo/website", "dev:website": "npm run dev --workspace=@paseo/website",
"build": "npm run build --workspaces --if-present", "build": "npm run build --workspaces --if-present",
+51 -26
View File
@@ -1,11 +1,22 @@
import { View, Text, Pressable, Modal, RefreshControl, FlatList, type ListRenderItem } from "react-native"; import {
View,
Text,
Pressable,
Modal,
RefreshControl,
FlatList,
type ListRenderItem,
} from "react-native";
import { useCallback, useState, type ReactElement } from "react"; import { useCallback, useState, type ReactElement } from "react";
import { router, usePathname } from "expo-router"; import { router, usePathname } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { formatTimeAgo } from "@/utils/time"; import { formatTimeAgo } from "@/utils/time";
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents"; import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigation-timing"; import {
buildAgentNavigationKey,
startNavigationTiming,
} from "@/utils/navigation-timing";
interface AgentListProps { interface AgentListProps {
agents: AggregatedAgent[]; agents: AggregatedAgent[];
@@ -30,7 +41,9 @@ export function AgentList({
// Get the methods for the specific server // Get the methods for the specific server
const methods = useSessionStore((state) => const methods = useSessionStore((state) =>
actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.methods : undefined actionAgent?.serverId
? state.sessions[actionAgent.serverId]?.methods
: undefined
); );
const deleteAgent = methods?.deleteAgent; const deleteAgent = methods?.deleteAgent;
@@ -94,7 +107,11 @@ export function AgentList({
const isRunning = agent.status === "running"; const isRunning = agent.status === "running";
const agentKey = `${agent.serverId}:${agent.id}`; const agentKey = `${agent.serverId}:${agent.id}`;
const isSelected = selectedAgentId === agentKey; const isSelected = selectedAgentId === agentKey;
const statusColor = isRunning ? theme.colors.palette.blue[500] : agent.requiresAttention ? theme.colors.success : null; const statusColor = isRunning
? theme.colors.palette.blue[500]
: agent.requiresAttention
? theme.colors.success
: null;
return ( return (
<Pressable <Pressable
@@ -108,26 +125,28 @@ export function AgentList({
onLongPress={() => handleAgentLongPress(agent)} onLongPress={() => handleAgentLongPress(agent)}
> >
{({ hovered }) => ( {({ hovered }) => (
<View style={styles.agentContent}> <View style={styles.agentContent}>
<View style={styles.row}> <View style={styles.row}>
{statusColor && ( {statusColor && (
<View style={[styles.statusDot, { backgroundColor: statusColor }]} /> <View
)} style={[styles.statusDot, { backgroundColor: statusColor }]}
<Text />
style={[ )}
styles.agentTitle, <Text
(isSelected || hovered) && styles.agentTitleHighlighted, style={[
]} styles.agentTitle,
numberOfLines={1} (isSelected || hovered) && styles.agentTitleHighlighted,
> ]}
{agent.title || "New Agent"} numberOfLines={1}
>
{agent.title || "New Agent"}
</Text>
</View>
<Text style={styles.secondaryRow} numberOfLines={1}>
{agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
</Text> </Text>
</View> </View>
<Text style={styles.secondaryRow} numberOfLines={1}>
{agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
</Text>
</View>
)} )}
</Pressable> </Pressable>
); );
@@ -176,7 +195,10 @@ export function AgentList({
onRequestClose={handleCloseActionSheet} onRequestClose={handleCloseActionSheet}
> >
<View style={styles.sheetOverlay}> <View style={styles.sheetOverlay}>
<Pressable style={styles.sheetBackdrop} onPress={handleCloseActionSheet} /> <Pressable
style={styles.sheetBackdrop}
onPress={handleCloseActionSheet}
/>
<View style={styles.sheetContainer}> <View style={styles.sheetContainer}>
<View style={styles.sheetHandle} /> <View style={styles.sheetHandle} />
<Text style={styles.sheetTitle}> <Text style={styles.sheetTitle}>
@@ -195,7 +217,8 @@ export function AgentList({
<Text <Text
style={[ style={[
styles.sheetDeleteText, styles.sheetDeleteText,
(!deleteAgent || isActionDaemonUnavailable) && styles.sheetDeleteTextDisabled, (!deleteAgent || isActionDaemonUnavailable) &&
styles.sheetDeleteTextDisabled,
]} ]}
> >
{isActionDaemonUnavailable ? "Host offline" : "Delete agent"} {isActionDaemonUnavailable ? "Host offline" : "Delete agent"}
@@ -246,7 +269,7 @@ const styles = StyleSheet.create((theme) => ({
row: { row: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: theme.spacing[2], gap: theme.spacing[0],
}, },
agentTitle: { agentTitle: {
flex: 1, flex: 1,
@@ -255,10 +278,12 @@ const styles = StyleSheet.create((theme) => ({
xs: "400", xs: "400",
md: "300", md: "300",
}, },
color: theme.colors.foregroundMuted, color: theme.colors.foreground,
opacity: 0.8,
}, },
agentTitleHighlighted: { agentTitleHighlighted: {
color: theme.colors.foreground, color: theme.colors.foreground,
opacity: 1,
}, },
secondaryRow: { secondaryRow: {
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
@@ -373,8 +373,6 @@ export function AgentStreamView({
return null; return null;
} }
const gap = getGapAbove(item, index);
// Check if this is the end of a turn (before a user message or end of stream when not running) // Check if this is the end of a turn (before a user message or end of stream when not running)
// In inverted list: index-1 is the next item (newer in time) // In inverted list: index-1 is the next item (newer in time)
const nextItem = flatListData[index - 1]; const nextItem = flatListData[index - 1];
@@ -386,18 +384,19 @@ export function AgentStreamView({
const getContent = () => collectTurnContent(index); const getContent = () => collectTurnContent(index);
return ( return (
<View <View style={[stylesheet.streamItemWrapper]}>
style={[
stylesheet.streamItemWrapper,
gap ? { marginBottom: gap } : null,
]}
>
{content} {content}
{isEndOfTurn ? <TurnCopyButton getContent={getContent} /> : null} {isEndOfTurn ? <TurnCopyButton getContent={getContent} /> : null}
</View> </View>
); );
}, },
[getGapAbove, renderStreamItemContent, flatListData, agent.status, collectTurnContent] [
getGapAbove,
renderStreamItemContent,
flatListData,
agent.status,
collectTurnContent,
]
); );
const pendingPermissionItems = useMemo( const pendingPermissionItems = useMemo(
@@ -419,7 +418,10 @@ export function AgentStreamView({
} }
const delta = Math.abs(totalCount - prevCount); const delta = Math.abs(totalCount - prevCount);
streamItemCountRef.current = totalCount; streamItemCountRef.current = totalCount;
if (totalCount < STREAM_ITEM_LOG_MIN_COUNT && delta < STREAM_ITEM_LOG_DELTA_THRESHOLD) { if (
totalCount < STREAM_ITEM_LOG_MIN_COUNT &&
delta < STREAM_ITEM_LOG_DELTA_THRESHOLD
) {
return; return;
} }
let userCount = 0; let userCount = 0;
@@ -452,7 +454,10 @@ export function AgentStreamView({
break; break;
} }
} }
const metrics = totalCount >= STREAM_ITEM_LOG_MIN_COUNT ? measurePayload(streamItems) : null; const metrics =
totalCount >= STREAM_ITEM_LOG_MIN_COUNT
? measurePayload(streamItems)
: null;
perfLog(AGENT_STREAM_LOG_TAG, { perfLog(AGENT_STREAM_LOG_TAG, {
event: "stream_items", event: "stream_items",
agentId, agentId,
@@ -945,11 +950,10 @@ function PermissionRequestCard({
const handleResponse = useCallback( const handleResponse = useCallback(
(response: AgentPermissionResponse) => { (response: AgentPermissionResponse) => {
respondToPermission({ respondToPermission({
agentId: permission.agentId, agentId: permission.agentId,
requestId: permission.request.id, requestId: permission.request.id,
response, response,
}) }).catch((error) => {
.catch((error) => {
console.error( console.error(
"[PermissionRequestCard] Failed to respond to permission:", "[PermissionRequestCard] Failed to respond to permission:",
error error
+25 -28
View File
@@ -1,4 +1,11 @@
import { View, Text, Pressable, Animated } from "react-native"; import {
View,
Text,
Pressable,
Animated,
StyleProp,
ViewStyle,
} from "react-native";
import { import {
useState, useState,
useEffect, useEffect,
@@ -31,6 +38,7 @@ import {
Search, Search,
Brain, Brain,
Copy, Copy,
TriangleAlertIcon,
} from "lucide-react-native"; } from "lucide-react-native";
import { import {
StyleSheet, StyleSheet,
@@ -173,14 +181,11 @@ export const UserMessage = memo(function UserMessage({
<View <View
style={[ style={[
userMessageStylesheet.container, userMessageStylesheet.container,
!resolvedDisableOuterSpacing && isFirstInGroup && { marginTop: theme.spacing[4] },
userMessageStylesheet.containerSpacing, isLastInGroup && { marginBottom: theme.spacing[4] },
!resolvedDisableOuterSpacing && !isFirstInGroup || !isLastInGroup
isFirstInGroup && ? { marginBottom: theme.spacing[1] }
userMessageStylesheet.containerFirstInGroup, : undefined,
!resolvedDisableOuterSpacing &&
isLastInGroup &&
userMessageStylesheet.containerLastInGroup,
]} ]}
> >
<Pressable <Pressable
@@ -364,7 +369,7 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
label: { label: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.base, fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.medium, fontWeight: theme.fontWeight.normal,
flexShrink: 0, flexShrink: 0,
}, },
secondaryLabel: { secondaryLabel: {
@@ -804,8 +809,7 @@ export const ActivityLog = memo(function ActivityLog({
disabled={!isInteractive} disabled={!isInteractive}
style={[ style={[
activityLogStylesheet.pressable, activityLogStylesheet.pressable,
!resolvedDisableOuterSpacing && !resolvedDisableOuterSpacing && activityLogStylesheet.pressableSpacing,
activityLogStylesheet.pressableSpacing,
config.bg, config.bg,
isInteractive && activityLogStylesheet.pressableActive, isInteractive && activityLogStylesheet.pressableActive,
]} ]}
@@ -985,8 +989,7 @@ export const TodoListCard = memo(function TodoListCard({
<View <View
style={[ style={[
todoListCardStylesheet.container, todoListCardStylesheet.container,
!resolvedDisableOuterSpacing && !resolvedDisableOuterSpacing && todoListCardStylesheet.containerSpacing,
todoListCardStylesheet.containerSpacing,
]} ]}
> >
<View style={todoListCardStylesheet.card}> <View style={todoListCardStylesheet.card}>
@@ -1056,6 +1059,7 @@ interface ExpandableBadgeProps {
secondaryLabel?: string; secondaryLabel?: string;
icon?: ComponentType<{ size?: number; color?: string }>; icon?: ComponentType<{ size?: number; color?: string }>;
isExpanded: boolean; isExpanded: boolean;
style?: StyleProp<ViewStyle>;
onToggle?: () => void; onToggle?: () => void;
renderDetails?: () => ReactNode; renderDetails?: () => ReactNode;
isLoading?: boolean; isLoading?: boolean;
@@ -1066,6 +1070,7 @@ interface ExpandableBadgeProps {
const ExpandableBadge = memo(function ExpandableBadge({ const ExpandableBadge = memo(function ExpandableBadge({
label, label,
style,
secondaryLabel, secondaryLabel,
icon, icon,
isExpanded, isExpanded,
@@ -1118,7 +1123,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
const IconComponent = icon; const IconComponent = icon;
const iconColor = isError const iconColor = isError
? theme.colors.destructive ? theme.colors.destructive
: theme.colors.foreground; : theme.colors.mutedForeground;
let iconNode: ReactNode = null; let iconNode: ReactNode = null;
if (isLoading) { if (isLoading) {
@@ -1128,22 +1133,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
</Animated.View> </Animated.View>
); );
} else if (isError) { } else if (isError) {
iconNode = <X size={12} color={iconColor} />; iconNode = <TriangleAlertIcon size={12} color={iconColor} opacity={0.8} />;
} else if (IconComponent) { } else if (IconComponent) {
iconNode = <IconComponent size={12} color={iconColor} />; iconNode = <IconComponent size={12} color={iconColor} />;
} }
return ( return (
<View <View style={[expandableBadgeStylesheet.container, style]}>
style={[
expandableBadgeStylesheet.container,
!resolvedDisableOuterSpacing &&
expandableBadgeStylesheet.containerSpacing,
!resolvedDisableOuterSpacing &&
isLastInSequence &&
expandableBadgeStylesheet.containerLastInSequence,
]}
>
<Pressable <Pressable
onPress={hasDetails ? onToggle : undefined} onPress={hasDetails ? onToggle : undefined}
disabled={!hasDetails} disabled={!hasDetails}
@@ -1507,12 +1503,13 @@ export const ToolCall = memo(function ToolCall({
hasDetails && !isMobile hasDetails && !isMobile
? renderDetails ? renderDetails
: hasDetails : hasDetails
? () => null ? () => null
: undefined : undefined
} }
isLoading={status === "executing"} isLoading={status === "executing"}
isError={status === "failed"} isError={status === "failed"}
isLastInSequence={isLastInSequence} isLastInSequence={isLastInSequence}
style={isLastInSequence ? undefined : { marginBottom: theme.spacing[1] }}
disableOuterSpacing={disableOuterSpacing} disableOuterSpacing={disableOuterSpacing}
/> />
); );
+3 -1
View File
@@ -67,7 +67,9 @@ function updateFavicon(status: FaviconStatus, colorScheme: ColorScheme) {
} }
function getSystemColorScheme(): ColorScheme { function getSystemColorScheme(): ColorScheme {
if (typeof window === "undefined") return "dark"; if (Platform.OS !== "web" || typeof window === "undefined" || typeof window.matchMedia !== "function") {
return "dark";
}
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
} }
+1 -1
View File
@@ -18,7 +18,7 @@ StyleSheet.configure({
xl: 1200, xl: 1200,
}, },
settings: { settings: {
initialTheme: "dark", adaptiveThemes: true,
}, },
}); });
+1 -1
View File
@@ -10,7 +10,7 @@ let child: ChildProcess | null = null;
let restarting = false; let restarting = false;
function spawnServer() { function spawnServer() {
child = fork(serverEntry, { child = fork(serverEntry, process.argv.slice(2), {
stdio: "inherit", stdio: "inherit",
env: process.env, env: process.env,
execArgv: ["--import", "tsx"], execArgv: ["--import", "tsx"],
@@ -1408,6 +1408,8 @@ export class DaemonClientV2 {
const parsed = WSOutboundMessageSchema.safeParse(parsedJson); const parsed = WSOutboundMessageSchema.safeParse(parsedJson);
if (!parsed.success) { if (!parsed.success) {
const msgType = (parsedJson as { message?: { type?: string } })?.message?.type ?? "unknown";
console.warn(`[DaemonClientV2] Message validation failed for "${msgType}":`, parsed.error.message);
return; return;
} }
@@ -879,7 +879,10 @@ export class AgentManager {
event: AgentStreamEvent, event: AgentStreamEvent,
options?: { fromHistory?: boolean } options?: { fromHistory?: boolean }
): void { ): void {
agent.updatedAt = new Date(); // Only update timestamp for live events, not history replay
if (!options?.fromHistory) {
agent.updatedAt = new Date();
}
switch (event.type) { switch (event.type) {
case "thread_started": case "thread_started":
+6 -1
View File
@@ -1,4 +1,9 @@
import "dotenv/config"; import dotenv from "dotenv";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
import { createPaseoDaemon } from "./bootstrap.js"; import { createPaseoDaemon } from "./bootstrap.js";
import { loadConfig } from "./config.js"; import { loadConfig } from "./config.js";
import { resolvePaseoHome } from "./paseo-home.js"; import { resolvePaseoHome } from "./paseo-home.js";
+2 -6
View File
@@ -430,14 +430,10 @@ export const GitDiffRequestSchema = z.object({
}); });
// Highlighted diff token schema // Highlighted diff token schema
// Note: style can be a compound class name (e.g., "heading meta") from the syntax highlighter
const HighlightTokenSchema = z.object({ const HighlightTokenSchema = z.object({
text: z.string(), text: z.string(),
style: z.enum([ style: z.string().nullable(),
"keyword", "comment", "string", "number", "literal",
"function", "definition", "class", "type", "tag",
"attribute", "property", "variable", "operator",
"punctuation", "regexp", "escape", "meta", "heading", "link",
]).nullable(),
}); });
const DiffLineSchema = z.object({ const DiffLineSchema = z.object({
+63 -25
View File
@@ -468,6 +468,8 @@ function runAgentWithModel(
"exec", "exec",
"--dangerously-bypass-approvals-and-sandbox", "--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check", "--skip-git-repo-check",
"-c",
"model_reasoning_effort=\"medium\"",
]; ];
if (config.model) { if (config.model) {
args.push("--model", config.model); args.push("--model", config.model);
@@ -540,11 +542,28 @@ function makePlannerPrompt(task: Task, context: string, iteration: number): stri
--- ---
You are a PLANNER agent. Your job is to analyze the task tree and organize work to achieve the top-level goal. You are a PLANNER agent. You ORCHESTRATE work - you do NOT do the work yourself.
Your job is to organize tasks so that WORKER agents can accomplish the top-level acceptance criteria.
Current iteration: ${iteration} Current iteration: ${iteration}
You have FULL ACCESS to the task CLI and can reorganize the ENTIRE task tree under the scope: ## Planning vs Working
As a planner you MAY:
- Look at file structure and test patterns to write good acceptance criteria
- Read the task tree to understand what's been done and what needs reorganizing
- Check naming conventions to make specific, verifiable criteria
As a planner you must NOT:
- Investigate WHY bugs happen (workers will do this)
- Debug or trace through code (workers will do this)
- Try to understand root causes (workers will do this)
- Fix or implement anything (workers will do this)
Take the task descriptions AS GIVEN and create well-organized subtasks for workers to investigate and implement.
## Task CLI Commands
- \`task show ${task.id}\` - view task details with parent context - \`task show ${task.id}\` - view task details with parent context
- \`task list\` - see all tasks - \`task list\` - see all tasks
- \`task tree ${task.id}\` - see full task hierarchy - \`task tree ${task.id}\` - see full task hierarchy
@@ -694,18 +713,17 @@ The ONLY question: Does the acceptance criterion pass when verified? Yes or No.
If an agent left notes explaining why something couldn't be done, IGNORE THE EXPLANATION. Just check: is the criterion met? If an agent left notes explaining why something couldn't be done, IGNORE THE EXPLANATION. Just check: is the criterion met?
## Output Format ## Output Format - CRITICAL
After verification, output EXACTLY one of these XML tags: You MUST output one of these XML tags at the END of your response. This is REQUIRED for the system to parse your verdict:
If ALL criteria pass verification: If ALL criteria pass:
<VERDICT>DONE</VERDICT> <VERDICT>DONE</VERDICT>
If ANY criterion fails verification: If ANY criterion fails:
<VERDICT>NOT_DONE</VERDICT> <VERDICT>NOT_DONE</VERDICT>
<FAILED_CRITERIA>
- criterion that failed: what you checked and what you found If you do not include this exact XML tag, your verdict will not be recorded and the task will retry.
</FAILED_CRITERIA>
Then add a note to the task with your findings: Then add a note to the task with your findings:
\`task note ${task.id} "Judge verdict: [DONE/NOT_DONE]. Details: ..."\` \`task note ${task.id} "Judge verdict: [DONE/NOT_DONE]. Details: ..."\`
@@ -785,10 +803,42 @@ program
}; };
const runTaskLoop = async (): Promise<void> => { const runTaskLoop = async (): Promise<void> => {
// First check for in_progress tasks (resuming from crash)
const allTasks = await store.list();
let candidates = scopeId
? [await store.get(scopeId), ...(await store.getDescendants(scopeId))].filter(Boolean) as Task[]
: allTasks;
const inProgress = candidates.filter((t) => t.status === "in_progress");
if (inProgress.length > 0) {
log(logFile, `Found ${inProgress.length} in_progress task(s) from previous run, resuming...`);
for (const t of inProgress) {
await store.update(t.id, { status: "open" });
log(logFile, `Reset ${t.id} to open`);
}
}
// Step 1: Initial planner run on scope root (if enabled)
if (enablePlanner && scopeId) {
const scopeTask = await store.get(scopeId);
if (scopeTask) {
log(logFile, `[PLANNER] Initial planning on scope root...`);
await runPlanner(scopeTask, "initial planning");
log(logFile, `[DEBUG] Planner finished, continuing to worker loop`);
}
}
log(logFile, `[DEBUG] Entering worker loop`);
// Step 2: Worker/Judge loop on ready tasks
while (true) { while (true) {
// Find ready tasks log(logFile, `[DEBUG] Checking for ready tasks...`);
const ready = await store.getReady(scopeId); const ready = await store.getReady(scopeId);
if (ready.length === 0) break; log(logFile, `[DEBUG] Found ${ready.length} ready tasks`);
if (ready.length === 0) {
log(logFile, `[DEBUG] No ready tasks, exiting loop`);
break;
}
const task = ready[0]; const task = ready[0];
const workerModel = (task.assignee === "codex" ? "gpt-5.2" : "sonnet") as ModelName; const workerModel = (task.assignee === "codex" ? "gpt-5.2" : "sonnet") as ModelName;
@@ -796,20 +846,7 @@ program
log(logFile, `\n=== Starting task: ${task.id} - ${task.title} ===`); log(logFile, `\n=== Starting task: ${task.id} - ${task.title} ===`);
await store.start(task.id); await store.start(task.id);
// Step 1: Initial planner run (if enabled) // Worker/Judge loop
if (enablePlanner) {
const shouldRestart = await runPlanner(task, "initial planning");
if (shouldRestart) {
// Reset task to open so we process children first
const currentTask = await store.get(task.id);
if (currentTask && currentTask.status === "in_progress") {
await store.update(task.id, { status: "open" });
}
continue;
}
}
// Step 2: Worker/Judge loop
let iteration = 1; let iteration = 1;
let taskDone = false; let taskDone = false;
@@ -852,6 +889,7 @@ program
const scopeTask = scopeId ? await store.get(scopeId) : nextReady[0]; const scopeTask = scopeId ? await store.get(scopeId) : nextReady[0];
if (scopeTask) { if (scopeTask) {
await runPlanner(scopeTask, "periodic reassessment"); await runPlanner(scopeTask, "periodic reassessment");
log(logFile, `[DEBUG] Periodic replan finished, continuing loop`);
} }
} }
} }