mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
fix(codex): retain terminal facts in oversized trajectories (#126050)
* fix(codex): delegate trajectory bounding to host * fix(trajectory): preserve compact terminal facts * style: format release validation skill
This commit is contained in:
committed by
GitHub
parent
50720c3b8e
commit
e2643afb2a
@@ -245,7 +245,6 @@ extensions/codex/src/app-server/thread-lifecycle-run.ts 1
|
||||
extensions/codex/src/app-server/thread-supervision.ts 2
|
||||
extensions/codex/src/app-server/tool-abort-terminal-reason.ts 1
|
||||
extensions/codex/src/app-server/tool-progress-normalization.ts 1
|
||||
extensions/codex/src/app-server/trajectory.ts 3
|
||||
extensions/codex/src/app-server/transcript-history-projection.ts 2
|
||||
extensions/codex/src/app-server/transcript-mirror-attestation.ts 2
|
||||
extensions/codex/src/app-server/transcript-mirror.ts 4
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// Codex tests cover SQLite-only trajectory plugin behavior.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createAgentHarnessHostCapabilitiesForTest } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
appendSqliteTrajectoryRuntimeEvents,
|
||||
createTrajectoryRuntimeRecorderForTest,
|
||||
exportTrajectoryBundleForTest,
|
||||
loadSqliteTrajectoryRuntimeEvents,
|
||||
type SqliteTrajectoryRuntimeEventForTest,
|
||||
} from "openclaw/plugin-sdk/sqlite-runtime-testing";
|
||||
@@ -84,7 +87,6 @@ function createMemoryBackedRecorder(params: {
|
||||
} as never,
|
||||
trajectory: host.trajectory,
|
||||
tools: params.tools,
|
||||
env: {},
|
||||
});
|
||||
return { events: host.events, recorder: expectTrajectoryRecorder(recorder) };
|
||||
}
|
||||
@@ -128,7 +130,6 @@ describe("Codex trajectory recorder", () => {
|
||||
sessionId: "session-1",
|
||||
model: { api: "responses" },
|
||||
} as never,
|
||||
env: {},
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
@@ -157,7 +158,6 @@ describe("Codex trajectory recorder", () => {
|
||||
sessionId: "session-1",
|
||||
storePath,
|
||||
}),
|
||||
env: {},
|
||||
});
|
||||
|
||||
const trajectoryRecorder = expectTrajectoryRecorder(recorder);
|
||||
@@ -172,20 +172,6 @@ describe("Codex trajectory recorder", () => {
|
||||
).resolves.toEqual([expect.objectContaining({ type: "session.started" })]);
|
||||
});
|
||||
|
||||
it("redacts secrets and keeps recorded strings UTF-16 safe", async () => {
|
||||
const { events, recorder } = createMemoryBackedRecorder({ tmpDir: testWorkspace.dir });
|
||||
recorder.recordEvent("model.output", {
|
||||
text: `${"x".repeat(19_999)}😀`,
|
||||
apiKey: "secret",
|
||||
authorization: "Bearer sk-test-secret-token",
|
||||
});
|
||||
await recorder.flush();
|
||||
|
||||
expect(events[0]?.data?.text).toBe(`${"x".repeat(19_999)}…`);
|
||||
expect(events[0]?.data?.apiKey).toBe("<redacted>");
|
||||
expect(events[0]?.data?.authorization).toBe("<redacted>");
|
||||
});
|
||||
|
||||
it("records namespace dynamic tools as callable trajectory definitions", async () => {
|
||||
const tools = [
|
||||
{
|
||||
@@ -218,32 +204,33 @@ describe("Codex trajectory recorder", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("honors explicit disablement", () => {
|
||||
const host = createMemoryTrajectoryFacade();
|
||||
const recorder = createCodexTrajectoryRecorder({
|
||||
cwd: testWorkspace.dir,
|
||||
attempt: {
|
||||
sessionFile: "agent:main:session-1",
|
||||
sessionId: "session-1",
|
||||
model: { api: "responses" },
|
||||
} as never,
|
||||
env: { OPENCLAW_TRAJECTORY: "0" },
|
||||
trajectory: host.trajectory,
|
||||
it("lets the host bound oversized Codex events without losing terminal facts", async () => {
|
||||
const tmpDir = testWorkspace.dir;
|
||||
const storePath = path.join(tmpDir, "sessions", "sessions.json");
|
||||
const sessionTarget = {
|
||||
agentId: "main",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
storePath,
|
||||
};
|
||||
await upsertSessionEntry({
|
||||
agentId: sessionTarget.agentId,
|
||||
sessionKey: sessionTarget.sessionKey,
|
||||
storePath,
|
||||
entry: { sessionId: sessionTarget.sessionId, updatedAt: 10 },
|
||||
});
|
||||
|
||||
expect(recorder).toBeNull();
|
||||
expect(host.events).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves usage when truncating oversized model completion events", async () => {
|
||||
const attempt = {
|
||||
agentId: "main",
|
||||
cwd: tmpDir,
|
||||
workspaceDir: tmpDir,
|
||||
sessionFile: sessionTarget.sessionKey,
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
runId: "run-1",
|
||||
provider: "codex",
|
||||
modelId: "gpt-5.4",
|
||||
model: { api: "responses" },
|
||||
} as never;
|
||||
};
|
||||
const usage = {
|
||||
input: 384_954,
|
||||
output: 5_624,
|
||||
@@ -251,34 +238,190 @@ describe("Codex trajectory recorder", () => {
|
||||
reasoningTokens: 2_038,
|
||||
total: 724_402,
|
||||
};
|
||||
const { events, recorder } = createMemoryBackedRecorder({
|
||||
tmpDir: testWorkspace.dir,
|
||||
attempt,
|
||||
const hostRecorder = createTrajectoryRuntimeRecorderForTest({
|
||||
sessionId: sessionTarget.sessionId,
|
||||
sessionKey: sessionTarget.sessionKey,
|
||||
sessionTarget,
|
||||
runId: attempt.runId,
|
||||
provider: "openai",
|
||||
modelId: attempt.modelId,
|
||||
modelApi: "responses",
|
||||
workspaceDir: tmpDir,
|
||||
});
|
||||
|
||||
recordCodexTrajectoryCompletion(recorder, {
|
||||
attempt,
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
timedOut: false,
|
||||
result: {
|
||||
terminal: { kind: "ok" },
|
||||
attemptUsage: usage,
|
||||
assistantTexts: ["done"],
|
||||
messagesSnapshot: Array.from({ length: 20 }, (_value, index) => ({
|
||||
role: index % 2 === 0 ? "user" : "assistant",
|
||||
content: `message-${index} ${"x".repeat(32_000)}`,
|
||||
})),
|
||||
} as never,
|
||||
if (!hostRecorder) {
|
||||
throw new Error("Expected host trajectory recorder");
|
||||
}
|
||||
const host = await createAgentHarnessHostCapabilitiesForTest({
|
||||
attempt: { ...attempt, trajectoryRecorder: hostRecorder } as never,
|
||||
pluginId: "codex",
|
||||
});
|
||||
await recorder.flush();
|
||||
const recorder = createCodexTrajectoryRecorder({
|
||||
attempt: attempt as never,
|
||||
cwd: tmpDir,
|
||||
trajectory: host.capabilities.trajectory,
|
||||
} as never);
|
||||
const trajectoryRecorder = expectTrajectoryRecorder(recorder);
|
||||
|
||||
expect(events[0]?.data).toMatchObject({
|
||||
try {
|
||||
recordCodexTrajectoryContext(trajectoryRecorder, {
|
||||
attempt: attempt as never,
|
||||
cwd: tmpDir,
|
||||
developerInstructions: `Bearer ${"s".repeat(40)} ${"x".repeat(40_000)}`,
|
||||
prompt: "inspect",
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
name: "huge_tool",
|
||||
description: "x".repeat(40_000),
|
||||
inputSchema: { type: "object" },
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
trajectoryRecorder.recordEvent("tool.result", {
|
||||
toolCallId: "call-1",
|
||||
toolName: "huge_tool",
|
||||
status: "completed",
|
||||
authorization: `Bearer ${"t".repeat(40)}`,
|
||||
output: `token=${"t".repeat(40)} ${"x".repeat(40_000)}`,
|
||||
});
|
||||
recordCodexTrajectoryCompletion(trajectoryRecorder, {
|
||||
attempt: attempt as never,
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
timedOut: true,
|
||||
yieldDetected: true,
|
||||
result: {
|
||||
terminal: {
|
||||
kind: "timeout",
|
||||
phase: "prompt",
|
||||
source: "runtime",
|
||||
aborted: true,
|
||||
failure: { source: "prompt", error: "terminal prompt error" },
|
||||
},
|
||||
attemptUsage: usage,
|
||||
assistantTexts: ["done"],
|
||||
// Twelve entries stay below the old plugin's post-sanitization cap,
|
||||
// but exceed the host cap when forwarded without pre-shrinking.
|
||||
messagesSnapshot: Array.from({ length: 12 }, (_value, index) => ({
|
||||
role: index % 2 === 0 ? "user" : "assistant",
|
||||
content: `message-${index} ${"x".repeat(32_000)}`,
|
||||
})),
|
||||
} as never,
|
||||
});
|
||||
recordCodexTrajectoryCompletion(trajectoryRecorder, {
|
||||
attempt: attempt as never,
|
||||
threadId: "thread-compact",
|
||||
turnId: "turn-compact",
|
||||
timedOut: true,
|
||||
result: {
|
||||
terminal: {
|
||||
kind: "timeout",
|
||||
phase: "prompt",
|
||||
source: "runtime",
|
||||
aborted: true,
|
||||
failure: { source: "prompt", error: "compact prompt error" },
|
||||
},
|
||||
attemptUsage: usage,
|
||||
assistantTexts: Array.from(
|
||||
{ length: 12 },
|
||||
(_value, index) => `assistant-${index} ${"x".repeat(32_000)}`,
|
||||
),
|
||||
messagesSnapshot: Array.from({ length: 12 }, (_value, index) => ({
|
||||
role: index % 2 === 0 ? "user" : "assistant",
|
||||
content: `message-${index} ${"x".repeat(32_000)}`,
|
||||
})),
|
||||
} as never,
|
||||
});
|
||||
await trajectoryRecorder.flush();
|
||||
} finally {
|
||||
host.close();
|
||||
}
|
||||
|
||||
const events = await loadSqliteTrajectoryRuntimeEvents({
|
||||
agentId: sessionTarget.agentId,
|
||||
sessionId: sessionTarget.sessionId,
|
||||
storePath,
|
||||
});
|
||||
const context = events.find((event) => event.type === "context.compiled");
|
||||
const tool = events.find((event) => event.type === "tool.result");
|
||||
const completion = events.find(
|
||||
(event) => event.type === "model.completed" && event.data?.turnId === "turn-1",
|
||||
);
|
||||
const compactCompletion = events.find(
|
||||
(event) => event.type === "model.completed" && event.data?.turnId === "turn-compact",
|
||||
);
|
||||
expect(context?.data).toMatchObject({
|
||||
prompt: "inspect",
|
||||
systemPrompt: {
|
||||
truncated: true,
|
||||
reason: "trajectory-field-size-limit",
|
||||
},
|
||||
tools: [
|
||||
{
|
||||
name: "huge_tool",
|
||||
description: {
|
||||
truncated: true,
|
||||
reason: "trajectory-field-size-limit",
|
||||
},
|
||||
parameters: { type: "object" },
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(JSON.stringify(context)).not.toContain("s".repeat(40));
|
||||
expect(tool?.data).toMatchObject({
|
||||
toolCallId: "call-1",
|
||||
toolName: "huge_tool",
|
||||
status: "completed",
|
||||
output: {
|
||||
truncated: true,
|
||||
reason: "trajectory-field-size-limit",
|
||||
},
|
||||
});
|
||||
expect(tool?.data?.authorization).toBeUndefined();
|
||||
expect(JSON.stringify(tool)).not.toContain(`token=${"t".repeat(40)}`);
|
||||
expect(completion?.data).toMatchObject({
|
||||
truncated: true,
|
||||
reason: "trajectory-event-size-limit",
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
timedOut: true,
|
||||
yieldDetected: true,
|
||||
aborted: true,
|
||||
promptError: "terminal prompt error",
|
||||
usage,
|
||||
assistantTexts: ["done"],
|
||||
});
|
||||
expect(completion?.data?.messagesSnapshot).toBeUndefined();
|
||||
expect(completion?.data?.droppedFields).toEqual(["messagesSnapshot"]);
|
||||
expect(compactCompletion?.data).toMatchObject({
|
||||
truncated: true,
|
||||
reason: "trajectory-event-size-limit",
|
||||
threadId: "thread-compact",
|
||||
turnId: "turn-compact",
|
||||
timedOut: true,
|
||||
yieldDetected: false,
|
||||
aborted: true,
|
||||
promptError: "compact prompt error",
|
||||
usage,
|
||||
});
|
||||
expect(events[0]?.data?.messagesSnapshot).toBeUndefined();
|
||||
expect(events[0]?.data?.droppedFields).toContain("messagesSnapshot");
|
||||
expect(compactCompletion?.data?.assistantTexts).toBeUndefined();
|
||||
expect(compactCompletion?.data?.messagesSnapshot).toBeUndefined();
|
||||
expect(compactCompletion?.data?.droppedFields).toEqual(["assistantTexts", "messagesSnapshot"]);
|
||||
|
||||
const bundle = await exportTrajectoryBundleForTest({
|
||||
outputDir: path.join(tmpDir, "bundle"),
|
||||
sessionTarget,
|
||||
sessionId: sessionTarget.sessionId,
|
||||
sessionKey: sessionTarget.sessionKey,
|
||||
workspaceDir: tmpDir,
|
||||
});
|
||||
const exportedCompletion = bundle.events.find(
|
||||
(event) => event.type === "model.completed" && event.data?.turnId === "turn-1",
|
||||
);
|
||||
const exportedCompactCompletion = bundle.events.find(
|
||||
(event) => event.type === "model.completed" && event.data?.turnId === "turn-compact",
|
||||
);
|
||||
expect(exportedCompletion?.data).toEqual(completion?.data);
|
||||
expect(exportedCompactCompletion?.data).toEqual(compactCompletion?.data);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
/**
|
||||
* Records optional Codex runtime trajectory events with bounded, redacted
|
||||
* context and completion payloads.
|
||||
*/
|
||||
/** Records optional Codex runtime trajectory events through the host recorder. */
|
||||
import type { EmbeddedRunAttemptParamsV2 as EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { attemptTerminal, type EmbeddedRunAttemptResult } from "./attempt-terminal.js";
|
||||
import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js";
|
||||
import { flattenCodexDynamicToolFunctions, type CodexDynamicToolSpec } from "./protocol.js";
|
||||
|
||||
/** Runtime trajectory recorder used by Codex run attempts and event projectors. */
|
||||
@@ -21,123 +16,19 @@ type CodexTrajectoryInit = {
|
||||
prompt?: string;
|
||||
trajectory?: NonNullable<EmbeddedRunAttemptParams["hostCapabilities"]["trajectory"]> | null;
|
||||
tools?: CodexDynamicToolSpec[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
const SENSITIVE_FIELD_RE = /(?:authorization|cookie|credential|key|password|passwd|secret|token)/iu;
|
||||
const PRIVATE_PAYLOAD_FIELD_RE = /(?:image|screenshot|attachment|fileData|dataUri)/iu;
|
||||
const AUTHORIZATION_VALUE_RE = /\b(Bearer|Basic)\s+[A-Za-z0-9+/._~=-]{8,}/giu;
|
||||
const JWT_VALUE_RE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gu;
|
||||
const COOKIE_PAIR_RE = /\b([A-Za-z][A-Za-z0-9_.-]{1,64})=([A-Za-z0-9+/._~%=-]{16,})(?=;|\s|$)/gu;
|
||||
const TRAJECTORY_RUNTIME_EVENT_MAX_BYTES = 256 * 1024;
|
||||
const TRAJECTORY_RUNTIME_OVERSIZE_PRESERVED_DATA_KEYS = ["usage", "promptCache"] as const;
|
||||
|
||||
type CodexTrajectoryEvent = Record<string, unknown> & {
|
||||
data?: Record<string, unknown>;
|
||||
type: string;
|
||||
};
|
||||
|
||||
function boundedTrajectoryEvent(event: Record<string, unknown>): CodexTrajectoryEvent | undefined {
|
||||
const line = JSON.stringify(event);
|
||||
const bytes = Buffer.byteLength(line, "utf8");
|
||||
if (bytes <= TRAJECTORY_RUNTIME_EVENT_MAX_BYTES) {
|
||||
return event as CodexTrajectoryEvent;
|
||||
}
|
||||
|
||||
const originalData =
|
||||
event.data && typeof event.data === "object" && !Array.isArray(event.data)
|
||||
? (event.data as Record<string, unknown>)
|
||||
: {};
|
||||
const originalDataKeys = Object.keys(originalData);
|
||||
const preservedDataKeys = new Set<string>();
|
||||
const baseData = {
|
||||
truncated: true,
|
||||
originalBytes: bytes,
|
||||
limitBytes: TRAJECTORY_RUNTIME_EVENT_MAX_BYTES,
|
||||
reason: "trajectory-event-size-limit",
|
||||
};
|
||||
const buildTruncatedEvent = (includeDroppedFields: boolean): CodexTrajectoryEvent | undefined => {
|
||||
const data: Record<string, unknown> = { ...baseData };
|
||||
for (const key of TRAJECTORY_RUNTIME_OVERSIZE_PRESERVED_DATA_KEYS) {
|
||||
if (preservedDataKeys.has(key)) {
|
||||
data[key] = originalData[key];
|
||||
}
|
||||
}
|
||||
if (includeDroppedFields) {
|
||||
const droppedFields = originalDataKeys.filter((key) => !preservedDataKeys.has(key));
|
||||
if (droppedFields.length > 0) {
|
||||
data.droppedFields = droppedFields;
|
||||
}
|
||||
}
|
||||
const truncatedEvent = { ...event, data };
|
||||
const truncated = JSON.stringify(truncatedEvent);
|
||||
if (Buffer.byteLength(truncated, "utf8") <= TRAJECTORY_RUNTIME_EVENT_MAX_BYTES) {
|
||||
return truncatedEvent as CodexTrajectoryEvent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
let best = buildTruncatedEvent(true) ?? buildTruncatedEvent(false);
|
||||
if (!best) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const key of TRAJECTORY_RUNTIME_OVERSIZE_PRESERVED_DATA_KEYS) {
|
||||
if (!Object.hasOwn(originalData, key)) {
|
||||
continue;
|
||||
}
|
||||
preservedDataKeys.add(key);
|
||||
const next = buildTruncatedEvent(true) ?? buildTruncatedEvent(false);
|
||||
if (next) {
|
||||
best = next;
|
||||
continue;
|
||||
}
|
||||
preservedDataKeys.delete(key);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** Creates a trajectory recorder when trajectory capture is enabled for the environment. */
|
||||
/** Creates a trajectory recorder when the host exposes its capture capability. */
|
||||
export function createCodexTrajectoryRecorder(
|
||||
params: CodexTrajectoryInit,
|
||||
): CodexTrajectoryRecorder | null {
|
||||
const env = params.env ?? process.env;
|
||||
const enabled = parseTrajectoryEnabled(env);
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!params.trajectory) {
|
||||
return null;
|
||||
}
|
||||
const trajectory = params.trajectory;
|
||||
let seq = 0;
|
||||
const attribution = resolveCodexLocalRuntimeAttribution(params.attempt);
|
||||
|
||||
return {
|
||||
recordEvent: (type, data) => {
|
||||
const event = boundedTrajectoryEvent({
|
||||
traceSchema: "openclaw-trajectory",
|
||||
schemaVersion: 1,
|
||||
traceId: params.attempt.sessionId,
|
||||
source: "runtime",
|
||||
type,
|
||||
ts: new Date().toISOString(),
|
||||
seq: (seq += 1),
|
||||
sourceSeq: seq,
|
||||
sessionId: params.attempt.sessionId,
|
||||
sessionKey: params.attempt.sessionKey,
|
||||
runId: params.attempt.runId,
|
||||
workspaceDir: params.cwd,
|
||||
provider: attribution.provider,
|
||||
modelId: params.attempt.modelId,
|
||||
modelApi: attribution.api,
|
||||
data: data ? sanitizeValue(data) : undefined,
|
||||
});
|
||||
if (event) {
|
||||
trajectory.recordEvent(event.type, event.data);
|
||||
}
|
||||
},
|
||||
recordEvent: trajectory.recordEvent,
|
||||
flush: trajectory.flush,
|
||||
};
|
||||
}
|
||||
@@ -187,17 +78,6 @@ export function recordCodexTrajectoryCompletion(
|
||||
});
|
||||
}
|
||||
|
||||
function parseTrajectoryEnabled(env: NodeJS.ProcessEnv): boolean {
|
||||
const value = env.OPENCLAW_TRAJECTORY?.trim().toLowerCase();
|
||||
if (value === "1" || value === "true" || value === "yes" || value === "on") {
|
||||
return true;
|
||||
}
|
||||
if (value === "0" || value === "false" || value === "no" || value === "off") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function toTrajectoryToolDefinitions(
|
||||
tools: readonly CodexDynamicToolSpec[] | undefined,
|
||||
): Array<{ name: string; description?: string; parameters?: unknown }> | undefined {
|
||||
@@ -214,55 +94,13 @@ function toTrajectoryToolDefinitions(
|
||||
{
|
||||
name,
|
||||
description: tool.description,
|
||||
parameters: sanitizeValue(tool.inputSchema),
|
||||
parameters: tool.inputSchema,
|
||||
},
|
||||
];
|
||||
})
|
||||
.toSorted((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
function sanitizeValue(value: unknown, depth = 0, key = ""): unknown {
|
||||
// Trajectory exports may leave the live process, so redact credentials and
|
||||
// private payloads before passing events to the SQLite host recorder.
|
||||
if (value == null || typeof value === "boolean" || typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (SENSITIVE_FIELD_RE.test(key)) {
|
||||
return "<redacted>";
|
||||
}
|
||||
if (value.startsWith("data:") && value.length > 256) {
|
||||
return `<redacted data-uri ${value.slice(0, value.indexOf(",")).length} chars>`;
|
||||
}
|
||||
if (PRIVATE_PAYLOAD_FIELD_RE.test(key) && value.length > 256) {
|
||||
return "<redacted payload>";
|
||||
}
|
||||
const redacted = redactSensitiveString(value);
|
||||
return redacted.length > 20_000 ? `${truncateUtf16Safe(redacted, 20_000)}…` : redacted;
|
||||
}
|
||||
if (depth >= 6) {
|
||||
return "<truncated>";
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.slice(0, 100).map((entry) => sanitizeValue(entry, depth + 1, key));
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
const next: Record<string, unknown> = {};
|
||||
for (const [keyLocal, child] of Object.entries(value).slice(0, 100)) {
|
||||
next[keyLocal] = sanitizeValue(child, depth + 1, keyLocal);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function redactSensitiveString(value: string): string {
|
||||
return value
|
||||
.replace(AUTHORIZATION_VALUE_RE, "$1 <redacted>")
|
||||
.replace(JWT_VALUE_RE, "<redacted-jwt>")
|
||||
.replace(COOKIE_PAIR_RE, "$1=<redacted>");
|
||||
}
|
||||
|
||||
/** Converts arbitrary prompt errors into trajectory-safe text. */
|
||||
export function normalizeCodexTrajectoryError(value: unknown): string | null {
|
||||
if (!value) {
|
||||
|
||||
@@ -19,6 +19,8 @@ export {
|
||||
loadSqliteTrajectoryRuntimeEvents,
|
||||
type SqliteTrajectoryRuntimeScope,
|
||||
} from "../trajectory/runtime-store.sqlite.js";
|
||||
export { createTrajectoryRuntimeRecorder as createTrajectoryRuntimeRecorderForTest } from "../trajectory/runtime.js";
|
||||
export { exportTrajectoryBundle as exportTrajectoryBundleForTest } from "../trajectory/export.js";
|
||||
export { type TrajectoryEvent as SqliteTrajectoryRuntimeEventForTest } from "../trajectory/types.js";
|
||||
export {
|
||||
closeOpenClawAgentDatabasesForTest,
|
||||
|
||||
@@ -391,9 +391,19 @@ describe("trajectory runtime", () => {
|
||||
|
||||
const runtimeRecorder = expectTrajectoryRuntimeRecorder(recorder);
|
||||
runtimeRecorder.recordEvent("model.completed", {
|
||||
threadId: "thread-compact",
|
||||
turnId: "turn-compact",
|
||||
timedOut: true,
|
||||
yieldDetected: false,
|
||||
aborted: true,
|
||||
promptError: "terminal prompt error",
|
||||
usage: oversizedUsage,
|
||||
promptCache,
|
||||
stopReason: "length",
|
||||
assistantTexts: Array.from(
|
||||
{ length: 12 },
|
||||
(_value, index) => `assistant-${index} ${"x".repeat(32_000)}`,
|
||||
),
|
||||
messagesSnapshot: [{ role: "user", content: "x".repeat(32_000) }],
|
||||
});
|
||||
|
||||
@@ -402,12 +412,19 @@ describe("trajectory runtime", () => {
|
||||
expect(parsed.data).toMatchObject({
|
||||
truncated: true,
|
||||
reason: "trajectory-event-size-limit",
|
||||
threadId: "thread-compact",
|
||||
turnId: "turn-compact",
|
||||
timedOut: true,
|
||||
yieldDetected: false,
|
||||
aborted: true,
|
||||
promptError: "terminal prompt error",
|
||||
promptCache,
|
||||
stopReason: "length",
|
||||
});
|
||||
expect(parsed.data.usage).toBeUndefined();
|
||||
expect(parsed.data.assistantTexts).toBeUndefined();
|
||||
expect(parsed.data.droppedFields).toEqual(
|
||||
expect.arrayContaining(["usage", "messagesSnapshot"]),
|
||||
expect.arrayContaining(["usage", "assistantTexts", "messagesSnapshot"]),
|
||||
);
|
||||
expect(
|
||||
Buffer.byteLength(expectDefined(writes[0], "writes[0] test invariant"), "utf8"),
|
||||
|
||||
@@ -61,7 +61,18 @@ const TRAJECTORY_RUNTIME_OVERSIZE_DROP_FIRST_DATA_KEYS = [
|
||||
"messages",
|
||||
"systemPrompt",
|
||||
] as const;
|
||||
const OVERSIZE_PRESERVED_DATA_KEYS = ["stopReason", "usage", "promptCache", "prompt"] as const;
|
||||
const OVERSIZE_PRESERVED_DATA_KEYS = [
|
||||
"threadId",
|
||||
"turnId",
|
||||
"timedOut",
|
||||
"yieldDetected",
|
||||
"aborted",
|
||||
"promptError",
|
||||
"stopReason",
|
||||
"usage",
|
||||
"promptCache",
|
||||
"prompt",
|
||||
] as const;
|
||||
|
||||
type TrajectoryRuntimeWriterDiagnostics = QueuedFileWriterDiagnostics;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user