Merge pull request #1478 from pikasTech/fix/issue-1422-terminal-effects-async
fix: 异步化终态 turn status 副作用
This commit is contained in:
@@ -2399,6 +2399,15 @@ test("cloud api turn status skips AgentRun refresh for complete terminal evidenc
|
|||||||
test("cloud api turn status records terminal side effects once (#1422)", async () => {
|
test("cloud api turn status records terminal side effects once (#1422)", async () => {
|
||||||
const ownerCalls = [];
|
const ownerCalls = [];
|
||||||
const factCalls = [];
|
const factCalls = [];
|
||||||
|
let releaseOwnerWrite;
|
||||||
|
let ownerWriteReleased = false;
|
||||||
|
const ownerWrite = new Promise((resolve) => {
|
||||||
|
releaseOwnerWrite = () => {
|
||||||
|
if (ownerWriteReleased) return;
|
||||||
|
ownerWriteReleased = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
const traceId = "trc_issue1422_terminal_effects_once";
|
const traceId = "trc_issue1422_terminal_effects_once";
|
||||||
const runId = "run_issue1422_terminal_effects_once";
|
const runId = "run_issue1422_terminal_effects_once";
|
||||||
const commandId = "cmd_issue1422_terminal_effects_once";
|
const commandId = "cmd_issue1422_terminal_effects_once";
|
||||||
@@ -2461,6 +2470,7 @@ test("cloud api turn status records terminal side effects once (#1422)", async (
|
|||||||
},
|
},
|
||||||
async recordAgentSessionOwner(input) {
|
async recordAgentSessionOwner(input) {
|
||||||
ownerCalls.push(input);
|
ownerCalls.push(input);
|
||||||
|
await ownerWrite;
|
||||||
return { ok: true, sessionId: input.sessionId };
|
return { ok: true, sessionId: input.sessionId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2469,19 +2479,31 @@ test("cloud api turn status records terminal side effects once (#1422)", async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { port } = server.address();
|
const { port } = server.address();
|
||||||
for (let i = 0; i < 2; i += 1) {
|
const fetchTurnStatus = async () => {
|
||||||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/turns/${traceId}`, {
|
const response = await Promise.race([
|
||||||
headers: { cookie: "hwlab_session=test-stub-session" }
|
fetch(`http://127.0.0.1:${port}/v1/agent/turns/${traceId}`, {
|
||||||
});
|
headers: { cookie: "hwlab_session=test-stub-session" }
|
||||||
|
}),
|
||||||
|
delay(250).then(() => null)
|
||||||
|
]);
|
||||||
|
assert.ok(response, "turn status should not wait for terminal side effects to finish");
|
||||||
assert.equal(response.status, 200);
|
assert.equal(response.status, 200);
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
assert.equal(body.status, "completed");
|
assert.equal(body.status, "completed");
|
||||||
assert.equal(body.finalResponse.text, finalText);
|
assert.equal(body.finalResponse.text, finalText);
|
||||||
|
};
|
||||||
|
for (let i = 0; i < 2; i += 1) {
|
||||||
|
await fetchTurnStatus();
|
||||||
}
|
}
|
||||||
|
for (let i = 0; i < 20 && ownerCalls.length < 1; i += 1) await delay(10);
|
||||||
assert.equal(ownerCalls.length, 1);
|
assert.equal(ownerCalls.length, 1);
|
||||||
assert.equal(factCalls.length, 1);
|
assert.equal(factCalls.length, 1);
|
||||||
|
assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects.pending, true);
|
||||||
|
releaseOwnerWrite();
|
||||||
|
for (let i = 0; i < 20 && codeAgentChatResults.get(traceId).turnStatusTerminalEffects?.recorded !== true; i += 1) await delay(10);
|
||||||
assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects.recorded, true);
|
assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects.recorded, true);
|
||||||
} finally {
|
} finally {
|
||||||
|
releaseOwnerWrite?.();
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
server.close((error) => (error ? reject(error) : resolve()));
|
server.close((error) => (error ? reject(error) : resolve()));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1222,7 +1222,7 @@ async function resolveCodeAgentTurnStatusSnapshot(traceId, options) {
|
|||||||
turnRefreshSatisfiedByResultSync = synced.eventsRefreshed === true || synced.resultSynced === true || synced.terminalRefreshSkipped === true;
|
turnRefreshSatisfiedByResultSync = synced.eventsRefreshed === true || synced.resultSynced === true || synced.terminalRefreshSkipped === true;
|
||||||
if (result && !canAccessOwnedResult(result, options.actor)) return forbiddenTurnSnapshot(traceId);
|
if (result && !canAccessOwnedResult(result, options.actor)) return forbiddenTurnSnapshot(traceId);
|
||||||
if (result && isTraceCommandTerminalStatus(result.status)) {
|
if (result && isTraceCommandTerminalStatus(result.status)) {
|
||||||
await recordCodeAgentTerminalTurnStatusEffects({ payload: result, params: result, options });
|
scheduleCodeAgentTerminalTurnStatusEffects({ payload: result, params: result, options });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
resultPollError = error;
|
resultPollError = error;
|
||||||
@@ -1263,7 +1263,7 @@ async function resolveCodeAgentTurnStatusSnapshot(traceId, options) {
|
|||||||
agentRunResult = synced.result ?? agentRunResult;
|
agentRunResult = synced.result ?? agentRunResult;
|
||||||
}
|
}
|
||||||
if (isTraceCommandTerminalStatus(agentRunResult?.status)) {
|
if (isTraceCommandTerminalStatus(agentRunResult?.status)) {
|
||||||
await recordCodeAgentTerminalTurnStatusEffects({ payload: agentRunResult, params: agentRunResult, options, preserveLastTraceId: true });
|
scheduleCodeAgentTerminalTurnStatusEffects({ payload: agentRunResult, params: agentRunResult, options, preserveLastTraceId: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
refreshError = error;
|
refreshError = error;
|
||||||
@@ -1788,20 +1788,64 @@ async function recordCodeAgentTerminalTurnStatusEffects({ payload = {}, params =
|
|||||||
const ownerSettled = !ownerRequired || Boolean(owner);
|
const ownerSettled = !ownerRequired || Boolean(owner);
|
||||||
const effects = {
|
const effects = {
|
||||||
recorded: billingSettled && ownerSettled,
|
recorded: billingSettled && ownerSettled,
|
||||||
|
pending: false,
|
||||||
billingSettled,
|
billingSettled,
|
||||||
ownerSettled,
|
ownerSettled,
|
||||||
preserveLastTraceId: Boolean(preserveLastTraceId),
|
preserveLastTraceId: Boolean(preserveLastTraceId),
|
||||||
recordedAt: new Date().toISOString(),
|
recordedAt: new Date().toISOString(),
|
||||||
valuesPrinted: false
|
valuesPrinted: false
|
||||||
};
|
};
|
||||||
if (effects.recorded) {
|
payload.turnStatusTerminalEffects = effects;
|
||||||
payload.turnStatusTerminalEffects = effects;
|
const traceId = safeTraceId(payload.traceId ?? params.traceId);
|
||||||
const traceId = safeTraceId(payload.traceId ?? params.traceId);
|
if (traceId) options.codeAgentChatResults?.set?.(traceId, payload);
|
||||||
if (traceId) options.codeAgentChatResults?.set?.(traceId, payload);
|
|
||||||
}
|
|
||||||
return effects;
|
return effects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleCodeAgentTerminalTurnStatusEffects({ payload = {}, params = {}, options = {}, preserveLastTraceId = false } = {}) {
|
||||||
|
if (!payload || typeof payload !== "object" || !isTraceCommandTerminalStatus(payload.status)) return null;
|
||||||
|
const existing = payload.turnStatusTerminalEffects;
|
||||||
|
if (existing?.recorded === true || existing?.pending === true) return existing;
|
||||||
|
const traceId = safeTraceId(payload.traceId ?? params.traceId);
|
||||||
|
const pending = {
|
||||||
|
recorded: false,
|
||||||
|
pending: true,
|
||||||
|
billingSettled: false,
|
||||||
|
ownerSettled: false,
|
||||||
|
preserveLastTraceId: Boolean(preserveLastTraceId),
|
||||||
|
scheduledAt: new Date().toISOString(),
|
||||||
|
valuesPrinted: false
|
||||||
|
};
|
||||||
|
payload.turnStatusTerminalEffects = pending;
|
||||||
|
if (traceId) options.codeAgentChatResults?.set?.(traceId, payload);
|
||||||
|
setImmediate(() => {
|
||||||
|
void recordCodeAgentTerminalTurnStatusEffects({ payload, params, options, preserveLastTraceId }).catch((error) => {
|
||||||
|
const failed = {
|
||||||
|
recorded: false,
|
||||||
|
pending: false,
|
||||||
|
billingSettled: false,
|
||||||
|
ownerSettled: false,
|
||||||
|
preserveLastTraceId: Boolean(preserveLastTraceId),
|
||||||
|
errorCode: error?.code ?? "terminal_turn_status_effects_failed",
|
||||||
|
recordedAt: new Date().toISOString(),
|
||||||
|
valuesPrinted: false
|
||||||
|
};
|
||||||
|
payload.turnStatusTerminalEffects = failed;
|
||||||
|
if (traceId) {
|
||||||
|
options.codeAgentChatResults?.set?.(traceId, payload);
|
||||||
|
(options.traceStore ?? defaultCodeAgentTraceStore).append(traceId, {
|
||||||
|
type: "turn-status",
|
||||||
|
status: "degraded",
|
||||||
|
label: "turn-status:terminal-effects-failed",
|
||||||
|
errorCode: failed.errorCode,
|
||||||
|
message: error?.message ?? "Terminal turn status side effects failed and will retry on the next poll.",
|
||||||
|
valuesPrinted: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
function codeAgentTerminalBillingSettled({ payload = {}, params = {}, options = {}, billing = null } = {}) {
|
function codeAgentTerminalBillingSettled({ payload = {}, params = {}, options = {}, billing = null } = {}) {
|
||||||
const reservation = params.userBillingReservation ?? payload.userBillingReservation;
|
const reservation = params.userBillingReservation ?? payload.userBillingReservation;
|
||||||
const reservationId = typeof reservation?.reservationId === "string" ? reservation.reservationId : "";
|
const reservationId = typeof reservation?.reservationId === "string" ? reservation.reservationId : "";
|
||||||
|
|||||||
Reference in New Issue
Block a user