mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 16:35:48 +01:00
fix(acpx): record turn activity at the moment a stop fires
Sandbox claude_local runs that hit the 4h wall-clock backstop report acpLastEventAgeMs at finalization, after the timeout's cancel. Some agents emit closing events on cancel, so a turn that went silent minutes into the run can look active at its deadline. Record, for a stopped turn, the last-event age and the oldest pending tool's age as of the stop, plus the pending tools' closed ACP ToolKind labels. The server forwards these to run-failure diagnostics only when they are non-negative integers or closed enum labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b189f586d2
commit
eed2d27675
6 files changed
+175
-6
No files matched your search
@@ -323,11 +323,29 @@ describe("run failure diagnostics", () => {
|
||||
expect(JSON.stringify(result)).not.toContain("private");
|
||||
});
|
||||
|
||||
it("includes stop-time ACP activity and closed pending tool-kind labels", () => {
|
||||
const result = sanitizeRunFailureDiagnostics(collectRunFailureDiagnostics(run({ resultJson: {
|
||||
acpLastEventAgeMs: 700, acpLastEventAgeAtStopMs: 14_000_000, acpOldestPendingToolAgeAtStopMs: 14_100_000,
|
||||
acpPendingToolKinds: "execute,unknown",
|
||||
} }), {}));
|
||||
expect(result.execution).toEqual({
|
||||
acpLastEventAgeMs: 700, acpLastEventAgeAtStopMs: 14_000_000, acpOldestPendingToolAgeAtStopMs: 14_100_000,
|
||||
acpPendingToolKinds: "execute,unknown",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["execute,private command", "", "execute,", "Execute", ["execute"], 1])(
|
||||
"omits pending tool kinds that are not closed labels (%j)", (value) => {
|
||||
const result = collectRunFailureDiagnostics(run({ resultJson: { acpPendingToolKinds: value } }), {});
|
||||
expect(result.execution).toEqual({});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([null, -1, Infinity, NaN, 1.5, Number.MAX_SAFE_INTEGER + 1, "private", {}])(
|
||||
"omits invalid ACP activity fields (%j)", (value) => {
|
||||
const result = collectRunFailureDiagnostics(run({ resultJson: {
|
||||
acpLastEventAgeMs: value, acpObservedEventCount: value, acpPendingToolCount: value,
|
||||
acpToolInventoryComplete: value,
|
||||
acpToolInventoryComplete: value, acpLastEventAgeAtStopMs: value, acpOldestPendingToolAgeAtStopMs: value,
|
||||
} }), {});
|
||||
expect(result.execution).toEqual({});
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import { readProcessLossDiagnostic } from "./process-loss-diagnostics.js";
|
||||
import { WORKSPACE_RESTORE_FAILURE_CODES } from "@paperclipai/shared";
|
||||
import { redactDiagnosticText } from "@paperclipai/adapter-utils/command-redaction";
|
||||
import { sanitizeWorkspaceRestoreDiagnostic } from "@paperclipai/adapter-utils/workspace-restore-diagnostics";
|
||||
import { ACP_TOOL_KINDS } from "@paperclipai/adapter-utils/acpx-engine/constants";
|
||||
import { redactCurrentUserText } from "../log-redaction.js";
|
||||
import { redactSensitiveText, REDACTED_EVENT_VALUE } from "../redaction.js";
|
||||
import { readNativeModelRejectionDiagnostic } from "./native-runtime/native-provider-failure.js";
|
||||
@@ -41,6 +42,15 @@ export interface RunFailureDiagnostics {
|
||||
truncatedFields: string[];
|
||||
}
|
||||
|
||||
const ACP_PENDING_TOOL_KIND_LABELS: ReadonlySet<string> = new Set([...ACP_TOOL_KINDS, "unknown"]);
|
||||
|
||||
/** Closed ACP tool-kind labels only; any other content drops the whole field. */
|
||||
function readAcpPendingToolKinds(value: unknown): string | null {
|
||||
if (typeof value !== "string" || value.length === 0 || value.length > 200) return null;
|
||||
const kinds = value.split(",");
|
||||
return kinds.every((kind) => ACP_PENDING_TOOL_KIND_LABELS.has(kind)) ? value : null;
|
||||
}
|
||||
|
||||
function read(value: unknown, key: string): unknown {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
try {
|
||||
@@ -197,10 +207,15 @@ export function collectRunFailureDiagnostics(run: Run, options: RunFailureReport
|
||||
"mode", "stopReason", "timeoutFired", "timeoutSource", "timeoutConfigured",
|
||||
"effectiveTimeoutSec", "errorFamily",
|
||||
]));
|
||||
for (const field of ["acpLastEventAgeMs", "acpObservedEventCount", "acpPendingToolCount"]) {
|
||||
for (const field of [
|
||||
"acpLastEventAgeMs", "acpObservedEventCount", "acpPendingToolCount",
|
||||
"acpLastEventAgeAtStopMs", "acpOldestPendingToolAgeAtStopMs",
|
||||
]) {
|
||||
const value = read(result, field);
|
||||
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) execution[field] = value;
|
||||
}
|
||||
const pendingToolKinds = readAcpPendingToolKinds(read(result, "acpPendingToolKinds"));
|
||||
if (pendingToolKinds) execution.acpPendingToolKinds = pendingToolKinds;
|
||||
const toolInventoryComplete = read(result, "acpToolInventoryComplete");
|
||||
if (typeof toolInventoryComplete === "boolean") execution.acpToolInventoryComplete = toolInventoryComplete;
|
||||
const restoreFailure = read(result, "workspaceRestoreFailure");
|
||||
|
||||
Reference in new issue
Block a user