fix(agent): keep background sandbox tasks running
Generic Container CI/CD / test-build-publish (push) Successful in 1m47s
Agent CI/CD / build-test-publish-and-deploy (push) Successful in 1m48s

Auto-approve sandbox Shell only for host-bound task execution, stage native results within their owning session, and preserve active lease state on response stream loss. The repeated failures came from interactive approval in unattended runs and transport cleanup invalidating execution state; focused regressions cover both boundaries.
This commit is contained in:
2026-11-08 01:54:48 +08:00
parent d8eb29466a
commit a7ddff7b64
10 changed files with 242 additions and 37 deletions
+43 -29
View File
@@ -116,6 +116,47 @@ const toSessionUiStateContext = (sessionRecord: SessionRecord) => ({
const toSse = (event: string, data: Record<string, unknown>) =>
`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
export async function runBackgroundTaskExecution(input: {
sessionId: string;
credential: ProductCredential;
model: string;
workflow?: string;
isRunning: () => boolean;
}) {
const { sessionId, credential, model, workflow } = input;
const baseUrl = process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? `http://127.0.0.1:${config.PORT}`;
await fetch(new URL("/api/v1/responses", baseUrl), {
method: "POST",
headers: {
Authorization: credential.authorization,
"Content-Type": "application/json",
"X-Project-Id": credential.projectId,
},
body: JSON.stringify({
conversation: sessionId,
model,
stream: true,
input: buildTaskExecutionPrompt(workflow),
}),
}).then(async (response) => {
const reader = response.body?.getReader();
if (reader) {
while (!(await reader.read()).done) {
// Drain the stream without retaining the model response in memory.
}
}
if (!response.ok) {
if (!input.isRunning()) clearOperationalTaskLease(sessionId);
logger.error({ sessionId, status: response.status, executionContinues: input.isRunning() }, "background task execution failed");
}
}).catch((error) => {
// A lost response stream does not end the host-owned execution. Its route
// still renews/finalizes the lease and owns cleanup of the cached revision.
if (!input.isRunning()) clearOperationalTaskLease(sessionId);
logger.warn({ err: error, sessionId, executionContinues: input.isRunning() }, "background task response stream failed");
});
}
export const registerChatSessionRoutes = (
chatRouter: Router,
options: RegisterChatSessionRoutesOptions,
@@ -183,35 +224,8 @@ export const registerChatSessionRoutes = (
workflow?: string,
) => {
queueMicrotask(() => {
const baseUrl = process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? `http://127.0.0.1:${config.PORT}`;
void fetch(new URL("/api/v1/responses", baseUrl), {
method: "POST",
headers: {
Authorization: credential.authorization,
"Content-Type": "application/json",
"X-Project-Id": credential.projectId,
},
body: JSON.stringify({
conversation: record.sessionId,
model,
stream: true,
input: buildTaskExecutionPrompt(workflow),
}),
}).then(async (response) => {
const reader = response.body?.getReader();
if (reader) {
while (!(await reader.read()).done) {
// Drain the stream without retaining the model response in memory.
}
}
if (!response.ok) {
clearOperationalTaskLease(record.sessionId);
logger.error({ sessionId: record.sessionId, status: response.status }, "background task execution failed");
}
}).catch((error) => {
clearOperationalTaskLease(record.sessionId);
logger.error({ err: error, sessionId: record.sessionId }, "background task execution request failed");
});
void runBackgroundTaskExecution({ sessionId: record.sessionId, credential, model, workflow,
isRunning: () => activeRuns.get(record.sessionId)?.status === "running" });
});
};