mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 09:07:39 +00:00
Runtime: stabilize tool/run state transitions under compaction and backpressure
Synthesize runtime state transition fixes for compaction tool-use integrity and long-running handler backpressure. Sources: #33630, #33583 Co-authored-by: Kevin Shenghui <shenghuikevin@gmail.com> Co-authored-by: Theo Tarr <theodore@tarr.com>
This commit is contained in:
@@ -120,6 +120,9 @@ export type ChannelAccountSnapshot = {
|
||||
lastStopAt?: number | null;
|
||||
lastInboundAt?: number | null;
|
||||
lastOutboundAt?: number | null;
|
||||
busy?: boolean;
|
||||
activeRuns?: number;
|
||||
lastRunActivityAt?: number | null;
|
||||
mode?: string;
|
||||
dmPolicy?: string;
|
||||
allowFrom?: string[];
|
||||
|
||||
42
src/channels/run-state-machine.test.ts
Normal file
42
src/channels/run-state-machine.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createRunStateMachine } from "./run-state-machine.js";
|
||||
|
||||
describe("createRunStateMachine", () => {
|
||||
it("resets stale busy fields on init", () => {
|
||||
const setStatus = vi.fn();
|
||||
createRunStateMachine({ setStatus });
|
||||
expect(setStatus).toHaveBeenCalledWith({ activeRuns: 0, busy: false });
|
||||
});
|
||||
|
||||
it("emits busy status while active and clears when done", () => {
|
||||
const setStatus = vi.fn();
|
||||
const machine = createRunStateMachine({
|
||||
setStatus,
|
||||
now: () => 123,
|
||||
});
|
||||
machine.onRunStart();
|
||||
machine.onRunEnd();
|
||||
expect(setStatus).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ activeRuns: 1, busy: true, lastRunActivityAt: 123 }),
|
||||
);
|
||||
expect(setStatus).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ activeRuns: 0, busy: false, lastRunActivityAt: 123 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("stops publishing after lifecycle abort", () => {
|
||||
const setStatus = vi.fn();
|
||||
const abortController = new AbortController();
|
||||
const machine = createRunStateMachine({
|
||||
setStatus,
|
||||
abortSignal: abortController.signal,
|
||||
now: () => 999,
|
||||
});
|
||||
machine.onRunStart();
|
||||
const callsBeforeAbort = setStatus.mock.calls.length;
|
||||
abortController.abort();
|
||||
machine.onRunEnd();
|
||||
expect(setStatus.mock.calls.length).toBe(callsBeforeAbort);
|
||||
});
|
||||
});
|
||||
99
src/channels/run-state-machine.ts
Normal file
99
src/channels/run-state-machine.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
export type RunStateStatusPatch = {
|
||||
busy?: boolean;
|
||||
activeRuns?: number;
|
||||
lastRunActivityAt?: number | null;
|
||||
};
|
||||
|
||||
export type RunStateStatusSink = (patch: RunStateStatusPatch) => void;
|
||||
|
||||
type RunStateMachineParams = {
|
||||
setStatus?: RunStateStatusSink;
|
||||
abortSignal?: AbortSignal;
|
||||
heartbeatMs?: number;
|
||||
now?: () => number;
|
||||
};
|
||||
|
||||
const DEFAULT_RUN_ACTIVITY_HEARTBEAT_MS = 60_000;
|
||||
|
||||
export function createRunStateMachine(params: RunStateMachineParams) {
|
||||
const heartbeatMs = params.heartbeatMs ?? DEFAULT_RUN_ACTIVITY_HEARTBEAT_MS;
|
||||
const now = params.now ?? Date.now;
|
||||
let activeRuns = 0;
|
||||
let runActivityHeartbeat: ReturnType<typeof setInterval> | null = null;
|
||||
let lifecycleActive = !params.abortSignal?.aborted;
|
||||
|
||||
const publish = () => {
|
||||
if (!lifecycleActive) {
|
||||
return;
|
||||
}
|
||||
params.setStatus?.({
|
||||
activeRuns,
|
||||
busy: activeRuns > 0,
|
||||
lastRunActivityAt: now(),
|
||||
});
|
||||
};
|
||||
|
||||
const clearHeartbeat = () => {
|
||||
if (!runActivityHeartbeat) {
|
||||
return;
|
||||
}
|
||||
clearInterval(runActivityHeartbeat);
|
||||
runActivityHeartbeat = null;
|
||||
};
|
||||
|
||||
const ensureHeartbeat = () => {
|
||||
if (runActivityHeartbeat || activeRuns <= 0 || !lifecycleActive) {
|
||||
return;
|
||||
}
|
||||
runActivityHeartbeat = setInterval(() => {
|
||||
if (!lifecycleActive || activeRuns <= 0) {
|
||||
clearHeartbeat();
|
||||
return;
|
||||
}
|
||||
publish();
|
||||
}, heartbeatMs);
|
||||
runActivityHeartbeat.unref?.();
|
||||
};
|
||||
|
||||
const deactivate = () => {
|
||||
lifecycleActive = false;
|
||||
clearHeartbeat();
|
||||
};
|
||||
|
||||
const onAbort = () => {
|
||||
deactivate();
|
||||
};
|
||||
|
||||
if (params.abortSignal?.aborted) {
|
||||
onAbort();
|
||||
} else {
|
||||
params.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
|
||||
if (lifecycleActive) {
|
||||
// Reset inherited status from previous process lifecycle.
|
||||
params.setStatus?.({
|
||||
activeRuns: 0,
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isActive() {
|
||||
return lifecycleActive;
|
||||
},
|
||||
onRunStart() {
|
||||
activeRuns += 1;
|
||||
publish();
|
||||
ensureHeartbeat();
|
||||
},
|
||||
onRunEnd() {
|
||||
activeRuns = Math.max(0, activeRuns - 1);
|
||||
if (activeRuns <= 0) {
|
||||
clearHeartbeat();
|
||||
}
|
||||
publish();
|
||||
},
|
||||
deactivate,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user