Merge pull request #1427 from pikasTech/fix/1422-trace-terminal-freeze
修正 Code Agent terminal trace 后续污染
This commit is contained in:
@@ -101,3 +101,71 @@ test("code agent trace store preserves upstream event timestamps", () => {
|
||||
assert.equal(snapshot.events[0].backend, "agentrun-v01/codex");
|
||||
assert.equal(snapshot.elapsedMs, 0);
|
||||
});
|
||||
|
||||
test("code agent trace store anchors trace time to replayed upstream events", () => {
|
||||
const traceStore = createCodeAgentTraceStore({ maxEvents: 20 });
|
||||
const traceId = "trc_trace-store-replayed-upstream-time";
|
||||
traceStore.ensure(traceId, { now: "2026-06-17T13:03:50.000Z" });
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:backend:run-created",
|
||||
createdAt: "2026-06-17T12:56:13.650Z"
|
||||
}, { now: "2026-06-17T13:03:51.000Z" });
|
||||
traceStore.append(traceId, {
|
||||
type: "assistant",
|
||||
status: "completed",
|
||||
label: "agentrun:assistant:message",
|
||||
message: "OK",
|
||||
terminal: true,
|
||||
createdAt: "2026-06-17T12:56:39.484Z"
|
||||
}, { now: "2026-06-17T13:03:52.000Z" });
|
||||
|
||||
const snapshot = traceStore.snapshot(traceId);
|
||||
assert.equal(snapshot.createdAt, "2026-06-17T12:56:13.650Z");
|
||||
assert.equal(snapshot.startedAt, "2026-06-17T12:56:13.650Z");
|
||||
assert.equal(snapshot.updatedAt, "2026-06-17T12:56:39.484Z");
|
||||
assert.equal(snapshot.finishedAt, "2026-06-17T12:56:39.484Z");
|
||||
assert.equal(snapshot.elapsedMs, 25834);
|
||||
});
|
||||
|
||||
test("code agent trace store freezes terminal traces against stale post-terminal events", () => {
|
||||
const traceStore = createCodeAgentTraceStore({ maxEvents: 20 });
|
||||
const traceId = "trc_trace-store-terminal-freeze";
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:backend:turn/started",
|
||||
createdAt: "2026-06-17T12:56:27.325Z"
|
||||
});
|
||||
traceStore.append(traceId, {
|
||||
type: "result",
|
||||
status: "completed",
|
||||
label: "agentrun:result:completed",
|
||||
message: "AgentRun result is ready for HWLAB short-connection polling.",
|
||||
terminal: true,
|
||||
createdAt: "2026-06-17T12:56:41.293Z"
|
||||
});
|
||||
traceStore.append(traceId, {
|
||||
type: "result",
|
||||
status: "completed",
|
||||
label: "agentrun:result:completed",
|
||||
message: "AgentRun result is ready for HWLAB short-connection polling.",
|
||||
terminal: true,
|
||||
createdAt: "2026-06-17T12:56:41.293Z"
|
||||
});
|
||||
traceStore.append(traceId, {
|
||||
type: "backend",
|
||||
status: "running",
|
||||
label: "agentrun:backend:runner-claim-waiting-for-stale-lease",
|
||||
createdAt: "2026-06-17T13:04:49.500Z"
|
||||
});
|
||||
|
||||
const snapshot = traceStore.snapshot(traceId);
|
||||
assert.equal(snapshot.eventCount, 2);
|
||||
assert.deepEqual(snapshot.events.map((item) => item.label), ["agentrun:backend:turn/started", "agentrun:result:completed"]);
|
||||
assert.equal(snapshot.status, "completed");
|
||||
assert.equal(snapshot.updatedAt, "2026-06-17T12:56:41.293Z");
|
||||
assert.equal(snapshot.finishedAt, "2026-06-17T12:56:41.293Z");
|
||||
assert.equal(snapshot.elapsedMs, 13968);
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ export function createCodeAgentTraceStore(options = {}) {
|
||||
nextSeq: 1,
|
||||
events: [],
|
||||
sourceEventKeys: new Set(),
|
||||
eventSignatures: new Set(),
|
||||
assistantStreams: new Map(),
|
||||
listeners: new Set(),
|
||||
meta: {
|
||||
@@ -68,27 +69,41 @@ export function createCodeAgentTraceStore(options = {}) {
|
||||
now: meta.now,
|
||||
fallbackRunnerKind: trace.meta.runnerKind
|
||||
});
|
||||
const normalizedSignature = traceEventSignature(normalized);
|
||||
if (normalizedSignature && trace.eventSignatures.has(normalizedSignature)) {
|
||||
return trace.events.find((candidate) => traceEventSignature(candidate) === normalizedSignature) ?? null;
|
||||
}
|
||||
if (shouldSuppressPostTerminalEvent(trace, normalized)) return null;
|
||||
const normalizedSourceKey = traceSourceEventKey(normalized);
|
||||
if (normalizedSourceKey && trace.sourceEventKeys.has(normalizedSourceKey)) {
|
||||
return trace.events.find((candidate) => traceSourceEventKey(candidate) === normalizedSourceKey) ?? null;
|
||||
}
|
||||
const hadEvents = trace.events.length > 0;
|
||||
trace.nextSeq += 1;
|
||||
trace.events.push(normalized);
|
||||
if (normalizedSourceKey) trace.sourceEventKeys.add(normalizedSourceKey);
|
||||
if (normalizedSignature) trace.eventSignatures.add(normalizedSignature);
|
||||
if (trace.events.length > maxEvents) {
|
||||
trace.events.splice(0, trace.events.length - maxEvents);
|
||||
rebuildTraceEventIndexes(trace);
|
||||
}
|
||||
trace.updatedAt = normalized.createdAt;
|
||||
if (normalized.terminal === true) {
|
||||
trace.status = normalized.status === "completed"
|
||||
? "completed"
|
||||
: normalized.status === "canceled"
|
||||
? "canceled"
|
||||
: normalized.type;
|
||||
trace.finishedAt = normalized.createdAt;
|
||||
} else if (normalized.status === "failed" || normalized.type === "error" || normalized.type === "timeout") {
|
||||
trace.status = normalized.type;
|
||||
trace.finishedAt = normalized.createdAt;
|
||||
trace.createdAt = minTimestamp(trace.createdAt, normalized.createdAt);
|
||||
trace.startedAt = minTimestamp(trace.startedAt, normalized.createdAt);
|
||||
trace.updatedAt = hadEvents ? maxTimestamp(trace.updatedAt, normalized.createdAt) : normalized.createdAt;
|
||||
if (!trace.finishedAt) {
|
||||
if (normalized.terminal === true) {
|
||||
trace.status = normalized.status === "completed"
|
||||
? "completed"
|
||||
: normalized.status === "canceled"
|
||||
? "canceled"
|
||||
: normalized.type;
|
||||
trace.finishedAt = normalized.createdAt;
|
||||
trace.updatedAt = maxTimestamp(trace.updatedAt, trace.finishedAt);
|
||||
} else if (normalized.status === "failed" || normalized.type === "error" || normalized.type === "timeout") {
|
||||
trace.status = normalized.type;
|
||||
trace.finishedAt = normalized.createdAt;
|
||||
trace.updatedAt = maxTimestamp(trace.updatedAt, trace.finishedAt);
|
||||
}
|
||||
}
|
||||
notify(trace, normalized);
|
||||
return normalized;
|
||||
@@ -630,6 +645,60 @@ function traceSourceEventKey(event) {
|
||||
return `${source}:${sourceSeq}:${label}`;
|
||||
}
|
||||
|
||||
function rebuildTraceEventIndexes(trace) {
|
||||
trace.sourceEventKeys = new Set();
|
||||
trace.eventSignatures = new Set();
|
||||
for (const event of trace.events) {
|
||||
const sourceKey = traceSourceEventKey(event);
|
||||
if (sourceKey) trace.sourceEventKeys.add(sourceKey);
|
||||
const signature = traceEventSignature(event);
|
||||
if (signature) trace.eventSignatures.add(signature);
|
||||
}
|
||||
}
|
||||
|
||||
function traceEventSignature(event) {
|
||||
if (!event || typeof event !== "object") return null;
|
||||
const type = textOrNull(event.type);
|
||||
const status = textOrNull(event.status);
|
||||
const label = textOrNull(event.label);
|
||||
const createdAt = textOrNull(event.createdAt);
|
||||
if (!type || !status || !label || !createdAt) return null;
|
||||
return [
|
||||
type,
|
||||
status,
|
||||
label,
|
||||
createdAt,
|
||||
textOrNull(event.runId),
|
||||
textOrNull(event.commandId),
|
||||
textOrNull(event.itemId),
|
||||
textOrNull(event.message)
|
||||
].map((item) => item ?? "").join("\u0001");
|
||||
}
|
||||
|
||||
function shouldSuppressPostTerminalEvent(trace, event) {
|
||||
if (!trace?.finishedAt) return false;
|
||||
const finishedMs = Date.parse(trace.finishedAt);
|
||||
const eventMs = Date.parse(event?.createdAt ?? "");
|
||||
if (!Number.isFinite(finishedMs) || !Number.isFinite(eventMs)) return true;
|
||||
return eventMs > finishedMs;
|
||||
}
|
||||
|
||||
function minTimestamp(left, right) {
|
||||
const leftMs = Date.parse(left ?? "");
|
||||
const rightMs = Date.parse(right ?? "");
|
||||
if (!Number.isFinite(leftMs)) return right;
|
||||
if (!Number.isFinite(rightMs)) return left;
|
||||
return rightMs < leftMs ? right : left;
|
||||
}
|
||||
|
||||
function maxTimestamp(left, right) {
|
||||
const leftMs = Date.parse(left ?? "");
|
||||
const rightMs = Date.parse(right ?? "");
|
||||
if (!Number.isFinite(leftMs)) return right;
|
||||
if (!Number.isFinite(rightMs)) return left;
|
||||
return rightMs > leftMs ? right : left;
|
||||
}
|
||||
|
||||
function textOrNull(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
return text ? text : null;
|
||||
|
||||
Reference in New Issue
Block a user