Skip to content

Commit 3ed2d5d

Browse files
committed
fix: preserve verified ingress across automatic task ownership
1 parent 4c6c640 commit 3ed2d5d

5 files changed

Lines changed: 91 additions & 19 deletions

File tree

‎hooks/_runtime/evidence/codex-skill-route-pass.v1.json‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,24 @@
55
"hostVariant": "codex-cli/exec-user-global-local-stdio",
66
"testedVersion": "codex-cli 0.145.0",
77
"protocol": "MCP 2024-11-05",
8-
"runtimeContractDigest": "711f86bf6394da14fda8a79fc1cb6ac9fcb3fe12824ccee9c26d05ed85fed285",
8+
"runtimeContractDigest": "25526fef23e1ef540578679095e55f6c40377e47e8f65db73778bee85fd99809",
99
"hostAdapterDigest": "845825776fc7a2f3e70ece637ab72f04157561c866f29bb331975163f0a758cc",
10-
"sourceEvidenceDigest": "32cdb7a1c994516cf8aaa7f0faf1435f09846b1ab2a6ba086acf4ff39a2d1815",
10+
"sourceEvidenceDigest": "6fa7c5963c5f14c7d21ee43c9f4d7407725fe6bcfed5b91e6ef7db4a56db089a",
1111
"sourceProbe": {
1212
"schemaVersion": "SkillRouteS15EvidenceV1",
13-
"probeRunId": "s15-codex-probe-4f1cb2f2-3c79-4ea8-8602-4ac8900d59df",
13+
"probeRunId": "s15-codex-probe-76a83fdc-692f-42af-8672-0db7930a19d9",
1414
"authorizationSource": "isolated-probe-authority",
1515
"contextSource": "host-hooks",
1616
"observationMode": "hook-post-history",
1717
"receiptStatus": "relevant-complete",
18-
"completedAt": "2026-09-07T17:54:10.966Z"
18+
"completedAt": "2026-09-07T18:57:30.766Z"
1919
},
2020
"runtimeBinding": {
2121
"source": "isolated-source-candidate",
22-
"expectedDigest": "711f86bf6394da14fda8a79fc1cb6ac9fcb3fe12824ccee9c26d05ed85fed285",
23-
"generationDigest": "711f86bf6394da14fda8a79fc1cb6ac9fcb3fe12824ccee9c26d05ed85fed285",
24-
"modeReceiptDigest": "711f86bf6394da14fda8a79fc1cb6ac9fcb3fe12824ccee9c26d05ed85fed285",
25-
"routeEnvelopeDigest": "711f86bf6394da14fda8a79fc1cb6ac9fcb3fe12824ccee9c26d05ed85fed285"
22+
"expectedDigest": "25526fef23e1ef540578679095e55f6c40377e47e8f65db73778bee85fd99809",
23+
"generationDigest": "25526fef23e1ef540578679095e55f6c40377e47e8f65db73778bee85fd99809",
24+
"modeReceiptDigest": "25526fef23e1ef540578679095e55f6c40377e47e8f65db73778bee85fd99809",
25+
"routeEnvelopeDigest": "25526fef23e1ef540578679095e55f6c40377e47e8f65db73778bee85fd99809"
2626
},
2727
"probe": {
2828
"schemaVersion": "SkillRouteProbeSummaryV1",

‎hooks/_runtime/host-skill-route-capabilities.v1.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
"status": "PASS",
1414
"testedVersion": "codex-cli 0.145.0",
1515
"protocol": "MCP 2024-11-05",
16-
"runtimeContractDigest": "711f86bf6394da14fda8a79fc1cb6ac9fcb3fe12824ccee9c26d05ed85fed285",
16+
"runtimeContractDigest": "25526fef23e1ef540578679095e55f6c40377e47e8f65db73778bee85fd99809",
1717
"hostAdapterDigest": "845825776fc7a2f3e70ece637ab72f04157561c866f29bb331975163f0a758cc",
1818
"evidenceRef": "hooks/_runtime/evidence/codex-skill-route-pass.v1.json",
19-
"evidenceDigest": "781474d583636cc9967ac67957497fe204d804dad887f94901cd384fa6a7927c",
19+
"evidenceDigest": "2ef17ab9b6d27c2a98090b44399239cd677b31caba02aa6b00e3ba3f6418ef15",
2020
"entrySurface": "codex exec --ephemeral",
2121
"bootstrapDelivery": "stable user-global Hook launcher UserPromptSubmit or profile_context_plan fallback",
2222
"defaultEligible": true

‎mcp/memory-server.js‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6135,6 +6135,9 @@ function handleMemoryTaskAdmitV2(args) {
61356135
ingress = preparedResume.ingress
61366136
}
61376137
}
6138+
// Capture before admission/route binding switches the lifecycle projection
6139+
// to the newly created canonical task.
6140+
const ownerIngressState = taskOwnerIngressState(target, ingress)
61386141
const admission = executeTaskAdmission({
61396142
operation: args.operation,
61406143
task: args.task,
@@ -6177,6 +6180,7 @@ function handleMemoryTaskAdmitV2(args) {
61776180
operation: 'acquire',
61786181
taskId: admission.taskId,
61796182
admissionId: admission.admissionId,
6183+
ingressState: ownerIngressState,
61806184
actualInstructionEnvelope: verifiedIngress.actualInstructionEnvelope,
61816185
workItemSet: verifiedIngress.workItemSet,
61826186
workflowRouteDecision: verifiedIngress.workflowRouteDecision,
@@ -6438,6 +6442,36 @@ function resolveCurrentTaskIngress(target, args, options = {}) {
64386442
return ingress
64396443
}
64406444

6445+
function taskOwnerIngressState(target, ingress) {
6446+
let contextState = ingress.resumeStateHandoff || ingress.lifecycleState
6447+
const sameContext = state => {
6448+
const context = state?.contextAcquisition
6449+
return context?.contextEpoch === ingress.actualInstructionEnvelope.contextEpoch &&
6450+
context.hostSessionId && crypto.createHash('sha256').update(context.hostSessionId).digest('hex') === ingress.actualInstructionEnvelope.hostSessionDigest &&
6451+
comparableActiveRoot(context.activeRoot) === comparableActiveRoot(target.activeRoot)
6452+
}
6453+
// Immutable ingress snapshots intentionally omit the mutable ContextRead
6454+
// receipt. Reuse it only from the same observed instruction/route/session.
6455+
if (!sameContext(contextState)) {
6456+
contextState = null
6457+
try {
6458+
const { state } = readServerOwnedLifecycleProjection(target)
6459+
if (state.activeProject === target.project && state.activeScope === 'project' &&
6460+
state.actualInstructionEnvelope?.envelopeDigest === ingress.actualInstructionEnvelope.envelopeDigest &&
6461+
state.workflowRouteDecision?.decisionDigest === ingress.workflowRouteDecision.decisionDigest &&
6462+
sameContext(state)) contextState = state
6463+
} catch { }
6464+
}
6465+
return {
6466+
activeProject: target.project, activeScope: 'project',
6467+
actualInstructionEnvelope: ingress.actualInstructionEnvelope,
6468+
workItemSet: ingress.workItemSet, workflowRouteDecision: ingress.workflowRouteDecision,
6469+
stickyProject: ingress.projectTargetLease,
6470+
contextAcquisition: contextState?.contextAcquisition,
6471+
workflowRoutePlanBinding: contextState?.workflowRoutePlanBinding
6472+
}
6473+
}
6474+
64416475
function handleMemoryTaskWriteOwner(args) {
64426476
const target = taskMemoryTransactionTarget(args)
64436477
if (target.scope !== 'project' || !target.project) {
@@ -6457,14 +6491,7 @@ function handleMemoryTaskWriteOwner(args) {
64576491
handoffRefDigest: args.handoffRefDigest,
64586492
takeoverRefDigest: args.takeoverRefDigest,
64596493
...(serverObservation ? { serverObservation } : {}),
6460-
ingressState: {
6461-
activeProject: target.project, activeScope: 'project',
6462-
actualInstructionEnvelope: ingress.actualInstructionEnvelope,
6463-
workItemSet: ingress.workItemSet, workflowRouteDecision: ingress.workflowRouteDecision,
6464-
stickyProject: ingress.projectTargetLease,
6465-
contextAcquisition: ingress.resumeStateHandoff?.contextAcquisition || ingress.lifecycleState?.contextAcquisition,
6466-
workflowRoutePlanBinding: ingress.resumeStateHandoff?.workflowRoutePlanBinding || ingress.lifecycleState?.workflowRoutePlanBinding
6467-
},
6494+
ingressState: taskOwnerIngressState(target, ingress),
64686495
expectedCommitFence: ingress.lifecycleState?.taskRecoveryCommitFence,
64696496
actualInstructionEnvelope: ingress.actualInstructionEnvelope,
64706497
workItemSet: ingress.workItemSet,

‎mcp/task-admission-authority.cjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2950,6 +2950,7 @@ function executeTaskWriteOwner(rawInput = {}, options = {}) {
29502950
expectedOwner: ownerRef(currentOwner),
29512951
owner: nextOwner,
29522952
transition: 'reacquire',
2953+
ingressState: input.ingressState,
29532954
transaction,
29542955
expectedAdmissionPhase: 'finalized',
29552956
reason: 'owner-reacquire'
@@ -3022,6 +3023,7 @@ function executeTaskWriteOwner(rawInput = {}, options = {}) {
30223023
expectedOwner: currentOwner ? ownerRef(currentOwner) : { mode: 'absent' },
30233024
owner: nextOwner,
30243025
transition: reopening ? 'reopen' : 'acquire',
3026+
ingressState: input.ingressState,
30253027
transaction: ownerFenced,
30263028
expectedAdmissionPhase: 'cp-state-written',
30273029
reason: reopening ? 'owner-reopen' : 'owner-acquire'

‎scripts/test-mcp-servers.js‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1293,7 +1293,13 @@ function testMemoryTaskAdmissionV2Contract() {
12931293
actualInstructionEnvelope: envelope,
12941294
workItemSet,
12951295
workflowRouteDecision: route,
1296-
stickyProject: projectTargetLease
1296+
stickyProject: projectTargetLease,
1297+
contextAcquisition: {
1298+
contextEpoch: envelope.contextEpoch,
1299+
hostSessionId: 'mcp-admission-session',
1300+
activeRoot,
1301+
project
1302+
}
12971303
}
12981304
fs.writeFileSync(lifecycleStatePath, JSON.stringify(lifecycleState, null, 2) + '\n')
12991305
const args = {
@@ -1421,6 +1427,20 @@ function testMemoryTaskAdmissionV2Contract() {
14211427
assert.strictEqual(admitted.structuredContent.ownerAcquisition.cp1Confirmed, false)
14221428
assert.strictEqual(admitted.structuredContent.continuationLease.status, 'consumed')
14231429
assert.strictEqual(admitted.structuredContent.ownerAcquisition.finalized, true)
1430+
// The next Hook reads canonical task state, not the pre-admission projection.
1431+
// Automatic owner acquisition must carry the verified ingress across that
1432+
// boundary, just as an explicit owner renewal does.
1433+
const admittedState = readTaskRecoveryState({
1434+
metaDir: resolveTaskRecoveryMetaDir({ activeRoot, project }),
1435+
identity: { activeRoot, project, taskId: admitted.structuredContent.taskId, taskStatus: 'active' }
1436+
})
1437+
assert.strictEqual(admittedState.status, 'fresh')
1438+
assert.deepStrictEqual(admittedState.state.actualInstructionEnvelope, envelope)
1439+
assert.deepStrictEqual(admittedState.state.workflowRouteDecision, route)
1440+
assert.deepStrictEqual(admittedState.state.workItemSet, workItemSet)
1441+
assert.deepStrictEqual(admittedState.state.stickyProject, projectTargetLease)
1442+
assert.strictEqual(admittedState.state.contextAcquisition.contextEpoch, envelope.contextEpoch)
1443+
assert.strictEqual(admittedState.state.contextAcquisition.hostSessionId, 'mcp-admission-session')
14241444
const replay = resultById(responses, 3)
14251445
assert.strictEqual(replay.isError, false)
14261446
assert.strictEqual(replay.structuredContent.admissionId, admitted.structuredContent.admissionId)
@@ -1459,6 +1479,29 @@ function testMemoryTaskAdmissionV2Contract() {
14591479
})], TEMP_ROOT)
14601480
assert.notStrictEqual(resultById(firstCp, 9).isError, true, resultById(firstCp, 9).content?.[0]?.text)
14611481
assert.doesNotMatch(fs.readFileSync(path.join(taskRoot, '.memory', 'sessions.md'), 'utf8'), /\| CP1 \| ⏳ \|/u)
1482+
for (const [label, drift] of [
1483+
['other-session', { hostSessionId: 'another-host-session' }],
1484+
['other-root', { activeRoot: path.join(TEMP_ROOT, 'another-active-root') }]
1485+
]) {
1486+
setupLegacyWorkspace()
1487+
fs.mkdirSync(path.dirname(lifecycleStatePath), { recursive: true })
1488+
fs.writeFileSync(lifecycleStatePath, JSON.stringify({ ...lifecycleState,
1489+
contextAcquisition: { ...lifecycleState.contextAcquisition, ...drift }
1490+
}) + '\n')
1491+
const observed = resultById(runServer('mcp/memory-server.js', [rpcRequest(10, 'tools/call', {
1492+
name: 'memory_task_admit_v2', arguments: { ...args,
1493+
task: { ...args.task, displayName: `MCP-context-${label}`, aliases: [] }
1494+
}
1495+
})], TEMP_ROOT), 10)
1496+
assert.notStrictEqual(observed.isError, true, observed.content?.[0]?.text)
1497+
const isolated = readTaskRecoveryState({
1498+
metaDir: resolveTaskRecoveryMetaDir({ activeRoot, project }),
1499+
identity: { activeRoot, project, taskId: observed.structuredContent.taskId, taskStatus: 'active' }
1500+
})
1501+
assert.strictEqual(isolated.status, 'fresh')
1502+
assert.deepStrictEqual(isolated.state.actualInstructionEnvelope, envelope)
1503+
assert(!isolated.state.contextAcquisition?.hostSessionId, `${label} context must not be inherited`)
1504+
}
14621505
}
14631506

14641507
function testMemoryFinalizedFreshResumeV3Contract() {

0 commit comments

Comments
 (0)