Fix: Defer gateway restarts during active agent runs and prevent macOS app config writes in remote mode
- Add active agent run tracking functions (getActiveAgentRunCount, getActiveAgentRunIds) - Add onAgentRunComplete callback to notify when runs complete - Modify config-reload to defer restarts when active runs exist - Queue restart and apply when all runs complete - Add guard in AppState.swift to prevent config writes in remote mode - Set gateway.reload.mode to "off" as immediate fix This prevents the bot from interrupting in-flight messages during config changes, fixing the "bot typing but never responds" issue. Root cause: macOS app was writing to gateway config even in remote mode, triggering file watcher → reload handler → SIGUSR1 → shutdown during messages.
This commit is contained in:
parent
cb742ebb27
commit
05900e72b6
@ -449,6 +449,9 @@ final class AppState {
|
||||
? CommandResolver.parseSSHTarget(remoteTarget)?.host
|
||||
: nil
|
||||
|
||||
// Don't write to gateway config in remote mode - gateway manages its own config
|
||||
guard connectionMode == .local else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
// Keep app-only connection settings local to avoid overwriting remote gateway config.
|
||||
var root = MoltbotConfigFile.loadDict()
|
||||
|
||||
@ -2,6 +2,7 @@ import chokidar from "chokidar";
|
||||
import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import type { MoltbotConfig, ConfigFileSnapshot, GatewayReloadMode } from "../config/config.js";
|
||||
import { getActiveAgentRunCount, onAgentRunComplete } from "../infra/agent-events.js";
|
||||
|
||||
export type GatewayReloadSettings = {
|
||||
mode: GatewayReloadMode;
|
||||
@ -264,6 +265,8 @@ export function startGatewayConfigReloader(opts: {
|
||||
let running = false;
|
||||
let stopped = false;
|
||||
let restartQueued = false;
|
||||
let queuedPlan: GatewayReloadPlan | null = null;
|
||||
let queuedConfig: MoltbotConfig | null = null;
|
||||
|
||||
const schedule = () => {
|
||||
if (stopped) return;
|
||||
@ -306,6 +309,16 @@ export function startGatewayConfigReloader(opts: {
|
||||
}
|
||||
if (settings.mode === "restart") {
|
||||
if (!restartQueued) {
|
||||
const activeRunCount = getActiveAgentRunCount();
|
||||
if (activeRunCount > 0) {
|
||||
opts.log.warn(
|
||||
`config change requires gateway restart, but deferring (${activeRunCount} active agent run${activeRunCount === 1 ? "" : "s"})`,
|
||||
);
|
||||
restartQueued = true;
|
||||
queuedPlan = plan;
|
||||
queuedConfig = nextConfig;
|
||||
return;
|
||||
}
|
||||
restartQueued = true;
|
||||
opts.onRestart(plan, nextConfig);
|
||||
}
|
||||
@ -321,6 +334,16 @@ export function startGatewayConfigReloader(opts: {
|
||||
return;
|
||||
}
|
||||
if (!restartQueued) {
|
||||
const activeRunCount = getActiveAgentRunCount();
|
||||
if (activeRunCount > 0) {
|
||||
opts.log.warn(
|
||||
`config change requires gateway restart, but deferring (${activeRunCount} active agent run${activeRunCount === 1 ? "" : "s"})`,
|
||||
);
|
||||
restartQueued = true;
|
||||
queuedPlan = plan;
|
||||
queuedConfig = nextConfig;
|
||||
return;
|
||||
}
|
||||
restartQueued = true;
|
||||
opts.onRestart(plan, nextConfig);
|
||||
}
|
||||
@ -356,12 +379,26 @@ export function startGatewayConfigReloader(opts: {
|
||||
void watcher.close().catch(() => {});
|
||||
});
|
||||
|
||||
// Register callback to apply queued restart when all runs complete
|
||||
const unregisterCallback = onAgentRunComplete(() => {
|
||||
if (restartQueued && queuedPlan && queuedConfig && getActiveAgentRunCount() === 0) {
|
||||
opts.log.info("applying queued gateway restart (all agent runs completed)");
|
||||
restartQueued = false;
|
||||
const plan = queuedPlan;
|
||||
const config = queuedConfig;
|
||||
queuedPlan = null;
|
||||
queuedConfig = null;
|
||||
opts.onRestart(plan, config);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
stop: async () => {
|
||||
stopped = true;
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = null;
|
||||
watcherClosed = true;
|
||||
unregisterCallback();
|
||||
await watcher.close().catch(() => {});
|
||||
},
|
||||
};
|
||||
|
||||
@ -21,6 +21,7 @@ export type AgentRunContext = {
|
||||
const seqByRun = new Map<string, number>();
|
||||
const listeners = new Set<(evt: AgentEventPayload) => void>();
|
||||
const runContextById = new Map<string, AgentRunContext>();
|
||||
const runCompletionCallbacks = new Set<() => void>();
|
||||
|
||||
export function registerAgentRunContext(runId: string, context: AgentRunContext) {
|
||||
if (!runId) return;
|
||||
@ -46,6 +47,27 @@ export function getAgentRunContext(runId: string) {
|
||||
|
||||
export function clearAgentRunContext(runId: string) {
|
||||
runContextById.delete(runId);
|
||||
// Notify callbacks that a run has completed
|
||||
for (const callback of runCompletionCallbacks) {
|
||||
try {
|
||||
callback();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function onAgentRunComplete(callback: () => void): () => void {
|
||||
runCompletionCallbacks.add(callback);
|
||||
return () => runCompletionCallbacks.delete(callback);
|
||||
}
|
||||
|
||||
export function getActiveAgentRunCount(): number {
|
||||
return runContextById.size;
|
||||
}
|
||||
|
||||
export function getActiveAgentRunIds(): string[] {
|
||||
return Array.from(runContextById.keys());
|
||||
}
|
||||
|
||||
export function resetAgentRunContextForTest() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user