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
|
? CommandResolver.parseSSHTarget(remoteTarget)?.host
|
||||||
: nil
|
: nil
|
||||||
|
|
||||||
|
// Don't write to gateway config in remote mode - gateway manages its own config
|
||||||
|
guard connectionMode == .local else { return }
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
// Keep app-only connection settings local to avoid overwriting remote gateway config.
|
// Keep app-only connection settings local to avoid overwriting remote gateway config.
|
||||||
var root = MoltbotConfigFile.loadDict()
|
var root = MoltbotConfigFile.loadDict()
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import chokidar from "chokidar";
|
|||||||
import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js";
|
import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js";
|
||||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
import type { MoltbotConfig, ConfigFileSnapshot, GatewayReloadMode } from "../config/config.js";
|
import type { MoltbotConfig, ConfigFileSnapshot, GatewayReloadMode } from "../config/config.js";
|
||||||
|
import { getActiveAgentRunCount, onAgentRunComplete } from "../infra/agent-events.js";
|
||||||
|
|
||||||
export type GatewayReloadSettings = {
|
export type GatewayReloadSettings = {
|
||||||
mode: GatewayReloadMode;
|
mode: GatewayReloadMode;
|
||||||
@ -264,6 +265,8 @@ export function startGatewayConfigReloader(opts: {
|
|||||||
let running = false;
|
let running = false;
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
let restartQueued = false;
|
let restartQueued = false;
|
||||||
|
let queuedPlan: GatewayReloadPlan | null = null;
|
||||||
|
let queuedConfig: MoltbotConfig | null = null;
|
||||||
|
|
||||||
const schedule = () => {
|
const schedule = () => {
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
@ -306,6 +309,16 @@ export function startGatewayConfigReloader(opts: {
|
|||||||
}
|
}
|
||||||
if (settings.mode === "restart") {
|
if (settings.mode === "restart") {
|
||||||
if (!restartQueued) {
|
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;
|
restartQueued = true;
|
||||||
opts.onRestart(plan, nextConfig);
|
opts.onRestart(plan, nextConfig);
|
||||||
}
|
}
|
||||||
@ -321,6 +334,16 @@ export function startGatewayConfigReloader(opts: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!restartQueued) {
|
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;
|
restartQueued = true;
|
||||||
opts.onRestart(plan, nextConfig);
|
opts.onRestart(plan, nextConfig);
|
||||||
}
|
}
|
||||||
@ -356,12 +379,26 @@ export function startGatewayConfigReloader(opts: {
|
|||||||
void watcher.close().catch(() => {});
|
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 {
|
return {
|
||||||
stop: async () => {
|
stop: async () => {
|
||||||
stopped = true;
|
stopped = true;
|
||||||
if (debounceTimer) clearTimeout(debounceTimer);
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
debounceTimer = null;
|
debounceTimer = null;
|
||||||
watcherClosed = true;
|
watcherClosed = true;
|
||||||
|
unregisterCallback();
|
||||||
await watcher.close().catch(() => {});
|
await watcher.close().catch(() => {});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export type AgentRunContext = {
|
|||||||
const seqByRun = new Map<string, number>();
|
const seqByRun = new Map<string, number>();
|
||||||
const listeners = new Set<(evt: AgentEventPayload) => void>();
|
const listeners = new Set<(evt: AgentEventPayload) => void>();
|
||||||
const runContextById = new Map<string, AgentRunContext>();
|
const runContextById = new Map<string, AgentRunContext>();
|
||||||
|
const runCompletionCallbacks = new Set<() => void>();
|
||||||
|
|
||||||
export function registerAgentRunContext(runId: string, context: AgentRunContext) {
|
export function registerAgentRunContext(runId: string, context: AgentRunContext) {
|
||||||
if (!runId) return;
|
if (!runId) return;
|
||||||
@ -46,6 +47,27 @@ export function getAgentRunContext(runId: string) {
|
|||||||
|
|
||||||
export function clearAgentRunContext(runId: string) {
|
export function clearAgentRunContext(runId: string) {
|
||||||
runContextById.delete(runId);
|
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() {
|
export function resetAgentRunContextForTest() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user