authorgravatar for maslinje@gmail.comMaslin Edwin <maslinje@gmail.com> 2026-08-18 19:39:23+07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-18 18:09:23+05:30
log82b8a9380298509d68170961d9717be62836e490
tree985b0bfd1d382b35eb3207b577060974bd7626f3
parentcebac353defde6211c9e8c3d8ecd140c92042930
signature Signed by PGP key B5690EEEBB952194

fix(orchestration): do not revive idle tasks from status-free progress (#7172)


2 files changed, 39 insertions(+), 0 deletions(-)

apps/server/src/orchestration/ThreadBackgroundLiveness.test.ts+26
......@@ -2,6 +2,32 @@ import { describe, expect, it } from "vite-plus/test";
22import * as ThreadBackgroundLiveness from "./ThreadBackgroundLiveness.ts";
33
44describe("ThreadBackgroundLiveness", () => {
5 it("does not let status-free progress restart an idle task", () => {
6 const liveness = ThreadBackgroundLiveness.make();
7 liveness.recordTaskLiveness({
8 threadId: "thread",
9 taskId: "task",
10 taskType: undefined,
11 status: undefined,
12 kind: "started",
13 });
14 liveness.recordTaskLiveness({
15 threadId: "thread",
16 taskId: "task",
17 taskType: undefined,
18 status: "idle",
19 kind: "updated",
20 });
21 liveness.recordTaskLiveness({
22 threadId: "thread",
23 taskId: "task",
24 taskType: undefined,
25 status: undefined,
26 kind: "progress",
27 });
28 expect(liveness.getThreadBackgroundLiveness("thread")).toBeNull();
29 });
30
531 it("agents present as working; monitors as monitoring; agents win", () => {
632 const liveness = ThreadBackgroundLiveness.make();
733 const threadId = "t-live-1";
apps/server/src/orchestration/ThreadBackgroundLiveness.ts+13
......@@ -130,6 +130,19 @@ export function make(): ThreadBackgroundLivenessService["Service"] {
130130 return;
131131 }
132132
133 // Status-free progress is a description tick, not a restart. A delayed
134 // progress event after idle must not put the task back in the live set
135 // (#7128).
136 if (input.kind === "progress" && input.status === undefined) {
137 const existing = stateByThreadId.get(input.threadId);
138 const stillLive =
139 existing !== undefined &&
140 (existing.agents.has(input.taskId) || existing.monitors.has(input.taskId));
141 if (!stillLive) {
142 return;
143 }
144 }
145
133146 drop(input.threadId, input.taskId);
134147 const state = stateFor(input.threadId);
135148 const bucket =