完成持久目标真实验收
修复文件写入正文被清洗和末尾换行被截断的问题 收紧运行事件与审计投影并兼容旧脱敏记录恢复 升级真实Goal验收到context v5和Provider lifecycle v2 记录正式gpt-5.5全链路验收证据并同步实施文档
This commit is contained in:
@@ -63,6 +63,10 @@ const webSearchSuite = 'web-search';
|
||||
const contextCompactionSuite = 'context-compaction';
|
||||
const mcpRuntimeSuite = 'mcp-runtime';
|
||||
const userInputRuntimeSuite = 'user-input-runtime';
|
||||
const runtimeContextBundleSchemaVersion =
|
||||
'game-creator-runtime-context-bundle.v5';
|
||||
const providerRequestLifecycleSchemaVersion =
|
||||
'game-creator-provider-request-lifecycle.v2';
|
||||
const mcpFixtureScript = path.join(
|
||||
appRoot,
|
||||
'src-tauri/test-fixtures/mcp-server.mjs',
|
||||
@@ -96,6 +100,9 @@ const goalInitialMarker = `GENARRATIVE_GOAL_REVISION_ONE_${randomUUID()
|
||||
const goalFinalMarker = `GENARRATIVE_GOAL_REVISION_TWO_${randomUUID()
|
||||
.replaceAll('-', '')
|
||||
.slice(0, 16)}`;
|
||||
const goalFailureEvidenceCanary = `GENARRATIVE_GOAL_FAILURE_EVIDENCE_${randomUUID()
|
||||
.replaceAll('-', '')
|
||||
.slice(0, 16)}`;
|
||||
const goalInitialPayload = {
|
||||
outcome: `在当前 disposable 项目的 ${goalDeliveryPath} 中交付一份新的变更证据,文件完整内容必须是唯一标记“${goalInitialMarker}”和一个结尾换行。基于仓库真实状态维护计划、完成变更并审阅实际结果,证据不足时不要结束。`,
|
||||
constraints: [
|
||||
@@ -111,6 +118,7 @@ const goalEditedPayload = {
|
||||
outcome: `前一版目标已经废止,项目交付文件不得保留标记“${goalInitialMarker}”。改为修复当前 disposable 项目唯一的真实验收失败,让项目声明的验收通过,并让 ${goalDeliveryPath} 的完整内容只包含唯一标记“${goalFinalMarker}”和一个结尾换行。基于真实运行反馈维护计划并审阅完整结果,证据不足时不要结束。`,
|
||||
constraints: [
|
||||
'保留既有可见内容、非空动画画布和仓库安全边界,只落地完成当前目标所需的原子变更。',
|
||||
`失败证据门禁 ${goalFailureEvidenceCanary}:任何修复动作前必须先运行项目清单声明的原始验收并观察到真实非零退出;只读源码或验收脚本得出的推断不能替代失败证据。`,
|
||||
'不得读取、提交或转述敏感诱饵、配置密钥、Runtime 私有正文或项目绝对路径。',
|
||||
`任何项目交付文件都不得出现已废止标记“${goalInitialMarker}”。`,
|
||||
],
|
||||
@@ -318,6 +326,7 @@ const state = {
|
||||
editedGoalSnapshotFingerprint: null,
|
||||
initialMarkerAbsenceCheckCount: 0,
|
||||
monitoredWriteActionIds: new Set(),
|
||||
preFailureDeliveryActionIds: new Set(),
|
||||
runner: isolatedRunnerState,
|
||||
},
|
||||
responseStream: {
|
||||
@@ -1208,10 +1217,10 @@ async function runGoalRuntimeE2e() {
|
||||
|
||||
const editedPending = await waitForGoalRevisionPendingAction({
|
||||
revision: state.goal.editedRevision,
|
||||
marker: goalFinalMarker,
|
||||
codePrefix: 'goal-edited',
|
||||
minimumPlanRevision: initialPending.plan.revision + 1,
|
||||
requiredCompletedStepHashes: state.goal.initialCompletedStepHashes,
|
||||
pendingMatcher: goalPendingMatchesRevisionTwoRepair,
|
||||
});
|
||||
state.goal.editedPending = summarizeGoalPending(editedPending.pending);
|
||||
|
||||
@@ -4609,6 +4618,7 @@ function goalPrivateBodyValues() {
|
||||
...goalEditedPayload.verification,
|
||||
goalInitialMarker,
|
||||
goalFinalMarker,
|
||||
goalFailureEvidenceCanary,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4622,6 +4632,7 @@ function goalPublicBodyValues() {
|
||||
...goalEditedPayload.verification,
|
||||
goalInitialMarker,
|
||||
goalFinalMarker,
|
||||
goalFailureEvidenceCanary,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5163,7 +5174,7 @@ function inspectStructuredPlanSnapshot(runtime, codePrefix) {
|
||||
|
||||
function assertStructuredPlanContextSnapshot(runtime, contextBundle, code) {
|
||||
assert(
|
||||
contextBundle.schemaVersion === 'game-creator-runtime-context-bundle.v3' &&
|
||||
contextBundle.schemaVersion === runtimeContextBundleSchemaVersion &&
|
||||
contextBundle.agentId === runtime.agentId &&
|
||||
contextBundle.taskId === runtime.taskId &&
|
||||
contextBundle.sessionId === runtime.sessionId &&
|
||||
@@ -5258,16 +5269,17 @@ function assertGoalMutationIdentity(mutation, expectedRevision, codePrefix) {
|
||||
}
|
||||
|
||||
function goalSnapshotFingerprint(goal) {
|
||||
// serde_json::Map uses lexicographically sorted keys without preserve_order.
|
||||
return hashValue(
|
||||
JSON.stringify({
|
||||
projectId: goal.projectId,
|
||||
goalId: goal.goalId,
|
||||
agentId: goal.agentId,
|
||||
sessionId: goal.sessionId,
|
||||
runId: goal.runId,
|
||||
revision: goal.revision,
|
||||
outcome: goal.outcome,
|
||||
constraints: goal.constraints,
|
||||
goalId: goal.goalId,
|
||||
outcome: goal.outcome,
|
||||
projectId: goal.projectId,
|
||||
revision: goal.revision,
|
||||
runId: goal.runId,
|
||||
sessionId: goal.sessionId,
|
||||
verification: goal.verification,
|
||||
}),
|
||||
);
|
||||
@@ -5291,7 +5303,7 @@ async function readGoalStatus() {
|
||||
function assertGoalContextSnapshot(runtime, contextBundle, goal, codePrefix) {
|
||||
const expectedGoalSnapshotFingerprint = goalSnapshotFingerprint(goal);
|
||||
assert(
|
||||
contextBundle.schemaVersion === 'game-creator-runtime-context-bundle.v4' &&
|
||||
contextBundle.schemaVersion === runtimeContextBundleSchemaVersion &&
|
||||
contextBundle.projectId === goal.projectId &&
|
||||
contextBundle.agentId === runtime.agentId &&
|
||||
contextBundle.taskId === runtime.taskId &&
|
||||
@@ -5366,13 +5378,39 @@ function goalPendingMatchesDelivery(pending, marker) {
|
||||
);
|
||||
}
|
||||
|
||||
function validateGoalPendingAction(
|
||||
pending,
|
||||
plan,
|
||||
revision,
|
||||
marker,
|
||||
codePrefix,
|
||||
) {
|
||||
function goalPendingIsStandaloneDeliveryMutation(pending, marker) {
|
||||
if (!goalPendingMatchesDelivery(pending, marker)) return false;
|
||||
const action = pending?.record?.action ?? pending?.action;
|
||||
return (
|
||||
action?.tool === 'file.write' ||
|
||||
(action?.tool === 'project.patchset' && action.input?.changes?.length === 1)
|
||||
);
|
||||
}
|
||||
|
||||
function goalPendingMatchesRevisionTwoRepair(pending) {
|
||||
const action = pending?.record?.action ?? pending?.action;
|
||||
const input = action?.input;
|
||||
if (!action || !input || typeof input !== 'object') return false;
|
||||
if (action.tool === 'file.patch') {
|
||||
return input.path === 'game/index.html';
|
||||
}
|
||||
if (action.tool === 'file.write') {
|
||||
return (
|
||||
input.path === 'game/index.html' || input.path === patchsetCreatedPath
|
||||
);
|
||||
}
|
||||
return (
|
||||
action.tool === 'project.patchset' &&
|
||||
Array.isArray(input.changes) &&
|
||||
input.changes.some(
|
||||
(change) =>
|
||||
change?.path === 'game/index.html' ||
|
||||
change?.path === patchsetCreatedPath,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function validateGoalPendingAction(pending, plan, revision, codePrefix) {
|
||||
const record = pending.record;
|
||||
const expectedGoalSnapshotFingerprint = goalSnapshotFingerprint(plan.goal);
|
||||
assert(
|
||||
@@ -5391,8 +5429,7 @@ function validateGoalPendingAction(
|
||||
record.actionId === pending.actionId &&
|
||||
record.action?.tool === pending.tool &&
|
||||
['pending', 'pending-confirmation'].includes(record.status) &&
|
||||
Number.isSafeInteger(record.plannedSteerCursor) &&
|
||||
goalPendingMatchesDelivery(pending, marker),
|
||||
Number.isSafeInteger(record.plannedSteerCursor),
|
||||
`${codePrefix}-pending-action-invalid`,
|
||||
);
|
||||
if (revision === state.goal.initialRevision) {
|
||||
@@ -5416,11 +5453,20 @@ function validateGoalPendingAction(
|
||||
|
||||
async function waitForGoalRevisionPendingAction({
|
||||
revision,
|
||||
marker,
|
||||
codePrefix,
|
||||
minimumPlanRevision = 1,
|
||||
requiredCompletedStepHashes = [],
|
||||
marker = null,
|
||||
pendingMatcher = null,
|
||||
}) {
|
||||
const matchesPending =
|
||||
pendingMatcher ??
|
||||
((pending) => goalPendingMatchesDelivery(pending, marker));
|
||||
assert(
|
||||
typeof matchesPending === 'function' &&
|
||||
(pendingMatcher || isNonEmptyString(marker)),
|
||||
`${codePrefix}-pending-matcher-invalid`,
|
||||
);
|
||||
const deadline = Date.now() + runTimeoutMs;
|
||||
let lastError = null;
|
||||
while (Date.now() < deadline) {
|
||||
@@ -5437,52 +5483,45 @@ async function waitForGoalRevisionPendingAction({
|
||||
throw codedError(`${codePrefix}-runtime-terminal-before-pending`);
|
||||
}
|
||||
|
||||
let plan = null;
|
||||
let targetPending = null;
|
||||
try {
|
||||
const plan = await readVerifiedGoalPlanSnapshot(`${codePrefix}-plan`);
|
||||
const pendingActions = (await findPendingActions()).filter(
|
||||
(pending) =>
|
||||
pending.agentId === mainAgentId &&
|
||||
pending.runId === state.initialRunId,
|
||||
);
|
||||
targetPending = pendingActions.find((pending) =>
|
||||
goalPendingMatchesDelivery(pending, marker),
|
||||
);
|
||||
if (targetPending) {
|
||||
validateGoalPendingAction(
|
||||
targetPending,
|
||||
plan,
|
||||
revision,
|
||||
marker,
|
||||
codePrefix,
|
||||
);
|
||||
assert(
|
||||
plan.revision >= minimumPlanRevision &&
|
||||
plan.completedStepHashes.length > 0 &&
|
||||
plan.incompleteStepCount > 0 &&
|
||||
requiredCompletedStepHashes.every((stepHash) =>
|
||||
plan.completedStepHashes.includes(stepHash),
|
||||
),
|
||||
`${codePrefix}-partial-plan-missing`,
|
||||
);
|
||||
const messages = await readOptionalJsonl(
|
||||
agentConversationPath(mainAgentId, state.initialSessionId),
|
||||
);
|
||||
assert(
|
||||
messages.filter((message) => message.role === 'assistant').length ===
|
||||
0,
|
||||
`${codePrefix}-assistant-before-control-point`,
|
||||
);
|
||||
return { pending: targetPending, plan };
|
||||
}
|
||||
targetPending = pendingActions.find(matchesPending);
|
||||
plan = await readVerifiedGoalPlanSnapshot(`${codePrefix}-plan`);
|
||||
} catch (error) {
|
||||
if (targetPending) {
|
||||
throw codedError(`${codePrefix}-pending-contract-invalid`, error);
|
||||
}
|
||||
lastError = error;
|
||||
}
|
||||
|
||||
await confirmPendingActions(
|
||||
null,
|
||||
(pending) => !goalPendingMatchesDelivery(pending, marker),
|
||||
);
|
||||
if (targetPending) {
|
||||
validateGoalPendingAction(targetPending, plan, revision, codePrefix);
|
||||
assert(
|
||||
plan.revision >= minimumPlanRevision &&
|
||||
plan.completedStepHashes.length > 0 &&
|
||||
plan.incompleteStepCount > 0 &&
|
||||
requiredCompletedStepHashes.every((stepHash) =>
|
||||
plan.completedStepHashes.includes(stepHash),
|
||||
),
|
||||
`${codePrefix}-partial-plan-missing`,
|
||||
);
|
||||
const messages = await readOptionalJsonl(
|
||||
agentConversationPath(mainAgentId, state.initialSessionId),
|
||||
);
|
||||
assert(
|
||||
messages.filter((message) => message.role === 'assistant').length === 0,
|
||||
`${codePrefix}-assistant-before-control-point`,
|
||||
);
|
||||
return { pending: targetPending, plan };
|
||||
}
|
||||
|
||||
await confirmPendingActions(null, (pending) => !matchesPending(pending));
|
||||
await sleep(pollIntervalMs);
|
||||
}
|
||||
throw codedError(`${codePrefix}-pending-action-timeout`, lastError);
|
||||
@@ -5567,8 +5606,7 @@ async function waitForGoalOldActionBlocked(initialPending) {
|
||||
runtime.sessionId === state.initialSessionId &&
|
||||
runtime.goalId === state.goal.goalId &&
|
||||
runtime.goalRevision === state.goal.editedRevision &&
|
||||
contextBundle.schemaVersion ===
|
||||
'game-creator-runtime-context-bundle.v4' &&
|
||||
contextBundle.schemaVersion === runtimeContextBundleSchemaVersion &&
|
||||
contextBundle.goalId === state.goal.goalId &&
|
||||
contextBundle.goalRevision === state.goal.editedRevision &&
|
||||
contextBundle.goalSnapshotFingerprint ===
|
||||
@@ -5651,15 +5689,26 @@ async function waitForGoalRevisionTwoAgentVerificationFailure() {
|
||||
(pending) =>
|
||||
pending.agentId === mainAgentId && pending.runId === state.initialRunId,
|
||||
);
|
||||
const earlyWrite = pendingActions.find((pending) =>
|
||||
goalProjectWriteTools.has(pending.tool),
|
||||
const unsupportedEarlyWrite = pendingActions.find(
|
||||
(pending) =>
|
||||
goalProjectWriteTools.has(pending.tool) &&
|
||||
!goalPendingIsStandaloneDeliveryMutation(pending, goalFinalMarker),
|
||||
);
|
||||
assert(
|
||||
!unsupportedEarlyWrite,
|
||||
'goal-revision-two-repair-before-failed-verification',
|
||||
);
|
||||
assert(!earlyWrite, 'goal-revision-two-write-before-failed-verification');
|
||||
await confirmPendingActions(
|
||||
new Set(['project.checkpoint', 'project.verify']),
|
||||
new Set([
|
||||
'project.checkpoint',
|
||||
'project.verify',
|
||||
'file.write',
|
||||
'project.patchset',
|
||||
]),
|
||||
(pending) =>
|
||||
pending.tool === 'project.checkpoint' ||
|
||||
pending.tool === 'project.verify',
|
||||
pending.tool === 'project.verify' ||
|
||||
goalPendingIsStandaloneDeliveryMutation(pending, goalFinalMarker),
|
||||
);
|
||||
lastError = codedError('goal-revision-two-agent-failure-not-yet-observed');
|
||||
await sleep(pollIntervalMs);
|
||||
@@ -5724,17 +5773,21 @@ function findGoalRevisionTwoAgentVerificationFailure(records) {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const earlyWrite = records
|
||||
const unsupportedEarlyWrite = records
|
||||
.slice(state.goal.revisionTwoEditAgentDbBoundary, observationIndex + 1)
|
||||
.some(
|
||||
(record) =>
|
||||
goalProjectWriteTools.has(record.tool) &&
|
||||
!state.goal.preFailureDeliveryActionIds.has(record.actionId) &&
|
||||
[
|
||||
'agent.runtime.tool_action.executing',
|
||||
'agent.runtime.tool_confirmation.approved',
|
||||
].includes(record.recordType),
|
||||
);
|
||||
assert(!earlyWrite, 'goal-revision-two-write-executed-before-failure');
|
||||
assert(
|
||||
!unsupportedEarlyWrite,
|
||||
'goal-revision-two-repair-executed-before-failure',
|
||||
);
|
||||
return { audit, auditIndex, observationIndex, startIndex };
|
||||
}
|
||||
return null;
|
||||
@@ -6522,6 +6575,7 @@ function validateResponseStreamProviderLifecycle(agentDb, stream) {
|
||||
'requestId',
|
||||
'requestKind',
|
||||
'requestSlot',
|
||||
'webSearchEnabled',
|
||||
'status',
|
||||
'schemaVersion',
|
||||
'updatedAt',
|
||||
@@ -6529,11 +6583,11 @@ function validateResponseStreamProviderLifecycle(agentDb, stream) {
|
||||
assert(records.length === 2, 'response-stream-final-lifecycle-count-invalid');
|
||||
for (const record of records) {
|
||||
assert(
|
||||
record.auditSchemaVersion ===
|
||||
'game-creator-provider-request-lifecycle.v1' &&
|
||||
record.auditSchemaVersion === providerRequestLifecycleSchemaVersion &&
|
||||
record.taskId === stream.taskId &&
|
||||
record.sessionId === state.initialSessionId &&
|
||||
record.requestKind === 'final-reply' &&
|
||||
record.webSearchEnabled === false &&
|
||||
record.requestSlot === stream.requestSlot &&
|
||||
isNonEmptyString(record.requestId) &&
|
||||
Number.isSafeInteger(record.updatedAt) &&
|
||||
@@ -7135,8 +7189,7 @@ function validateWebSearchProviderLifecycle(agentDb, runtimeState) {
|
||||
);
|
||||
for (const record of records) {
|
||||
assert(
|
||||
record.auditSchemaVersion ===
|
||||
'game-creator-provider-request-lifecycle.v2' &&
|
||||
record.auditSchemaVersion === providerRequestLifecycleSchemaVersion &&
|
||||
record.taskId === runtimeState.taskId &&
|
||||
record.sessionId === state.initialSessionId &&
|
||||
['tool-plan', 'final-reply'].includes(record.requestKind) &&
|
||||
@@ -7987,8 +8040,7 @@ async function captureGoalPausedSnapshot(expectedPending, codePrefix) {
|
||||
latest?.status === 'paused' &&
|
||||
latest?.phase === 'paused' &&
|
||||
JSON.stringify(targetRuns) === JSON.stringify([state.initialRunId]) &&
|
||||
contextBundle.schemaVersion ===
|
||||
'game-creator-runtime-context-bundle.v4' &&
|
||||
contextBundle.schemaVersion === runtimeContextBundleSchemaVersion &&
|
||||
contextBundle.goalId === state.goal.goalId &&
|
||||
contextBundle.goalRevision === state.goal.editedRevision &&
|
||||
contextBundle.goalStatus === 'active' &&
|
||||
@@ -8381,11 +8433,19 @@ async function confirmPendingActions(
|
||||
const monitorsGoalWrite =
|
||||
isGoalRuntimeSuite() && goalProjectWriteTools.has(pending.tool);
|
||||
if (monitorsGoalWrite) {
|
||||
const allowedRevisionTwoDelivery =
|
||||
state.goal.editedRevision > 0 &&
|
||||
state.goal.revisionTwoFailureObserved !== true &&
|
||||
goalPendingIsStandaloneDeliveryMutation(pending, goalFinalMarker);
|
||||
assert(
|
||||
state.goal.editedRevision === 0 ||
|
||||
state.goal.revisionTwoFailureObserved === true,
|
||||
state.goal.revisionTwoFailureObserved === true ||
|
||||
allowedRevisionTwoDelivery,
|
||||
'goal-write-confirmed-before-revision-two-failure',
|
||||
);
|
||||
if (allowedRevisionTwoDelivery) {
|
||||
state.goal.preFailureDeliveryActionIds.add(pending.actionId);
|
||||
}
|
||||
await assertGoalInitialMarkerAbsent(
|
||||
'goal-write-before-confirm',
|
||||
pending.actionId,
|
||||
@@ -10133,7 +10193,7 @@ async function validateLandedEvidence() {
|
||||
const runtimeStatePath = mainRuntimeStatePath();
|
||||
const runtimeState = await readJson(runtimeStatePath);
|
||||
assert(
|
||||
contextBundle.schemaVersion === 'game-creator-runtime-context-bundle.v3' &&
|
||||
contextBundle.schemaVersion === runtimeContextBundleSchemaVersion &&
|
||||
contextBundle.agentId === mainAgentId &&
|
||||
contextBundle.runId === state.initialRunId &&
|
||||
typeof contextBundle.repositoryContextFingerprint === 'string' &&
|
||||
@@ -12032,9 +12092,12 @@ async function validateGoalRuntimeEvidence() {
|
||||
Buffer.from(JSON.stringify(records)),
|
||||
goalPublicBodyValues(),
|
||||
);
|
||||
assert(
|
||||
goalPublicBodyLeakCounts[surface] === 0,
|
||||
`goal-body-public-${surface}-leak-detected`,
|
||||
);
|
||||
}
|
||||
const goalPublicBodyLeakCount = sumObjectValues(goalPublicBodyLeakCounts);
|
||||
assert(goalPublicBodyLeakCount === 0, 'goal-body-public-leak-detected');
|
||||
const projectPathPublicLeakCounts = validateProjectRootPublicLeakBoundary(
|
||||
publicSurfaces,
|
||||
'goal-public',
|
||||
@@ -12088,6 +12151,8 @@ async function validateGoalRuntimeEvidence() {
|
||||
goalInitialMarkerAbsenceCheckCount:
|
||||
state.goal.initialMarkerAbsenceCheckCount,
|
||||
goalMonitoredWriteActionCount: state.goal.monitoredWriteActionIds.size,
|
||||
goalPreFailureDeliveryActionCount:
|
||||
state.goal.preFailureDeliveryActionIds.size,
|
||||
goalFinalStatus: goal.status,
|
||||
goalEditProviderInterrupted: state.goal.editProviderInterrupted,
|
||||
goalPauseProviderInterrupted: state.goal.pauseProviderInterrupted,
|
||||
@@ -12356,12 +12421,14 @@ function validateGoalProviderRequestLifecycle(agentDb) {
|
||||
const byRequest = new Map();
|
||||
for (const record of records) {
|
||||
assert(
|
||||
record.auditSchemaVersion ===
|
||||
'game-creator-provider-request-lifecycle.v1' &&
|
||||
record.auditSchemaVersion === providerRequestLifecycleSchemaVersion &&
|
||||
record.taskId &&
|
||||
record.sessionId === state.initialSessionId &&
|
||||
['tool-plan', 'final-reply'].includes(record.requestKind) &&
|
||||
typeof record.requestSlot === 'string' &&
|
||||
typeof record.webSearchEnabled === 'boolean' &&
|
||||
(record.requestKind !== 'final-reply' ||
|
||||
record.webSearchEnabled === false) &&
|
||||
!['prompt', 'response', 'error', 'baseUrl', 'model'].some((key) =>
|
||||
Object.hasOwn(record, key),
|
||||
),
|
||||
@@ -13108,6 +13175,7 @@ function emptyGoalEvidence() {
|
||||
goalSnapshotFingerprintChanged: false,
|
||||
goalInitialMarkerAbsenceCheckCount: 0,
|
||||
goalMonitoredWriteActionCount: 0,
|
||||
goalPreFailureDeliveryActionCount: 0,
|
||||
goalFinalStatus: null,
|
||||
goalEditProviderInterrupted: false,
|
||||
goalPauseProviderInterrupted: false,
|
||||
@@ -13629,6 +13697,8 @@ async function collectPartialGoalEvidence() {
|
||||
goalInitialMarkerAbsenceCheckCount:
|
||||
state.goal.initialMarkerAbsenceCheckCount,
|
||||
goalMonitoredWriteActionCount: state.goal.monitoredWriteActionIds.size,
|
||||
goalPreFailureDeliveryActionCount:
|
||||
state.goal.preFailureDeliveryActionIds.size,
|
||||
goalFinalStatus: goal?.status ?? runtime?.goalStatus ?? null,
|
||||
goalRunnerKillMethod:
|
||||
state.goal.runner.pidfdClaimCount > 0 ? 'linux-pidfd' : null,
|
||||
@@ -15326,21 +15396,27 @@ function validateToolActionReplays(records) {
|
||||
'tool-action-replay-identity-missing',
|
||||
);
|
||||
const attempt = {
|
||||
recordType: record.recordType,
|
||||
agentId: record.agentId,
|
||||
runId: record.runId,
|
||||
actionId: record.actionId,
|
||||
actionFingerprint: record.actionFingerprint,
|
||||
tool: record.tool,
|
||||
status: record.status ?? null,
|
||||
rawInputSummary: record.inputSummary ?? null,
|
||||
inputSummary: canonicalAuditInputSummary(record.inputSummary),
|
||||
};
|
||||
const existing = attemptsByActionId.get(attempt.actionId);
|
||||
if (existing) {
|
||||
assert(
|
||||
const sameIdentity =
|
||||
existing.agentId === attempt.agentId &&
|
||||
existing.runId === attempt.runId &&
|
||||
existing.actionFingerprint === attempt.actionFingerprint &&
|
||||
existing.tool === attempt.tool &&
|
||||
existing.inputSummary === attempt.inputSummary,
|
||||
existing.runId === attempt.runId &&
|
||||
existing.actionFingerprint === attempt.actionFingerprint &&
|
||||
existing.tool === attempt.tool &&
|
||||
existing.inputSummary === attempt.inputSummary;
|
||||
assert(
|
||||
sameIdentity ||
|
||||
goalStaleActionReceiptMatchesOriginalAction(existing, attempt),
|
||||
'tool-action-replay-identity-conflict',
|
||||
);
|
||||
} else {
|
||||
@@ -15392,6 +15468,26 @@ function validateToolActionReplays(records) {
|
||||
};
|
||||
}
|
||||
|
||||
function goalStaleActionReceiptMatchesOriginalAction(original, receipt) {
|
||||
if (
|
||||
!isGoalRuntimeSuite() ||
|
||||
receipt.recordType !== 'agent.runtime.action_receipt' ||
|
||||
receipt.tool !== 'runtime.goal' ||
|
||||
receipt.status !== 'blocked' ||
|
||||
!goalProjectWriteTools.has(original.tool) ||
|
||||
!isNonEmptyString(original.rawInputSummary) ||
|
||||
original.agentId !== receipt.agentId ||
|
||||
original.runId !== receipt.runId ||
|
||||
original.actionFingerprint !== receipt.actionFingerprint
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const expectedReceiptSummary = canonicalAuditInputSummary(
|
||||
`inputSummarySha256=${hashValue(original.rawInputSummary)} · inputSummaryChars=${[...original.rawInputSummary].length}`,
|
||||
);
|
||||
return receipt.inputSummary === expectedReceiptSummary;
|
||||
}
|
||||
|
||||
function canonicalAuditInputSummary(summary) {
|
||||
if (summary == null || summary === '') return '[empty]';
|
||||
assert(typeof summary === 'string', 'audit-input-summary-invalid');
|
||||
|
||||
@@ -4174,6 +4174,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
};
|
||||
runtime.pending_tool_action = Some(pending.summary());
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let public_observation_detail = agent_runtime_public_observation_detail(&root, &observation);
|
||||
let persisted =
|
||||
append_game_creator_agent_runtime_task_projection_once(&root, &runtime, &pending.action_id)
|
||||
.and_then(|_| refresh_game_creator_agent_runtime_task_queue(&root, &mut runtime))
|
||||
@@ -4186,7 +4187,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
|
||||
"running",
|
||||
"observation",
|
||||
&observation_summary,
|
||||
agent_runtime_public_observation_detail(&observation),
|
||||
public_observation_detail.as_deref(),
|
||||
&pending.action_id,
|
||||
)
|
||||
})
|
||||
@@ -4375,7 +4376,7 @@ fn mark_game_creator_agent_runtime_tool_observation_needs_reconciliation_at(
|
||||
Some(&pending.action_id),
|
||||
);
|
||||
complete_agent_runtime_active_plan_step(runtime, "failed", &observation_summary);
|
||||
let mut error = agent_runtime_public_observation_detail(observation)
|
||||
let mut error = agent_runtime_public_observation_detail(root, observation)
|
||||
.filter(|detail| !detail.trim().is_empty())
|
||||
.map(|detail| format!("{observation_summary};{detail}"))
|
||||
.unwrap_or(observation_summary);
|
||||
@@ -6355,6 +6356,8 @@ async fn run_game_creator_agent_background_task_pass_with_context(
|
||||
};
|
||||
}
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let public_observation_detail =
|
||||
agent_runtime_public_observation_detail(&root, &observation);
|
||||
let persistence = append_game_creator_agent_runtime_task_projection_once(
|
||||
&root,
|
||||
&runtime,
|
||||
@@ -6377,7 +6380,7 @@ async fn run_game_creator_agent_background_task_pass_with_context(
|
||||
"observation"
|
||||
},
|
||||
observation_summary.as_str(),
|
||||
agent_runtime_public_observation_detail(&observation),
|
||||
public_observation_detail.as_deref(),
|
||||
observation_action_identity
|
||||
.as_ref()
|
||||
.map(|identity| identity.0.as_str())
|
||||
@@ -11643,7 +11646,7 @@ impl AgentRuntimeToolObservation {
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_runtime_public_observation_detail(
|
||||
fn agent_runtime_local_observation_detail(
|
||||
observation: &AgentRuntimeToolObservation,
|
||||
) -> Option<&str> {
|
||||
if matches!(
|
||||
@@ -11659,6 +11662,18 @@ fn agent_runtime_public_observation_detail(
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_runtime_public_observation_detail(
|
||||
root: &Path,
|
||||
observation: &AgentRuntimeToolObservation,
|
||||
) -> Option<String> {
|
||||
if observation.tool == "agent.action_history" && observation.status == "ok" {
|
||||
let detail = observation.detail.as_deref()?;
|
||||
validate_agent_runtime_pending_serialized_content(root, detail).ok()?;
|
||||
return Some(detail.to_string());
|
||||
}
|
||||
agent_runtime_action_receipt_safe_detail(root, observation)
|
||||
}
|
||||
|
||||
pub(crate) fn advance_agent_runtime_project_revision_locked(root: &Path) -> Result<u64, String> {
|
||||
let mut revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
let next_revision = revision
|
||||
@@ -12505,7 +12520,7 @@ pub(crate) fn append_agent_runtime_tool_call_record(
|
||||
.map(|value| sanitize_agent_runtime_text(value, 240))
|
||||
.filter(|value| !value.trim().is_empty()),
|
||||
summary: sanitize_agent_runtime_text(&observation.summary, 240),
|
||||
detail: agent_runtime_public_observation_detail(observation)
|
||||
detail: agent_runtime_local_observation_detail(observation)
|
||||
.map(|value| sanitize_agent_runtime_text(value, 500))
|
||||
.filter(|value| !value.trim().is_empty()),
|
||||
updated_at: unix_timestamp(),
|
||||
@@ -17496,6 +17511,7 @@ fn observe_agent_runtime_memory_write(
|
||||
write_isolated_agent_private_memory_at(root, &target_agent_id, &next_content)
|
||||
})
|
||||
.and_then(|path| {
|
||||
let relative_path = normalize_relative_path(&path)?;
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
@@ -17503,7 +17519,7 @@ fn observe_agent_runtime_memory_write(
|
||||
"agentId": agent_id,
|
||||
"targetAgentId": target_agent_id,
|
||||
"scope": "agent",
|
||||
"path": path,
|
||||
"path": relative_path,
|
||||
"mode": if overwrite { "overwrite" } else { "append" },
|
||||
"memoryLane": "isolated-instance-private",
|
||||
}),
|
||||
@@ -17518,6 +17534,7 @@ fn observe_agent_runtime_memory_write(
|
||||
write_local_agent_memory_at(root, &target_agent_id, &next_content)
|
||||
})
|
||||
.and_then(|memory| {
|
||||
let relative_path = relative_project_path(root, Path::new(&memory.path))?;
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
@@ -17525,7 +17542,7 @@ fn observe_agent_runtime_memory_write(
|
||||
"agentId": agent_id,
|
||||
"targetAgentId": target_agent_id,
|
||||
"scope": "agent",
|
||||
"path": memory.path,
|
||||
"path": relative_path,
|
||||
"mode": if overwrite { "overwrite" } else { "append" },
|
||||
}),
|
||||
)
|
||||
@@ -17553,13 +17570,14 @@ fn observe_agent_runtime_memory_write(
|
||||
write_local_game_memory_at(root, game_scope, &next_content)
|
||||
})
|
||||
.and_then(|memory| {
|
||||
let relative_path = relative_project_path(root, Path::new(&memory.path))?;
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.memory.write",
|
||||
"agentId": agent_id,
|
||||
"scope": memory.scope,
|
||||
"path": memory.path,
|
||||
"path": relative_path,
|
||||
"mode": if overwrite { "overwrite" } else { "append" },
|
||||
}),
|
||||
)
|
||||
@@ -18942,6 +18960,26 @@ fn observe_agent_runtime_file_write(
|
||||
detail: None,
|
||||
};
|
||||
};
|
||||
if content.trim().is_empty() {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "file.write".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: "缺少非空 content".to_string(),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
let content_chars = content.chars().count();
|
||||
if content_chars > AGENT_RUNTIME_TOOL_WRITE_MAX_CHARS {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "file.write".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: format!(
|
||||
"content 不能超过 {} 字符",
|
||||
AGENT_RUNTIME_TOOL_WRITE_MAX_CHARS
|
||||
),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
let _lock = match acquire_project_write_lock(root, "file.write") {
|
||||
Ok(lock) => lock,
|
||||
Err(error) => {
|
||||
@@ -18958,18 +18996,17 @@ fn observe_agent_runtime_file_write(
|
||||
{
|
||||
return agent_runtime_mutation_gate_failure_observation(root, "file.write", &error);
|
||||
}
|
||||
let content = truncate_agent_runtime_text(
|
||||
let observation_content = truncate_agent_runtime_text(
|
||||
sanitize_prompt_context(content).as_str(),
|
||||
AGENT_RUNTIME_TOOL_WRITE_MAX_CHARS,
|
||||
);
|
||||
let result = write_local_project_file_at(root, path, &content).and_then(|written| {
|
||||
let result = write_local_project_file_at(root, path, content).and_then(|written| {
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.file.write",
|
||||
"agentId": agent_id,
|
||||
"path": written.path,
|
||||
"absolutePath": written.absolute_path,
|
||||
}),
|
||||
)
|
||||
.map(|()| written)
|
||||
@@ -18979,7 +19016,7 @@ fn observe_agent_runtime_file_write(
|
||||
tool: "file.write".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: format!("已写入 {}", written.path),
|
||||
detail: Some(content),
|
||||
detail: Some(observation_content),
|
||||
},
|
||||
Err(error) => AgentRuntimeToolObservation {
|
||||
tool: "file.write".to_string(),
|
||||
@@ -27113,6 +27150,9 @@ fn append_game_creator_agent_runtime_event_with_action(
|
||||
&& candidate.phase == event.phase
|
||||
})
|
||||
{
|
||||
let legacy_detail_is_stricter_projection_compatible = event.event_type == "observation"
|
||||
&& event.detail.is_none()
|
||||
&& existing.detail.is_some();
|
||||
if existing.agent_id != event.agent_id
|
||||
|| existing.task_id != event.task_id
|
||||
|| existing.session_id != event.session_id
|
||||
@@ -27121,7 +27161,8 @@ fn append_game_creator_agent_runtime_event_with_action(
|
||||
|| existing.status != event.status
|
||||
|| existing.phase != event.phase
|
||||
|| existing.summary != event.summary
|
||||
|| existing.detail != event.detail
|
||||
|| (existing.detail != event.detail
|
||||
&& !legacy_detail_is_stricter_projection_compatible)
|
||||
{
|
||||
return Err(format!(
|
||||
"Agent Runtime action event 幂等身份冲突:actionId={action_id}"
|
||||
|
||||
@@ -11538,7 +11538,11 @@ async fn runtime_v11_closure_isolated_child_memory_is_instance_private() {
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert_eq!(write.status, "ok", "{}", instance.instance_id);
|
||||
assert_eq!(
|
||||
write.status, "ok",
|
||||
"{}: {}",
|
||||
instance.instance_id, write.summary
|
||||
);
|
||||
}
|
||||
|
||||
for (instance, own_marker, sibling_marker) in [
|
||||
@@ -13510,11 +13514,21 @@ async fn background_agent_runtime_can_write_memory_and_project_files() {
|
||||
.content
|
||||
.contains("项目主题是月光厨房对抗暗影厨具。"));
|
||||
let notes = fs::read_to_string(root.join("game/agent-notes.md")).expect("notes");
|
||||
assert!(notes.contains("月光厨房:收集食材,躲避暗影厨具。"));
|
||||
assert_eq!(
|
||||
notes,
|
||||
"# Agent Notes\n\n月光厨房:收集食材,躲避暗影厨具。\n"
|
||||
);
|
||||
let public_events =
|
||||
fs::read_to_string(root.join(".agent/runtime/events/design-director.jsonl"))
|
||||
.expect("public runtime events");
|
||||
assert!(!public_events.contains("# Agent Notes"));
|
||||
assert!(!public_events.contains("月光厨房:收集食材,躲避暗影厨具。"));
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
|
||||
assert!(agent_db.contains("\"recordType\":\"agent.runtime.memory.write\""));
|
||||
assert!(agent_db.contains("\"recordType\":\"agent.runtime.file.write\""));
|
||||
assert!(agent_db.contains("\"path\":\"game/agent-notes.md\""));
|
||||
assert!(!agent_db.contains("\"absolutePath\""));
|
||||
assert!(!agent_db.contains(root.to_string_lossy().as_ref()));
|
||||
let records = read_agent_db_records_for_test(&root);
|
||||
let executing_records = records
|
||||
.iter()
|
||||
@@ -16890,7 +16904,7 @@ async fn background_agent_runtime_requires_verification_after_project_mutation()
|
||||
.any(|item| item.contains("project.verify:ok · check:agent 已通过")));
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("game/revision-gate.txt")).expect("read gated file"),
|
||||
"revision=1"
|
||||
"revision=1\n"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
|
||||
@@ -4648,6 +4648,14 @@
|
||||
- 客户端:Project Supervisor 主聊天、启动器开发 Agent 聊天和项目内 Agent 弹窗复用同一问题卡;等待时普通输入/steer 禁用,卡片不随 Runtime 详情折叠,失败重试保持同一 responseId。
|
||||
- 真实验收:2026-07-16 正式 `openai_chat / gpt-5.5` 的 `user-input-runtime` suite PASS。Project Supervisor 自主提出 1 题/2 选项,Runner pidfd 强杀换 boot 后 Provider started 保持 `1 -> 1`,回答后同 Agent/Session/run 完成唯一最终 assistant;会话问题/答案各 1,重复 message、公共正文、API Key、项目/配置路径和报告泄漏均为 0,隔离现场已清理。
|
||||
|
||||
## 2026-07-16 AI 游戏创作 Agent Runtime V1.18 真实 Goal Provider 验收收口
|
||||
|
||||
- 真实结论:正式 AppData 的 `openai_chat / gpt-5.5` 路由通过隔离 `goal-runtime` suite。Goal revision 1 的旧待确认写动作在 revision 2 形成唯一 `runtime.goal / blocked` receipt 且零执行/零重放;Agent 取得真实退出码 1 后用一个 patchset 修复,暂停、Linux pidfd 强杀、Runner 换 boot 与显式 resume 全部保持原 Agent/Session/run,稳定窗口中 task/plan/conversation/Provider/action 零推进。
|
||||
- 完成证据:最终代码快照复跑的结构化计划 revision 11 的 8 步全部完成,11 组 Provider lifecycle 均唯一闭合,finalization v3 四阶段与两层 completed projection 完整,Session 只有 1 条 assistant;3 个副作用无重放,重复 action/message/receipt、Goal 正文和失败证据 canary、API Key、诱饵、项目/配置绝对路径以及报告泄漏均为 0。当前 context bundle 生产 schema 为 v5,Provider lifecycle 为 v2。
|
||||
- 同轮修正:`file.write` 只校验非空和上限,合法正文按原字符落盘,不能再经 prompt 清洗或静默截断末尾换行;旧 Goal action 的 `runtime.goal / blocked` receipt 是合法终态转换,验收器必须核对同 actionId/指纹及原输入摘要哈希,不能误判为重放身份冲突;公共 event 的 observation detail 复用安全 receipt 元数据,不保存 `file.read / project.diff` 正文;file/memory 专用审计只保存项目内相对路径,不写 `absolutePath` 或项目根。
|
||||
- 恢复兼容:公开 observation event 收紧为安全 receipt 元数据后,恢复旧 action event 时只允许“旧 detail 存在、当前投影省略 detail”这一种向更严格投影迁移;Agent/Task/Session/Run/action、状态、阶段和摘要仍须完全一致。其他 payload 差异继续按幂等身份冲突失败关闭,避免旧项目因脱敏升级永久停在 `needs-reconciliation`。
|
||||
- 验收约束:revision 2 的 Goal 明确要求任何修复前先运行项目声明的原始验收并观察非零退出,不指定命令或修复配方;一次性失败证据 canary 按 event/Agent DB/receipt/activity/output 分面扫描。保留现场完成二次核对后,隔离 Runner、AppData 和 disposable 项目均按 sentinel 清理。
|
||||
|
||||
## 2026-07-13 普通微信支付 V3 退款使用统一观察事务闭环
|
||||
|
||||
- 背景:普通微信支付 V3 的退款申请响应、退款结果回调、主动查单和商户平台手工退款发现可能重复、乱序或只出现其中一种;原充值订单只有单一终态,无法表达多次部分退款、权益回收欠款和会员人工处理。
|
||||
|
||||
@@ -775,13 +775,13 @@ V1.18 对标 Codex CLI `/goal` 的长任务语义:目标文本既是首轮任
|
||||
|
||||
### 暂停、恢复与清理
|
||||
|
||||
- pause 写 durable request,并通过 typed Runner `runtime.pause` 中断当前 planning/final Provider;已进入工具的动作允许返回后再停。Provider 注册、durable Goal control/cancel 二次复核与 `started` 审计使用同一项目写锁形成线性化边界:锁内同时核对 cancel tombstone、queued steer、Goal 状态和 task 绑定的 Goal revision,edit 已提交新 revision 但 steer 尚未落盘时也不得轮询旧 Provider 或写伪 `started`;请求先提交时才属于可中断或等待安全边界的在途调用。Provider 中断或返回边界先把可恢复 continuation 写入 v4 context bundle,其中恢复快照按 `active` Goal 语义保存,随后 Runtime 才在 LLM/工具/observation/finalization 安全边界把同一 run 收束为 `paused`。Runner 重启时先处理 cancel / Goal control,再进入 process reconciliation、finalization 和 pending action;`pause-requested` 会先收束成 `paused`,`paused` 直接保持休眠,不生成 assistant、不创建新 run、不调用 Provider。
|
||||
- pause 写 durable request,并通过 typed Runner `runtime.pause` 中断当前 planning/final Provider;已进入工具的动作允许返回后再停。Provider 注册、durable Goal control/cancel 二次复核与 `started` 审计使用同一项目写锁形成线性化边界:锁内同时核对 cancel tombstone、queued steer、Goal 状态和 task 绑定的 Goal revision,edit 已提交新 revision 但 steer 尚未落盘时也不得轮询旧 Provider 或写伪 `started`;请求先提交时才属于可中断或等待安全边界的在途调用。Provider 中断或返回边界先把可恢复 continuation 写入当前 v5 context bundle(V1.18 初版为 v4,V1.21 增加压缩状态后升级),其中恢复快照按 `active` Goal 语义保存,随后 Runtime 才在 LLM/工具/observation/finalization 安全边界把同一 run 收束为 `paused`。Runner 重启时先处理 cancel / Goal control,再进入 process reconciliation、finalization 和 pending action;`pause-requested` 会先收束成 `paused`,`paused` 直接保持休眠,不生成 assistant、不创建新 run、不调用 Provider。
|
||||
- 暂停父 Agent 不撤销已经 durable 投递的专业 child;child 可以把结果写成 ready,但父 run 在显式 resume 前不能认领或继续 Provider。Runner-owned process session 在暂停提交前终止,恢复后由 Agent 根据 observation 重规划,禁止按 PID 重连或重放未知 start。
|
||||
- resume 的有效状态迁移只接受 `paused -> active`,先清理同一 run 遗留的 cancel tombstone,再把原 Agent/Session/run 重新投影为 pending 并唤醒 External Runner;不创建 retry run。若进程在 Goal sidecar 已写成 `active`、Runtime 投影或 Runner 唤醒尚未完成时失败,重复 resume 必须识别同一 run 的半提交并继续补齐,不能把 `active` 单独当成恢复成功或直接返回。pending confirmation 仍回到确认态,普通 planning 从 v4 context bundle 继续。clear 对活跃 Goal 复用取消 tombstone,待安全取消后把 Goal 写为 cleared;paused/completed Goal 可直接清理。clear 不删除历史 conversation、task、event 或 Goal history。
|
||||
- resume 的有效状态迁移只接受 `paused -> active`,先清理同一 run 遗留的 cancel tombstone,再把原 Agent/Session/run 重新投影为 pending 并唤醒 External Runner;不创建 retry run。若进程在 Goal sidecar 已写成 `active`、Runtime 投影或 Runner 唤醒尚未完成时失败,重复 resume 必须识别同一 run 的半提交并继续补齐,不能把 `active` 单独当成恢复成功或直接返回。pending confirmation 仍回到确认态,普通 planning 从当前 v5 context bundle 继续。clear 对活跃 Goal 复用取消 tombstone,待安全取消后把 Goal 写为 cleared;paused/completed Goal 可直接清理。clear 不删除历史 conversation、task、event 或 Goal history。
|
||||
|
||||
### 恢复与完成门禁
|
||||
|
||||
- context bundle 升级为 `game-creator-runtime-context-bundle.v4`,绑定 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`;v3 在现有身份、计划和 verification 校验通过后从 Runtime/Goal sidecar 补齐,v2 继续先按 V1.17 迁移计划再补 Goal。v4 任一 Goal 身份、revision 或快照不一致都失败关闭。当前 Agent/Session/run 的 Goal sidecar 无法读取、损坏或身份冲突时,即使 legacy/半写 Runtime 尚无 `goalId` 投影也必须进入 `needs-reconciliation`,不得退化成无 Goal run 或绕过完成门禁。
|
||||
- V1.18 首次把 context bundle 升级为 v4 并绑定 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`;当前生产 schema 已由 V1.21 扩展为 `game-creator-runtime-context-bundle.v5`。v4 在原 Goal/计划/verification 身份通过后补入当前压缩字段并写回 v5,v3 先补 Goal,v2 继续先按 V1.17 迁移计划再补 Goal;任一当前 Goal 身份、revision 或快照不一致都失败关闭。当前 Agent/Session/run 的 Goal sidecar 无法读取、损坏或身份冲突时,即使 legacy/半写 Runtime 尚无 `goalId` 投影也必须进入 `needs-reconciliation`,不得退化成无 Goal run 或绕过完成门禁。
|
||||
- pending action 升级为 `game-creator-pending-action.v5`,在既有 project revision、verification gate、repository context fingerprint 和 steer cursor 外,固定绑定 `goalId / goalRevision / goalSnapshotFingerprint`。旧 v1-v4 一律失败关闭,不能从当前 Goal 猜回缺失绑定;Goal edit 后,无论动作原为自动还是待确认,都把旧记录收束成稳定 `blocked` observation 并在同一 run 重规划。
|
||||
- finalization journal 升级为 `game-creator-runtime-finalization.v3`,把 Goal 快照指纹纳入 finalizationId。prepared 回复必须绑定当前 active Goal、同一 run/revision、全部完成的结构化计划、清空的 process/join/delegate 屏障和现有 verification gate。Goal 编辑、暂停、清理或 revision 漂移会让未提交 assistant 的旧 finalization 失效并回到同 run;assistant 已提交后只允许按 journal 原快照补齐 Runtime 与 Goal completed,不能重新请求 Provider。
|
||||
- assistant 持久化后,finalization 先写 Runtime completed task/state,再把规范 Goal 写为 completed,并补写携带 completed Goal 状态的 task/state projection;两层投影均可靠后,journal 才推进 `runtime-completed` 并删除。完成证据由系统从最终结构化计划、verification gate、run/session 身份和 response fingerprint 生成,不保存模型 thinking 或原始私有 observation。Goal 未完成、paused、clearing、sidecar 缺失或 revision 不匹配时,response 不能绕过完成门禁。
|
||||
@@ -794,13 +794,13 @@ V1.18 对标 Codex CLI `/goal` 的长任务语义:目标文本既是首轮任
|
||||
- background planning / final reply 的专用 Provider 客户端强制 `max_retries=0`;每个 `agent.runtime.provider_request.lifecycle` 从 `started` 到唯一 `completed / failed / interrupted` 最多对应一次物理请求。`Timeout / Connectivity / Transport / EmptyResponse / 408 / 429 / 5xx` 以及无法证明请求未被上游接收的其它错误,不得在同一 lifecycle 内自动原样重放;只记录 error kind、SHA-256、字符数或脱敏摘要。显式 steer、Goal resume 或人工 reconciliation 决定再次调用时,必须使用新的 request slot/lifecycle;格式修复同样使用 `loop-<n>-repair-<m>` 新 slot,不能伪装成底层 retry。
|
||||
- 每次 request snapshot 固定绑定 `projectId / agentId / taskId / sessionId / runId / source / goalId / goalRevision / goalSnapshotFingerprint / appliedSteerCursor / requestKind / requestSlot`,requestId 从该闭集稳定派生。真正进入 Provider future 前,Runtime 在同一项目写锁内重读 task/Runtime 身份、queued steer、cancel tombstone、规范 Goal 状态与快照;已生效控制只返回未启动,不得写伪 `started`。Provider lifecycle 的生产字段闭集只允许 `recordType / auditSchemaVersion / agentId / taskId / sessionId / runId / source / requestId / requestKind / requestSlot / status`,持久层只可再添加统一 `schemaVersion / updatedAt` envelope;不包含 prompt、工具输入、URL、模型、回复或错误正文。
|
||||
- 启动新请求前必须在 Agent DB 锁内全量扫描同 Agent/run 的 Provider lifecycle,不依赖 recent tail。只要发现 `started` 后没有可信唯一终态,就把原 run/task/state 收束到 `needs-reconciliation` orphan barrier,阻断后续 Provider、工具和 finalization;同 request 多终态、缺 started、物理顺序倒置、重复阶段、身份/字段冲突或额外生产字段同样失败关闭,禁止自动补发。paused Runner 重启窗口必须以 started 数量零增长证明没有暗中请求,不能只看 plan/action 是否落盘。
|
||||
- 确定性验收覆盖:单 Session 单 Goal、跨 Agent/Session 隔离、expectedRevision 幂等/冲突、活跃 Goal 阻止新 run、Provider in-flight pause、工具返回后 pause、paused 重启不自启、同 run resume、pending confirmation 恢复、编辑触发 steer 与旧动作失效、clear/cancel 竞态、v4/v3/v2 恢复、v3 finalization、assistant 后崩溃补齐 completed,以及 Goal 元数据零 project revision/policy 变化。
|
||||
- 确定性验收覆盖:单 Session 单 Goal、跨 Agent/Session 隔离、expectedRevision 幂等/冲突、活跃 Goal 阻止新 run、Provider in-flight pause、工具返回后 pause、paused 重启不自启、同 run resume、pending confirmation 恢复、编辑触发 steer 与旧动作失效、clear/cancel 竞态、v5/v4/v3/v2 恢复、v3 finalization、assistant 后崩溃补齐 completed,以及 Goal 元数据零 project revision/policy 变化。
|
||||
- 真实 Provider 使用现有 `agent-runtime-real-e2e.mjs` 的独立 `goal-runtime` suite 和一次性项目证明,不新增平行验收器。suite 只要求 AppData 中 `code-prototype` 的真实 LLM 配置,不要求 Chrome 或 External Editor API。revision 1 必须先形成至少三步、已有 completed 且仍有未完成步骤的计划,并停在一个绑定 Goal revision 1 的 `game-creator-pending-action.v5` 写动作;编辑到 revision 2 后,该旧动作必须形成 `runtime.goal / blocked` observation 且旧标记从未落盘,同一 run 再形成绑定 revision 2 的 v5 写动作。
|
||||
- `goal-runtime` 不复用正在运行的正式 AppData Runner。验收器在用户提供的 AppData 下创建 `0700` 专用子目录和带随机 owner token/PID/时间的 sentinel;主配置与可选 local 配置只以普通文件 hardlink 复用,结束时复核 source/link 的 device、inode 与 SHA-256,全程不复制或输出 API Key。所有 Goal/Runner CLI 统一附加该专用 `runtimeConfigDir`。SIGKILL 前必须同时核对 sentinel、endpoint、Runner boot/PID/port、实际 CLI 路径、`--agent-runner --config-dir` argv 和 OS 启动指纹;endpoint 在已认领后异常消失时,只允许按先前同一启动指纹回收。成功或失败都先停止自有 Runner,再按 sentinel 和受限目录前缀删除专用 AppData;身份不一致时保留现场并失败,禁止猜测或清理其它 Runner。
|
||||
- revision 2 验证 fixture 只在 revision 1 待确认动作与隔离证明完成后注入,并先由宿主真实执行一次失败命令形成不可伪造的失败边界。失败报告对 task、event、Agent DB 和 conversation 分面容错读取,截断尾行保留已完成 JSONL 记录并单独报告读取错误;不得把某一分面损坏折算成其它分面全为零。
|
||||
- revision 2 写动作待确认时执行 pause;命令返回、Goal status 与 Runtime state 都必须是 durable `paused`。随后记录 Goal、`game-creator-runtime-context-bundle.v4`、pending v5、计划、task/event/Agent DB、conversation 和副作用计数,SIGKILL Runner 并使用全局 `--agent-resume` 启动新 boot;至少两个稳定采样窗口内上述运行证据不得推进,不得新增 Provider plan、工具执行、assistant 或主 run。只有显式 `--agent-goal-resume` 后才允许确认 revision 2 动作并继续。
|
||||
- 最终 Goal、Runtime 和最新 task 必须在原 Agent/Session/run 上 completed,结构化计划全部完成,Goal completion evidence 必须精确匹配当前 Goal/plan/verification/run/session;同一 finalizationId 必须按严格七槽物理顺序形成完整记录,finalization sidecar 最终不残留,目标 Session 只允许一个 assistant。Goal sidecar、task JSONL、Runtime state、v4 context 和 conversation 属于本地私有执行事实,可包含完成目标所需正文;event、Agent DB、receipt、activity、output 与最终报告不得保存 task、Goal/steer、委派任务、verify 命令或 error 正文,只允许身份/状态、SHA-256、字符/字节/条目计数和经 URL、项目根、其它绝对路径及凭据清洗的有界摘要。公共 task 统一不保留正文;委派只保留 `taskSha256 / taskChars`,verify 只保留脚本安全标识、`expectedCommandSha256 / expectedCommandChars`、timeout 和结果计数,error 只保留 kind/fingerprint/chars 或脱敏摘要,禁止任何正文、preview、head 或 tail。验收必须扫描完整 Goal/编辑/委派/verify/error canary、已加载密钥和一次性项目绝对路径在全部公共持久面泄漏为 0,并确认动作、消息、receipt 和 Provider lifecycle 均无重复。
|
||||
- 截至 2026-07-15,V1.18 真实 Provider 门禁尚未通过。最新保留现场在首轮 planning、`planRevision=0`、零 pending action/observation 时由对端关闭长 TLS 连接;同一发布配置、模型和 Rust native-tls 客户端的最小单 Agent 请求在 25.2 秒成功,证明基础鉴权与短请求通道可用,但不能外推为工具 planning 或 Goal 长链路 PASS。Provider 长请求恢复后仍需完整执行上一条一次性项目验收。
|
||||
- revision 2 写动作待确认时执行 pause;命令返回、Goal status 与 Runtime state 都必须是 durable `paused`。随后记录 Goal、`game-creator-runtime-context-bundle.v5`、pending v5、计划、task/event/Agent DB、conversation 和副作用计数,SIGKILL Runner 并使用全局 `--agent-resume` 启动新 boot;至少两个稳定采样窗口内上述运行证据不得推进,不得新增 Provider plan、工具执行、assistant 或主 run。只有显式 `--agent-goal-resume` 后才允许确认 revision 2 动作并继续。
|
||||
- 最终 Goal、Runtime 和最新 task 必须在原 Agent/Session/run 上 completed,结构化计划全部完成,Goal completion evidence 必须精确匹配当前 Goal/plan/verification/run/session;同一 finalizationId 必须按严格七槽物理顺序形成完整记录,finalization sidecar 最终不残留,目标 Session 只允许一个 assistant。Goal sidecar、task JSONL、Runtime state、v5 context 和 conversation 属于本地私有执行事实,可包含完成目标所需正文;event、Agent DB、receipt、activity、output 与最终报告不得保存 task、Goal/steer、委派任务、verify 命令或 error 正文,只允许身份/状态、SHA-256、字符/字节/条目计数和经 URL、项目根、其它绝对路径及凭据清洗的有界摘要。公共 task 统一不保留正文;委派只保留 `taskSha256 / taskChars`,verify 只保留脚本安全标识、`expectedCommandSha256 / expectedCommandChars`、timeout 和结果计数,error 只保留 kind/fingerprint/chars 或脱敏摘要,禁止任何正文、preview、head 或 tail。验收必须扫描完整 Goal/编辑/委派/verify/error canary、已加载密钥和一次性项目绝对路径在全部公共持久面泄漏为 0,并确认动作、消息、receipt 和 Provider lifecycle 均无重复。
|
||||
- 2026-07-16 使用正式 AppData 的 `openai_chat / gpt-5.5` 路由执行隔离 `goal-runtime` suite,V1.18 真实 Provider 门禁 **PASS**。同一 Agent/Session/run 从 Goal revision 1 编辑到 revision 2,保留 1 个已完成计划步骤;旧写动作形成 1 条 `runtime.goal / blocked` receipt,执行与重放均为 0。Agent 先取得真实退出码 1,再用 1 个 patchset 修复并通过 revision 2 verification;暂停前后、Runner pidfd 强杀换 boot 后的稳定窗口中 task/plan/conversation/Provider/action 均零推进,显式 resume 后恢复原 execution owner 和原 run。最终代码快照复跑的计划 revision 11 的 8 步全部完成,11 组 Provider lifecycle 均唯一闭合,finalization v3 四阶段和两层 completed projection 完整,Session 只有 1 条 assistant;3 个副作用无重放,重复 action/message/receipt、Goal 正文、失败证据 canary、API Key、诱饵和项目绝对路径公共泄漏均为 0。验收过程同时修正 `file.write` 静默裁剪末尾换行、旧 Goal action receipt 的合法 tool/摘要转换、file/project diff 正文进入公共 event、file/memory 审计保存绝对路径,以及旧 event detail 在新脱敏投影下被误判幂等冲突五类真实缺陷;隔离 Runner、AppData 和 disposable 项目均已按 sentinel 清理。
|
||||
|
||||
## V1.19 后台 Agent 真流式最终回复
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user