Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 8 additions & 19 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ import {
restoreClaudePassthroughToolNames,
mergeResponseToolNameMap,
} from "./chatCore/passthroughToolNames.ts";
import { recordContextEditingTelemetryHook } from "./chatCore/contextEditingTelemetry.ts";
import {
appendNonStreamingSseTerminalSignal,
type NonStreamingSseTerminalState,
Expand Down Expand Up @@ -3540,25 +3541,13 @@ export async function handleChatCore({
// Context Editing telemetry: when the delegated server-side clear actually ran,
// record the provider's cleared-token receipt under engine "context-editing" so
// it surfaces in compression analytics. Best-effort, Claude-only, non-streaming.
if (contextEditingEnabled && provider === "claude") {
void (async () => {
try {
const { extractContextEditingTelemetry } = await import("../config/contextEditing.ts");
const tele = extractContextEditingTelemetry(responseBody);
if (tele) {
const { recordContextEditingTelemetry } =
await import("../../src/lib/db/compressionAnalytics.ts");
recordContextEditingTelemetry(skillRequestId, tele, provider);
log?.debug?.(
"CONTEXT_EDITING",
`cleared ${tele.clearedInputTokens} input tokens / ${tele.clearedToolUses} tool uses (${tele.editCount} edits)`
);
}
} catch {
// Telemetry is best-effort and must never affect the response.
}
})();
}
recordContextEditingTelemetryHook({
contextEditingEnabled,
provider,
responseBody,
skillRequestId,
log,
});
appendRequestLog({ model, provider, connectionId, tokens: usage, status: "200 OK" }).catch(
() => {}
);
Expand Down
40 changes: 40 additions & 0 deletions open-sse/handlers/chatCore/contextEditingTelemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* chatCore context-editing telemetry hook (Quality Gate v2 / Fase 9 — chatCore god-file
* decomposition, #3501).
*
* Extracted from handleChatCore's non-streaming success path: when the delegated server-side
* context-clear actually ran (Claude-only), record the provider's cleared-token receipt under the
* "context-editing" engine so it surfaces in compression analytics. Best-effort, fire-and-forget,
* and must never affect the response — the inner work is an un-awaited IIFE that swallows its own
* errors. Behaviour is byte-identical to the previous inline block.
*/

type LoggerLike = { debug?: (...args: unknown[]) => void } | null | undefined;

export function recordContextEditingTelemetryHook(args: {
contextEditingEnabled: boolean;
provider: string | null | undefined;
responseBody: unknown;
skillRequestId: string;
log?: LoggerLike;
}): void {
const { contextEditingEnabled, provider, responseBody, skillRequestId, log } = args;
if (!contextEditingEnabled || provider !== "claude") return;

void (async () => {
try {
const { extractContextEditingTelemetry } = await import("../../config/contextEditing.ts");
const tele = extractContextEditingTelemetry(responseBody);
if (tele) {
const { recordContextEditingTelemetry } = await import("@/lib/db/compressionAnalytics");
recordContextEditingTelemetry(skillRequestId, tele, provider);
log?.debug?.(
"CONTEXT_EDITING",
`cleared ${tele.clearedInputTokens} input tokens / ${tele.clearedToolUses} tool uses (${tele.editCount} edits)`
);
}
} catch {
// Telemetry is best-effort and must never affect the response.
}
})();
}
106 changes: 106 additions & 0 deletions tests/unit/chatcore-context-editing-telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Characterization of recordContextEditingTelemetryHook — the Claude-only context-editing
// telemetry hook extracted from handleChatCore's non-streaming success path (chatCore god-file
// decomposition, #3501). The work is a fire-and-forget IIFE; uses a real temp DB and polls the
// captured log. Locks: the enabled+claude guard, the no-telemetry no-op, and that a valid
// applied_edits payload records and logs the cleared-token receipt.
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-ctxedit-test-"));
process.env.DATA_DIR = testDataDir;

const coreDb = await import("../../src/lib/db/core.ts");
const { recordContextEditingTelemetryHook } = await import(
"../../open-sse/handlers/chatCore/contextEditingTelemetry.ts"
);

function makeLog() {
const debug: string[] = [];
return {
log: { debug: (tag: string, msg: string) => debug.push(`${tag} ${msg}`) },
debug,
};
}

const telemetryBody = {
context_management: {
applied_edits: [{ cleared_input_tokens: 120, cleared_tool_uses: 3 }],
},
};

async function waitFor(pred: () => boolean, timeoutMs = 3000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline && !pred()) {
await new Promise((r) => setTimeout(r, 25));
}
}

before(async () => {
await coreDb.ensureDbInitialized();
});

after(() => {
coreDb.resetDbInstance();
try {
fs.rmSync(testDataDir, { recursive: true, force: true });
} catch {
// best-effort cleanup
}
});

test("disabled context-editing is a no-op", async () => {
const { log, debug } = makeLog();
recordContextEditingTelemetryHook({
contextEditingEnabled: false,
provider: "claude",
responseBody: telemetryBody,
skillRequestId: "req-1",
log,
});
await new Promise((r) => setTimeout(r, 100));
assert.equal(debug.length, 0);
});

test("non-claude provider is a no-op", async () => {
const { log, debug } = makeLog();
recordContextEditingTelemetryHook({
contextEditingEnabled: true,
provider: "openai",
responseBody: telemetryBody,
skillRequestId: "req-2",
log,
});
await new Promise((r) => setTimeout(r, 100));
assert.equal(debug.length, 0);
});

test("valid applied_edits records and logs the cleared-token receipt", async () => {
const { log, debug } = makeLog();
recordContextEditingTelemetryHook({
contextEditingEnabled: true,
provider: "claude",
responseBody: telemetryBody,
skillRequestId: "req-3",
log,
});
await waitFor(() => debug.length > 0);
assert.ok(debug.length >= 1, "expected a CONTEXT_EDITING debug line");
assert.match(debug[0], /CONTEXT_EDITING/);
assert.match(debug[0], /cleared 120 input tokens \/ 3 tool uses \(1 edits\)/);
});

test("response without applied_edits is a silent no-op (no throw)", async () => {
const { log, debug } = makeLog();
recordContextEditingTelemetryHook({
contextEditingEnabled: true,
provider: "claude",
responseBody: { choices: [] },
skillRequestId: "req-4",
log,
});
await new Promise((r) => setTimeout(r, 100));
assert.equal(debug.length, 0);
});
Loading