be89296492
拆分 App 认证、壳层、运行配置与项目摘要模块 拆分 Tauri 项目能力与 Rust 测试领域模块 拆分界面测试与 Agent Runtime 真实 E2E 套件 补充源码扫描和客户端模块化文档约定
821 lines
29 KiB
JavaScript
821 lines
29 KiB
JavaScript
import {
|
||
assert,
|
||
codedError,
|
||
hashValue,
|
||
isFailedTask,
|
||
sleep,
|
||
} from '../assertions/core.mjs';
|
||
import {
|
||
countExactSecrets,
|
||
disposableProjectPathVariants,
|
||
duplicateCount,
|
||
formalConfigPathVariants,
|
||
isNonEmptyString,
|
||
sumObjectValues,
|
||
} from '../assertions/runtime.mjs';
|
||
import { path } from '../dependencies.mjs';
|
||
import {
|
||
claimOwnedRunner,
|
||
ensureOwnedRunnerStableKillSupport,
|
||
prepareIsolatedSuiteAppData,
|
||
} from '../harness/app-data.mjs';
|
||
import { isPlainObject } from '../harness/config.mjs';
|
||
import {
|
||
listFiles,
|
||
parseAssignedJson,
|
||
readJson,
|
||
readOptionalJsonl,
|
||
} from '../harness/io.mjs';
|
||
import { prepareCliBinary, runCli } from '../harness/process.mjs';
|
||
import { seedDisposableProject } from '../harness/project.mjs';
|
||
import {
|
||
agentConversationPath,
|
||
countLureLeaks,
|
||
countOccurrences,
|
||
countSecretsInProject,
|
||
countSensitiveValuesBySurface,
|
||
killRunnerOnce,
|
||
mainRuntimeStatePath,
|
||
readAllRuntimeEvents,
|
||
readRunnerStatus,
|
||
readRuntime,
|
||
readTaskSnapshot,
|
||
runnerBootId,
|
||
validateProjectRootPublicLeakBoundary,
|
||
waitForRunnerBootChange,
|
||
} from '../harness/runtime.mjs';
|
||
import {
|
||
contextCompactionConstraintCanary,
|
||
contextCompactionRoundCount,
|
||
contextCompactionSuite,
|
||
contextCompactionTriggerTurns,
|
||
mainAgentId,
|
||
requestedRunId,
|
||
state,
|
||
} from '../runtime-state.mjs';
|
||
|
||
export async function runContextCompactionE2e() {
|
||
await ensureOwnedRunnerStableKillSupport();
|
||
await seedDisposableProject();
|
||
state.cliBinary = await prepareCliBinary();
|
||
await prepareIsolatedSuiteAppData();
|
||
state.isolatedRunner.launchAttempted = true;
|
||
|
||
for (let turn = 1; turn <= contextCompactionRoundCount; turn += 1) {
|
||
const runId = `${requestedRunId}-turn-${String(turn).padStart(2, '0')}`;
|
||
const prompt = buildContextCompactionTurnPrompt(turn);
|
||
const args = ['--agent-enqueue'];
|
||
if (turn === 1) args.push('--init');
|
||
args.push(state.projectRoot, mainAgentId, runId, prompt);
|
||
const accepted = parseAssignedJson(
|
||
(await runCli(args, { timeoutMs: 120_000 })).stdout,
|
||
['runtimeJson'],
|
||
);
|
||
const acceptedRuntime = accepted?.state;
|
||
const acceptedTask = accepted?.recentTasks?.find(
|
||
(task) => task.agentId === mainAgentId && task.runId === runId,
|
||
);
|
||
assert(
|
||
isPlainObject(acceptedRuntime),
|
||
'context-compaction-enqueue-state-missing',
|
||
);
|
||
assert(
|
||
acceptedTask?.agentId === mainAgentId &&
|
||
acceptedTask?.runId === runId &&
|
||
isNonEmptyString(acceptedTask?.sessionId),
|
||
'context-compaction-enqueue-task-identity-invalid',
|
||
);
|
||
if (turn === 1) {
|
||
state.initialRunId = runId;
|
||
state.initialSessionId = acceptedTask.sessionId;
|
||
state.initialTask = {
|
||
chars: [...prompt].length,
|
||
sha256: hashValue(prompt),
|
||
};
|
||
await claimOwnedRunner();
|
||
} else {
|
||
assert(
|
||
acceptedTask.sessionId === state.initialSessionId,
|
||
'context-compaction-session-changed-on-enqueue',
|
||
);
|
||
}
|
||
|
||
const terminal = await waitForContextCompactionTurn(runId);
|
||
state.contextCompaction.turnRunIds.push(runId);
|
||
state.contextCompaction.turnsCompleted = turn;
|
||
const estimated = Number(terminal.contextUsage?.estimatedInputTokens ?? 0);
|
||
const limit = Number(terminal.contextUsage?.autoCompactTokenLimit ?? 0);
|
||
assert(
|
||
Number.isSafeInteger(estimated) &&
|
||
estimated > 0 &&
|
||
Number.isSafeInteger(limit) &&
|
||
limit > 0 &&
|
||
estimated <= limit,
|
||
'context-compaction-turn-token-budget-invalid',
|
||
);
|
||
state.contextCompaction.maxEstimatedInputTokens = Math.max(
|
||
state.contextCompaction.maxEstimatedInputTokens,
|
||
estimated,
|
||
);
|
||
if (state.contextCompaction.autoCompactTokenLimit === 0) {
|
||
state.contextCompaction.autoCompactTokenLimit = limit;
|
||
} else {
|
||
assert(
|
||
state.contextCompaction.autoCompactTokenLimit === limit,
|
||
'context-compaction-token-limit-changed',
|
||
);
|
||
}
|
||
|
||
if (contextCompactionTriggerTurns.has(turn)) {
|
||
const revision = state.contextCompaction.compactionRevisions.length + 1;
|
||
const sidecar = await triggerManualContextCompaction(revision);
|
||
if (revision === 1) {
|
||
await restartContextCompactionRunner(sidecar);
|
||
}
|
||
}
|
||
}
|
||
|
||
state.identityStable = true;
|
||
state.evidence = await validateContextCompactionEvidence();
|
||
assert(state.evidence.secretLeakCount === 0, 'loaded-key-leak-detected');
|
||
}
|
||
|
||
export function buildContextCompactionTurnPrompt(turn) {
|
||
const filler = Array.from(
|
||
{ length: 36 },
|
||
(_, index) => `第${turn}轮背景片段${String(index + 1).padStart(2, '0')}`,
|
||
).join(';');
|
||
if (turn === 1) {
|
||
return `这是持久多轮聊天测试。必须记住项目约束代号 ${contextCompactionConstraintCanary},在第 30 轮明确询问前不要复述该代号;现在只用一句话确认已经记住。不要调用工具,不要修改项目。背景:${filler}`;
|
||
}
|
||
if (turn === contextCompactionRoundCount) {
|
||
return `这是第 ${turn} 轮。请只回复第 1 轮要求记住的项目约束代号,必须原样且只出现一次;不要调用工具,不要修改项目。背景:${filler}`;
|
||
}
|
||
return `这是第 ${turn} 轮持久多轮聊天测试。请只用一句话确认本轮已收到,不要复述第 1 轮代号,不要调用工具,不要修改项目。背景:${filler}`;
|
||
}
|
||
|
||
export async function waitForContextCompactionTurn(runId) {
|
||
const deadline = Date.now() + 6 * 60 * 1000;
|
||
while (Date.now() < deadline) {
|
||
const [runtime, tasks] = await Promise.all([
|
||
readRuntime(mainAgentId).catch(() => null),
|
||
readTaskSnapshot().catch(() => null),
|
||
]);
|
||
const task = tasks?.latest.find(
|
||
(candidate) =>
|
||
candidate.agentId === mainAgentId && candidate.runId === runId,
|
||
);
|
||
if (task && isFailedTask(task)) {
|
||
throw codedError('context-compaction-turn-failed');
|
||
}
|
||
if (
|
||
runtime?.runId === runId &&
|
||
runtime?.sessionId === state.initialSessionId &&
|
||
(runtime.status === 'waiting-for-confirmation' ||
|
||
runtime.phase === 'waiting-for-confirmation')
|
||
) {
|
||
throw codedError('context-compaction-unexpected-pending-action');
|
||
}
|
||
if (
|
||
runtime?.runId === runId &&
|
||
runtime?.sessionId === state.initialSessionId &&
|
||
runtime?.status === 'idle' &&
|
||
runtime?.phase === 'completed' &&
|
||
task?.status === 'completed' &&
|
||
task?.phase === 'completed'
|
||
) {
|
||
return runtime;
|
||
}
|
||
if (runtime?.phase === 'needs-reconciliation') {
|
||
throw codedError('context-compaction-runtime-needs-reconciliation');
|
||
}
|
||
await sleep(250);
|
||
}
|
||
throw codedError('context-compaction-turn-timeout');
|
||
}
|
||
|
||
export function contextCompactionSidecarPath() {
|
||
assert(
|
||
isNonEmptyString(state.initialSessionId),
|
||
'context-compaction-session-identity-missing',
|
||
);
|
||
return path.join(
|
||
state.projectRoot,
|
||
'.agent/runtime/context-compactions',
|
||
hashValue(mainAgentId).slice(0, 32),
|
||
`${hashValue(state.initialSessionId).slice(0, 32)}.json`,
|
||
);
|
||
}
|
||
|
||
export async function triggerManualContextCompaction(expectedRevision) {
|
||
const result = await runCli(
|
||
[
|
||
'--agent-context-compact',
|
||
state.projectRoot,
|
||
mainAgentId,
|
||
state.initialSessionId,
|
||
],
|
||
{ timeoutMs: 6 * 60 * 1000, allowNonZero: true },
|
||
);
|
||
if (result.code !== 0) {
|
||
const diagnostic = `${result.stdout}\n${result.stderr}`;
|
||
const failureKind = diagnostic.includes('需要人工核对')
|
||
? 'reconciliation'
|
||
: diagnostic.includes('正在运行')
|
||
? 'runtime-busy'
|
||
: diagnostic.includes('context bundle')
|
||
? 'context-bundle'
|
||
: diagnostic.includes('Provider') || diagnostic.includes('LLM')
|
||
? 'provider'
|
||
: diagnostic.includes('Session') || diagnostic.includes('会话')
|
||
? 'session'
|
||
: 'unknown';
|
||
throw codedError(`context-compaction-cli-${failureKind}-failed`);
|
||
}
|
||
const compacted = parseAssignedJson(result.stdout, ['contextCompactionJson']);
|
||
assert(
|
||
compacted?.agentId === mainAgentId &&
|
||
compacted?.sessionId === state.initialSessionId &&
|
||
compacted?.trigger === 'manual' &&
|
||
compacted?.revision === expectedRevision &&
|
||
compacted?.reused === false &&
|
||
Number.isSafeInteger(compacted?.coveredAgentMessages) &&
|
||
compacted.coveredAgentMessages > 0 &&
|
||
Number.isSafeInteger(compacted?.estimatedTokensBefore) &&
|
||
Number.isSafeInteger(compacted?.estimatedTokensAfter) &&
|
||
compacted.estimatedTokensAfter <= compacted.estimatedTokensBefore,
|
||
'context-compaction-manual-result-invalid',
|
||
);
|
||
const sidecar = await readJson(contextCompactionSidecarPath());
|
||
const previousSummaryFingerprint =
|
||
state.contextCompaction.compactionSummaryFingerprints.at(-1) ?? null;
|
||
assert(
|
||
sidecar?.schemaVersion === 'game-creator-runtime-context-compaction.v1' &&
|
||
sidecar?.agentId === mainAgentId &&
|
||
sidecar?.sessionId === state.initialSessionId &&
|
||
sidecar?.trigger === 'manual' &&
|
||
sidecar?.revision === expectedRevision &&
|
||
sidecar?.previousSummaryFingerprint === previousSummaryFingerprint &&
|
||
sidecar?.coveredAgentMessages === compacted.coveredAgentMessages &&
|
||
isNonEmptyString(sidecar?.sourceFingerprint) &&
|
||
/^[0-9a-f]{64}$/u.test(sidecar.sourceFingerprint) &&
|
||
isNonEmptyString(sidecar?.summary) &&
|
||
sidecar.summary.includes(contextCompactionConstraintCanary) &&
|
||
isNonEmptyString(sidecar?.summaryFingerprint) &&
|
||
/^[0-9a-f]{64}$/u.test(sidecar.summaryFingerprint),
|
||
'context-compaction-sidecar-invalid',
|
||
);
|
||
if (expectedRevision > 1) {
|
||
const previousCovered =
|
||
state.contextCompaction.lastCoveredAgentMessages ?? 0;
|
||
assert(
|
||
sidecar.coveredAgentMessages > previousCovered,
|
||
'context-compaction-source-did-not-advance',
|
||
);
|
||
}
|
||
state.contextCompaction.lastCoveredAgentMessages =
|
||
sidecar.coveredAgentMessages;
|
||
state.contextCompaction.compactionRevisions.push(sidecar.revision);
|
||
state.contextCompaction.compactionSourceFingerprints.push(
|
||
sidecar.sourceFingerprint,
|
||
);
|
||
state.contextCompaction.compactionSummaryFingerprints.push(
|
||
sidecar.summaryFingerprint,
|
||
);
|
||
state.contextCompaction.privateSummaries.push(sidecar.summary);
|
||
return sidecar;
|
||
}
|
||
|
||
export async function restartContextCompactionRunner(sidecarBeforeKill) {
|
||
const beforeKill = await readRunnerStatus();
|
||
state.contextCompaction.oldRunnerBootId = runnerBootId(beforeKill);
|
||
assert(
|
||
isNonEmptyString(state.contextCompaction.oldRunnerBootId),
|
||
'context-compaction-runner-boot-before-kill-missing',
|
||
);
|
||
await killRunnerOnce();
|
||
await runCli(['--agent-resume', state.projectRoot], { timeoutMs: 120_000 });
|
||
state.resumed = true;
|
||
const restarted = await waitForRunnerBootChange(
|
||
state.contextCompaction.oldRunnerBootId,
|
||
);
|
||
state.contextCompaction.newRunnerBootId = runnerBootId(restarted);
|
||
await claimOwnedRunner(restarted);
|
||
const sidecarAfterKill = await readJson(contextCompactionSidecarPath());
|
||
assert(
|
||
state.contextCompaction.newRunnerBootId !==
|
||
state.contextCompaction.oldRunnerBootId &&
|
||
sidecarAfterKill.revision === sidecarBeforeKill.revision &&
|
||
sidecarAfterKill.sourceFingerprint ===
|
||
sidecarBeforeKill.sourceFingerprint &&
|
||
sidecarAfterKill.summaryFingerprint ===
|
||
sidecarBeforeKill.summaryFingerprint,
|
||
'context-compaction-runner-recovery-invalid',
|
||
);
|
||
}
|
||
|
||
export async function readContextCompactionPersistence() {
|
||
const [
|
||
taskSnapshot,
|
||
events,
|
||
agentDb,
|
||
conversations,
|
||
activity,
|
||
output,
|
||
runtimeState,
|
||
sidecar,
|
||
] = await Promise.all([
|
||
readTaskSnapshot(),
|
||
readAllRuntimeEvents(),
|
||
readOptionalJsonl(path.join(state.projectRoot, '.agent/agent.db')),
|
||
readOptionalJsonl(
|
||
agentConversationPath(mainAgentId, state.initialSessionId),
|
||
),
|
||
readOptionalJsonl(path.join(state.projectRoot, '.agent/activity.jsonl')),
|
||
readOptionalJsonl(path.join(state.projectRoot, '.agent/output.jsonl')),
|
||
readJson(mainRuntimeStatePath()),
|
||
readJson(contextCompactionSidecarPath()),
|
||
]);
|
||
return {
|
||
taskSnapshot,
|
||
events,
|
||
agentDb,
|
||
conversations,
|
||
activity,
|
||
output,
|
||
runtimeState,
|
||
sidecar,
|
||
};
|
||
}
|
||
|
||
export function validateContextCompactionProviderLifecycle(agentDb) {
|
||
const lifecycle = agentDb.filter(
|
||
(record) =>
|
||
record.recordType === 'agent.runtime.provider_request.lifecycle' &&
|
||
record.agentId === mainAgentId &&
|
||
state.contextCompaction.turnRunIds.includes(record.runId),
|
||
);
|
||
const byRequest = new Map();
|
||
for (const record of lifecycle) {
|
||
assert(
|
||
isNonEmptyString(record.requestId) &&
|
||
isNonEmptyString(record.requestSlot) &&
|
||
isNonEmptyString(record.requestKind),
|
||
'context-compaction-provider-lifecycle-identity-invalid',
|
||
);
|
||
const records = byRequest.get(record.requestId) ?? [];
|
||
records.push(record);
|
||
byRequest.set(record.requestId, records);
|
||
}
|
||
for (const records of byRequest.values()) {
|
||
assert(
|
||
records.length === 2 &&
|
||
records[0].status === 'started' &&
|
||
records[1].status === 'completed' &&
|
||
records[0].requestSlot === records[1].requestSlot &&
|
||
records[0].requestKind === records[1].requestKind &&
|
||
records[0].runId === records[1].runId,
|
||
'context-compaction-provider-lifecycle-sequence-invalid',
|
||
);
|
||
}
|
||
const started = lifecycle.filter((record) => record.status === 'started');
|
||
const toolPlan = started.filter(
|
||
(record) => record.requestKind === 'tool-plan',
|
||
);
|
||
const compaction = started.filter(
|
||
(record) => record.requestKind === 'context-compaction',
|
||
);
|
||
assert(
|
||
new Set(toolPlan.map((record) => record.runId)).size ===
|
||
contextCompactionRoundCount &&
|
||
compaction.length === contextCompactionTriggerTurns.size &&
|
||
compaction.every((record) => record.webSearchEnabled === false),
|
||
'context-compaction-provider-request-count-invalid',
|
||
);
|
||
return {
|
||
requestIdentityCount: byRequest.size,
|
||
startedCount: started.length,
|
||
terminalCount: lifecycle.filter((record) => record.status === 'completed')
|
||
.length,
|
||
toolPlanCount: toolPlan.length,
|
||
compactionCount: compaction.length,
|
||
compactionRequestSlotSetHash: hashValue(
|
||
JSON.stringify(compaction.map((record) => record.requestSlot).sort()),
|
||
),
|
||
};
|
||
}
|
||
|
||
export async function validateContextCompactionEvidence() {
|
||
const persistence = await readContextCompactionPersistence();
|
||
const {
|
||
taskSnapshot,
|
||
events,
|
||
agentDb,
|
||
conversations,
|
||
activity,
|
||
output,
|
||
runtimeState,
|
||
sidecar,
|
||
} = persistence;
|
||
assert(
|
||
state.contextCompaction.turnsCompleted === contextCompactionRoundCount &&
|
||
state.contextCompaction.turnRunIds.length ===
|
||
contextCompactionRoundCount &&
|
||
new Set(state.contextCompaction.turnRunIds).size ===
|
||
contextCompactionRoundCount,
|
||
'context-compaction-turn-ledger-invalid',
|
||
);
|
||
const runIds = new Set(state.contextCompaction.turnRunIds);
|
||
const targetTasks = taskSnapshot.latest.filter(
|
||
(task) => task.agentId === mainAgentId && runIds.has(task.runId),
|
||
);
|
||
assert(
|
||
targetTasks.length === contextCompactionRoundCount &&
|
||
targetTasks.every(
|
||
(task) =>
|
||
task.sessionId === state.initialSessionId &&
|
||
task.status === 'completed' &&
|
||
task.phase === 'completed',
|
||
),
|
||
'context-compaction-task-completion-invalid',
|
||
);
|
||
|
||
const userMessages = conversations.filter(
|
||
(message) => message.role === 'user',
|
||
);
|
||
const assistantMessages = conversations.filter(
|
||
(message) => message.role === 'assistant',
|
||
);
|
||
const finalAssistant = assistantMessages.at(-1);
|
||
assert(
|
||
userMessages.length === contextCompactionRoundCount &&
|
||
assistantMessages.length === contextCompactionRoundCount &&
|
||
assistantMessages
|
||
.slice(0, -1)
|
||
.every(
|
||
(message) =>
|
||
countOccurrences(
|
||
message.content,
|
||
contextCompactionConstraintCanary,
|
||
) === 0,
|
||
) &&
|
||
countOccurrences(
|
||
finalAssistant?.content,
|
||
contextCompactionConstraintCanary,
|
||
) === 1,
|
||
'context-compaction-early-constraint-recall-invalid',
|
||
);
|
||
state.contextCompaction.finalReplyFingerprint = hashValue(
|
||
finalAssistant.content,
|
||
);
|
||
|
||
assert(
|
||
sidecar.schemaVersion === 'game-creator-runtime-context-compaction.v1' &&
|
||
sidecar.agentId === mainAgentId &&
|
||
sidecar.sessionId === state.initialSessionId &&
|
||
sidecar.trigger === 'manual' &&
|
||
sidecar.revision === contextCompactionTriggerTurns.size &&
|
||
sidecar.summary.includes(contextCompactionConstraintCanary) &&
|
||
state.contextCompaction.compactionRevisions.join(',') === '1,2' &&
|
||
new Set(state.contextCompaction.compactionSourceFingerprints).size === 2,
|
||
'context-compaction-final-sidecar-invalid',
|
||
);
|
||
assert(
|
||
runtimeState.agentId === mainAgentId &&
|
||
runtimeState.sessionId === state.initialSessionId &&
|
||
runtimeState.runId === state.contextCompaction.turnRunIds.at(-1) &&
|
||
runtimeState.status === 'idle' &&
|
||
runtimeState.phase === 'completed' &&
|
||
runtimeState.contextUsage?.compactionRevision === 2 &&
|
||
runtimeState.contextUsage?.compactionCount === 2 &&
|
||
runtimeState.contextUsage?.lastCompactionTrigger === 'manual' &&
|
||
runtimeState.contextUsage?.estimatedInputTokens <=
|
||
runtimeState.contextUsage?.autoCompactTokenLimit,
|
||
'context-compaction-final-runtime-usage-invalid',
|
||
);
|
||
|
||
const lifecycle = validateContextCompactionProviderLifecycle(agentDb);
|
||
const compactionAudits = agentDb.filter(
|
||
(record) =>
|
||
record.recordType === 'agent.runtime.context.compacted' &&
|
||
record.agentId === mainAgentId &&
|
||
record.sessionId === state.initialSessionId,
|
||
);
|
||
const compactionEvents = events.filter(
|
||
(event) =>
|
||
event.agentId === mainAgentId &&
|
||
event.sessionId === state.initialSessionId &&
|
||
event.eventType === 'context.compacted',
|
||
);
|
||
assert(
|
||
compactionAudits.length === 2 &&
|
||
compactionAudits.map((record) => record.revision).join(',') === '1,2' &&
|
||
compactionEvents.length === 2,
|
||
'context-compaction-public-audit-count-invalid',
|
||
);
|
||
|
||
const assistantAudits = agentDb.filter(
|
||
(record) =>
|
||
record.recordType === 'conversation.message' &&
|
||
record.role === 'assistant' &&
|
||
record.agentId === mainAgentId &&
|
||
record.sessionId === state.initialSessionId,
|
||
);
|
||
const receipts = agentDb.filter(
|
||
(record) =>
|
||
record.recordType === 'agent.runtime.action_receipt' &&
|
||
record.agentId === mainAgentId &&
|
||
runIds.has(record.runId),
|
||
);
|
||
assert(
|
||
assistantAudits.length === contextCompactionRoundCount &&
|
||
receipts.length === 0,
|
||
'context-compaction-assistant-or-tool-replay-invalid',
|
||
);
|
||
const duplicateMessageCount = duplicateCount(
|
||
conversations.map((message) => message.messageId).filter(Boolean),
|
||
);
|
||
const duplicateAssistantAuditCount = duplicateCount(
|
||
assistantAudits.map(
|
||
(record) =>
|
||
`${record.agentId}\0${record.sessionId}\0${record.finalizationId}\0${record.messageId}`,
|
||
),
|
||
);
|
||
assert(
|
||
duplicateMessageCount === 0 && duplicateAssistantAuditCount === 0,
|
||
'context-compaction-duplicate-message-identity',
|
||
);
|
||
|
||
const publicSurfaces = {
|
||
event: events,
|
||
agentDb,
|
||
receipt: receipts,
|
||
activity,
|
||
output,
|
||
};
|
||
const privateBodies = [
|
||
...conversations.map((message) => message.content),
|
||
...state.contextCompaction.privateSummaries,
|
||
].filter(isNonEmptyString);
|
||
const privateBodyPublicCounts = countSensitiveValuesBySurface(
|
||
publicSurfaces,
|
||
privateBodies,
|
||
'context-compaction-private-body-public',
|
||
);
|
||
const apiKeyPublicCounts = countSensitiveValuesBySurface(
|
||
publicSurfaces,
|
||
state.secrets,
|
||
'context-compaction-api-key-public',
|
||
);
|
||
const lurePublicCounts = countSensitiveValuesBySurface(
|
||
publicSurfaces,
|
||
state.lures,
|
||
'context-compaction-lure-public',
|
||
);
|
||
const projectPathPublicCounts = validateProjectRootPublicLeakBoundary(
|
||
publicSurfaces,
|
||
'context-compaction-public',
|
||
);
|
||
const formalConfigPathPublicCounts = countSensitiveValuesBySurface(
|
||
publicSurfaces,
|
||
formalConfigPathVariants(),
|
||
'context-compaction-formal-config-path-public',
|
||
);
|
||
const sidecarSensitiveLeakCount = countExactSecrets(
|
||
Buffer.from(JSON.stringify(sidecar)),
|
||
[
|
||
...state.secrets,
|
||
...state.lures,
|
||
...disposableProjectPathVariants(),
|
||
...formalConfigPathVariants(),
|
||
],
|
||
);
|
||
assert(
|
||
sidecarSensitiveLeakCount === 0,
|
||
'context-compaction-private-sidecar-sensitive-leak',
|
||
);
|
||
|
||
const finalizationFiles = (
|
||
await listFiles(
|
||
path.join(state.projectRoot, '.agent/runtime/finalizations'),
|
||
)
|
||
).filter((file) => file.endsWith('.json'));
|
||
assert(
|
||
finalizationFiles.length === 0,
|
||
'context-compaction-finalization-journal-present',
|
||
);
|
||
state.lureLeakCount = await countLureLeaks();
|
||
assert(state.lureLeakCount === 0, 'sensitive-lure-leak-detected');
|
||
const projectSecretLeakCount = await countSecretsInProject(
|
||
state.projectRoot,
|
||
state.secrets,
|
||
);
|
||
const secretLeakCount =
|
||
(state.transcriptScanner?.count ?? 0) + projectSecretLeakCount;
|
||
assert(secretLeakCount === 0, 'loaded-key-leak-detected');
|
||
|
||
return {
|
||
scenario: 'thirty-turn-persistent-context-compaction',
|
||
isolatedAppDataUsed: true,
|
||
formalConfigCliCallCount: state.isolatedRunner.sourceConfigCliCallCount,
|
||
sourceRunnerEndpointUnchanged: false,
|
||
sourceConfigHardlinkCount: state.isolatedRunner.configLinks.length,
|
||
sourceConfigLinksVerified: false,
|
||
turnCount: contextCompactionRoundCount,
|
||
completedTurnCount: targetTasks.length,
|
||
targetRunCount: runIds.size,
|
||
stableSessionCount: new Set(targetTasks.map((task) => task.sessionId)).size,
|
||
compactionCount: state.contextCompaction.compactionRevisions.length,
|
||
compactionRevisions: [...state.contextCompaction.compactionRevisions],
|
||
compactionSourceFingerprintSetHash: hashValue(
|
||
JSON.stringify(
|
||
[...state.contextCompaction.compactionSourceFingerprints].sort(),
|
||
),
|
||
),
|
||
compactionSummaryFingerprintSetHash: hashValue(
|
||
JSON.stringify(
|
||
[...state.contextCompaction.compactionSummaryFingerprints].sort(),
|
||
),
|
||
),
|
||
coveredAgentMessageCount: sidecar.coveredAgentMessages,
|
||
coveredProjectMessageCount: sidecar.coveredProjectMessages,
|
||
coveredObservationCount: sidecar.coveredObservations,
|
||
earlyConstraintRecalled: true,
|
||
earlyConstraintCanaryHash: hashValue(contextCompactionConstraintCanary),
|
||
finalReplyFingerprint: state.contextCompaction.finalReplyFingerprint,
|
||
maxEstimatedInputTokens: state.contextCompaction.maxEstimatedInputTokens,
|
||
autoCompactTokenLimit: state.contextCompaction.autoCompactTokenLimit,
|
||
requestStayedUnderAutoCompactLimit:
|
||
state.contextCompaction.maxEstimatedInputTokens <=
|
||
state.contextCompaction.autoCompactTokenLimit,
|
||
runnerBootChanged:
|
||
state.contextCompaction.oldRunnerBootId !==
|
||
state.contextCompaction.newRunnerBootId,
|
||
taskCount: taskSnapshot.all.length,
|
||
eventCount: events.length,
|
||
agentDbRecordCount: agentDb.length,
|
||
conversationMessageCount: conversations.length,
|
||
userMessageCount: userMessages.length,
|
||
finalAssistantCount: assistantMessages.length,
|
||
finalAssistantAuditCount: assistantAudits.length,
|
||
successfulToolExecutionCount: receipts.length,
|
||
duplicateMessageCount,
|
||
duplicateAssistantAuditCount,
|
||
providerRequestIdentityCount: lifecycle.requestIdentityCount,
|
||
providerLifecycleStartedCount: lifecycle.startedCount,
|
||
providerLifecycleTerminalCount: lifecycle.terminalCount,
|
||
toolPlanProviderRequestCount: lifecycle.toolPlanCount,
|
||
compactionProviderRequestCount: lifecycle.compactionCount,
|
||
compactionRequestSlotSetHash: lifecycle.compactionRequestSlotSetHash,
|
||
providerFallbackReplayCount: 0,
|
||
finalizationJournalCount: finalizationFiles.length,
|
||
privateBodyPublicLeakCount: sumObjectValues(privateBodyPublicCounts),
|
||
apiKeyPublicLeakCount: sumObjectValues(apiKeyPublicCounts),
|
||
lurePublicLeakCount: sumObjectValues(lurePublicCounts),
|
||
projectPathPublicLeakCount: sumObjectValues(projectPathPublicCounts),
|
||
projectPathPublicSurfaceCount: Object.keys(projectPathPublicCounts).length,
|
||
formalConfigPathPublicLeakCount: sumObjectValues(
|
||
formalConfigPathPublicCounts,
|
||
),
|
||
formalConfigPathPublicSurfaceCount: Object.keys(
|
||
formalConfigPathPublicCounts,
|
||
).length,
|
||
privateSidecarSensitiveLeakCount: sidecarSensitiveLeakCount,
|
||
contextCompactionReportLeakCount: state.contextCompaction.reportLeakCount,
|
||
contextCompactionRunnerKillMethod: null,
|
||
contextCompactionRunnerPidfdClaimCount:
|
||
state.isolatedRunner.pidfdClaimCount,
|
||
contextCompactionRunnerPidfdSignalCount:
|
||
state.isolatedRunner.pidfdSignalCount,
|
||
contextCompactionRunnerStopped: false,
|
||
contextCompactionAppDataCleanupPerformed: false,
|
||
secretLeakCount,
|
||
lureLeakCount: state.lureLeakCount,
|
||
paths: [
|
||
'.agent/runtime/context-compactions',
|
||
'.agent/runtime/tasks',
|
||
'.agent/runtime/events',
|
||
'.agent/agent.db',
|
||
'.agent/conversations',
|
||
],
|
||
};
|
||
}
|
||
|
||
export function emptyContextCompactionEvidence() {
|
||
return {
|
||
scenario: 'thirty-turn-persistent-context-compaction',
|
||
isolatedAppDataUsed: false,
|
||
formalConfigCliCallCount: 0,
|
||
sourceRunnerEndpointUnchanged: false,
|
||
sourceConfigHardlinkCount: 0,
|
||
sourceConfigLinksVerified: false,
|
||
turnCount: contextCompactionRoundCount,
|
||
completedTurnCount: 0,
|
||
targetRunCount: 0,
|
||
stableSessionCount: 0,
|
||
compactionCount: 0,
|
||
compactionRevisions: [],
|
||
compactionSourceFingerprintSetHash: null,
|
||
compactionSummaryFingerprintSetHash: null,
|
||
coveredAgentMessageCount: 0,
|
||
coveredProjectMessageCount: 0,
|
||
coveredObservationCount: 0,
|
||
earlyConstraintRecalled: false,
|
||
earlyConstraintCanaryHash: hashValue(contextCompactionConstraintCanary),
|
||
finalReplyFingerprint: null,
|
||
maxEstimatedInputTokens: 0,
|
||
autoCompactTokenLimit: 0,
|
||
requestStayedUnderAutoCompactLimit: false,
|
||
runnerBootChanged: false,
|
||
taskCount: 0,
|
||
eventCount: 0,
|
||
agentDbRecordCount: 0,
|
||
conversationMessageCount: 0,
|
||
userMessageCount: 0,
|
||
finalAssistantCount: 0,
|
||
finalAssistantAuditCount: 0,
|
||
successfulToolExecutionCount: 0,
|
||
duplicateMessageCount: 0,
|
||
duplicateAssistantAuditCount: 0,
|
||
providerRequestIdentityCount: 0,
|
||
providerLifecycleStartedCount: 0,
|
||
providerLifecycleTerminalCount: 0,
|
||
toolPlanProviderRequestCount: 0,
|
||
compactionProviderRequestCount: 0,
|
||
compactionRequestSlotSetHash: null,
|
||
providerFallbackReplayCount: 0,
|
||
finalizationJournalCount: 0,
|
||
privateBodyPublicLeakCount: 0,
|
||
apiKeyPublicLeakCount: 0,
|
||
lurePublicLeakCount: 0,
|
||
projectPathPublicLeakCount: 0,
|
||
projectPathPublicSurfaceCount: 0,
|
||
formalConfigPathPublicLeakCount: 0,
|
||
formalConfigPathPublicSurfaceCount: 0,
|
||
formalConfigPathTranscriptLeakCount: 0,
|
||
formalConfigPathReportLeakCount: 0,
|
||
privateSidecarSensitiveLeakCount: 0,
|
||
contextCompactionReportLeakCount: 0,
|
||
contextCompactionRunnerKillMethod: null,
|
||
contextCompactionRunnerPidfdClaimCount: 0,
|
||
contextCompactionRunnerPidfdSignalCount: 0,
|
||
contextCompactionRunnerStopped: false,
|
||
contextCompactionAppDataCleanupPerformed: false,
|
||
secretLeakCount: 0,
|
||
lureLeakCount: 0,
|
||
paths: [],
|
||
};
|
||
}
|
||
|
||
export async function collectPartialContextCompactionEvidence() {
|
||
const [tasks, events, agentDb, conversations, sidecar] = await Promise.all([
|
||
readTaskSnapshot().catch(() => ({ all: [], latest: [] })),
|
||
readAllRuntimeEvents().catch(() => []),
|
||
readOptionalJsonl(path.join(state.projectRoot, '.agent/agent.db')).catch(
|
||
() => [],
|
||
),
|
||
isNonEmptyString(state.initialSessionId)
|
||
? readOptionalJsonl(
|
||
agentConversationPath(mainAgentId, state.initialSessionId),
|
||
).catch(() => [])
|
||
: [],
|
||
isNonEmptyString(state.initialSessionId)
|
||
? readJson(contextCompactionSidecarPath()).catch(() => null)
|
||
: null,
|
||
]);
|
||
const targetRuns = new Set(state.contextCompaction.turnRunIds);
|
||
return {
|
||
completedTurnCount: tasks.latest.filter(
|
||
(task) =>
|
||
task.agentId === mainAgentId &&
|
||
targetRuns.has(task.runId) &&
|
||
task.status === 'completed' &&
|
||
task.phase === 'completed',
|
||
).length,
|
||
targetRunCount: targetRuns.size,
|
||
compactionCount: Number(sidecar?.revision ?? 0),
|
||
compactionRevisions: [...state.contextCompaction.compactionRevisions],
|
||
coveredAgentMessageCount: Number(sidecar?.coveredAgentMessages ?? 0),
|
||
taskCount: tasks.all.length,
|
||
eventCount: events.length,
|
||
agentDbRecordCount: agentDb.length,
|
||
conversationMessageCount: conversations.length,
|
||
userMessageCount: conversations.filter((message) => message.role === 'user')
|
||
.length,
|
||
finalAssistantCount: conversations.filter(
|
||
(message) => message.role === 'assistant',
|
||
).length,
|
||
maxEstimatedInputTokens: state.contextCompaction.maxEstimatedInputTokens,
|
||
autoCompactTokenLimit: state.contextCompaction.autoCompactTokenLimit,
|
||
runnerBootChanged:
|
||
isNonEmptyString(state.contextCompaction.oldRunnerBootId) &&
|
||
isNonEmptyString(state.contextCompaction.newRunnerBootId) &&
|
||
state.contextCompaction.oldRunnerBootId !==
|
||
state.contextCompaction.newRunnerBootId,
|
||
};
|
||
}
|
||
|
||
export function isContextCompactionSuite() {
|
||
return state.suite === contextCompactionSuite;
|
||
}
|