Skip to content

Commit d20fdf3

Browse files
ooiuuiisteipete
andauthored
fix(gateway): mark active main sessions before restart shutdown aborts (#91357)
* Mark active main sessions during restart shutdown * Type restart marker mock in close tests * fix(gateway): preserve active run ownership across restart * fix(gateway): preserve active runs across restart * fix(gateway): close restart recovery edge cases * fix(cron): preserve lifecycle ownership across restart * fix(gateway): release rejected run contexts * fix(gateway): preserve restart lifecycle ownership * fix(cron): retain overlapping run ownership * fix(agents): preserve restart terminal precedence --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 689ebc8 commit d20fdf3

78 files changed

Lines changed: 6621 additions & 401 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/sdk/src/client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ function runStatusFromWaitPayload(payload: unknown): RunResult["status"] {
7070
const statusAlreadyTimeoutAttributed = status === "timeout" || status === "timed_out";
7171
const hardTimeout =
7272
!pendingError &&
73-
((record.providerStarted === true && statusAlreadyTimeoutAttributed) ||
73+
((stopReason !== "restart" &&
74+
record.providerStarted === true &&
75+
statusAlreadyTimeoutAttributed) ||
7476
timeoutPhase === "preflight" ||
7577
timeoutPhase === "provider" ||
7678
timeoutPhase === "post_turn");
@@ -93,6 +95,7 @@ function runStatusFromWaitPayload(payload: unknown): RunResult["status"] {
9395
stopReason === "canceled" ||
9496
stopReason === "killed" ||
9597
stopReason === "auth-revoked" ||
98+
stopReason === "restart" ||
9699
stopReason === "rpc" ||
97100
stopReason === "user" ||
98101
(record.aborted === true && stopReason === "stop")

packages/sdk/src/index.test.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,23 @@ describe("OpenClaw SDK", () => {
156156
expect(result.error?.message).toBe("aborted by operator");
157157
});
158158

159+
it("maps restart wait snapshots to cancelled", async () => {
160+
const transport = new FakeTransport({
161+
"agent.wait": {
162+
status: "timeout",
163+
runId: "run_restart",
164+
stopReason: "restart",
165+
providerStarted: true,
166+
},
167+
});
168+
const oc = new OpenClaw({ transport });
169+
170+
const result = await oc.runs.wait("run_restart");
171+
172+
expect(result.runId).toBe("run_restart");
173+
expect(result.status).toBe("cancelled");
174+
});
175+
159176
it("maps provider-started rpc timeout wait snapshots to timed_out", async () => {
160177
const transport = new FakeTransport({
161178
"agent.wait": {
@@ -1209,9 +1226,57 @@ describe("OpenClaw SDK", () => {
12091226
expect(cancelled.runId).toBe("run_1");
12101227
expect(cancelled.data).toEqual({ phase: "end", aborted: true, stopReason: "rpc" });
12111228

1212-
const hardTimeout = normalizeGatewayEvent({
1229+
const restartCancelled = normalizeGatewayEvent({
12131230
event: "agent",
12141231
seq: 6,
1232+
payload: {
1233+
runId: "run_1",
1234+
stream: "lifecycle",
1235+
ts,
1236+
data: {
1237+
phase: "end",
1238+
aborted: true,
1239+
stopReason: "restart",
1240+
providerStarted: true,
1241+
},
1242+
},
1243+
});
1244+
expect(restartCancelled.type).toBe("run.cancelled");
1245+
expect(restartCancelled.runId).toBe("run_1");
1246+
expect(restartCancelled.data).toEqual({
1247+
phase: "end",
1248+
aborted: true,
1249+
stopReason: "restart",
1250+
providerStarted: true,
1251+
});
1252+
1253+
const restartErrorCancelled = normalizeGatewayEvent({
1254+
event: "agent",
1255+
seq: 7,
1256+
payload: {
1257+
runId: "run_1",
1258+
stream: "lifecycle",
1259+
ts,
1260+
data: {
1261+
phase: "error",
1262+
aborted: true,
1263+
stopReason: "restart",
1264+
error: "agent run aborted for restart",
1265+
},
1266+
},
1267+
});
1268+
expect(restartErrorCancelled.type).toBe("run.cancelled");
1269+
expect(restartErrorCancelled.runId).toBe("run_1");
1270+
expect(restartErrorCancelled.data).toEqual({
1271+
phase: "error",
1272+
aborted: true,
1273+
stopReason: "restart",
1274+
error: "agent run aborted for restart",
1275+
});
1276+
1277+
const hardTimeout = normalizeGatewayEvent({
1278+
event: "agent",
1279+
seq: 8,
12151280
payload: {
12161281
runId: "run_1",
12171282
stream: "lifecycle",
@@ -1237,7 +1302,7 @@ describe("OpenClaw SDK", () => {
12371302

12381303
const hardTimeoutError = normalizeGatewayEvent({
12391304
event: "agent",
1240-
seq: 7,
1305+
seq: 9,
12411306
payload: {
12421307
runId: "run_1",
12431308
stream: "lifecycle",
@@ -1261,7 +1326,7 @@ describe("OpenClaw SDK", () => {
12611326

12621327
const providerStartedError = normalizeGatewayEvent({
12631328
event: "agent",
1264-
seq: 8,
1329+
seq: 10,
12651330
payload: {
12661331
runId: "run_1",
12671332
stream: "lifecycle",
@@ -1283,7 +1348,7 @@ describe("OpenClaw SDK", () => {
12831348

12841349
const hardTimeoutEnd = normalizeGatewayEvent({
12851350
event: "agent",
1286-
seq: 9,
1351+
seq: 11,
12871352
payload: {
12881353
runId: "run_1",
12891354
stream: "lifecycle",
@@ -1305,7 +1370,7 @@ describe("OpenClaw SDK", () => {
13051370

13061371
const providerStartedEnd = normalizeGatewayEvent({
13071372
event: "agent",
1308-
seq: 10,
1373+
seq: 12,
13091374
payload: {
13101375
runId: "run_1",
13111376
stream: "lifecycle",
@@ -1325,7 +1390,7 @@ describe("OpenClaw SDK", () => {
13251390

13261391
const authRevoked = normalizeGatewayEvent({
13271392
event: "agent",
1328-
seq: 10,
1393+
seq: 13,
13291394
payload: {
13301395
runId: "run_1",
13311396
stream: "lifecycle",
@@ -1343,7 +1408,7 @@ describe("OpenClaw SDK", () => {
13431408

13441409
const timedOut = normalizeGatewayEvent({
13451410
event: "agent",
1346-
seq: 11,
1411+
seq: 14,
13471412
payload: {
13481413
runId: "run_1",
13491414
stream: "lifecycle",

packages/sdk/src/normalize.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,10 @@ function hasHardTimeoutMetadata(data: JsonObject, statusAlreadyTimeoutAttributed
2828
);
2929
}
3030

31-
function normalizeLifecycleEndEventType(data: JsonObject): OpenClawEventType {
31+
function isLifecycleCancellation(data: JsonObject): boolean {
3232
const status = readLowerString(data.status);
3333
const stopReason = readLowerString(data.stopReason);
34-
const statusAlreadyTimeoutAttributed =
35-
status === "timeout" || status === "timed_out" || data.aborted === true;
36-
if (hasHardTimeoutMetadata(data, statusAlreadyTimeoutAttributed)) {
37-
return "run.timed_out";
38-
}
39-
if (
34+
return (
4035
status === "aborted" ||
4136
status === "cancelled" ||
4237
status === "canceled" ||
@@ -46,10 +41,23 @@ function normalizeLifecycleEndEventType(data: JsonObject): OpenClawEventType {
4641
stopReason === "canceled" ||
4742
stopReason === "killed" ||
4843
stopReason === "auth-revoked" ||
44+
stopReason === "restart" ||
4945
stopReason === "rpc" ||
5046
stopReason === "user" ||
5147
(data.aborted === true && stopReason === "stop")
52-
) {
48+
);
49+
}
50+
51+
function normalizeLifecycleEndEventType(data: JsonObject): OpenClawEventType {
52+
const status = readLowerString(data.status);
53+
const stopReason = readLowerString(data.stopReason);
54+
const statusAlreadyTimeoutAttributed =
55+
stopReason !== "restart" &&
56+
(status === "timeout" || status === "timed_out" || data.aborted === true);
57+
if (hasHardTimeoutMetadata(data, statusAlreadyTimeoutAttributed)) {
58+
return "run.timed_out";
59+
}
60+
if (isLifecycleCancellation(data)) {
5361
return "run.cancelled";
5462
}
5563
if (
@@ -91,6 +99,9 @@ function normalizeAgentEventType(payload: JsonObject): OpenClawEventType {
9199
if (hasHardTimeoutMetadata(data, false)) {
92100
return "run.timed_out";
93101
}
102+
if (isLifecycleCancellation(data)) {
103+
return "run.cancelled";
104+
}
94105
return "run.failed";
95106
}
96107
}

0 commit comments

Comments
 (0)