From fd69bd47d192c91c98f7f75704c258371e2e7499 Mon Sep 17 00:00:00 2001 From: Mac Anderson Date: Thu, 10 Sep 2026 00:36:46 -0700 Subject: [PATCH 1/4] build(website): raise the sharp and js-yaml floors past their high-severity advisories --- website/pnpm-lock.yaml | 259 ++++++++++++++++++------------------ website/pnpm-workspace.yaml | 9 +- 2 files changed, 138 insertions(+), 130 deletions(-) diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index 64aa0956d0..c748daeb1f 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -6,7 +6,8 @@ settings: overrides: postcss: '>=8.5.23' - sharp: '>=0.35.0' + sharp: '>=0.35.4' + js-yaml: ^4.3.2 nanoid: ^3.3.18 importers: @@ -319,8 +320,8 @@ packages: resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} engines: {node: '>=18.0.0'} - '@emnapi/runtime@1.11.2': - resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} '@esbuild/aix-ppc64@0.25.4': resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} @@ -658,160 +659,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.3': - resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.3': - resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.3': - resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.2': - resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.2': - resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.2': - resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.2': - resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.2': - resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.2': - resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.2': - resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.2': - resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': - resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.2': - resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.3': - resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.3': - resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.3': - resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.3': - resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.3': - resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.3': - resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.3': - resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.3': - resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.3': - resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.3': - resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.3': - resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.3': - resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.3': - resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -2176,8 +2177,8 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - js-yaml@4.3.1: - resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true lightningcss-android-arm64@1.32.0: @@ -2721,8 +2722,8 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.35.3: - resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' @@ -3501,7 +3502,7 @@ snapshots: '@aws/lambda-invoke-store@0.3.0': {} - '@emnapi/runtime@1.11.2': + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true @@ -3684,108 +3685,108 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.35.3': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.35.3': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.3 optional: true - '@img/sharp-freebsd-wasm32@0.35.3': + '@img/sharp-freebsd-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.2': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.3.2': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.3.2': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.3.2': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.2': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.2': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.3.2': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.3.2': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.2': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.35.3': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.35.3': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.35.3': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.35.3': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.35.3': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.35.3': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.35.3': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.35.3': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.35.3': + '@img/sharp-wasm32@0.35.4': dependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.3': + '@img/sharp-webcontainers-wasm32@0.35.4': dependencies: - '@img/sharp-wasm32': 0.35.3 + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.35.3': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.35.3': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.35.3': + '@img/sharp-win32-x64@0.35.4': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -4963,7 +4964,7 @@ snapshots: github-slugger: 2.0.0 hast-util-to-estree: 3.1.3 hast-util-to-jsx-runtime: 2.3.6 - js-yaml: 4.3.1 + js-yaml: 4.3.2 mdast-util-mdx: 3.0.0 mdast-util-to-markdown: 2.1.2 remark: 15.0.1 @@ -4997,7 +4998,7 @@ snapshots: esbuild: 0.28.1 estree-util-value-to-estree: 3.5.0 fumadocs-core: 16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@0.556.0(react@19.2.6))(next@16.3.3(@types/node@25.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3) - js-yaml: 4.3.1 + js-yaml: 4.3.2 mdast-util-mdx: 3.0.0 picocolors: 1.1.1 picomatch: 4.0.5 @@ -5243,7 +5244,7 @@ snapshots: jiti@2.7.0: {} - js-yaml@4.3.1: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -5815,7 +5816,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.3.3 '@next/swc-win32-arm64-msvc': 16.3.3 '@next/swc-win32-x64-msvc': 16.3.3 - sharp: 0.35.3(@types/node@25.9.1) + sharp: 0.35.4(@types/node@25.9.1) transitivePeerDependencies: - '@babel/core' - '@types/node' @@ -6084,37 +6085,37 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.35.3(@types/node@25.9.1): + sharp@0.35.4(@types/node@25.9.1): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.3 - '@img/sharp-darwin-x64': 0.35.3 - '@img/sharp-freebsd-wasm32': 0.35.3 - '@img/sharp-libvips-darwin-arm64': 1.3.2 - '@img/sharp-libvips-darwin-x64': 1.3.2 - '@img/sharp-libvips-linux-arm': 1.3.2 - '@img/sharp-libvips-linux-arm64': 1.3.2 - '@img/sharp-libvips-linux-ppc64': 1.3.2 - '@img/sharp-libvips-linux-riscv64': 1.3.2 - '@img/sharp-libvips-linux-s390x': 1.3.2 - '@img/sharp-libvips-linux-x64': 1.3.2 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 - '@img/sharp-linux-arm': 0.35.3 - '@img/sharp-linux-arm64': 0.35.3 - '@img/sharp-linux-ppc64': 0.35.3 - '@img/sharp-linux-riscv64': 0.35.3 - '@img/sharp-linux-s390x': 0.35.3 - '@img/sharp-linux-x64': 0.35.3 - '@img/sharp-linuxmusl-arm64': 0.35.3 - '@img/sharp-linuxmusl-x64': 0.35.3 - '@img/sharp-webcontainers-wasm32': 0.35.3 - '@img/sharp-win32-arm64': 0.35.3 - '@img/sharp-win32-ia32': 0.35.3 - '@img/sharp-win32-x64': 0.35.3 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 '@types/node': 25.9.1 optional: true diff --git a/website/pnpm-workspace.yaml b/website/pnpm-workspace.yaml index 315a893e44..d23eb01d39 100644 --- a/website/pnpm-workspace.yaml +++ b/website/pnpm-workspace.yaml @@ -44,7 +44,14 @@ minimumReleaseAgeExclude: # direct `postcss` devDependency in `package.json` moving together. overrides: postcss: ">=8.5.23" - sharp: ">=0.35.0" + # sharp <0.35.4 and js-yaml <4.3.2 each carry a high-severity advisory + # (GHSA-rgj7-g3m4-5g8c, GHSA-2883-xcg3-v3hh). Both arrive transitively — + # sharp through `next`, js-yaml through `fumadocs-core` and `fumadocs-mdx` — + # so, as with postcss above, no direct bump reaches them; the floor does. + # js-yaml takes a caret, like nanoid below: a bare floor resolves to 5.x, + # which fumadocs does not call. + sharp: ">=0.35.4" + js-yaml: "^4.3.2" # nanoid <3.3.18: a custom alphabet with a non-integer or zero `size` makes # the generator loop indefinitely (GHSA high, patched at 3.3.18). It arrives # transitively through postcss@8.5.23, which both `next` and From 3b1c09a5a14bfd1932e6f5a895dc9a11d360e0bb Mon Sep 17 00:00:00 2001 From: Mac Anderson Date: Thu, 10 Sep 2026 01:11:57 -0700 Subject: [PATCH 2/4] ci(dependency-review): exempt the docs site's libvips tuples from the license check, as arenabench's once were --- .github/workflows/dependency-review.yml | 29 +++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 4357ce49ff..5dc9fffa4e 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -50,9 +50,16 @@ jobs: # touches is distributed, so the AGPL/dual-license reasoning in the # header does not apply to it. # - # The @img/sharp-* tuples that used to sit here arrived through - # arenabench/web's lockfile; that folder left with the ejection to - # its own repository (#2380), so the exemption left with it. + # The @img/sharp-* tuples first arrived through arenabench/web's + # lockfile and left with its ejection (#2380). They are back through + # `website/`, the docs site: `next` lists sharp as an optional + # dependency for every platform, and fourteen of its tuples ship + # LGPL-3.0 libvips binaries. The site is `"private": true`, imports + # no `next/image` (so sharp is never loaded, even at build), and is + # deployed to Vercel — nothing here reaches the AGPL workspace or the + # commercial binary. The action has no purl globs, so each tuple is + # named; a new libvips tuple reds this gate again and is added the + # same way. #2532 tracks the recurrence. # # Must stay ABOVE `allow-licenses`: check-license-allowlist-parity.sh # reads that key's folded scalar by consuming every following indented @@ -66,7 +73,21 @@ jobs: # nothing — so a NEW libvips tuple reds this gate again and has to be # added by hand. #2532 tracks removing the recurrence instead. allow-dependencies-licenses: >- - pkg:githubactions/Swatinem/rust-cache + pkg:githubactions/Swatinem/rust-cache, + pkg:npm/%40img/sharp-libvips-darwin-arm64, + pkg:npm/%40img/sharp-libvips-darwin-x64, + pkg:npm/%40img/sharp-libvips-linux-arm, + pkg:npm/%40img/sharp-libvips-linux-arm64, + pkg:npm/%40img/sharp-libvips-linux-ppc64, + pkg:npm/%40img/sharp-libvips-linux-riscv64, + pkg:npm/%40img/sharp-libvips-linux-s390x, + pkg:npm/%40img/sharp-libvips-linux-x64, + pkg:npm/%40img/sharp-libvips-linuxmusl-arm64, + pkg:npm/%40img/sharp-libvips-linuxmusl-x64, + pkg:npm/%40img/sharp-wasm32, + pkg:npm/%40img/sharp-win32-arm64, + pkg:npm/%40img/sharp-win32-ia32, + pkg:npm/%40img/sharp-win32-x64 allow-licenses: >- AGPL-3.0-only, Apache-2.0, Apache-2.0 WITH LLVM-exception, MIT, BSD-2-Clause, BSD-3-Clause, ISC, Zlib, BSL-1.0, MPL-2.0, Unlicense, From c6af5f7f5307d0f70b33612a22a462311dfcfa1b Mon Sep 17 00:00:00 2001 From: Mac Anderson Date: Thu, 10 Sep 2026 11:34:17 -0700 Subject: [PATCH 3/4] refactor(stella-time): one home for the real time sources behind the Sleeper and Clock ports #6486 routed every instant, wait and timeout in stella-core through retry::Sleeper and left the real sources copied: a wall clock in stella-cli, stella-runtime and stella-serve, a Tokio sleeper in two of them, and a sleeper trait of stella-fleet's own with a third. stella-time now holds TokioSleeper, WallClock and MonotonicClock (which replaces the CLI's per-construction SystemClock), and ships PausedSleeper and NoopSleeper behind a test-util feature for the test sweep that follows. The copies are deleted; stella-fleet's monitor takes the engine's Sleeper. tests/one_home.rs reads the shipping tree and fails on any other copy. ADR 0042 records the design #6486 merged and where the sources live. Refs #6484 --- AGENTS.md | 8 +- Cargo.lock | 14 ++ Cargo.toml | 1 + crates/stella-cli/Cargo.toml | 2 + crates/stella-cli/src/agent.rs | 2 +- crates/stella-cli/src/agent/engine.rs | 2 +- crates/stella-cli/src/agent/persistence.rs | 2 +- .../src/agent/tests/engine_wiring.rs | 4 +- crates/stella-cli/src/fleet_cmd.rs | 9 +- crates/stella-cli/src/fleet_cmd/tests.rs | 6 +- crates/stella-cli/src/runtime.rs | 117 +----------- crates/stella-core/README.md | 13 +- crates/stella-fleet/Cargo.toml | 2 + crates/stella-fleet/src/monitor.rs | 33 ++-- crates/stella-runtime/Cargo.toml | 2 + crates/stella-runtime/src/wrapper/dispatch.rs | 4 +- crates/stella-runtime/src/wrapper/stamp.rs | 29 +-- crates/stella-serve/Cargo.toml | 2 + crates/stella-serve/src/extensions.rs | 2 +- crates/stella-serve/src/remote.rs | 57 +----- crates/stella-serve/src/session.rs | 5 +- crates/stella-serve/src/subagents.rs | 2 +- crates/stella-time/Cargo.toml | 30 +++ crates/stella-time/README.md | 80 ++++++++ crates/stella-time/src/lib.rs | 164 ++++++++++++++++ crates/stella-time/src/test_util.rs | 64 +++++++ crates/stella-time/tests/one_home.rs | 177 ++++++++++++++++++ ...ine-reads-time-through-the-sleeper-port.md | 102 ++++++++++ docs/adr/README.md | 7 + docs/manifest.json | 5 + 30 files changed, 713 insertions(+), 234 deletions(-) create mode 100644 crates/stella-time/Cargo.toml create mode 100644 crates/stella-time/README.md create mode 100644 crates/stella-time/src/lib.rs create mode 100644 crates/stella-time/src/test_util.rs create mode 100644 crates/stella-time/tests/one_home.rs create mode 100644 docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md diff --git a/AGENTS.md b/AGENTS.md index 18e2b566fa..7e7397736d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1341,8 +1341,9 @@ empty and is meant to stay empty. ## Workspace layout — where a change goes -Twenty-nine crates, every one under the `crates/` directory (`crates/stella-core`, -`crates/stella-cli`, …; the two bench members stay under `bench/`). The +Every crate lives under the `crates/` directory (`crates/stella-core`, +`crates/stella-cli`, …; the two bench members stay under `bench/`); the +`members` list in the root `Cargo.toml` is the roll. The one-sentence rule of thumb below routes you to the right one; **each crate's own `README.md`** (linked from the table) then covers its boundary, layout, invariants, gotchas, and extension recipe in depth. Read that before changing @@ -1365,6 +1366,7 @@ the files you must plan around (see below). | Change the wrapper socket's **trait** — `TurnWrapper`, `admissible`, `judge`/`again`, the in-process/subprocess transports | [`stella-runtime`](crates/stella-runtime/README.md) | `src/wrapper/` (#3380, landed #3479, `doc:wrapper-socket`). Lives one layer above `stella-core` because `before_turn`/`after_turn` do I/O, which invariant 2 bans in the engine; consumes `stella-plugin`'s wire types rather than redefining them. It is reached four ways now: a `--pipeline` run, a goal round, a fleet attempt, and an auto-resolved plugin — see the crate's own README. | | Change how a plugin is **installed, listed, removed, or trusted** — `.stella/plugins/` and `~/.stella/plugins/` resolution, install consent, the project-tier trust gate | [`stella-cli`](crates/stella-cli/README.md) | `src/plugin_cmd.rs` + `src/plugin_cmd/{roster,process}.rs` — renders `stella_plugin::consent_text` before anything executes, gates a cloned repository's plugins on `project_code_execution_trusted()` (#3509), and is the one place `LoopGrant::permits_hook`/`permits_point` get consulted against an installed manifest. | | Decide whether a human is present to see/answer a mid-run prompt | [`stella-tty`](crates/stella-tty/README.md) | **A leaf with NO dependencies at all** (#3036) — one pure `human_can_answer(interactive_output, stdin_is_terminal, prompt_is_visible)`, which is what lets `stella-cli`'s approval prompts and `stella-model`'s credential prompt share one derivation without `stella-model` depending on `stella-cli` (invariant 1). | +| Read the real clock, wait on the real timer, or stand in for either under test, behind `stella-core`'s `Sleeper` and `Clock` ports | [`stella-time`](crates/stella-time/README.md) | **Near-leaf: `stella-core` is its only workspace dependency.** `TokioSleeper` (the engine's real sleeper and `now`), `WallClock` (Unix epoch, for a stamp another process reads) and `MonotonicClock` (one origin per process, for a span compared as a number), plus `test_util`'s `PausedSleeper` and `NoopSleeper` behind the `test-util` feature. One home, because `stella-serve` may not link `stella-cli` or `stella-runtime` and `stella-core` may not carry a timer (ADR 0042). | | Emit a diagnostic — a record explaining *why* the program did something | [`stella-diag`](crates/stella-diag/README.md) | **A leaf: `serde` only, so anything may depend on it.** Field values cannot hold a `String`, a `Path`, or model output — that is a compile error, not a review question. Design: [`docs/spec/diagnostics.md`](docs/spec/diagnostics.md). | | Compute a line-oriented unified diff (`@@` hunks, git's exact shape) | [`stella-diff`](crates/stella-diff/README.md) | **A leaf with NO dependencies at all** (#1511) — pure functions over borrowed strings, which is what lets [`stella-observatory`](crates/stella-observatory/README.md) and [`stella-cli`](crates/stella-cli/README.md) share one differ without costing the observatory its isolation. | | Strip ANSI escape sequences from tool output | [`stella-ansi`](crates/stella-ansi/README.md) | **A leaf with NO dependencies at all** — one pure function over a borrowed `&str`, extracted from `stella-tui` so [`stella-observatory`](crates/stella-observatory/README.md) could strip a colourised tool's output before it reaches the journal route's `
` without linking `ratatui`. `stella-tui`'s `ansi` module re-exports it and keeps only the `ratatui`-shaped emission half. |
@@ -1492,7 +1494,7 @@ a plan needs and the part that rarely changes:
 | `stella-store` | `src/tests.rs`, `src/lib.rs`, `src/usage.rs` |
 | `stella-tui` | `src/deck_ui.rs` |
 
-The other twenty-four crates carry no god files — keep it that way. Each crate's
+Every crate not named there carries no god files — keep it that way. Each crate's
 README repeats its own list under "God files — do not add lines", so the
 constraint is in view wherever planning starts.
 
diff --git a/Cargo.lock b/Cargo.lock
index 87c96cf4cf..c57bb0126b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3240,6 +3240,7 @@ dependencies = [
  "stella-records",
  "stella-runtime",
  "stella-store",
+ "stella-time",
  "stella-tools",
  "stella-transcript",
  "stella-tty",
@@ -3345,6 +3346,7 @@ dependencies = [
  "stella-core",
  "stella-protocol",
  "stella-store",
+ "stella-time",
  "stella-tools",
  "tempfile",
  "thiserror 2.0.20",
@@ -3529,6 +3531,7 @@ dependencies = [
  "stella-plugin",
  "stella-protocol",
  "stella-store",
+ "stella-time",
  "stella-tools",
  "tempfile",
  "thiserror 2.0.20",
@@ -3548,6 +3551,7 @@ dependencies = [
  "stella-core",
  "stella-engine",
  "stella-protocol",
+ "stella-time",
  "tempfile",
  "thiserror 2.0.20",
  "tokio",
@@ -3571,6 +3575,16 @@ dependencies = [
  "thiserror 2.0.20",
 ]
 
+[[package]]
+name = "stella-time"
+version = "0.9.416"
+dependencies = [
+ "async-trait",
+ "rand 0.10.2",
+ "stella-core",
+ "tokio",
+]
+
 [[package]]
 name = "stella-tool-facts"
 version = "0.9.416"
diff --git a/Cargo.toml b/Cargo.toml
index 6d2c47b062..5638e5f763 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,6 +23,7 @@ members = [
   "crates/stella-mcp",
   "crates/stella-engine",
   "crates/stella-runtime",
+  "crates/stella-time",
   "crates/stella-fleet",
   "crates/stella-observatory",
   "crates/stella-serve",
diff --git a/crates/stella-cli/Cargo.toml b/crates/stella-cli/Cargo.toml
index e40d57942f..88e22578e7 100644
--- a/crates/stella-cli/Cargo.toml
+++ b/crates/stella-cli/Cargo.toml
@@ -44,6 +44,8 @@ contextgraph-host.workspace = true
 # see the exact-version note in the root manifest.
 contextgraph-trace.workspace = true
 stella-core = { path = "../stella-core" }
+# The real sleeper and clocks behind stella-core's time ports.
+stella-time = { path = "../stella-time" }
 # The context-record plane — the typed taxonomy, the ingestion boundary, and
 # the registry the prompt's rule block renders from. It left `stella-core`
 # because the engine reached it through one hash call and nothing else;
diff --git a/crates/stella-cli/src/agent.rs b/crates/stella-cli/src/agent.rs
index e06ea73edb..8bb82fa4b8 100644
--- a/crates/stella-cli/src/agent.rs
+++ b/crates/stella-cli/src/agent.rs
@@ -37,7 +37,7 @@ use crate::memory::{
     turn_warrants_reflection,
 };
 use crate::plain::{self, accent};
-use crate::runtime::{SystemClock, TokioSleeper};
+use crate::runtime::{MonotonicClock, TokioSleeper};
 use crate::{OutputFormat, config::Config};
 use stella_context::EpisodeOutcome;
 
diff --git a/crates/stella-cli/src/agent/engine.rs b/crates/stella-cli/src/agent/engine.rs
index a3b31bc023..7376bde9ea 100644
--- a/crates/stella-cli/src/agent/engine.rs
+++ b/crates/stella-cli/src/agent/engine.rs
@@ -754,7 +754,7 @@ pub(crate) fn session_router(cfg: &Config, worker_ref: &ModelRef) -> Router {
     Router::new(
         wiring.pins,
         wiring.profiles,
-        CircuitBreaker::new(Box::new(SystemClock::new())),
+        CircuitBreaker::new(Box::new(MonotonicClock)),
     )
 }
 
diff --git a/crates/stella-cli/src/agent/persistence.rs b/crates/stella-cli/src/agent/persistence.rs
index 3bd518888c..8946e79255 100644
--- a/crates/stella-cli/src/agent/persistence.rs
+++ b/crates/stella-cli/src/agent/persistence.rs
@@ -223,7 +223,7 @@ pub(crate) fn spawn_renderer(
             crate::diag_boot::dx(),
             Some(crate::diag_boot::workspace_root()),
         );
-        // The stream-json sink's clock. Wall-anchored, not `SystemClock`: a
+        // The stream-json sink's clock. Wall-anchored, not `MonotonicClock`: a
         // journal stamp has to stay comparable across processes and runs, and a
         // per-construction origin is exactly the wrong shape for that (#2111).
         let clock = WallClock;
diff --git a/crates/stella-cli/src/agent/tests/engine_wiring.rs b/crates/stella-cli/src/agent/tests/engine_wiring.rs
index 72e46fd282..027fd9d299 100644
--- a/crates/stella-cli/src/agent/tests/engine_wiring.rs
+++ b/crates/stella-cli/src/agent/tests/engine_wiring.rs
@@ -209,7 +209,7 @@ fn the_settings_model_is_inert_without_the_fix_but_routes_with_it() {
     // The full round trip through the actual router — `SessionFallback` is the
     // one live `Router::resolve` caller, and `Role::Worker` is the one role it
     // asks for, which is why that pin is the one #3908 kept.
-    let breaker = CircuitBreaker::new(Box::new(SystemClock::new()));
+    let breaker = CircuitBreaker::new(Box::new(MonotonicClock));
     let router = Router::new(wiring.pins.clone(), wiring.profiles.clone(), breaker);
     assert_eq!(
         router.resolve(Role::Worker).unwrap().model_ref,
@@ -255,7 +255,7 @@ fn an_explicit_model_flag_outranks_the_settings_model() {
 
     // The round trip through the real router is the claim that matters: the
     // pin table being empty is only useful if resolution lands on the flag.
-    let breaker = CircuitBreaker::new(Box::new(SystemClock::new()));
+    let breaker = CircuitBreaker::new(Box::new(MonotonicClock));
     let router = Router::new(wiring.pins.clone(), wiring.profiles.clone(), breaker);
     assert_eq!(
         router.resolve(Role::Worker).unwrap().model_ref,
diff --git a/crates/stella-cli/src/fleet_cmd.rs b/crates/stella-cli/src/fleet_cmd.rs
index e12461304b..b1171e63f0 100644
--- a/crates/stella-cli/src/fleet_cmd.rs
+++ b/crates/stella-cli/src/fleet_cmd.rs
@@ -76,7 +76,7 @@ use tokio::sync::{mpsc, oneshot, watch};
 
 use crate::config::Config;
 use crate::lane_capabilities;
-use crate::runtime::{SystemClock, TokioSleeper, WallClock};
+use crate::runtime::{MonotonicClock, TokioSleeper, WallClock};
 // The trait is in scope for `AttemptPointStream::publish` below — a fleet
 // attempt publishes its own channel across the dispatch's points (#4730).
 use crate::wrapper_plugin::PointStream;
@@ -235,10 +235,10 @@ pub async fn run_fleet(
         WorktreeManager::new(SystemGitCli, root.clone()).with_run_scope(&run_id),
         ledger,
         agent::build_budget_guard(budget_limit),
-        // Wall-anchored, NOT `SystemClock`: every stamp this clock feeds is
+        // Wall-anchored, NOT `MonotonicClock`: every stamp this clock feeds is
         // a durable ledger row that must stay comparable across runs — the
         // warmth projection (#1222) reads a PRIOR run's `finished_at_ms`.
-        // `SystemClock`'s per-process origin made every run start near zero.
+        // A per-process origin would make every run start near zero.
         WallClock,
         {
             let mut config =
@@ -365,8 +365,7 @@ pub async fn run_fleet(
             );
         } else {
             let config = WatchConfig::default();
-            let monitor =
-                Monitor::new(SystemGhCli, Box::new(SystemClock::new())).with_config(config);
+            let monitor = Monitor::new(SystemGhCli, Box::new(MonotonicClock)).with_config(config);
             println!(
                 "  watching CI for {} fleet branch(es) — polling every {}s, wall cap {}m\n",
                 targets.len(),
diff --git a/crates/stella-cli/src/fleet_cmd/tests.rs b/crates/stella-cli/src/fleet_cmd/tests.rs
index 8522af93f6..0df580c00a 100644
--- a/crates/stella-cli/src/fleet_cmd/tests.rs
+++ b/crates/stella-cli/src/fleet_cmd/tests.rs
@@ -427,7 +427,7 @@ async fn watch_branch_reports_green_ci_and_open_pr() {
         Some(r#"{"state":"OPEN","isDraft":false}"#),
     );
     let calls = gh.calls.clone();
-    let monitor = Monitor::new(gh, Box::new(SystemClock::new()));
+    let monitor = Monitor::new(gh, Box::new(MonotonicClock));
 
     let watched = branch_watch::watch_branch(&monitor, "t1", "fleet/t1-abc").await;
     assert!(watched.is_green());
@@ -455,7 +455,7 @@ async fn watch_branch_red_ci_and_a_missing_pr_are_states_not_errors() {
         r#"[{"status":"completed","conclusion":"failure","name":"ci"}]"#,
         None,
     );
-    let monitor = Monitor::new(gh, Box::new(SystemClock::new()));
+    let monitor = Monitor::new(gh, Box::new(MonotonicClock));
 
     let watched = branch_watch::watch_branch(&monitor, "t1", "fleet/t1-abc").await;
     assert!(!watched.is_green());
@@ -475,7 +475,7 @@ async fn watch_branch_treats_a_ci_timeout_as_red() {
     // first decision (elapsed >= grace with a 0ms grace) — the watch ends
     // as NoRunsStarted without sleeping, and the branch is red.
     let gh = RoutedGh::new("[]", None);
-    let monitor = Monitor::new(gh, Box::new(SystemClock::new())).with_config(WatchConfig {
+    let monitor = Monitor::new(gh, Box::new(MonotonicClock)).with_config(WatchConfig {
         poll_interval_ms: 1,
         max_total_ms: 60_000,
         stall_timeout_ms: 60_000,
diff --git a/crates/stella-cli/src/runtime.rs b/crates/stella-cli/src/runtime.rs
index ac0f85bb81..9b0d2136a3 100644
--- a/crates/stella-cli/src/runtime.rs
+++ b/crates/stella-cli/src/runtime.rs
@@ -1,82 +1,9 @@
-//! Production implementations of `stella-core`'s time ports. The engine
-//! exports only the [`Clock`] and [`Sleeper`] traits (ports, not
-//! specific implementations), so the binary owns the concrete
-//! wall-clock/tokio impls and wires them at construction. This is what
-//! keeps `stella-core` free of production `tokio::time` calls.
+//! The binary's side of `stella-core`'s time ports. The engine exports only
+//! the `Sleeper` and `Clock` traits; the real sources behind them are
+//! `stella-time`'s, re-exported here under the names the rest of this crate
+//! wires at construction. One-shot runs also arm their task deadline here.
 
-use async_trait::async_trait;
-use rand::RngExt;
-use stella_core::ports::Clock;
-use stella_core::retry::Sleeper;
-
-/// The production clock: monotonic milliseconds since construction.
-pub struct SystemClock {
-    origin: std::time::Instant,
-}
-
-impl SystemClock {
-    pub fn new() -> Self {
-        Self {
-            origin: std::time::Instant::now(),
-        }
-    }
-}
-
-impl Default for SystemClock {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl Clock for SystemClock {
-    fn now_ms(&self) -> u64 {
-        self.origin.elapsed().as_millis() as u64
-    }
-}
-
-/// A [`Clock`] anchored to the **wall** (Unix-epoch milliseconds), for
-/// stamps that must stay comparable across processes and runs — the fleet
-/// ledger's rows, where issue #1222's cache-warmth projection compares a
-/// prior run's `finished_at_ms` against "now". [`SystemClock`]'s
-/// per-construction origin is the right shape for in-process elapsed
-/// arithmetic (retry pacing, watch caps) and exactly the wrong one for a
-/// durable timestamp: two runs' stamps share no origin, so their difference
-/// means nothing. Not strictly monotonic (NTP can step it), which is why it
-/// does not replace [`SystemClock`] everywhere; a pre-epoch system clock
-/// reads as `0` rather than panicking.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct WallClock;
-
-impl Clock for WallClock {
-    fn now_ms(&self) -> u64 {
-        std::time::SystemTime::now()
-            .duration_since(std::time::UNIX_EPOCH)
-            .map(|d| d.as_millis() as u64)
-            .unwrap_or(0)
-    }
-}
-
-/// The production [`Sleeper`]: `tokio::time::sleep` for the wait, and the
-/// OS entropy pool for the jitter that spreads concurrent retriers across
-/// the backoff window. Both live here so `stella-core` links neither a
-/// timer nor an entropy source.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct TokioSleeper;
-
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, duration_ms: u64) {
-        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
-    }
-
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, upper: u64) -> u64 {
-        rand::rng().random_range(0..=upper)
-    }
-}
+pub use stella_time::{MonotonicClock, TokioSleeper, WallClock};
 
 /// The budget guard for a one-shot invocation, with its wall-clock task
 /// deadline armed (#1503).
@@ -123,28 +50,6 @@ pub(crate) fn one_shot_budget_guard(
 mod tests {
     use super::*;
 
-    #[test]
-    fn system_clock_starts_near_zero_and_advances_monotonically() {
-        let clock = SystemClock::new();
-        let first = clock.now_ms();
-        std::thread::sleep(std::time::Duration::from_millis(5));
-        let second = clock.now_ms();
-        assert!(second >= first, "clock must never go backwards");
-        assert!(
-            second - first >= 4,
-            "clock must actually advance with wall time"
-        );
-    }
-
-    #[test]
-    fn default_constructs_a_fresh_clock() {
-        let clock = SystemClock::default();
-        assert!(
-            clock.now_ms() < 1000,
-            "a freshly constructed clock starts near zero"
-        );
-    }
-
     /// The #1503 witness: #1481's deadline mechanism was proven by a driver
     /// test but nothing in the shipping binary armed it — this is the arming,
     /// so a one-shot task stops itself at a safe boundary instead of running
@@ -206,16 +111,4 @@ mod tests {
              harness's kill with its work discarded (#3868)"
         );
     }
-
-    #[test]
-    fn wall_clock_reads_epoch_milliseconds_not_a_process_origin() {
-        let now = WallClock.now_ms();
-        // Any plausible present is decades past the epoch — a process-origin
-        // clock would read near zero here, which is exactly the bug this
-        // clock exists to avoid for durable stamps.
-        assert!(
-            now > 1_600_000_000_000,
-            "wall clock must be epoch-anchored, got {now}"
-        );
-    }
 }
diff --git a/crates/stella-core/README.md b/crates/stella-core/README.md
index be4c5ee855..0f3f6bb2a7 100644
--- a/crates/stella-core/README.md
+++ b/crates/stella-core/README.md
@@ -161,7 +161,7 @@ lib.rs), never as a planning assumption.
 | [`src/estimator.rs`](src/estimator.rs) | Conservative token estimate plus `Calibration`/`CalibrationMap`, the per-model drift correction fed by reported usage. |
 | [`src/loop_detect.rs`](src/loop_detect.rs) + [`src/loop_detect/`](src/loop_detect) | `detect_loop()` — five rungs over `CallRecord`s: exact repeats, short cycles, stagnation, interleaved repeats, and a wrapped monotonic sweep. |
 | [`src/shell_text.rs`](src/shell_text.rs) | Reading a shell command as *text*: the quote-aware `shell_words` splitter, `is_operator_word`, and `bare_sleep_seconds` — the stall classifier the `bash` advisory and the engine's stall rung ([`src/driver/loop_escalation.rs`](src/driver/loop_escalation.rs)) share, so one operator list serves both (#2022). |
-| [`src/retry.rs`](src/retry.rs) | `RetryPolicy`, backoff computation, `retry_with_backoff*`, and the `Sleeper` port. |
+| [`src/retry.rs`](src/retry.rs) | `RetryPolicy`, backoff computation, `retry_with_backoff*`, `bounded`, and the `Sleeper` port; its real impl and its two test doubles are `stella-time`'s. |
 | [`src/starvation.rs`](src/starvation.rs) | Reasoning-starvation arithmetic: output-contract headroom, the empty-`length` signature, and the retry cap. Written to serve two chokepoints from one copy, because one copy is what makes a fix reach both (#2128, #2174): `stella-cli`'s standalone-call chokepoint, and the staged pipeline's management chokepoint until that crate was deleted (#3865). |
 | [`src/speculation.rs`](src/speculation.rs) | Early execution of read-only tool calls announced mid-stream (`pub(crate)`). |
 | [`src/receipts.rs`](src/receipts.rs) | `ReceiptLedger` — `BlockRegistered` + `StepManifest` context receipts, content-free (digests, never payloads). |
@@ -293,9 +293,11 @@ system message and the latest user message are never touched.
   with: a nested turn that dropped `gate`, `steering` and `hooks` at once.
   Do not hand-roll a child engine. Call
   [`src/subagent.rs`](src/subagent.rs)'s `run_sub_agent`, which constructs the
-  child in-crate and carries every seam. The crate still exports the `Sleeper`
-  port with no production implementation — wiring a real one is the binary's
-  job, and tests wire a no-op to run retries at zero wall-clock cost.
+  child in-crate and carries every seam. The crate exports the `Sleeper` and
+  `Clock` ports with no production implementation — `stella-time` holds
+  those, and every host passes them to `Engine::assemble`. Tests wire a
+  no-op or a paused-clock double, so a retry costs no wall clock and a
+  timeout still means what it says.
 - **A sub-agent's steering is filtered, not inherited.** `drain_steering` is
   destructive by contract, so a child that inherited the parent's `TurnSteering`
   would swallow a message the user addressed to the parent. `ChildSteering`
@@ -330,7 +332,8 @@ turn-driver audit witnesses; also `budget_boundaries.rs`,
 [`src/loop_detect.rs`](src/loop_detect.rs); a failing case writes its seed to
 `proptest-regressions/`, and that seed is committed. No feature flag, no env var, no
 fixture server and no network — driver tests wire scripted `Provider`s, counting
-`ToolExecutor`s and no-op `Sleeper`s, so the suite runs in seconds. Keep it that
+`ToolExecutor`s and sleeper doubles on a paused runtime, so the suite runs in
+seconds. Keep it that
 way: a test here that needs a file or a socket means the logic under test is in
 the wrong crate.
 
diff --git a/crates/stella-fleet/Cargo.toml b/crates/stella-fleet/Cargo.toml
index 3270a48f47..2d440d84d1 100644
--- a/crates/stella-fleet/Cargo.toml
+++ b/crates/stella-fleet/Cargo.toml
@@ -15,6 +15,8 @@ publish.workspace = true
 stella-autonomy = { path = "../stella-autonomy" }
 stella-protocol = { path = "../stella-protocol" }
 stella-core = { path = "../stella-core" }
+# The real sleeper and clocks behind stella-core's time ports.
+stella-time = { path = "../stella-time" }
 stella-store = { path = "../stella-store" }
 stella-tools = { path = "../stella-tools" }
 serde.workspace = true
diff --git a/crates/stella-fleet/src/monitor.rs b/crates/stella-fleet/src/monitor.rs
index f0907be713..1ac67d663d 100644
--- a/crates/stella-fleet/src/monitor.rs
+++ b/crates/stella-fleet/src/monitor.rs
@@ -142,24 +142,13 @@ async fn run_with_timeout(
 
 // Sleeper (deferred-wait pacing; injectable so caps are testable)
 
-/// The pacing seam for the poll loop — real impl sleeps, the test impl
-/// advances the injected [`Clock`] instead so a 2h cap is proven in
+/// The pacing seam for the poll loop: the engine's own [`Sleeper`] port, so
+/// the fleet keeps no second one. The real impl sleeps; the test impl
+/// advances the injected [`Clock`] instead, so a 2h cap is proven in
 /// microseconds.
-#[async_trait]
-pub trait Sleeper: Send + Sync {
-    async fn sleep(&self, ms: u64);
-}
-
-/// Production [`Sleeper`] — a real `tokio` sleep.
-#[derive(Debug, Clone, Copy, Default)]
-pub struct TokioSleeper;
-
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, ms: u64) {
-        tokio::time::sleep(Duration::from_millis(ms)).await;
-    }
-}
+pub use stella_core::retry::Sleeper;
+/// Production [`Sleeper`] — a real `tokio` sleep, from `stella-time`.
+pub use stella_time::TokioSleeper;
 
 // Errors
 
@@ -837,6 +826,16 @@ mod tests {
         async fn sleep(&self, ms: u64) {
             self.0.fetch_add(ms, Ordering::SeqCst);
         }
+
+        // The monitor reads the injected `Clock`, never this: the port's
+        // `now` and `jitter` exist for the engine's retry ladder.
+        fn now(&self) -> std::time::Instant {
+            std::time::Instant::now()
+        }
+
+        fn jitter(&self, _upper: u64) -> u64 {
+            0
+        }
     }
 
     /// A scripted `gh`: records calls, pops one response per call, and
diff --git a/crates/stella-runtime/Cargo.toml b/crates/stella-runtime/Cargo.toml
index fa74fd3432..f5f1258fd6 100644
--- a/crates/stella-runtime/Cargo.toml
+++ b/crates/stella-runtime/Cargo.toml
@@ -11,6 +11,8 @@ publish.workspace = true
 [dependencies]
 stella-protocol = { path = "../stella-protocol" }
 stella-core = { path = "../stella-core" }
+# The real sleeper and clocks behind stella-core's time ports.
+stella-time = { path = "../stella-time" }
 stella-model = { path = "../stella-model" }
 stella-tools = { path = "../stella-tools" }
 stella-store = { path = "../stella-store" }
diff --git a/crates/stella-runtime/src/wrapper/dispatch.rs b/crates/stella-runtime/src/wrapper/dispatch.rs
index 5a101e803b..a0ca54d733 100644
--- a/crates/stella-runtime/src/wrapper/dispatch.rs
+++ b/crates/stella-runtime/src/wrapper/dispatch.rs
@@ -83,7 +83,7 @@ use stella_plugin::{
 use stella_protocol::completion::CompletionMessage;
 use stella_protocol::{GateBoard, LadderRung, LadderSnapshot, VerdictEvidence};
 
-use super::stamp::{HostClock, StampTiming};
+use super::stamp::StampTiming;
 use super::{
     ArbiterClaim, Arbitration, TurnHoldBudget, TurnWrapper, WrapperError, admissible, again,
     fold_stamps, judge, stamp,
@@ -585,7 +585,7 @@ impl WrapperDispatch {
             hold_grant: composition.hold_grant,
             host_max_holds: DEFAULT_HOST_MAX_HOLDS,
             context: None,
-            clock: Arc::new(HostClock),
+            clock: Arc::new(stella_time::WallClock),
         })
     }
 
diff --git a/crates/stella-runtime/src/wrapper/stamp.rs b/crates/stella-runtime/src/wrapper/stamp.rs
index 34a0281fa1..b2c67b9a43 100644
--- a/crates/stella-runtime/src/wrapper/stamp.rs
+++ b/crates/stella-runtime/src/wrapper/stamp.rs
@@ -13,9 +13,8 @@
 //!
 //! [`stamped`] reads no clock and touches no file. The caller passes the two
 //! times in, which is what lets a test pin a whole stamp to the byte.
-//! [`HostClock`] is the source a real run passes them from.
+//! `stella-time`'s `WallClock` is the source a real run passes them from.
 
-use stella_core::ports::Clock;
 use stella_plugin::{
     EvidenceProvenance, EvidenceSet, FlipObservation, TamperFinding, UndecidedReason, Verdict,
     VerdictRule,
@@ -28,28 +27,6 @@ use super::arbitration::ArbiterClaim;
 /// The name a stamp carries when the host reached the answer itself.
 pub const HOST_AUTHOR: &str = "engine";
 
-/// The host's own clock, counting from the Unix epoch.
-///
-/// Two stamps are compared across runs and across machines, so they have to
-/// count from a shared start. A clock that counts from the moment a process
-/// began would make the gap between two stamps mean nothing. `stella-cli`'s
-/// `WallClock` answers the same port the same way and for the same reason.
-///
-/// A system clock set before the epoch reads as `0` rather than failing: a
-/// wrong time is a bad stamp, and a run that stops for one is worse.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct HostClock;
-
-impl Clock for HostClock {
-    fn now_ms(&self) -> u64 {
-        std::time::SystemTime::now()
-            .duration_since(std::time::UNIX_EPOCH)
-            .map_or(0, |since| {
-                u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
-            })
-    }
-}
-
 /// When an observer decided, and how long it took.
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
 pub struct StampTiming {
@@ -182,8 +159,8 @@ pub fn stamped(
 /// verdict could not say whether a check answered, stood aside, or was never
 /// heard from. This is what carries them.
 ///
-/// They go **ahead** of the arbiter's stamp, because they arrived first and
-/// the list is arrival order — the same order the fold's rows are in, so a
+/// They go **ahead** of the arbiter's stamp. They arrived first, and the
+/// list is in arrival order. That is the order of the fold's rows too, so a
 /// reader of the record and a reader of the fold see one sequence.
 ///
 /// Every stamp on one record shares a hash. The preimage drops the stamp
diff --git a/crates/stella-serve/Cargo.toml b/crates/stella-serve/Cargo.toml
index 945ffd700f..72509abb31 100644
--- a/crates/stella-serve/Cargo.toml
+++ b/crates/stella-serve/Cargo.toml
@@ -27,6 +27,8 @@ schema = ["dep:schemars", "stella-protocol/schema"]
 schemars = { workspace = true, optional = true }
 stella-protocol = { path = "../stella-protocol" }
 stella-core = { path = "../stella-core" }
+# The real sleeper and clocks behind stella-core's time ports.
+stella-time = { path = "../stella-time" }
 # The step-scoped facade (#971 phase 1). `stella-serve` drives `run_step`
 # rather than `Engine::run_turn` so a turn can be cancelled at a step
 # boundary and checkpointed between steps (#1129).
diff --git a/crates/stella-serve/src/extensions.rs b/crates/stella-serve/src/extensions.rs
index d1262bb28e..fddf5f48e0 100644
--- a/crates/stella-serve/src/extensions.rs
+++ b/crates/stella-serve/src/extensions.rs
@@ -144,7 +144,7 @@ pub(crate) fn install_for_turn(
     if extensions.is_empty() {
         return None;
     }
-    let bus = HookBus::new(turn_id, crate::remote::WallClock);
+    let bus = HookBus::new(turn_id, stella_time::WallClock);
     for extension in extensions {
         extension.install(&bus);
     }
diff --git a/crates/stella-serve/src/remote.rs b/crates/stella-serve/src/remote.rs
index 085915eb08..ba3514a91b 100644
--- a/crates/stella-serve/src/remote.rs
+++ b/crates/stella-serve/src/remote.rs
@@ -16,14 +16,10 @@ use std::sync::atomic::{AtomicU64, Ordering};
 use std::time::{Duration, Instant};
 
 use async_trait::async_trait;
-use rand::RngExt;
 use serde_json::Value;
 use stella_core::bus::{self, HookBus, HookEventDraft, names as hook_names};
 use stella_core::hooks::decision::{GateVerdict, OperatorPosture, resolve_precedence};
-use stella_core::ports::{
-    AuthzGate, Clock, DispatchAdmission, DispatchGate, Principal, ToolExecutor,
-};
-use stella_core::retry::Sleeper;
+use stella_core::ports::{AuthzGate, DispatchAdmission, DispatchGate, Principal, ToolExecutor};
 use stella_protocol::{
     CompletionRequestRef, CompletionResult, Provider, ProviderError, ToolCallObserver, ToolOutput,
     ToolSchema,
@@ -129,47 +125,6 @@ fn forward_delta(observer: Option<&dyn ToolCallObserver>, delta: &ProviderDelta)
     }
 }
 
-/// A Tokio-backed [`Sleeper`] for the session runtime's retry backoff. The
-/// session runtime is built with the time driver enabled, so `sleep` resolves
-/// there. The jitter draws from the OS entropy pool, which is the host's to
-/// hold: `stella-core` takes the draw through the port and links no entropy
-/// source of its own.
-pub(crate) struct TokioSleeper;
-
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, duration_ms: u64) {
-        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
-    }
-
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, upper: u64) -> u64 {
-        rand::rng().random_range(0..=upper)
-    }
-}
-
-/// The host's own clock, counting from the Unix epoch, for the stamp on
-/// every hook event an installed extension sees. Those stamps are read on
-/// the other side of the wire and compared with the host's own, so they have
-/// to share the host's origin; `stella-cli`'s `WallClock` answers the same
-/// port the same way. A system clock set before the epoch reads as `0`
-/// rather than failing the turn.
-#[derive(Debug, Default, Clone, Copy)]
-pub(crate) struct WallClock;
-
-impl Clock for WallClock {
-    fn now_ms(&self) -> u64 {
-        std::time::SystemTime::now()
-            .duration_since(std::time::UNIX_EPOCH)
-            .map_or(0, |since| {
-                u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
-            })
-    }
-}
-
 /// The `Provider` port as a reverse-RPC to the host. `complete_ref` emits a
 /// [`ServerFrame::ProviderRequest`] and blocks the step on the host's answer.
 pub(crate) struct RemoteProvider {
@@ -235,12 +190,12 @@ impl RemoteProvider {
 impl RemoteProvider {
     /// The one remoted completion path, shared by both trait methods.
     ///
-    /// With an observer this is what closes #1165: fragments the host POSTs to
-    /// `/v1/turns/{id}/provider-delta` land on the registered feed and are
+    /// With an observer this is what closes #1165. Fragments the host POSTs to
+    /// `/v1/turns/{id}/provider-delta` land on the registered feed. They are
     /// forwarded inline, so the engine's gate emits `TextDelta` / `Reasoning`
-    /// events and the frames flow into `FrameHistory` — ordering, `seq`, and
-    /// replay for free, exactly as with a local streaming adapter. Without an
-    /// observer the fragments are drained and dropped; the aggregated result
+    /// events and the frames flow into `FrameHistory`. Ordering, `seq` and
+    /// replay come for free, as with a local streaming adapter. Without an
+    /// observer the fragments are drained and dropped. The aggregated result
     /// stays authoritative either way.
     async fn complete_remoted(
         &self,
diff --git a/crates/stella-serve/src/session.rs b/crates/stella-serve/src/session.rs
index df0e46264f..e485f0a5f6 100644
--- a/crates/stella-serve/src/session.rs
+++ b/crates/stella-serve/src/session.rs
@@ -42,9 +42,8 @@ use crate::observe::SharedObserver;
 use crate::observe::event::{ServeEvent, SettledOutcome, TurnRef, TurnTally, millis};
 use crate::observe::tally::TallyFold;
 use crate::pending::Pending;
-use crate::remote::{
-    DEFAULT_REVERSE_REQUEST_TIMEOUT, RemoteProvider, RemoteToolExecutor, TokioSleeper,
-};
+use crate::remote::{DEFAULT_REVERSE_REQUEST_TIMEOUT, RemoteProvider, RemoteToolExecutor};
+use stella_time::TokioSleeper;
 
 /// Everything needed to run one turn. The host assembles this — it owns prompt
 /// construction, recall, model selection, and the tool set (advertised as
diff --git a/crates/stella-serve/src/subagents.rs b/crates/stella-serve/src/subagents.rs
index b94b6c0ee8..20e4807aec 100644
--- a/crates/stella-serve/src/subagents.rs
+++ b/crates/stella-serve/src/subagents.rs
@@ -270,7 +270,7 @@ impl SubAgentDispatcher for ServedSubAgents {
                         &*provider,
                         &read_only,
                         config,
-                        &crate::remote::TokioSleeper,
+                        &stella_time::TokioSleeper,
                         seams,
                     );
                     // Carve from the shared pool, run, settle. The lock is
diff --git a/crates/stella-time/Cargo.toml b/crates/stella-time/Cargo.toml
new file mode 100644
index 0000000000..89e5984e7f
--- /dev/null
+++ b/crates/stella-time/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "stella-time"
+description = "The real time sources behind stella-core's Sleeper and Clock ports — one Tokio sleeper, one wall clock, one monotonic clock — shared by every host that assembles an engine, none of which may depend on another; plus, behind `test-util`, the two sleeper doubles every test against the engine shares."
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+publish.workspace = true
+
+# The ports come from stella-core; the effects behind them come from the
+# runtime and the entropy pool. stella-core may link neither (its manifest
+# says so, and `make core-no-io` holds it there), and stella-serve may not
+# link stella-cli or stella-runtime, so the copies these replaced had no
+# home below all of their callers until this crate.
+[dependencies]
+stella-core = { path = "../stella-core" }
+tokio = { workspace = true, features = ["time"] }
+async-trait = { workspace = true }
+rand = { workspace = true }
+
+[features]
+# The two sleeper doubles every test against the engine shares: `NoopSleeper`
+# and `PausedSleeper` (`test_util`). Off in a release build. This is the
+# tokio-test shape: the doubles live beside the real sources, and the crate
+# under test takes them as a dev-dependency.
+test-util = []
+
+[dev-dependencies]
+tokio = { workspace = true, features = ["rt", "macros", "test-util"] }
diff --git a/crates/stella-time/README.md b/crates/stella-time/README.md
new file mode 100644
index 0000000000..f7430272fa
--- /dev/null
+++ b/crates/stella-time/README.md
@@ -0,0 +1,80 @@
+# stella-time
+
+The real time sources behind `stella-core`'s two time ports, and the two
+test doubles every test against the engine shares.
+
+```rust
+stella_time::TokioSleeper    // Sleeper: tokio::time::sleep, Instant::now, jitter from the OS
+stella_time::WallClock       // Clock: milliseconds since the Unix epoch
+stella_time::MonotonicClock  // Clock: milliseconds since this process's first read, never backwards
+
+stella_time::test_util::PausedSleeper  // Sleeper on tokio's paused clock (feature `test-util`)
+stella_time::test_util::NoopSleeper    // Sleeper that never waits (feature `test-util`)
+```
+
+`stella_core::retry::Sleeper` and `stella_core::ports::Clock` are the ports.
+The engine reads time, waits and times out only through them (ADR 0042). A
+host picks a source here and hands it to `Engine::assemble`.
+
+## Which source
+
+- **The engine, in a real run:** `TokioSleeper`. Its `now` is the monotonic
+  clock, so a deadline armed from it is checked on the same timeline.
+- **A stamp read by another process or a later run:** `WallClock`. A hook
+  event's stamp, a fleet ledger row, a verdict stamp. Two such stamps must
+  count from one start. The epoch is the only start every process agrees
+  on.
+- **A span measured inside one process and compared as a number:**
+  `MonotonicClock`. A circuit breaker's cooling window, a CI monitor's wall
+  cap. Every instance shares one origin, so two holders built at different
+  moments read one timeline.
+- **A test that arms a timeout:** `PausedSleeper`, on a paused tokio
+  runtime. A sleep is free while the runtime is idle and still lets a
+  pending call finish first. A sleeper that returned at once would fire
+  every engine timeout the moment a provider future waited on another task.
+- **A test that arms none:** `NoopSleeper`.
+
+## Boundary — does this change belong here?
+
+This crate implements the ports. It decides nothing. A change belongs here
+only if it is a new way to read the real clock, wait on the real timer, or
+stand in for either under test. What to do with a reading belongs in
+`stella-core`, where it is a pure function over the reading. A double with
+a special shape, such as a seeded jitter or a sleep that records its calls,
+belongs beside the one test that needs it.
+
+## Why it is a crate
+
+AGENTS.md § "When a new crate is justified" names two of its three cases.
+These items are the effects the ports keep out of `stella-core`. Its
+manifest may not take the Tokio timer or an entropy source, and
+`make core-no-io` holds it there. And they sit below every host.
+`stella-cli`, `stella-runtime` and `stella-serve` each build an engine, and
+`stella-serve` may not link the other two. Before this crate each host kept
+its own copy, and about thirty test files each kept a double.
+
+`test_util` lives here and not in `stella-core` for the same reason: a
+faithful sleeper double needs the Tokio timer, and `stella-core` may not
+link it. A crate that tests the engine takes this crate as a dev-dependency
+with the feature on; for `stella-core`'s integration tests that is a
+cycle, which cargo allows. That is the tokio / tokio-test shape.
+`stella-core`'s own unit tests are the one place that cannot: a lib's unit
+tests are a second build of the lib, and a dev-dependency that links the
+lib implements the trait for the first. They keep one copy, and
+`tests/one_home.rs` names it and fails on any other.
+
+## God files — do not add lines
+
+This crate has no god files. No file exceeds the gate's 1500-line ratchet
+(`scripts/check-file-size.sh`), and none may appear. A new file crossing
+1500 lines fails the gate outright, and `scripts/file-size-baseline.txt`
+accepts no new entries. When a file here nears the limit, split it before
+it crosses.
+
+## Consumers
+
+- `stella-cli` re-exports the three real sources from its `runtime` module.
+- `stella-serve` builds its engines on `TokioSleeper` and stamps its
+  extension bus from `WallClock`.
+- `stella-runtime` stamps wrapper verdicts from `WallClock`.
+- `stella-fleet` paces its CI monitor with `TokioSleeper`.
diff --git a/crates/stella-time/src/lib.rs b/crates/stella-time/src/lib.rs
new file mode 100644
index 0000000000..8e222925cc
--- /dev/null
+++ b/crates/stella-time/src/lib.rs
@@ -0,0 +1,164 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright (c) 2026 Oxagen, Inc. Commercial licensing: licensing@oxagen.sh
+
+//! The real time sources behind `stella-core`'s two time ports.
+//!
+//! [`stella_core::retry::Sleeper`] and [`stella_core::ports::Clock`] are
+//! traits. The engine reads time, waits and times out only through them. A
+//! real run gets its answers here: a sleeper on the Tokio timer, a clock
+//! counting from the Unix epoch, and one counting from the first read. The
+//! README says why the three share one crate below every host, and
+//! `test_util` holds the two doubles every test against the engine shares.
+
+use std::sync::OnceLock;
+use std::time::{Instant, SystemTime, UNIX_EPOCH};
+
+use async_trait::async_trait;
+use rand::RngExt;
+use stella_core::ports::Clock;
+use stella_core::retry::Sleeper;
+
+#[cfg(feature = "test-util")]
+pub mod test_util;
+
+/// The real [`Sleeper`]. It waits on `tokio::time::sleep`, reads `now` off
+/// the monotonic clock, and draws its jitter from the OS entropy pool, which
+/// spreads retriers across the backoff window.
+///
+/// It needs a Tokio runtime with its time driver on. Every host here builds
+/// one.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct TokioSleeper;
+
+#[async_trait]
+impl Sleeper for TokioSleeper {
+    async fn sleep(&self, duration_ms: u64) {
+        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
+    }
+
+    fn now(&self) -> Instant {
+        Instant::now()
+    }
+
+    fn jitter(&self, upper: u64) -> u64 {
+        rand::rng().random_range(0..=upper)
+    }
+}
+
+/// A [`Clock`] counting milliseconds from the Unix epoch.
+///
+/// For a stamp read by another process or a later run: a hook event on the
+/// far side of a socket, a fleet ledger row, a verdict stamp. Two such
+/// stamps must count from one start. The epoch is the only start every
+/// process agrees on.
+///
+/// Not for a deadline. The system clock can be stepped, by NTP or by hand.
+/// A deadline set against it moves with it. The engine's deadlines come off
+/// [`Sleeper::now`] instead.
+///
+/// A system clock set before the epoch reads as `0`. One past what `u64`
+/// holds reads as `u64::MAX`. A wrong time makes a bad stamp. A run that
+/// stops for one is worse.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct WallClock;
+
+impl Clock for WallClock {
+    fn now_ms(&self) -> u64 {
+        SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .map_or(0, |since| {
+                u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
+            })
+    }
+}
+
+/// The origin every [`MonotonicClock`] counts from: the first read in this
+/// process. One cell, so two instances built at different moments agree.
+static ORIGIN: OnceLock = OnceLock::new();
+
+/// A [`Clock`] counting milliseconds from the process's first read of it.
+/// It never goes backwards.
+///
+/// For a span measured inside one process and compared as a number: a
+/// circuit breaker's cooling window, a CI monitor's wall cap. Every
+/// instance shares the origin, so two holders built at different moments
+/// read one timeline. A clock whose origin was the moment it was built
+/// would put their readings apart, by the time between the two builds, and
+/// say nothing.
+///
+/// The first read in the process reads `0`.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct MonotonicClock;
+
+impl Clock for MonotonicClock {
+    fn now_ms(&self) -> u64 {
+        let origin = ORIGIN.get_or_init(Instant::now);
+        u64::try_from(origin.elapsed().as_millis()).unwrap_or(u64::MAX)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn the_monotonic_clock_never_goes_backwards() {
+        let first = MonotonicClock.now_ms();
+        std::thread::sleep(std::time::Duration::from_millis(5));
+        let second = MonotonicClock.now_ms();
+        assert!(second >= first, "clock must never go backwards");
+        assert!(
+            second - first >= 4,
+            "5ms of sleep should register: {first} -> {second}"
+        );
+    }
+
+    /// Two instances share one origin, so a reading taken on one is a
+    /// reading on the other.
+    #[test]
+    fn every_monotonic_clock_shares_one_origin() {
+        let first = MonotonicClock;
+        let mark = first.now_ms() + 10_000;
+        std::thread::sleep(std::time::Duration::from_millis(5));
+        let second = MonotonicClock;
+        let now = second.now_ms();
+        assert!(
+            now < mark,
+            "a fresh instance reads the same clock: {now} vs {mark}"
+        );
+        assert!(
+            mark - now <= 10_000,
+            "and it has moved on from the first read"
+        );
+    }
+
+    #[test]
+    fn the_wall_clock_counts_from_the_unix_epoch() {
+        // 2026-01-01T00:00:00Z. The test runs after that date. The point is
+        // the origin, not the exact reading.
+        let epoch_2026_ms: u64 = 1_767_225_600_000;
+        assert!(WallClock.now_ms() > epoch_2026_ms);
+    }
+
+    #[test]
+    fn jitter_stays_inside_the_window() {
+        for _ in 0..1_000 {
+            assert!(TokioSleeper.jitter(7) <= 7);
+        }
+        assert_eq!(TokioSleeper.jitter(0), 0);
+    }
+
+    #[tokio::test(start_paused = true)]
+    async fn the_sleeper_waits_on_the_tokio_timer() {
+        let before = tokio::time::Instant::now();
+        TokioSleeper.sleep(250).await;
+        assert!(before.elapsed() >= std::time::Duration::from_millis(250));
+    }
+
+    #[test]
+    fn the_sleepers_now_is_the_monotonic_clock() {
+        let a = TokioSleeper.now();
+        let b = TokioSleeper.now();
+        assert!(b >= a);
+    }
+}
diff --git a/crates/stella-time/src/test_util.rs b/crates/stella-time/src/test_util.rs
new file mode 100644
index 0000000000..67b9f8349f
--- /dev/null
+++ b/crates/stella-time/src/test_util.rs
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright (c) 2026 Oxagen, Inc. Commercial licensing: licensing@oxagen.sh
+
+//! The two [`Sleeper`] doubles every test against the engine shares.
+//!
+//! Behind the `test-util` feature, so a release build carries neither. A
+//! crate that tests the engine takes this crate as a dev-dependency with the
+//! feature on, the way tokio's tests take `tokio-test`.
+
+use std::time::Instant;
+
+use async_trait::async_trait;
+use stella_core::retry::Sleeper;
+
+/// A [`Sleeper`] on tokio's clock, for a test that runs its runtime paused.
+///
+/// A sleep costs nothing while the runtime is idle. It still lets a pending
+/// call finish first, and `now` reads the same virtual timeline. That is
+/// what a timeout is. So this is the double for any test that arms a tool
+/// or model timeout. A sleeper that returned at once would fire every
+/// engine timeout as soon as a provider future waited on another task.
+///
+/// Jitter is zero: a test that asserts on retry timing wants no spread.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct PausedSleeper;
+
+#[async_trait]
+impl Sleeper for PausedSleeper {
+    async fn sleep(&self, duration_ms: u64) {
+        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
+    }
+
+    fn now(&self) -> Instant {
+        tokio::time::Instant::now().into_std()
+    }
+
+    fn jitter(&self, _upper: u64) -> u64 {
+        0
+    }
+}
+
+/// A [`Sleeper`] that never waits.
+///
+/// For a test that arms no timeout and wants a retry ladder to run at once.
+/// `now` reads the real monotonic clock, so a deadline set from it still
+/// means what it says. Do not drive a timed turn with it: see
+/// [`PausedSleeper`] for why.
+///
+/// Jitter is zero, for the same reason as [`PausedSleeper`]'s.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct NoopSleeper;
+
+#[async_trait]
+impl Sleeper for NoopSleeper {
+    async fn sleep(&self, _duration_ms: u64) {}
+
+    fn now(&self) -> Instant {
+        Instant::now()
+    }
+
+    fn jitter(&self, _upper: u64) -> u64 {
+        0
+    }
+}
diff --git a/crates/stella-time/tests/one_home.rs b/crates/stella-time/tests/one_home.rs
new file mode 100644
index 0000000000..eb629523d2
--- /dev/null
+++ b/crates/stella-time/tests/one_home.rs
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright (c) 2026 Oxagen, Inc. Commercial licensing: licensing@oxagen.sh
+
+//! The witness for this crate: the real time sources and the shared sleeper
+//! doubles live here and nowhere else.
+//!
+//! Three crates each kept a wall clock. Two kept a Tokio sleeper. One kept a
+//! sleeper trait of its own. This test reads the shipping tree and fails on
+//! the first copy that comes back. It reads source, which a test of "where
+//! does this live" has to do. `stella-core` would refuse that read. This
+//! crate does not.
+//!
+//! Test files are skipped here: the per-file test doubles are the next
+//! sweep, and it widens this scan to them.
+
+use std::fs;
+use std::path::{Path, PathBuf};
+
+/// A `Sleeper` impl in shipping source outside this crate that stays on
+/// purpose, with the reason. Each one is a test double with a shape a shared
+/// double cannot have, kept in the file its one test lives in.
+const SLEEPER_IMPLS_KEPT: &[(&str, &str)] = &[
+    (
+        "crates/stella-core/src/retry.rs",
+        "the retry tests' recording sleeper: it logs every requested delay and draws a seeded jitter",
+    ),
+    (
+        "crates/stella-fleet/src/monitor.rs",
+        "the monitor tests' advancing sleeper: a sleep moves the injected clock instead of waiting",
+    ),
+    (
+        "crates/stella-core/src/accounted_call.rs",
+        "two doubles beside the accounted-call tests; the test-double sweep retires them",
+    ),
+    (
+        "crates/stella-core/src/driver/dispatch.rs",
+        "a no-op double beside the dispatch tests; the test-double sweep retires it",
+    ),
+    (
+        "crates/stella-core/src/driver/drive.rs",
+        "a no-op double beside the drive tests; the test-double sweep retires it",
+    ),
+    (
+        "crates/stella-core/src/driver/restore.rs",
+        "a no-op double beside the restore tests; the test-double sweep retires it",
+    ),
+    (
+        "crates/stella-core/src/goal.rs",
+        "a no-op double beside the goal tests; the test-double sweep retires it",
+    ),
+];
+
+/// A clock struct named like one of ours that is not ours.
+const CLOCK_STRUCTS_KEPT: &[(&str, &str)] = &[(
+    "crates/stella-context/src/clock.rs",
+    "stella-context's own Clock trait, which reads RFC 3339 text rather than milliseconds",
+)];
+
+fn workspace_root() -> PathBuf {
+    Path::new(env!("CARGO_MANIFEST_DIR"))
+        .ancestors()
+        .nth(2)
+        .expect("crates/stella-time sits two levels below the workspace root")
+        .to_path_buf()
+}
+
+/// Every `.rs` file under `crates/*/src` and `crates/*/tests`, as paths
+/// relative to the workspace root, skipping this crate.
+fn rust_sources(root: &Path) -> Vec<(String, String)> {
+    let mut out = Vec::new();
+    let crates = root.join("crates");
+    for entry in fs::read_dir(&crates).expect("crates/ is readable") {
+        let dir = entry.expect("a crate directory").path();
+        if dir.file_name().is_some_and(|name| name == "stella-time") {
+            continue;
+        }
+        collect(&dir.join("src"), root, &mut out);
+    }
+    out.sort();
+    out
+}
+
+/// A test file, by the names `make core-no-io` uses: a `tests/` directory
+/// or a file called `tests.rs`.
+fn is_test_path(path: &Path) -> bool {
+    path.file_name().is_some_and(|name| name == "tests.rs")
+        || path.components().any(|part| part.as_os_str() == "tests")
+}
+
+fn collect(dir: &Path, root: &Path, out: &mut Vec<(String, String)>) {
+    let Ok(entries) = fs::read_dir(dir) else {
+        return;
+    };
+    for entry in entries {
+        let path = entry.expect("a directory entry").path();
+        if is_test_path(&path) {
+            continue;
+        }
+        if path.is_dir() {
+            collect(&path, root, out);
+        } else if path.extension().is_some_and(|ext| ext == "rs") {
+            let rel = path
+                .strip_prefix(root)
+                .expect("under the root")
+                .to_string_lossy()
+                .replace('\\', "/");
+            let text = fs::read_to_string(&path).expect("a source file is readable");
+            out.push((rel, text));
+        }
+    }
+}
+
+fn is_kept(path: &str, kept: &[(&str, &str)]) -> bool {
+    kept.iter().any(|(kept_path, _)| *kept_path == path)
+}
+
+#[test]
+fn every_sleeper_impl_outside_this_crate_is_one_a_shared_double_cannot_be() {
+    let root = workspace_root();
+    let mut strays = Vec::new();
+    for (path, text) in rust_sources(&root) {
+        let impls = text
+            .lines()
+            .filter(|line| {
+                let line = line.trim_start();
+                line.starts_with("impl ")
+                    && line.contains("Sleeper for ")
+                    && !line.contains("ParkSupervisor")
+            })
+            .count();
+        if impls > 0 && !is_kept(&path, SLEEPER_IMPLS_KEPT) {
+            strays.push(format!("{path}: {impls} impl(s)"));
+        }
+    }
+    assert!(
+        strays.is_empty(),
+        "a Sleeper impl outside stella-time is a copy of one it already has: take \
+         `stella_time::TokioSleeper`, or `stella_time::test_util::{{PausedSleeper, NoopSleeper}}` \
+         behind the `test-util` feature. Found:\n  {}",
+        strays.join("\n  ")
+    );
+}
+
+#[test]
+fn every_kept_sleeper_impl_is_still_there() {
+    let root = workspace_root();
+    for (path, reason) in SLEEPER_IMPLS_KEPT {
+        let text = fs::read_to_string(root.join(path)).expect("a kept path exists");
+        assert!(
+            text.contains("Sleeper for "),
+            "{path} has no Sleeper impl; drop it from SLEEPER_IMPLS_KEPT ({reason})"
+        );
+    }
+}
+
+#[test]
+fn no_crate_keeps_its_own_wall_or_monotonic_clock() {
+    let root = workspace_root();
+    let names = ["WallClock", "HostClock", "SystemClock", "MonotonicClock"];
+    let mut strays = Vec::new();
+    for (path, text) in rust_sources(&root) {
+        if is_kept(&path, CLOCK_STRUCTS_KEPT) {
+            continue;
+        }
+        for name in names {
+            let needle = format!("struct {name}");
+            if text.contains(&needle) {
+                strays.push(format!("{path}: {needle}"));
+            }
+        }
+    }
+    assert!(
+        strays.is_empty(),
+        "a clock struct outside stella-time is a copy of one it already has. Found:\n  {}",
+        strays.join("\n  ")
+    );
+}
diff --git a/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md b/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md
new file mode 100644
index 0000000000..c1aa003266
--- /dev/null
+++ b/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md
@@ -0,0 +1,102 @@
+---
+id: adr/0042-the-engine-reads-time-through-the-sleeper-port
+title: "ADR 0042: The engine reads time through the Sleeper port"
+status: implemented
+---
+
+# ADR 0042: The engine reads time through the Sleeper port
+
+- Status: accepted
+- Date: 2026-09-10
+- Decides: `#6486`, `#6484`
+- Not part of the Phase 0 series.
+
+## Context
+
+`stella-core` has had a `Clock` port since the router's circuit breaker
+needed one. It reads `now_ms() -> u64`. One module used it. The deadline
+code read `std::time::Instant::now()` itself, in nineteen places, and nine
+more read `.elapsed()`, which is the same call in disguise. Four timeouts
+went to `tokio::time::timeout` directly. `#6482` counted the plain reads
+into a down-only baseline instead of removing them.
+
+A seam nobody routes through is a claim. A test that wanted to see a
+deadline fire had to arm a real one and wait. A turn could not be replayed
+from its record. And the prose about the crate's time source drifted from
+the code twice.
+
+Two sessions fixed this at once. One made every instant a `u64` reading of
+`Clock`, with a hand-moved clock as the double. The other put `now` on
+`Sleeper` and routed the timeouts through it. The second merged as
+`#6486`. This record states that decision and why it holds.
+
+The three hosts that build an engine each kept their own copy of the real
+sources. A wall clock lived in `stella-cli`, `stella-runtime` and
+`stella-serve`. A Tokio sleeper lived in two of them, and `stella-fleet`
+had a sleeper trait of its own. About thirty test files each wrote the same
+no-op sleeper. `stella-serve` may not link the other two, and `stella-core`
+may not carry a timer, so the copies had nowhere to go.
+
+## Decision
+
+**`retry::Sleeper` is the engine's whole time port.** `now() -> Instant`
+sits beside `sleep` and `jitter`. Every deadline the engine holds is an
+`Instant` from that reading. Every elapsed time is the difference of two.
+Every timeout is `retry::bounded`, a sleep racing a call through the same
+port. Nothing in the crate calls `Instant::now()`, `.elapsed()` or
+`tokio::time`; `make core-no-io` refuses all three, and its baseline is
+empty.
+
+**One port, not two, because a double cannot answer them apart.** A sleeper
+that suspends virtually has moved its own `now`. A timeout is a sleep
+racing a call. Tokio's paused runtime is the same shape: one virtual clock
+behind `sleep` and `Instant::now`. A `u64` reading on a second port would
+have made the test move two clocks and keep them in step by hand.
+
+**The reading is an `Instant`, not a `u64`.** The deadline arithmetic
+already spoke `Instant`, `tokio::time::Instant` converts to it, and a
+paused runtime produces one. The cost is that a record cannot hold an
+`Instant`. Replay against a host that answers `now` from a record is later
+work either way.
+
+**`ports::Clock` stays, for stamps.** The hook bus and the router read
+milliseconds from an epoch the holder picks, and a hook script on the far
+side of a socket compares those stamps with its own. That is a different
+question from a deadline, and it keeps its own port.
+
+**The real sources live in one crate, `stella-time`.** `TokioSleeper`
+waits on the Tokio timer, reads `now` off the monotonic clock, and draws
+its jitter from the OS entropy pool. `WallClock` counts from the Unix
+epoch, for a stamp another process reads. `MonotonicClock` counts from the
+first read, for a span compared as a number. AGENTS.md § "When a new crate
+is justified" allows the crate on two counts. It holds the effects the
+ports keep out of `stella-core`. And it sits below `stella-serve`, which
+may not link `stella-cli` or `stella-runtime`. `stella-fleet`'s own
+`Sleeper` trait is gone; its monitor takes the engine's.
+
+**The test doubles ship from `stella-time` behind a `test-util` feature.**
+`PausedSleeper` sleeps on tokio's clock and reads its `Instant`, for a test
+that arms a timeout on a paused runtime. `NoopSleeper` never waits, for a
+test that arms none. They live beside the real sources and not in
+`stella-core` because a faithful sleeper needs the Tokio timer, which
+`stella-core` may not link. `stella-core`'s integration tests and every
+other crate take it as a dev-dependency, which cargo allows in a cycle.
+That is the tokio / tokio-test shape. `stella-core`'s own unit tests keep
+one copy in `src/tests.rs`, because a lib's unit tests are a second build
+of the lib and a dev-dependency that links the lib implements the trait
+for the first. The compiler forces that copy, and the witness names it.
+
+## Consequences
+
+`stella-core`'s test suite runs on a paused clock. Backoff sleeps that used
+to cost real seconds cost nothing, and a timeout still lets a pending call
+finish first.
+
+Each host passes `stella_time::TokioSleeper` to `Engine::assemble`. Each
+test passes `PausedSleeper` or `NoopSleeper`. A new double with a special
+shape, such as a seeded jitter or a sleep that records its calls, stays
+beside the one test that needs it.
+
+`stella-time`'s `one_home` test reads the tree and fails on any other
+`impl Sleeper for` in shipping code, so the copies cannot come back
+unnoticed.
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 67569284c4..e6d259822f 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -108,6 +108,7 @@ open; nothing before Phase 3 forces it.
 | [0039](0039-a-live-smoke-provider-is-armed-or-declared-unarmed.md) | A Live Smoke Provider Is Armed or Declared Unarmed | Accepted |
 | [0040](0040-host-does-not-pick-the-tests.md) | The Host Does Not Pick the Tests | Accepted |
 | [0041](0041-the-turn-clock-reaches-dispatch-by-value.md) | The Turn Clock Reaches Dispatch by Value | Accepted |
+| [0042](0042-the-engine-reads-time-through-the-sleeper-port.md) | The Engine Reads Time Through the Sleeper Port | Accepted |
 
 ADR 0013 draws the line between what Stella owes a caller that moves a session
 between machines (an artifact, a fingerprint, a version contract, a visible
@@ -256,6 +257,12 @@ a defaulted port method, because the key that carries it belongs to one tool.
 A call that cannot finish and still leave room to report back is refused before
 it starts, which is the only moment AGENTS.md #6 leaves.
 
+ADR 0042 records how the engine reads time at all: every instant, wait and
+timeout goes through `retry::Sleeper`, whose `now` joined `sleep` and `jitter`
+because one double has to answer all three from one timeline. The real sources
+live in one crate, `stella-time`, below every host, and the two test doubles
+ship from it behind a `test-util` feature.
+
 ## The number is a shared cell
 
 Take the number one past the highest in the table above. Two branches doing
diff --git a/docs/manifest.json b/docs/manifest.json
index 30bf33d1b9..45739ff094 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -210,6 +210,11 @@
       "status": "implemented",
       "title": "ADR 0041: The turn clock reaches dispatch by value"
     },
+    "adr/0042-the-engine-reads-time-through-the-sleeper-port": {
+      "path": "docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md",
+      "status": "implemented",
+      "title": "ADR 0042: The engine reads time through the Sleeper port"
+    },
     "agent-monitor-protocol": {
       "path": "docs/spec/agent-monitor-protocol.md",
       "status": "living",

From 94063678c4c4c0c30932ecebceba7a8dd52834fc Mon Sep 17 00:00:00 2001
From: Mac Anderson 
Date: Thu, 10 Sep 2026 11:35:17 -0700
Subject: [PATCH 4/4] refactor(stella-core): take the two shared sleeper
 doubles from stella-time

About thirty test files across stella-core, stella-engine and their
tests/ directories each wrote the same Sleeper double, a no-op one under
three names and one on tokio's paused clock, and each copy grew a now()
body in #6486. They are deleted. Integration tests and stella-engine take
PausedSleeper and NoopSleeper from stella_time::test_util through a
dev-dependency; stella-core's own unit tests keep one copy in
src/tests.rs, because a lib's unit tests are a second build of the lib
and a dev-dependency that links the lib implements the trait for the
first. stella-time's one_home witness now scans test files too.

Closes #6484
---
 AGENTS.md                                     |   2 +-
 Cargo.lock                                    |   2 +
 crates/stella-core/Cargo.toml                 |   4 +
 crates/stella-core/README.md                  |  11 +-
 crates/stella-core/src/accounted_call.rs      |  54 ++-------
 crates/stella-core/src/driver/capabilities.rs |   4 +-
 crates/stella-core/src/driver/dispatch.rs     |  18 +--
 crates/stella-core/src/driver/drive.rs        |  18 +--
 crates/stella-core/src/driver/restore.rs      |  33 ++----
 crates/stella-core/src/driver/tests.rs        |  95 ++++++----------
 .../src/driver/tests/audit_fixes.rs           |  28 ++---
 .../src/driver/tests/budget_boundaries.rs     |  16 +--
 .../src/driver/tests/calibration.rs           |  10 +-
 .../src/driver/tests/compute_passes.rs        |  10 +-
 .../src/driver/tests/context_efficiency.rs    |   6 +-
 .../src/driver/tests/context_overflow.rs      |  23 +---
 .../src/driver/tests/deadline_notice.rs       |  19 +---
 .../src/driver/tests/lifecycle_bus.rs         |  23 +---
 .../src/driver/tests/live_services.rs         |   2 +-
 .../src/driver/tests/loop_abort.rs            |  18 +--
 .../src/driver/tests/model_fallback.rs        |  19 +---
 .../src/driver/tests/output_budget.rs         |  19 +---
 .../src/driver/tests/parked_wait.rs           |  18 +--
 .../src/driver/tests/provider_outcomes.rs     |  19 +---
 .../stella-core/src/driver/tests/requery.rs   |  17 +--
 .../src/driver/tests/steer_midturn.rs         |   6 +-
 .../src/driver/tests/streaming_deadline.rs    |   4 +-
 .../src/driver/tests/unbounded_by_default.rs  |   2 +-
 .../src/driver/tests/usage_anchor.rs          |   2 +-
 .../src/driver/tests/usage_completeness.rs    |  22 ++--
 .../src/driver/tests/user_hooks.rs            |  34 +++---
 .../src/driver/tests/user_hooks/verdicts.rs   |   6 +-
 .../src/driver/tests/zero_copy_request.rs     |   4 +-
 crates/stella-core/src/goal.rs                | 106 +++++++++++++-----
 crates/stella-core/src/lib.rs                 |   5 +
 crates/stella-core/src/subagent/tests.rs      |  22 +---
 .../src/subagent/tests/failure_and_events.rs  |  18 +--
 .../src/subagent/tests/fork_scope.rs          |   6 +-
 .../stella-core/src/subagent/tests/seams.rs   |  16 +--
 .../subagent/tests/spend_and_cancellation.rs  |  10 +-
 .../src/subagent/tests/tools_and_budget.rs    |  12 +-
 .../stella-core/src/subagent/tests/witness.rs |   6 +-
 crates/stella-core/src/tests.rs               |  66 +++++++++++
 .../tests/engine_emits_no_stage.rs            |  17 +--
 .../stella-core/tests/hard_drop_write_back.rs |  26 +----
 crates/stella-core/tests/parallel_dispatch.rs |  27 +----
 crates/stella-core/tests/spend_gate.rs        |  19 +---
 crates/stella-core/tests/tool_wall_clock.rs   |  17 +--
 crates/stella-engine/Cargo.toml               |   2 +
 crates/stella-engine/src/tests.rs             |  31 ++---
 crates/stella-engine/tests/embedding.rs       |  23 +---
 crates/stella-time/README.md                  |  14 +--
 crates/stella-time/tests/one_home.rs          |  55 +++------
 ...ine-reads-time-through-the-sleeper-port.md |   6 +-
 54 files changed, 417 insertions(+), 655 deletions(-)
 create mode 100644 crates/stella-core/src/tests.rs

diff --git a/AGENTS.md b/AGENTS.md
index 7e7397736d..b519cb066a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1366,7 +1366,7 @@ the files you must plan around (see below).
 | Change the wrapper socket's **trait** — `TurnWrapper`, `admissible`, `judge`/`again`, the in-process/subprocess transports | [`stella-runtime`](crates/stella-runtime/README.md) | `src/wrapper/` (#3380, landed #3479, `doc:wrapper-socket`). Lives one layer above `stella-core` because `before_turn`/`after_turn` do I/O, which invariant 2 bans in the engine; consumes `stella-plugin`'s wire types rather than redefining them. It is reached four ways now: a `--pipeline` run, a goal round, a fleet attempt, and an auto-resolved plugin — see the crate's own README. |
 | Change how a plugin is **installed, listed, removed, or trusted** — `.stella/plugins/` and `~/.stella/plugins/` resolution, install consent, the project-tier trust gate | [`stella-cli`](crates/stella-cli/README.md) | `src/plugin_cmd.rs` + `src/plugin_cmd/{roster,process}.rs` — renders `stella_plugin::consent_text` before anything executes, gates a cloned repository's plugins on `project_code_execution_trusted()` (#3509), and is the one place `LoopGrant::permits_hook`/`permits_point` get consulted against an installed manifest. |
 | Decide whether a human is present to see/answer a mid-run prompt | [`stella-tty`](crates/stella-tty/README.md) | **A leaf with NO dependencies at all** (#3036) — one pure `human_can_answer(interactive_output, stdin_is_terminal, prompt_is_visible)`, which is what lets `stella-cli`'s approval prompts and `stella-model`'s credential prompt share one derivation without `stella-model` depending on `stella-cli` (invariant 1). |
-| Read the real clock, wait on the real timer, or stand in for either under test, behind `stella-core`'s `Sleeper` and `Clock` ports | [`stella-time`](crates/stella-time/README.md) | **Near-leaf: `stella-core` is its only workspace dependency.** `TokioSleeper` (the engine's real sleeper and `now`), `WallClock` (Unix epoch, for a stamp another process reads) and `MonotonicClock` (one origin per process, for a span compared as a number), plus `test_util`'s `PausedSleeper` and `NoopSleeper` behind the `test-util` feature. One home, because `stella-serve` may not link `stella-cli` or `stella-runtime` and `stella-core` may not carry a timer (ADR 0042). |
+| Read the real clock, wait on the real timer, or stand in for either under test, behind `stella-core`'s `Sleeper` and `Clock` ports | [`stella-time`](crates/stella-time/README.md) | **Near-leaf: `stella-core` is its only workspace dependency.** `TokioSleeper` (the engine's real sleeper and `now`), `WallClock` (Unix epoch, for a stamp another process reads) and `MonotonicClock` (one origin per process, for a span compared as a number), plus `test_util`'s `PausedSleeper` and `NoopSleeper` behind the `test-util` feature. One home, because `stella-serve` may not link `stella-cli` or `stella-runtime` and `stella-core` may not carry a timer (ADR 0042); `stella-core` tests against it through a dev-dependency cycle, the tokio / tokio-test shape. |
 | Emit a diagnostic — a record explaining *why* the program did something | [`stella-diag`](crates/stella-diag/README.md) | **A leaf: `serde` only, so anything may depend on it.** Field values cannot hold a `String`, a `Path`, or model output — that is a compile error, not a review question. Design: [`docs/spec/diagnostics.md`](docs/spec/diagnostics.md). |
 | Compute a line-oriented unified diff (`@@` hunks, git's exact shape) | [`stella-diff`](crates/stella-diff/README.md) | **A leaf with NO dependencies at all** (#1511) — pure functions over borrowed strings, which is what lets [`stella-observatory`](crates/stella-observatory/README.md) and [`stella-cli`](crates/stella-cli/README.md) share one differ without costing the observatory its isolation. |
 | Strip ANSI escape sequences from tool output | [`stella-ansi`](crates/stella-ansi/README.md) | **A leaf with NO dependencies at all** — one pure function over a borrowed `&str`, extracted from `stella-tui` so [`stella-observatory`](crates/stella-observatory/README.md) could strip a colourised tool's output before it reaches the journal route's `
` without linking `ratatui`. `stella-tui`'s `ansi` module re-exports it and keeps only the `ratatui`-shaped emission half. |
diff --git a/Cargo.lock b/Cargo.lock
index c57bb0126b..5739003343 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3284,6 +3284,7 @@ dependencies = [
  "serde_json_canonicalizer",
  "sha2 0.11.0",
  "stella-protocol",
+ "stella-time",
  "thiserror 2.0.20",
  "tokio",
 ]
@@ -3329,6 +3330,7 @@ dependencies = [
  "serde_json",
  "stella-core",
  "stella-protocol",
+ "stella-time",
  "tokio",
 ]
 
diff --git a/crates/stella-core/Cargo.toml b/crates/stella-core/Cargo.toml
index a11be42137..f47d338ff9 100644
--- a/crates/stella-core/Cargo.toml
+++ b/crates/stella-core/Cargo.toml
@@ -20,6 +20,10 @@ futures-util = { workspace = true }
 sha2 = { workspace = true }
 
 [dev-dependencies]
+# The engine's tests take the shared sleeper doubles from the crate that
+# holds the real sleeper. A dev-dependency cycle is allowed, and this is the
+# tokio / tokio-test shape.
+stella-time = { path = "../stella-time", features = ["test-util"] }
 tokio = { workspace = true, features = ["test-util"] }
 # Seeds the retry tests' jitter double. Shipping code draws nothing: the
 # entropy behind backoff jitter is a port (`retry::Sleeper::jitter`), and
diff --git a/crates/stella-core/README.md b/crates/stella-core/README.md
index 0f3f6bb2a7..778ef310c5 100644
--- a/crates/stella-core/README.md
+++ b/crates/stella-core/README.md
@@ -295,9 +295,10 @@ system message and the latest user message are never touched.
   [`src/subagent.rs`](src/subagent.rs)'s `run_sub_agent`, which constructs the
   child in-crate and carries every seam. The crate exports the `Sleeper` and
   `Clock` ports with no production implementation — `stella-time` holds
-  those, and every host passes them to `Engine::assemble`. Tests wire a
-  no-op or a paused-clock double, so a retry costs no wall clock and a
-  timeout still means what it says.
+  those, and every host passes them to `Engine::assemble`. Tests take
+  `PausedSleeper` and `NoopSleeper` from `stella_time::test_util` — the unit
+  tests from `crate::tests`, the one copy the compiler forces — so a
+  retry costs no wall clock and a timeout still means what it says.
 - **A sub-agent's steering is filtered, not inherited.** `drain_steering` is
   destructive by contract, so a child that inherited the parent's `TurnSteering`
   would swallow a message the user addressed to the parent. `ChildSteering`
@@ -332,8 +333,8 @@ turn-driver audit witnesses; also `budget_boundaries.rs`,
 [`src/loop_detect.rs`](src/loop_detect.rs); a failing case writes its seed to
 `proptest-regressions/`, and that seed is committed. No feature flag, no env var, no
 fixture server and no network — driver tests wire scripted `Provider`s, counting
-`ToolExecutor`s and sleeper doubles on a paused runtime, so the suite runs in
-seconds. Keep it that
+`ToolExecutor`s and `stella-time`'s shared sleeper doubles on a paused runtime,
+so the suite runs in seconds. Keep it that
 way: a test here that needs a file or a socket means the logic under test is in
 the wrong crate.
 
diff --git a/crates/stella-core/src/accounted_call.rs b/crates/stella-core/src/accounted_call.rs
index 0bd3355ead..6f142fb137 100644
--- a/crates/stella-core/src/accounted_call.rs
+++ b/crates/stella-core/src/accounted_call.rs
@@ -144,12 +144,12 @@ pub enum AccountedCallError {
 /// measured since the last streamed fragment, so it re-arms every time the
 /// dispatch is observed to still be producing — the same distinction
 /// `crate::step::bounded_generation` draws for the engine's own step loop.
-/// A flat wall-clock deadline here used to abandon a call the instant total
-/// elapsed time crossed the ceiling even while the provider was actively
-/// answering, which lost OpenRouter's trailing usage/cost frame (it arrives
-/// in a final SSE frame *after* the content, once the gateway has settled
-/// the routed call's price) to a ceiling sized to catch silence, not a
-/// slow-but-live generation (#1467).
+/// A flat wall-clock deadline here abandoned a call the instant total
+/// elapsed time crossed the ceiling, even while the provider was still
+/// answering. That lost OpenRouter's trailing usage/cost frame, which
+/// arrives in a final SSE frame *after* the content, once the gateway has
+/// settled the routed call's price. The ceiling was sized to catch silence,
+/// not a slow-but-live generation (#1467).
 pub async fn run_accounted_call(
     call: AccountedCall<'_>,
     budget: &mut BudgetGuard,
@@ -460,24 +460,9 @@ mod tests {
     use stella_protocol::{BudgetMode, CompletionMessage, CompletionRequestRef, CompletionUsage};
 
     use super::*;
+    use crate::tests::{NoopSleeper, PausedSleeper};
     use std::time::Instant;
 
-    struct NoopSleeper;
-
-    #[async_trait]
-    impl Sleeper for NoopSleeper {
-        async fn sleep(&self, _duration_ms: u64) {}
-
-        // The floor: a test that asserts on retry timing wants no spread in it.
-        fn now(&self) -> std::time::Instant {
-            std::time::Instant::now()
-        }
-
-        fn jitter(&self, _upper: u64) -> u64 {
-            0
-        }
-    }
-
     struct RetryThenSuccess {
         attempts: Mutex,
     }
@@ -982,27 +967,6 @@ mod tests {
         );
     }
 
-    /// A [`Sleeper`] backed by real (here, paused-virtual) tokio time so a
-    /// caller-supplied per-call timeout can expire *during* a backoff sleep.
-    struct TokioSleeper;
-
-    #[async_trait]
-    impl Sleeper for TokioSleeper {
-        async fn sleep(&self, duration_ms: u64) {
-            tokio::time::sleep(Duration::from_millis(duration_ms)).await;
-        }
-
-        // The floor: the timeout under test is placed against the exact
-        // backoff, so the draw must not move it.
-        fn now(&self) -> std::time::Instant {
-            std::time::Instant::now()
-        }
-
-        fn jitter(&self, _upper: u64) -> u64 {
-            0
-        }
-    }
-
     struct AlwaysRetryable;
 
     #[async_trait]
@@ -1048,7 +1012,7 @@ mod tests {
             },
             &mut budget,
             &EventSender::new(tx),
-            &TokioSleeper,
+            &PausedSleeper,
         )
         .await;
 
@@ -1170,7 +1134,7 @@ mod tests {
             },
             &mut budget,
             &EventSender::new(tx),
-            &TokioSleeper,
+            &PausedSleeper,
         )
         .await
         .expect("the trailing gap must not abandon a call that was actively answering");
diff --git a/crates/stella-core/src/driver/capabilities.rs b/crates/stella-core/src/driver/capabilities.rs
index 3d9b0ecc04..3c88ea2ebd 100644
--- a/crates/stella-core/src/driver/capabilities.rs
+++ b/crates/stella-core/src/driver/capabilities.rs
@@ -431,7 +431,7 @@ mod tests {
 
         let provider = crate::subagent::tests::ScriptedProvider::new(vec![]);
         let tools = crate::subagent::tests::MixedTools::default();
-        let sleeper = crate::subagent::tests::TokioSleeper;
+        let sleeper = crate::tests::PausedSleeper;
 
         let owned = built_elsewhere();
         let seams = owned.as_borrowed();
@@ -473,7 +473,7 @@ mod tests {
     fn assemble_carries_the_bare_capability_set_onto_the_engine() {
         let provider = crate::subagent::tests::ScriptedProvider::new(vec![]);
         let tools = crate::subagent::tests::MixedTools::default();
-        let sleeper = crate::subagent::tests::TokioSleeper;
+        let sleeper = crate::tests::PausedSleeper;
 
         let seams = TurnCapabilities::none();
         let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
diff --git a/crates/stella-core/src/driver/dispatch.rs b/crates/stella-core/src/driver/dispatch.rs
index 919298ac64..736a095bf0 100644
--- a/crates/stella-core/src/driver/dispatch.rs
+++ b/crates/stella-core/src/driver/dispatch.rs
@@ -348,8 +348,8 @@ mod tests {
 
     use super::super::TurnHalt;
     use crate::event_sender::EventSender;
-    use crate::retry::Sleeper;
     use crate::step::{BudgetSnapshot, CHECKPOINT_VERSION, Checkpoint, TurnState};
+    use crate::tests::NoopSleeper;
     use crate::{Engine, EngineConfig, TurnCapabilities, TurnOutcome};
     use stella_protocol::{
         AgentEvent, BudgetMode, CompletionMessage, CompletionRequestRef, CompletionResult,
@@ -449,22 +449,6 @@ mod tests {
         }
     }
 
-    #[derive(Debug)]
-    struct NoopSleeper;
-    #[async_trait]
-    impl Sleeper for NoopSleeper {
-        async fn sleep(&self, _duration_ms: u64) {}
-
-        // The floor: a test that asserts on retry timing wants no spread in it.
-        fn now(&self) -> std::time::Instant {
-            std::time::Instant::now()
-        }
-
-        fn jitter(&self, _upper: u64) -> u64 {
-            0
-        }
-    }
-
     /// Fires once the executor's flag is up — the shape of the pipeline's
     /// `FlipHalt`, which latches synchronously inside the `ToolResult` tap.
     #[derive(Debug)]
diff --git a/crates/stella-core/src/driver/drive.rs b/crates/stella-core/src/driver/drive.rs
index e86a1a1759..7cb1ba7c34 100644
--- a/crates/stella-core/src/driver/drive.rs
+++ b/crates/stella-core/src/driver/drive.rs
@@ -207,8 +207,8 @@ mod tests {
 
     use super::super::TurnHalt;
     use crate::event_sender::EventSender;
-    use crate::retry::Sleeper;
     use crate::step::{BudgetSnapshot, CHECKPOINT_VERSION, Checkpoint, TurnState};
+    use crate::tests::NoopSleeper;
     use crate::{Engine, EngineConfig, TurnCapabilities, TurnOutcome};
     use stella_protocol::{
         BudgetMode, CompletionMessage, CompletionRequestRef, CompletionResult, CompletionUsage,
@@ -278,22 +278,6 @@ mod tests {
         }
     }
 
-    #[derive(Debug)]
-    struct NoopSleeper;
-    #[async_trait]
-    impl Sleeper for NoopSleeper {
-        async fn sleep(&self, _duration_ms: u64) {}
-
-        // The floor: a test that asserts on retry timing wants no spread in it.
-        fn now(&self) -> std::time::Instant {
-            std::time::Instant::now()
-        }
-
-        fn jitter(&self, _upper: u64) -> u64 {
-            0
-        }
-    }
-
     /// Armed from the start, so the first committed boundary ends the turn.
     #[derive(Debug)]
     struct AlwaysHalt;
diff --git a/crates/stella-core/src/driver/restore.rs b/crates/stella-core/src/driver/restore.rs
index e04190db5f..09d24d8ab8 100644
--- a/crates/stella-core/src/driver/restore.rs
+++ b/crates/stella-core/src/driver/restore.rs
@@ -575,25 +575,10 @@ mod tests {
     use crate::driver::{Engine, EngineConfig};
     use crate::event_sender::EventSender;
     use crate::ports::ToolExecutor;
-    use crate::retry::Sleeper;
     use crate::step::SummarizerHealth;
+    use crate::tests::NoopSleeper;
     use stella_protocol::BudgetMode;
 
-    struct NoSleep;
-    #[async_trait]
-    impl Sleeper for NoSleep {
-        async fn sleep(&self, _duration_ms: u64) {}
-
-        // The floor: a test that asserts on retry timing wants no spread in it.
-        fn now(&self) -> std::time::Instant {
-            std::time::Instant::now()
-        }
-
-        fn jitter(&self, _upper: u64) -> u64 {
-            0
-        }
-    }
-
     /// Always answers "SUMMARY" — the summarizer path under test is the
     /// restoration that follows the splice, not the summary itself.
     struct SummaryProvider;
@@ -752,7 +737,7 @@ mod tests {
             active: vec![],
         };
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
+        let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
         let mut messages = vec![
             CompletionMessage::system("sys"),
             CompletionMessage::user("the task"),
@@ -793,7 +778,7 @@ mod tests {
             active: vec![],
         };
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
+        let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
         let mut messages = vec![
             CompletionMessage::system("sys"),
             CompletionMessage::user("the task"),
@@ -837,7 +822,7 @@ mod tests {
             active: vec![],
         };
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
+        let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
         let mut messages = vec![
             CompletionMessage::system("sys"),
             CompletionMessage::user("the task"),
@@ -883,7 +868,7 @@ mod tests {
             active: vec!["deploy".into()],
         };
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
+        let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
         let mut messages = vec![
             CompletionMessage::system("sys"),
             CompletionMessage::user("the task"),
@@ -924,7 +909,7 @@ mod tests {
             active: vec![],
         };
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
+        let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
         let mut messages = vec![
             CompletionMessage::system("sys"),
             CompletionMessage::user("the task"),
@@ -957,7 +942,7 @@ mod tests {
             active: vec![],
         };
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
+        let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
         let mut messages = vec![
             CompletionMessage::system("sys"),
             CompletionMessage::user("the task"),
@@ -1083,7 +1068,7 @@ mod tests {
             active: vec![],
         };
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
+        let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
         let mut messages = vec![
             CompletionMessage::system("sys"),
             CompletionMessage::user("the task"),
@@ -1138,7 +1123,7 @@ mod tests {
             active: vec![],
         };
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&provider, &tools, config(), &NoSleep, seams);
+        let engine = Engine::assemble(&provider, &tools, config(), &NoopSleeper, seams);
         let mut messages = vec![
             CompletionMessage::system("sys"),
             CompletionMessage::user("the task"),
diff --git a/crates/stella-core/src/driver/tests.rs b/crates/stella-core/src/driver/tests.rs
index 086053f76e..4b96489b04 100644
--- a/crates/stella-core/src/driver/tests.rs
+++ b/crates/stella-core/src/driver/tests.rs
@@ -11,30 +11,7 @@ use tokio::sync::mpsc;
 use super::{CONTINUATION_MARKER_PREFIX as NUDGE, *};
 use crate::TurnCapabilities;
 use crate::hooks::{HookAction, HookExecError, HookExecResult, HookMatcher};
-use crate::retry::Sleeper;
-
-/// A `Sleeper` on tokio's clock, which every test here runs paused: a sleep
-/// costs nothing while the runtime is idle and still lets a pending call
-/// finish first, and `now` reads the same virtual timeline. A sleeper that
-/// returned at once would make every engine timeout fire the moment a
-/// provider future waited on another task, which is not what a timeout is.
-#[derive(Default)]
-struct TokioSleeper;
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, duration_ms: u64) {
-        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
-    }
-
-    fn now(&self) -> std::time::Instant {
-        tokio::time::Instant::now().into_std()
-    }
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
+use crate::tests::PausedSleeper;
 
 /// A `ToolExecutor` that always succeeds and counts real invocations — the
 /// counter is what `retry_never_re_executes_a_tool_call` asserts against.
@@ -252,7 +229,7 @@ async fn run_speculation_turn(
     provider: &SpeculatingProvider,
     tools: &dyn ToolExecutor,
 ) -> (TurnOutcome, Vec) {
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(provider, tools, EngineConfig::default(), &sleeper, seams);
     let (tx, mut rx) = mpsc::unbounded_channel();
@@ -444,7 +421,7 @@ async fn budget_abort_after_speculation_discards_the_pool() {
         executed,
     };
 
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let (tx, mut rx) = mpsc::unbounded_channel();
@@ -557,7 +534,7 @@ async fn a_failed_attempts_speculative_pool_emits_discarded_events() {
         calls: calls.clone(),
         executed,
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![CompletionMessage::user("read a.rs")];
@@ -617,7 +594,7 @@ async fn text_deltas_precede_the_authoritative_text_and_concatenate_to_it() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let (tx, mut rx) = mpsc::unbounded_channel();
@@ -737,7 +714,7 @@ async fn a_wedged_generation_trips_the_deadline_once_instead_of_burning_the_retr
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         model_timeout: Some(Duration::from_millis(50)),
         ..EngineConfig::default()
@@ -778,7 +755,7 @@ async fn simple_turn_with_no_tool_calls_completes() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -848,7 +825,7 @@ async fn a_halt_ends_the_turn_at_the_next_step_boundary_as_completed() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         turn_halt: Some(Arc::new(AlwaysHalt)),
         ..EngineConfig::default()
@@ -896,7 +873,7 @@ async fn a_halt_that_never_fires_leaves_the_turn_exactly_as_it_was() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         turn_halt: Some(Arc::new(NeverHalt)),
         ..EngineConfig::default()
@@ -995,7 +972,7 @@ async fn steered_messages_inject_before_the_next_model_call() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let steering = TestSteering {
         queue: std::sync::Mutex::new(vec!["also check the tests".into()]),
         stop_after_drains: None,
@@ -1053,7 +1030,7 @@ async fn soft_stop_ends_the_turn_keeping_completed_steps() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     // Stop latches after the first boundary: step 0 runs fully (model
     // call + tool), step 1's boundary honors the stop.
     let steering = TestSteering {
@@ -1100,7 +1077,7 @@ async fn overflow_of_protected_content_is_summarized_and_metered() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     let mut messages = vec![
@@ -1165,7 +1142,7 @@ async fn summarization_disabled_leaves_history_untouched() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         summarize_overflow: false,
         ..overflow_config()
@@ -1205,7 +1182,7 @@ async fn summarizer_failure_is_non_fatal_and_leaves_history() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     let mut messages = vec![
@@ -1244,7 +1221,7 @@ async fn summarization_never_orphans_tool_results_at_the_span_edge() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     // The naive span end (len - keep_recent) lands ON the tool-result
@@ -1330,7 +1307,7 @@ async fn empty_completion_aborts_with_a_visible_message_not_a_silent_success() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1381,7 +1358,7 @@ async fn a_step_out_of_time_completes_with_a_truthful_partial_instead_of_abortin
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(
         &provider,
@@ -1475,7 +1452,7 @@ async fn a_length_truncated_tool_less_step_continues_the_turn_instead_of_complet
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1535,7 +1512,7 @@ async fn length_continuations_are_bounded_per_turn() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1592,7 +1569,7 @@ async fn tool_calls_execute_and_feed_back_into_history() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1645,7 +1622,7 @@ async fn retry_never_re_executes_a_tool_call() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1693,7 +1670,7 @@ async fn malformed_tool_call_input_is_repaired_not_executed_blindly() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1737,7 +1714,7 @@ async fn stuck_loop_aborts_the_turn_cleanly_before_the_step_cap() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1776,7 +1753,7 @@ async fn stuck_loop_steers_once_then_aborts_on_re_detection() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1898,7 +1875,7 @@ async fn identical_polls_with_changing_output_complete_without_abort() {
     let tools = PollingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1960,7 +1937,7 @@ async fn period_three_cycle_with_no_progress_steers_then_aborts() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         loop_detection: LoopDetectionConfig {
             exact_repeat_threshold: 3,
@@ -2014,7 +1991,7 @@ async fn enforced_budget_aborts_the_turn_cleanly_between_steps() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -2141,10 +2118,10 @@ async fn run_synthetic_survival_turn(dialect: &str, id_style: fn(u32) -> String)
         }
     }
     let tools = GrowingTools;
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         // Keep the retry backoff floor at 0 so 200 steps with injected
-        // 429s/drops still runs near-instantly under TokioSleeper.
+        // 429s/drops still runs near-instantly under PausedSleeper.
         retry_policy: RetryPolicy::new(3, 0, 0),
         // A tight-ish compaction budget so the growing tool output
         // actually forces multiple compaction passes over 200 steps.
@@ -2263,7 +2240,7 @@ async fn read_only_calls_in_one_step_execute_concurrently() {
     let tools = BarrierTools {
         barrier: tokio::sync::Barrier::new(2),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -2361,7 +2338,7 @@ async fn mutating_calls_are_barriers_and_history_keeps_call_order() {
         read1_started: tokio::sync::Notify::new(),
         read2_done: tokio::sync::Notify::new(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -2464,7 +2441,7 @@ async fn every_committed_step_emits_exactly_one_step_usage_record() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -2555,7 +2532,7 @@ async fn a_wedged_tool_trips_the_dispatch_ceiling_instead_of_hanging() {
         ]),
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         tool_timeout: Some(Duration::from_secs(900)),
         ..EngineConfig::default()
@@ -2611,7 +2588,7 @@ async fn a_none_ceiling_leaves_tool_dispatch_unbounded() {
         ]),
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         tool_timeout: None,
         ..EngineConfig::default()
@@ -2704,7 +2681,7 @@ async fn a_turn_checkpoints_at_every_step_boundary_and_clears_when_it_ends() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let sink = Arc::new(RecordingSink::default());
     let config = EngineConfig {
         checkpoint_sink: Some(sink.clone() as Arc),
@@ -2766,7 +2743,7 @@ async fn a_turn_without_a_sink_is_unchanged() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     assert!(
         EngineConfig::default().checkpoint_sink.is_none(),
         "durability is opt-in: a default engine writes no checkpoints"
diff --git a/crates/stella-core/src/driver/tests/audit_fixes.rs b/crates/stella-core/src/driver/tests/audit_fixes.rs
index 4877b53fcc..88d470e7d5 100644
--- a/crates/stella-core/src/driver/tests/audit_fixes.rs
+++ b/crates/stella-core/src/driver/tests/audit_fixes.rs
@@ -19,7 +19,7 @@ async fn summarize_keep_recent_zero_does_not_panic() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         summarize_keep_recent: 0,
         ..overflow_config()
@@ -61,7 +61,7 @@ async fn observed_budget_breach_emits_a_warning_event() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -116,7 +116,7 @@ async fn budget_abort_synthetic_results_are_visible_in_the_event_stream() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -160,7 +160,7 @@ async fn whitespace_only_completion_aborts_without_a_text_event() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -335,7 +335,7 @@ async fn hard_cancel_mid_stream_emits_a_cancelled_usage_envelope() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![CompletionMessage::user("secret prompt text")];
@@ -497,7 +497,7 @@ async fn overflow_summarizer_retries_a_transient_error() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     let mut messages = overflow_messages();
@@ -551,7 +551,7 @@ async fn budget_aborted_summary_is_applied_not_discarded() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     let mut messages = overflow_messages();
@@ -615,7 +615,7 @@ async fn overflow_summary_names_the_folded_tool_result_blocks() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
 
@@ -716,7 +716,7 @@ async fn repeated_summarizer_failures_emit_events_and_latch() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     let mut budget = BudgetGuard::new(BudgetMode::Off, None, None);
@@ -814,7 +814,7 @@ async fn observed_budget_breach_warns_once_per_axis_per_turn() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -871,7 +871,7 @@ async fn enforced_session_breach_abort_reason_names_the_axis() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -906,7 +906,7 @@ async fn enforced_session_breach_at_step_boundary_names_the_axis() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -1267,7 +1267,7 @@ async fn a_recycled_speculation_call_id_reports_the_execution_it_displaces() {
     let tools = CountingReadTools {
         executions: executions.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let (tx, mut rx) = mpsc::unbounded_channel();
@@ -1385,7 +1385,7 @@ async fn the_system_prefix_stays_byte_stable_across_a_compacting_turn() {
         prefixes: std::sync::Mutex::new(Vec::new()),
         step: AtomicU32::new(0),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         // Small enough that every step after the first compacts, and small
         // enough that the pure passes cannot get under it alone — so the
diff --git a/crates/stella-core/src/driver/tests/budget_boundaries.rs b/crates/stella-core/src/driver/tests/budget_boundaries.rs
index d5c8d354da..5b7bfafe62 100644
--- a/crates/stella-core/src/driver/tests/budget_boundaries.rs
+++ b/crates/stella-core/src/driver/tests/budget_boundaries.rs
@@ -71,7 +71,7 @@ async fn an_over_cap_budget_abort_hands_back_a_well_paired_transcript() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = transcript_with_an_unanswered_tool_call();
@@ -107,7 +107,7 @@ async fn a_past_deadline_abort_hands_back_a_well_paired_transcript() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = transcript_with_an_unanswered_tool_call();
@@ -211,7 +211,7 @@ async fn summary_induced_budget_breach_aborts_with_cost_before_next_provider_cal
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     let mut messages = vec![
@@ -263,7 +263,7 @@ async fn an_existing_budget_breach_stops_before_paid_compaction() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     let mut messages = vec![
@@ -310,7 +310,7 @@ async fn a_past_task_deadline_stops_the_turn_before_the_next_call_with_partial_w
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -363,7 +363,7 @@ async fn cancellation_after_billed_completion_before_speculation_finishes_keeps_
         provider_completed: provider_completed.clone(),
     };
     let tools = ForeverRead;
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![CompletionMessage::user("read")];
@@ -406,7 +406,7 @@ async fn a_normal_completion_charges_the_budget_exactly_once() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![CompletionMessage::user("answer")];
@@ -479,7 +479,7 @@ async fn a_slow_tool_stops_the_turn_before_the_deadline() {
     };
     let provider_calls = provider.calls.clone();
     let tools = SlowTool { took: tool_time };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
diff --git a/crates/stella-core/src/driver/tests/calibration.rs b/crates/stella-core/src/driver/tests/calibration.rs
index 034d4c71d4..43e3d99074 100644
--- a/crates/stella-core/src/driver/tests/calibration.rs
+++ b/crates/stella-core/src/driver/tests/calibration.rs
@@ -62,7 +62,7 @@ async fn calibrated_estimate_changes_the_compaction_decision() {
         let tools = CountingTools {
             calls: Arc::new(AtomicU32::new(0)),
         };
-        let sleeper = TokioSleeper;
+        let sleeper = PausedSleeper;
         let mut messages = compactable_history();
         // A budget the RAW estimate just fits under: uncalibrated, no
         // compaction can fire.
@@ -141,7 +141,7 @@ async fn each_committed_step_feeds_the_calibration_and_reports_its_estimate() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let calibration = CalibrationMap::new();
     let seams = TurnCapabilities {
         calibration: Some(&calibration),
@@ -228,7 +228,7 @@ async fn cache_write_tokens_count_toward_the_calibration_actual() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let calibration = CalibrationMap::new();
     let seams = TurnCapabilities {
         calibration: Some(&calibration),
@@ -273,7 +273,7 @@ async fn attachment_weight_is_excluded_from_the_drift_sample_estimate() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let mut messages = vec![
         CompletionMessage::system("sys"),
         CompletionMessage::user_with_attachments(
@@ -359,7 +359,7 @@ async fn a_fresh_sessions_calibration_factor_leaves_identity_within_the_turn() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     // Unseeded, exactly like a bench container's first run: warm-up happens
     // (or fails to matter) entirely inside this one turn.
     let calibration = CalibrationMap::new();
diff --git a/crates/stella-core/src/driver/tests/compute_passes.rs b/crates/stella-core/src/driver/tests/compute_passes.rs
index 007400c0b6..26c77d9a63 100644
--- a/crates/stella-core/src/driver/tests/compute_passes.rs
+++ b/crates/stella-core/src/driver/tests/compute_passes.rs
@@ -39,7 +39,7 @@ async fn a_step_walks_the_transcript_to_estimate_it_at_most_twice() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -94,7 +94,7 @@ async fn the_receipt_and_the_usage_record_report_one_estimate_per_step() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -197,7 +197,7 @@ async fn per_step_hashing_grows_with_the_turn_not_with_its_square() {
             }
         }
         let tools = EchoingTools;
-        let sleeper = TokioSleeper;
+        let sleeper = PausedSleeper;
         let seams = TurnCapabilities::none();
         let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
         let mut messages = vec![
@@ -384,7 +384,7 @@ async fn compaction_mid_turn_invalidates_the_receipt_ledgers_digest_memo() {
         calls: Arc::new(AtomicU32::new(0)),
     };
     let tools = BigOutputTools { filler: 4_000 };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         // Small enough that a couple of 4 KB results blow it, and no summarizer so
         // the rewrite comes purely from eviction/aging.
@@ -481,7 +481,7 @@ async fn a_compaction_pass_journals_the_replacement_bytes_it_wrote() {
         calls: Arc::new(AtomicU32::new(0)),
     };
     let tools = BigOutputTools { filler: 4_000 };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         compaction_budget_tokens: 800,
         summarize_overflow: false,
diff --git a/crates/stella-core/src/driver/tests/context_efficiency.rs b/crates/stella-core/src/driver/tests/context_efficiency.rs
index 4b8b2c8983..63417eec4f 100644
--- a/crates/stella-core/src/driver/tests/context_efficiency.rs
+++ b/crates/stella-core/src/driver/tests/context_efficiency.rs
@@ -59,7 +59,7 @@ async fn a_long_turn_ages_old_tool_results_far_below_the_compaction_budget() {
     let tools = BigOutputTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         compaction_budget_tokens: 20_000,
         ..EngineConfig::default()
@@ -147,7 +147,7 @@ async fn a_spent_allowance_retains_an_elided_partial_not_the_scratchpad() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -213,7 +213,7 @@ async fn a_truncated_step_with_tool_calls_retains_elided_narration() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
diff --git a/crates/stella-core/src/driver/tests/context_overflow.rs b/crates/stella-core/src/driver/tests/context_overflow.rs
index c7805aaeef..4970592f73 100644
--- a/crates/stella-core/src/driver/tests/context_overflow.rs
+++ b/crates/stella-core/src/driver/tests/context_overflow.rs
@@ -28,6 +28,7 @@ use stella_protocol::{CompletionResult, ToolSchema, UsageIncompleteReason};
 
 use super::super::*;
 use crate::TurnCapabilities;
+use crate::tests::NoopSleeper;
 
 /// Rejects the first `overflows` calls as context overflow, then completes.
 struct OverflowThenComplete {
@@ -101,22 +102,6 @@ impl ToolExecutor for NoTools {
     }
 }
 
-struct NoSleep;
-
-#[async_trait::async_trait]
-impl crate::retry::Sleeper for NoSleep {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// Completes every call — the wider-window replacement the spent ladder
 /// hands the transcript to.
 struct HealthyFallback {
@@ -186,7 +171,7 @@ async fn run_turn_with_fallback(
     resolver: Option<&dyn crate::ports::FallbackResolver>,
 ) -> (TurnOutcome, Vec) {
     let tools = NoTools;
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let seams = TurnCapabilities {
         fallback: resolver,
         ..TurnCapabilities::none()
@@ -444,7 +429,7 @@ async fn a_summarizer_request_that_overflows_drops_its_head_and_still_folds_the_
     let tools = super::CountingTools {
         calls: std::sync::Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, super::overflow_config(), &sleeper, seams);
     let mut messages = vec![
@@ -496,7 +481,7 @@ async fn head_dropping_is_bounded_when_no_span_size_is_accepted() {
     let tools = super::CountingTools {
         calls: std::sync::Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, super::overflow_config(), &sleeper, seams);
     let mut messages = vec![
diff --git a/crates/stella-core/src/driver/tests/deadline_notice.rs b/crates/stella-core/src/driver/tests/deadline_notice.rs
index ef2af88b07..9535f9c7f9 100644
--- a/crates/stella-core/src/driver/tests/deadline_notice.rs
+++ b/crates/stella-core/src/driver/tests/deadline_notice.rs
@@ -17,6 +17,7 @@ use stella_protocol::{CompletionResult, ToolSchema};
 
 use super::super::*;
 use crate::TurnCapabilities;
+use crate::tests::NoopSleeper;
 
 /// Answers immediately, recording the transcript it was handed.
 #[derive(Default)]
@@ -59,22 +60,6 @@ impl Provider for RecordingProvider {
     }
 }
 
-struct NoSleep;
-
-#[async_trait::async_trait]
-impl crate::retry::Sleeper for NoSleep {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 struct NoTools;
 
 #[async_trait::async_trait]
@@ -96,7 +81,7 @@ async fn run_turn_with_deadline(
     deadline: Option,
 ) {
     let tools = NoTools;
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(provider, &tools, EngineConfig::default(), &sleeper, seams);
     let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
diff --git a/crates/stella-core/src/driver/tests/lifecycle_bus.rs b/crates/stella-core/src/driver/tests/lifecycle_bus.rs
index b5c3c5533c..794b614ec6 100644
--- a/crates/stella-core/src/driver/tests/lifecycle_bus.rs
+++ b/crates/stella-core/src/driver/tests/lifecycle_bus.rs
@@ -17,6 +17,7 @@ use std::sync::{Arc, Mutex};
 use super::super::*;
 use crate::TurnCapabilities;
 use crate::bus::{HookBus, HookEvent, names};
+use crate::tests::NoopSleeper;
 use serde_json::Value;
 use stella_protocol::{CompletionResult, ToolSchema};
 
@@ -129,25 +130,9 @@ impl ToolExecutor for NoTools {
     }
 }
 
-struct NoSleep;
-
-#[async_trait::async_trait]
-impl crate::retry::Sleeper for NoSleep {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 async fn run_one_turn(provider: &dyn Provider, bus: &HookBus) -> TurnOutcome {
     let tools = NoTools;
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let seams = TurnCapabilities {
         bus: Some(bus),
         ..TurnCapabilities::none()
@@ -166,7 +151,7 @@ async fn run_one_turn_in_lane(
     lane: stella_protocol::TurnLane,
 ) -> TurnOutcome {
     let tools = NoTools;
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let seams = crate::driver::capabilities::TurnCapabilities {
         bus: Some(bus),
         lane: Some(lane),
@@ -337,7 +322,7 @@ async fn a_step_reports_how_it_ended() {
 #[tokio::test]
 async fn an_engine_without_a_bus_runs_identically() {
     let tools = NoTools;
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(
         &OneShotProvider,
diff --git a/crates/stella-core/src/driver/tests/live_services.rs b/crates/stella-core/src/driver/tests/live_services.rs
index 45cb2e98e1..a740719644 100644
--- a/crates/stella-core/src/driver/tests/live_services.rs
+++ b/crates/stella-core/src/driver/tests/live_services.rs
@@ -98,7 +98,7 @@ async fn run(
         script: TokioMutex::new(script.into_iter().map(Ok).collect()),
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
diff --git a/crates/stella-core/src/driver/tests/loop_abort.rs b/crates/stella-core/src/driver/tests/loop_abort.rs
index 48a3e39c48..13de49bcaa 100644
--- a/crates/stella-core/src/driver/tests/loop_abort.rs
+++ b/crates/stella-core/src/driver/tests/loop_abort.rs
@@ -124,7 +124,7 @@ async fn a_loop_that_resumes_after_a_successful_course_correction_still_aborts()
     let tools = ConstantTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     // A low cap, so a broken ladder shows up as "ground to the cap" in a
     // second.
     let config = EngineConfig {
@@ -231,7 +231,7 @@ async fn a_second_distinct_loop_earns_its_own_steer_before_the_turn_dies() {
     let tools = ConstantTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, exact_repeat_only(30), &sleeper, seams);
     let mut messages = vec![
@@ -341,7 +341,7 @@ async fn a_third_loop_is_not_steered_and_is_not_blamed_on_the_warned_one() {
     let tools = ConstantTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, exact_repeat_only(30), &sleeper, seams);
     let mut messages = vec![
@@ -442,7 +442,7 @@ async fn a_tool_answering_identically_to_every_new_argument_is_steered_then_kill
     let tools = ConstantTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         max_steps: Some(60),
         ..EngineConfig::default()
@@ -512,7 +512,7 @@ async fn a_confident_zero_never_reports_as_completed() {
     let tools = ConstantTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -565,7 +565,7 @@ async fn a_direct_zero_tool_answer_still_completes() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -602,7 +602,7 @@ async fn a_turn_that_did_mutating_work_still_completes_despite_a_bare_closing_li
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -641,7 +641,7 @@ async fn a_terminated_short_answer_after_investigation_still_completes() {
     let tools = ConstantTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -692,7 +692,7 @@ async fn a_stuck_loop_steer_names_the_loop_rung_as_its_cause() {
     let tools = ConstantTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, exact_repeat_only(30), &sleeper, seams);
     let mut messages = vec![
diff --git a/crates/stella-core/src/driver/tests/model_fallback.rs b/crates/stella-core/src/driver/tests/model_fallback.rs
index 08be2a88f1..a72f5af5d3 100644
--- a/crates/stella-core/src/driver/tests/model_fallback.rs
+++ b/crates/stella-core/src/driver/tests/model_fallback.rs
@@ -24,6 +24,7 @@ use super::super::*;
 use crate::TurnCapabilities;
 use crate::ports::{FallbackResolver, ResolvedFallback};
 use crate::retry::RetryPolicy;
+use crate::tests::NoopSleeper;
 
 /// Fails every call with the error `build` produces, counting the calls.
 struct AlwaysFailing {
@@ -165,22 +166,6 @@ impl ToolExecutor for NoTools {
     }
 }
 
-struct NoSleep;
-
-#[async_trait::async_trait]
-impl crate::retry::Sleeper for NoSleep {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// One real turn against `provider` with `resolver` attached (when given),
 /// returning the outcome and every event.
 async fn run_turn_collecting(
@@ -190,7 +175,7 @@ async fn run_turn_collecting(
     config: EngineConfig,
 ) -> (TurnOutcome, Vec) {
     let tools = NoTools;
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let seams = TurnCapabilities {
         fallback: resolver,
         ..TurnCapabilities::none()
diff --git a/crates/stella-core/src/driver/tests/output_budget.rs b/crates/stella-core/src/driver/tests/output_budget.rs
index bd8da7f5bd..36232569a3 100644
--- a/crates/stella-core/src/driver/tests/output_budget.rs
+++ b/crates/stella-core/src/driver/tests/output_budget.rs
@@ -26,6 +26,7 @@ use stella_protocol::{CompletionResult, ToolSchema};
 use super::super::*;
 use crate::TurnCapabilities;
 use crate::driver::output_budget_recovery::SessionOutputCeilings;
+use crate::tests::NoopSleeper;
 
 /// Rejects the first `refusals` calls as an unaffordable ceiling, naming
 /// `affordable` each time, then completes. Records the ceiling every attempt
@@ -153,22 +154,6 @@ impl ToolExecutor for NoTools {
     }
 }
 
-struct NoSleep;
-
-#[async_trait::async_trait]
-impl crate::retry::Sleeper for NoSleep {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// One real turn against `provider` with a configured output ceiling, and
 /// optionally the session-scoped carry a host attaches (#3307). Passing the
 /// same handle to two calls is what makes them two turns of ONE session
@@ -179,7 +164,7 @@ async fn run_turn_with_ceiling_and_carry(
     carry: Option<&std::sync::Arc>,
 ) -> TurnOutcome {
     let tools = NoTools;
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let config = EngineConfig {
         max_output_tokens: Some(ceiling),
         session_output_ceilings: carry.cloned(),
diff --git a/crates/stella-core/src/driver/tests/parked_wait.rs b/crates/stella-core/src/driver/tests/parked_wait.rs
index b28e9d2055..fc66bc48fd 100644
--- a/crates/stella-core/src/driver/tests/parked_wait.rs
+++ b/crates/stella-core/src/driver/tests/parked_wait.rs
@@ -124,7 +124,7 @@ async fn a_change_on_the_nth_probe_re_invokes_the_model_exactly_once() {
     let tools = ParkingTools::depositing(ci_wait_request(600), 3);
     let probe_calls = tools.probe_calls.clone();
     let wake_calls = tools.wake_calls.clone();
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -239,7 +239,7 @@ async fn an_unchanged_condition_wakes_once_at_the_deadline() {
     let tools = ParkingTools::depositing(ci_wait_request(20), u32::MAX);
     let probe_calls = tools.probe_calls.clone();
     let wake_calls = tools.wake_calls.clone();
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -297,7 +297,7 @@ async fn a_non_read_only_probe_is_refused_not_replayed() {
     request.probe.name = "bash".into(); // not in the read-only schema set
     let tools = ParkingTools::depositing(request, 1);
     let probe_calls = tools.probe_calls.clone();
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -361,7 +361,7 @@ async fn sustained_rate_limiting_parks_within_budget_and_recovers() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -425,7 +425,7 @@ async fn a_long_retry_after_hint_is_honored_by_parking_when_budget_allows() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -475,7 +475,7 @@ async fn a_hint_past_the_remaining_deadline_still_fails_fast_without_parking() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -534,7 +534,7 @@ async fn a_sustained_529_brownout_parks_within_budget_and_recovers() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -597,7 +597,7 @@ async fn a_sustained_transport_fault_still_exhausts_the_ladder_and_aborts() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -662,7 +662,7 @@ async fn a_soft_stop_during_a_park_ends_the_turn_as_a_deliberate_stop() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     // Ask 1 is the step boundary's, which must answer "no" or the turn ends
     // before a park exists; ask 2 is the park's first per-chunk tick.
     let steering = StopOnAsk {
diff --git a/crates/stella-core/src/driver/tests/provider_outcomes.rs b/crates/stella-core/src/driver/tests/provider_outcomes.rs
index c97570d1a1..0ada4f429b 100644
--- a/crates/stella-core/src/driver/tests/provider_outcomes.rs
+++ b/crates/stella-core/src/driver/tests/provider_outcomes.rs
@@ -15,6 +15,7 @@ use super::super::*;
 use crate::TurnCapabilities;
 use crate::ports::Clock;
 use crate::router::{CircuitBreaker, ProviderProfile, RoleTable, Router};
+use crate::tests::NoopSleeper;
 use serde_json::Value;
 use stella_protocol::{CompletionResult, ModelRef, Role, ToolSchema};
 
@@ -76,22 +77,6 @@ impl ToolExecutor for NoTools {
     }
 }
 
-struct NoSleep;
-
-#[async_trait::async_trait]
-impl crate::retry::Sleeper for NoSleep {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// Cooldown timing is irrelevant here (nothing advances), so a frozen clock
 /// keeps the breaker's open state deterministic for the whole test.
 struct FrozenClock;
@@ -121,7 +106,7 @@ fn router() -> Router {
 /// production wiring shape (`&Router`, shared, at the call site).
 async fn run_turn_feeding(provider: &dyn Provider, router: &Router) -> TurnOutcome {
     let tools = NoTools;
-    let sleeper = NoSleep;
+    let sleeper = NoopSleeper;
     let seams = TurnCapabilities {
         outcomes: Some(router),
         ..TurnCapabilities::none()
diff --git a/crates/stella-core/src/driver/tests/requery.rs b/crates/stella-core/src/driver/tests/requery.rs
index a648d3dc97..3008189d81 100644
--- a/crates/stella-core/src/driver/tests/requery.rs
+++ b/crates/stella-core/src/driver/tests/requery.rs
@@ -22,22 +22,7 @@ use crate::TurnCapabilities;
 use crate::budget::BudgetGuard;
 use crate::driver::*;
 use crate::ports::SteeringRequery;
-use crate::retry::Sleeper;
-
-struct NoopSleeper;
-#[async_trait]
-impl Sleeper for NoopSleeper {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
+use crate::tests::NoopSleeper;
 
 struct OkTools;
 #[async_trait]
diff --git a/crates/stella-core/src/driver/tests/steer_midturn.rs b/crates/stella-core/src/driver/tests/steer_midturn.rs
index 0f42d8ea51..12453ec487 100644
--- a/crates/stella-core/src/driver/tests/steer_midturn.rs
+++ b/crates/stella-core/src/driver/tests/steer_midturn.rs
@@ -62,7 +62,7 @@ async fn a_soft_stop_closes_caller_supplied_open_tool_calls() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let steering = StopNow;
     let seams = TurnCapabilities {
         steering: Some(&steering),
@@ -126,7 +126,7 @@ async fn a_steer_after_a_tool_round_keeps_every_call_paired_with_its_result() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let steering = SteerAtDrain {
         queue: std::sync::Mutex::new(vec!["also check the tests".into()]),
         fire_on_drain: 2,
@@ -199,7 +199,7 @@ async fn a_mid_tool_round_steer_leaves_a_tool_message_immediately_before_it() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let steering = SteerAtDrain {
         queue: std::sync::Mutex::new(vec!["actually, stop after this".into()]),
         fire_on_drain: 2,
diff --git a/crates/stella-core/src/driver/tests/streaming_deadline.rs b/crates/stella-core/src/driver/tests/streaming_deadline.rs
index 95e21e0518..d8d0adda4e 100644
--- a/crates/stella-core/src/driver/tests/streaming_deadline.rs
+++ b/crates/stella-core/src/driver/tests/streaming_deadline.rs
@@ -61,7 +61,7 @@ async fn a_streaming_generation_outlives_the_deadline_because_it_is_not_stalled(
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         model_timeout: Some(Duration::from_millis(50)),
         ..EngineConfig::default()
@@ -153,7 +153,7 @@ async fn a_call_only_stream_outlives_the_deadline_because_it_is_not_stalled() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         model_timeout: Some(Duration::from_millis(50)),
         ..EngineConfig::default()
diff --git a/crates/stella-core/src/driver/tests/unbounded_by_default.rs b/crates/stella-core/src/driver/tests/unbounded_by_default.rs
index 2fa0b165d8..86a27137ac 100644
--- a/crates/stella-core/src/driver/tests/unbounded_by_default.rs
+++ b/crates/stella-core/src/driver/tests/unbounded_by_default.rs
@@ -65,7 +65,7 @@ async fn run_productive_turn(steps: u32, config: EngineConfig) -> (TurnOutcome,
         script: TokioMutex::new(productive_script(steps)),
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &DistinctTools, config, &sleeper, seams);
     let mut messages = vec![
diff --git a/crates/stella-core/src/driver/tests/usage_anchor.rs b/crates/stella-core/src/driver/tests/usage_anchor.rs
index 5bed702a63..f6b0313d30 100644
--- a/crates/stella-core/src/driver/tests/usage_anchor.rs
+++ b/crates/stella-core/src/driver/tests/usage_anchor.rs
@@ -61,7 +61,7 @@ async fn compaction_fired_with_report(usage: CompletionUsage) -> bool {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let mut messages = compactable_history();
     let raw = crate::estimator::estimate_conversation_tokens(&messages);
     let config = EngineConfig {
diff --git a/crates/stella-core/src/driver/tests/usage_completeness.rs b/crates/stella-core/src/driver/tests/usage_completeness.rs
index 3bbf8f7d5b..e7152a08e2 100644
--- a/crates/stella-core/src/driver/tests/usage_completeness.rs
+++ b/crates/stella-core/src/driver/tests/usage_completeness.rs
@@ -14,7 +14,7 @@ async fn exhausted_worker_call_emits_one_content_free_incompleteness_event() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -97,7 +97,7 @@ async fn a_failed_call_names_the_model_that_made_it() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -164,7 +164,7 @@ async fn a_failed_attempts_recovered_usage_reaches_the_event_stream() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -221,7 +221,7 @@ async fn exhausted_retries_emit_typed_reasons_before_the_error() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -281,7 +281,7 @@ async fn auth_failure_on_first_attempt_reports_not_retryable() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -338,7 +338,7 @@ async fn successful_retry_keeps_the_failed_attempt_usage_incomplete() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         retry_policy: RetryPolicy::new(1, 0, 0),
         ..EngineConfig::default()
@@ -394,7 +394,7 @@ async fn step_usage_carries_the_requests_effort_and_output_ceiling() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let config = EngineConfig {
         effort: Some(stella_protocol::completion::ReasoningEffort::High),
         max_output_tokens: Some(32_000),
@@ -450,7 +450,7 @@ async fn step_usage_carries_the_requests_generation_params() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let asked = stella_protocol::completion::GenerationParams {
         top_p: Some(0.9),
         seed: Some(4_621),
@@ -520,7 +520,7 @@ async fn overflow_summarizer_emits_its_own_usage_envelope() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     let mut messages = overflow_messages();
@@ -555,7 +555,7 @@ async fn failed_overflow_summarizer_emits_content_free_incompleteness() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, overflow_config(), &sleeper, seams);
     let mut messages = overflow_messages();
@@ -613,7 +613,7 @@ async fn two_calls_at_one_step_are_separable_by_their_usage_rows() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams)
         .with_turn_instance(TURN);
diff --git a/crates/stella-core/src/driver/tests/user_hooks.rs b/crates/stella-core/src/driver/tests/user_hooks.rs
index 8271002bce..ceb30bbab3 100644
--- a/crates/stella-core/src/driver/tests/user_hooks.rs
+++ b/crates/stella-core/src/driver/tests/user_hooks.rs
@@ -54,7 +54,7 @@ async fn pre_tool_use_hook_nonzero_exit_blocks_the_tool_and_model_sees_it() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let payloads = Arc::new(TokioMutex::new(Vec::new()));
     let runner = RecordingHookRunner {
         exit_code: 1,
@@ -128,7 +128,7 @@ async fn post_tool_use_hook_runs_after_the_tool_and_never_blocks() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let payloads = Arc::new(TokioMutex::new(Vec::new()));
     // Exit 3 (non-zero) proves a *failing* PostToolUse hook is still a
     // pure observation — it can neither block nor abort the turn.
@@ -196,7 +196,7 @@ async fn non_blocking_hook_failure_surfaces_as_one_retryable_error_event() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let payloads = Arc::new(TokioMutex::new(Vec::new()));
     let runner = RecordingHookRunner {
         exit_code: 3,
@@ -270,7 +270,7 @@ async fn no_hooks_configured_leaves_the_turn_path_unchanged() {
     let tools = CountingTools {
         calls: tool_calls.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     // Built WITHOUT `with_hooks` — `hooks` stays `None`.
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
@@ -319,7 +319,7 @@ async fn run_turn_never_fires_session_start_hooks() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let payloads = Arc::new(TokioMutex::new(Vec::new()));
     let runner = RecordingHookRunner {
         exit_code: 0,
@@ -383,7 +383,7 @@ async fn a_hooked_read_fires_its_hook_once_never_for_a_dropped_speculative_attem
         calls: calls.clone(),
         executed,
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let payloads = Arc::new(TokioMutex::new(Vec::new()));
     // exit 0: non-blocking, so the tool runs and the hook is a pure
     // observation — what matters is how many times it is invoked.
@@ -493,7 +493,7 @@ async fn pre_tool_use_modify_decision_rewrites_the_input_the_tool_receives() {
         schemas: vec![bash_schema(false)],
         inputs: inputs.clone(),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let payloads = Arc::new(TokioMutex::new(Vec::new()));
     let runner = RecordingHookRunner {
         exit_code: 0,
@@ -624,7 +624,7 @@ async fn a_hook_require_approval_parks_on_the_route_and_the_answer_decides() {
         runner,
         hooks,
     } = require_approval_fixture(ask);
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let route = ScriptedRoute {
         resolution: crate::hooks::decision::ApprovalRouteResolution::Approved,
         calls: Arc::new(AtomicU32::new(0)),
@@ -722,7 +722,7 @@ async fn require_approval_without_a_route_refuses_with_the_grant_path() {
         runner,
         hooks,
     } = require_approval_fixture(ask);
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities {
         hooks: Some((&hooks, &runner)),
         ..TurnCapabilities::none()
@@ -773,7 +773,7 @@ async fn pre_tool_use_payload_carries_the_schemas_read_only_bit() {
         schemas: vec![bash_schema(true)],
         inputs: Arc::new(TokioMutex::new(Vec::new())),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let payloads = Arc::new(TokioMutex::new(Vec::new()));
     let runner = RecordingHookRunner {
         exit_code: 0,
@@ -869,7 +869,7 @@ async fn an_always_denying_stop_hook_is_consulted_to_the_bound_then_the_turn_sta
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let payloads = Arc::new(TokioMutex::new(Vec::new()));
     let runner = RecordingHookRunner {
         exit_code: 0,
@@ -977,7 +977,7 @@ async fn a_deny_then_allow_is_reconsulted_and_the_allowed_completion_stands() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let payloads = Arc::new(TokioMutex::new(Vec::new()));
     let runner = ScriptedHookRunner {
         stdouts: TokioMutex::new(vec![
@@ -1051,7 +1051,7 @@ async fn a_failing_stop_hook_never_holds_the_turn_open() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let runner = RecordingHookRunner {
         exit_code: 3,
         stdout: String::new(),
@@ -1125,7 +1125,7 @@ async fn a_stop_hooks_require_approval_resolves_both_ways() {
         let tools = CountingTools {
             calls: Arc::new(AtomicU32::new(0)),
         };
-        let sleeper = TokioSleeper;
+        let sleeper = PausedSleeper;
         let runner = RecordingHookRunner {
             exit_code: 0,
             stdout: r#"{"action":"require_approval","reason":"verification budget exhausted, continue?"}"#
@@ -1220,7 +1220,7 @@ async fn a_stop_hooks_require_approval_with_no_route_lets_the_turn_complete() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let runner = RecordingHookRunner {
         exit_code: 0,
         stdout: r#"{"action":"require_approval","reason":"ask a human"}"#.into(),
@@ -1275,7 +1275,7 @@ async fn pre_compact_hook_veto_skips_the_summarization_round() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let hook_payloads = Arc::new(TokioMutex::new(Vec::new()));
     let runner = RecordingHookRunner {
         exit_code: 0,
@@ -1346,7 +1346,7 @@ async fn pre_compact_modify_instructions_reach_the_summarizer_request() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let runner = RecordingHookRunner {
         exit_code: 0,
         stdout: r#"{"action":"modify","payload":{"instructions":"keep every file path verbatim"}}"#
diff --git a/crates/stella-core/src/driver/tests/user_hooks/verdicts.rs b/crates/stella-core/src/driver/tests/user_hooks/verdicts.rs
index 723c7927d1..11dbfcc46f 100644
--- a/crates/stella-core/src/driver/tests/user_hooks/verdicts.rs
+++ b/crates/stella-core/src/driver/tests/user_hooks/verdicts.rs
@@ -29,7 +29,7 @@ async fn a_structured_stop_denial_reaches_the_model_and_the_journal_intact() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let runner = ScriptedHookRunner {
         stdouts: TokioMutex::new(vec![
             r#"{"action":"deny","reason":"the witness is still red",
@@ -123,7 +123,7 @@ async fn a_prose_only_stop_denial_grows_no_evidence_section() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let runner = ScriptedHookRunner {
         stdouts: TokioMutex::new(vec![
             r#"{"action":"deny","reason":"the checklist is not done"}"#.into(),
@@ -209,7 +209,7 @@ async fn run_always_denying_stop_gate(allowance: Option, answers: usize) ->
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let runner = RecordingHookRunner {
         exit_code: 0,
         stdout: r#"{"action":"deny","reason":"not yet"}"#.into(),
diff --git a/crates/stella-core/src/driver/tests/zero_copy_request.rs b/crates/stella-core/src/driver/tests/zero_copy_request.rs
index 9433d58298..a1917f3352 100644
--- a/crates/stella-core/src/driver/tests/zero_copy_request.rs
+++ b/crates/stella-core/src/driver/tests/zero_copy_request.rs
@@ -65,7 +65,7 @@ async fn a_retried_attempt_re_sends_the_same_slices_it_did_the_first_time() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let (tx, _rx) = mpsc::unbounded_channel();
@@ -122,7 +122,7 @@ async fn the_adapter_serializes_off_the_callers_own_transcript() {
     let tools = CountingTools {
         calls: Arc::new(AtomicU32::new(0)),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let (tx, _rx) = mpsc::unbounded_channel();
diff --git a/crates/stella-core/src/goal.rs b/crates/stella-core/src/goal.rs
index 46815c5aa1..2ef31ff755 100644
--- a/crates/stella-core/src/goal.rs
+++ b/crates/stella-core/src/goal.rs
@@ -554,23 +554,7 @@ mod tests {
     use crate::TurnCapabilities;
     use crate::driver::EngineConfig;
     use crate::ports::ToolExecutor;
-    use crate::retry::Sleeper;
-
-    /// Sleeper that never really sleeps — goal tests run instantly.
-    struct NoSleep;
-    #[async_trait]
-    impl Sleeper for NoSleep {
-        async fn sleep(&self, _duration_ms: u64) {}
-
-        // The floor: a test that asserts on retry timing wants no spread in it.
-        fn now(&self) -> std::time::Instant {
-            std::time::Instant::now()
-        }
-
-        fn jitter(&self, _upper: u64) -> u64 {
-            0
-        }
-    }
+    use crate::tests::NoopSleeper;
 
     /// A provider that returns a fixed sequence of results, then errors.
     struct ScriptedProvider {
@@ -670,7 +654,13 @@ mod tests {
         ]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         let (tx, rx) = mpsc::unbounded_channel();
@@ -736,7 +726,13 @@ mod tests {
         ]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         let (tx, rx) = mpsc::unbounded_channel();
@@ -823,7 +819,13 @@ mod tests {
         ]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         let (tx, _rx) = mpsc::unbounded_channel();
@@ -883,7 +885,13 @@ mod tests {
         ]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         // Mirrors `build_budget_guard(Some(0.05))`: the cap is on the session
         // axis. (A per-turn axis here would reset each round and let the loop
@@ -963,7 +971,13 @@ mod tests {
         ]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         let (tx, _rx) = mpsc::unbounded_channel();
@@ -1014,7 +1028,13 @@ mod tests {
             gate: Some(&gate),
             ..TurnCapabilities::none()
         };
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         let (tx, _rx) = mpsc::unbounded_channel();
@@ -1065,7 +1085,13 @@ mod tests {
         ]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         let (tx, _rx) = mpsc::unbounded_channel();
@@ -1105,7 +1131,13 @@ mod tests {
         let verifier = ScriptedProvider::new(vec![Err(ProviderError::Auth("bad key".into()))]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         let (tx, _rx) = mpsc::unbounded_channel();
@@ -1137,7 +1169,13 @@ mod tests {
         let verifier = ScriptedProvider::new(vec![]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         let (tx, _rx) = mpsc::unbounded_channel();
@@ -1195,7 +1233,13 @@ mod tests {
         let verifier = ScriptedProvider::new(vec![]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         let (tx, _rx) = mpsc::unbounded_channel();
@@ -1230,7 +1274,13 @@ mod tests {
         ))]);
         let tools = NoTools;
         let seams = TurnCapabilities::none();
-        let engine = Engine::assemble(&worker, &tools, EngineConfig::default(), &NoSleep, seams);
+        let engine = Engine::assemble(
+            &worker,
+            &tools,
+            EngineConfig::default(),
+            &NoopSleeper,
+            seams,
+        );
         let mut messages = vec![CompletionMessage::system("sys")];
         let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
         budget.reseed_session_spend(0.75);
diff --git a/crates/stella-core/src/lib.rs b/crates/stella-core/src/lib.rs
index b61507c014..dcb3421567 100644
--- a/crates/stella-core/src/lib.rs
+++ b/crates/stella-core/src/lib.rs
@@ -27,6 +27,9 @@ pub mod ports;
 pub mod receipts;
 pub mod restore;
 pub mod retry;
+
+// The two sleeper doubles the unit tests share. The module doc says why
+// this copy of `stella-time`'s exists.
 pub mod router;
 pub mod running_task;
 pub mod shell_text;
@@ -37,6 +40,8 @@ pub mod steering;
 pub mod step;
 pub mod subagent;
 mod summarize;
+#[cfg(test)]
+pub(crate) mod tests;
 pub mod waiting;
 
 pub use budget::{BudgetGuard, BudgetOutcome};
diff --git a/crates/stella-core/src/subagent/tests.rs b/crates/stella-core/src/subagent/tests.rs
index c980411cd9..535234a587 100644
--- a/crates/stella-core/src/subagent/tests.rs
+++ b/crates/stella-core/src/subagent/tests.rs
@@ -23,30 +23,10 @@ use super::*;
 use crate::TurnCapabilities;
 use crate::budget::BudgetOutcome;
 use crate::ports::TurnGate;
-use crate::retry::Sleeper;
+pub(crate) use crate::tests::PausedSleeper;
 
 // ---- fakes -----------------------------------------------------------
 
-/// A `Sleeper` on tokio's clock, run paused by every test here: a sleep is
-/// free while the runtime is idle and still lets a pending child finish
-/// first, and `now` reads the same virtual timeline.
-pub(crate) struct TokioSleeper;
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, duration_ms: u64) {
-        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
-    }
-
-    fn now(&self) -> std::time::Instant {
-        tokio::time::Instant::now().into_std()
-    }
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// A provider that returns a fixed sequence of results, then errors.
 pub(crate) struct ScriptedProvider {
     script: Mutex>>,
diff --git a/crates/stella-core/src/subagent/tests/failure_and_events.rs b/crates/stella-core/src/subagent/tests/failure_and_events.rs
index 124ed91230..fa60e577bf 100644
--- a/crates/stella-core/src/subagent/tests/failure_and_events.rs
+++ b/crates/stella-core/src/subagent/tests/failure_and_events.rs
@@ -33,7 +33,7 @@ async fn an_aborted_child_salvages_the_last_answer_it_paid_for() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -72,7 +72,7 @@ async fn a_failed_child_never_becomes_an_error_the_parent_has_to_handle() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -106,7 +106,7 @@ async fn nesting_deeper_than_the_cap_is_refused_before_spending() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -149,7 +149,7 @@ async fn the_childs_stage_and_narration_never_reach_the_parents_stream() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -210,7 +210,7 @@ async fn a_childs_metering_records_name_the_child_that_spent_them() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -258,7 +258,7 @@ async fn the_leads_own_calls_name_no_sub_agent() {
         &provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -293,7 +293,7 @@ async fn step_usage_and_tool_activity_reach_the_parent_so_cost_rolls_up() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -358,7 +358,7 @@ async fn a_childs_tool_calls_name_the_child_that_ran_them() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -406,7 +406,7 @@ async fn the_leads_own_tool_calls_name_no_sub_agent() {
         &provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
diff --git a/crates/stella-core/src/subagent/tests/fork_scope.rs b/crates/stella-core/src/subagent/tests/fork_scope.rs
index ed35a2d248..2f12cfdcd5 100644
--- a/crates/stella-core/src/subagent/tests/fork_scope.rs
+++ b/crates/stella-core/src/subagent/tests/fork_scope.rs
@@ -15,7 +15,7 @@ use async_trait::async_trait;
 use stella_protocol::{BudgetMode, CompletionRequestRef, CompletionResult, ProviderError};
 use tokio::sync::mpsc;
 
-use super::{MixedTools, ScriptedProvider, TokioSleeper, text_result, tool_call_result};
+use super::{MixedTools, PausedSleeper, ScriptedProvider, text_result, tool_call_result};
 use crate::subagent::*;
 
 // ---- forked-skill scoping: allowed_tools + effort (#2682) ---------------
@@ -81,7 +81,7 @@ async fn a_grant_scoped_child_cannot_see_or_call_outside_its_grant() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -134,7 +134,7 @@ async fn a_forked_skills_effort_overrides_the_parents_and_absent_inherits() {
         ..EngineConfig::default()
     };
     let seams = TurnCapabilities::none();
-    let parent = Engine::assemble(&parent_provider, &tools, config, &TokioSleeper, seams);
+    let parent = Engine::assemble(&parent_provider, &tools, config, &PausedSleeper, seams);
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
     let (tx, _rx) = mpsc::unbounded_channel();
 
diff --git a/crates/stella-core/src/subagent/tests/seams.rs b/crates/stella-core/src/subagent/tests/seams.rs
index 65f9e6612d..3214a37c95 100644
--- a/crates/stella-core/src/subagent/tests/seams.rs
+++ b/crates/stella-core/src/subagent/tests/seams.rs
@@ -36,7 +36,7 @@ async fn a_child_honors_the_soft_stop_but_never_eats_the_parents_steering() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -111,7 +111,7 @@ async fn a_child_polls_the_parents_pause_gate() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -161,7 +161,7 @@ async fn owned_turn_controls_stop_a_child_without_clobbering_an_attached_gate()
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     )
     .with_turn_controls(&controls);
@@ -220,7 +220,7 @@ fn turn_controls_carrying_both_seams_give_a_child_both() {
         &provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     )
     .with_turn_controls(&both);
@@ -247,7 +247,7 @@ fn empty_turn_controls_leave_an_engine_exactly_as_it_was() {
         &provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     )
     .with_turn_controls(¬hing);
@@ -313,7 +313,7 @@ async fn the_child_claims_its_own_receipt_turn_slot() {
         ..EngineConfig::default()
     };
     let seams = TurnCapabilities::none();
-    let parent = Engine::assemble(&parent_provider, &tools, config, &TokioSleeper, seams);
+    let parent = Engine::assemble(&parent_provider, &tools, config, &PausedSleeper, seams);
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
     let (tx, mut rx) = mpsc::unbounded_channel();
 
@@ -398,7 +398,7 @@ async fn subagent_start_and_stop_hooks_fire_around_a_child_turn() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -484,7 +484,7 @@ async fn a_forked_child_stamps_the_subagent_fork_lane() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
diff --git a/crates/stella-core/src/subagent/tests/spend_and_cancellation.rs b/crates/stella-core/src/subagent/tests/spend_and_cancellation.rs
index 34be303bf3..882ab6756d 100644
--- a/crates/stella-core/src/subagent/tests/spend_and_cancellation.rs
+++ b/crates/stella-core/src/subagent/tests/spend_and_cancellation.rs
@@ -62,7 +62,7 @@ async fn tool_dispatched_child_spend_aborts_the_parent_at_the_next_step_boundary
         &provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut messages = vec![CompletionMessage::user("go")];
@@ -135,7 +135,7 @@ async fn the_drain_is_destructive_so_child_spend_is_never_charged_twice() {
         &provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut messages = vec![CompletionMessage::user("go")];
@@ -244,7 +244,7 @@ async fn a_cancelled_child_closes_its_bracket_with_committed_steps_and_cost() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -379,7 +379,7 @@ async fn a_child_is_bounded_by_the_ceiling_its_whole_run_sits_under() {
             model_timeout: None,
             ..EngineConfig::default()
         },
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -491,7 +491,7 @@ async fn a_ceiling_too_short_refuses_the_whole_spawn_loudly() {
             tool_timeout: Some(Duration::from_secs(30)),
             ..EngineConfig::default()
         },
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
diff --git a/crates/stella-core/src/subagent/tests/tools_and_budget.rs b/crates/stella-core/src/subagent/tests/tools_and_budget.rs
index daae2369a1..39e5f26e5a 100644
--- a/crates/stella-core/src/subagent/tests/tools_and_budget.rs
+++ b/crates/stella-core/src/subagent/tests/tools_and_budget.rs
@@ -15,7 +15,7 @@ async fn a_read_only_child_cannot_execute_a_mutating_tool_even_when_it_tries() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -50,7 +50,7 @@ async fn write_access_is_opt_in_per_spawn() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -82,7 +82,7 @@ async fn child_spend_settles_into_the_parent_exactly_once() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
@@ -124,7 +124,7 @@ async fn an_enforced_carve_stops_the_child_without_touching_the_parents_turn() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Enforced, None, Some(100.0));
@@ -167,7 +167,7 @@ async fn a_child_can_never_be_carved_past_the_parents_remaining_headroom() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Enforced, None, Some(1.0));
@@ -214,7 +214,7 @@ async fn an_enforced_parent_with_no_headroom_refuses_before_spending_anything()
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Enforced, None, Some(1.0));
diff --git a/crates/stella-core/src/subagent/tests/witness.rs b/crates/stella-core/src/subagent/tests/witness.rs
index 7c0703a69f..a36615fdaa 100644
--- a/crates/stella-core/src/subagent/tests/witness.rs
+++ b/crates/stella-core/src/subagent/tests/witness.rs
@@ -30,7 +30,7 @@ async fn the_parent_transcript_does_not_grow_by_the_childs_intermediate_work() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
 
@@ -139,7 +139,7 @@ async fn a_child_turn_never_writes_or_clears_the_parents_resume_point() {
         ..EngineConfig::default()
     };
     let seams = TurnCapabilities::none();
-    let parent = Engine::assemble(&parent_provider, &tools, config, &TokioSleeper, seams);
+    let parent = Engine::assemble(&parent_provider, &tools, config, &PausedSleeper, seams);
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
     let (tx, mut rx) = mpsc::unbounded_channel();
 
@@ -188,7 +188,7 @@ async fn the_report_is_clamped_to_the_spec_cap_and_says_so() {
         &parent_provider,
         &tools,
         EngineConfig::default(),
-        &TokioSleeper,
+        &PausedSleeper,
         seams,
     );
     let mut budget = BudgetGuard::new(BudgetMode::Observed, None, None);
diff --git a/crates/stella-core/src/tests.rs b/crates/stella-core/src/tests.rs
new file mode 100644
index 0000000000..a80e9f9d29
--- /dev/null
+++ b/crates/stella-core/src/tests.rs
@@ -0,0 +1,66 @@
+//! The two [`Sleeper`] doubles this crate's own unit tests share.
+//!
+//! They are the same two `stella-time` ships behind its `test-util`
+//! feature. Every other crate takes them from there. This crate's unit
+//! tests cannot. A lib's unit tests are a second build of the lib. A
+//! dev-dependency that links the lib links the first build, so its
+//! `impl Sleeper` is for a trait these tests do not see. The integration
+//! tests under `tests/` are separate crates and use `stella-time`'s.
+//!
+//! So this is the one copy the compiler forces, and `stella-time`'s
+//! `one_home` test names it as such.
+
+use std::time::Instant;
+
+use async_trait::async_trait;
+
+use crate::retry::Sleeper;
+
+/// A [`Sleeper`] on tokio's clock, for a test that runs its runtime paused.
+///
+/// A sleep costs nothing while the runtime is idle. It still lets a pending
+/// call finish first, and `now` reads the same virtual timeline. That is
+/// what a timeout is. So this is the double for any test that arms a tool
+/// or model timeout. A sleeper that returned at once would fire every
+/// engine timeout as soon as a provider future waited on another task.
+///
+/// Jitter is zero: a test that asserts on retry timing wants no spread.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub(crate) struct PausedSleeper;
+
+#[async_trait]
+impl Sleeper for PausedSleeper {
+    async fn sleep(&self, duration_ms: u64) {
+        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
+    }
+
+    fn now(&self) -> Instant {
+        tokio::time::Instant::now().into_std()
+    }
+
+    fn jitter(&self, _upper: u64) -> u64 {
+        0
+    }
+}
+
+/// A [`Sleeper`] that never waits.
+///
+/// For a test that arms no timeout and wants a retry ladder to run at once.
+/// `now` reads the real monotonic clock, so a deadline set from it still
+/// means what it says. Do not drive a timed turn with it: see
+/// [`PausedSleeper`] for why.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub(crate) struct NoopSleeper;
+
+#[async_trait]
+impl Sleeper for NoopSleeper {
+    async fn sleep(&self, _duration_ms: u64) {}
+
+    fn now(&self) -> Instant {
+        Instant::now()
+    }
+
+    fn jitter(&self, _upper: u64) -> u64 {
+        0
+    }
+}
diff --git a/crates/stella-core/tests/engine_emits_no_stage.rs b/crates/stella-core/tests/engine_emits_no_stage.rs
index 519c75f926..35233c148f 100644
--- a/crates/stella-core/tests/engine_emits_no_stage.rs
+++ b/crates/stella-core/tests/engine_emits_no_stage.rs
@@ -23,27 +23,12 @@ use serde_json::Value;
 use stella_core::budget::BudgetGuard;
 use stella_core::event_sender::EventSender;
 use stella_core::ports::ToolExecutor;
-use stella_core::retry::Sleeper;
 use stella_core::{Engine, EngineConfig, TurnCapabilities};
 use stella_protocol::{
     AgentEvent, BudgetMode, CompletionMessage, CompletionRequestRef, CompletionResult,
     CompletionUsage, Provider, ProviderError, ToolOutput, ToolSchema,
 };
-
-struct NoopSleeper;
-#[async_trait]
-impl Sleeper for NoopSleeper {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
+use stella_time::test_util::NoopSleeper;
 
 /// Answers once, with text and no tool calls, so the turn completes in one
 /// step — the shortest turn that still passes through the whole framing.
diff --git a/crates/stella-core/tests/hard_drop_write_back.rs b/crates/stella-core/tests/hard_drop_write_back.rs
index a545d371e3..ddf28db08a 100644
--- a/crates/stella-core/tests/hard_drop_write_back.rs
+++ b/crates/stella-core/tests/hard_drop_write_back.rs
@@ -22,34 +22,12 @@ use serde_json::Value;
 use stella_core::budget::BudgetGuard;
 use stella_core::event_sender::EventSender;
 use stella_core::ports::ToolExecutor;
-use stella_core::retry::Sleeper;
 use stella_core::{Engine, EngineConfig, TurnCapabilities};
 use stella_protocol::{
     BudgetMode, CompletionMessage, CompletionRequestRef, CompletionResult, CompletionUsage,
     MessageRole, Provider, ProviderError, ToolCall, ToolOutput, ToolSchema,
 };
-
-/// A `Sleeper` on tokio's clock. The tool below hangs for an hour, and the
-/// engine's tool timeout is that same clock racing it: a sleeper that
-/// returned at once would time the tool out on its first poll and the turn
-/// this test drops mid-tool would already be over.
-struct TokioSleeper;
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, duration_ms: u64) {
-        tokio::time::sleep(Duration::from_millis(duration_ms)).await;
-    }
-
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
+use stella_time::test_util::PausedSleeper;
 
 /// Always answers with the same single tool call, so the turn reaches tool
 /// dispatch and parks there.
@@ -108,7 +86,7 @@ impl ToolExecutor for WedgedTool {
 async fn dropping_a_turn_mid_tool_still_leaves_the_partial_history_with_the_caller() {
     let provider = AlwaysCallsATool;
     let tools = WedgedTool;
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
 
diff --git a/crates/stella-core/tests/parallel_dispatch.rs b/crates/stella-core/tests/parallel_dispatch.rs
index 73b2052064..bdc081f6c5 100644
--- a/crates/stella-core/tests/parallel_dispatch.rs
+++ b/crates/stella-core/tests/parallel_dispatch.rs
@@ -25,35 +25,14 @@ use async_trait::async_trait;
 use serde_json::Value;
 use stella_core::budget::BudgetGuard;
 use stella_core::ports::ToolExecutor;
-use stella_core::retry::Sleeper;
 use stella_core::{Engine, EngineConfig, TurnCapabilities, TurnOutcome};
 use stella_protocol::{
     BudgetMode, CompletionMessage, CompletionRequestRef, CompletionResult, CompletionUsage,
     Provider, ProviderError, ToolCall, ToolOutput, ToolSchema,
 };
+use stella_time::test_util::PausedSleeper;
 use tokio::sync::mpsc;
 
-/// A `Sleeper` on tokio's clock. The engine's tool timeout is this sleep racing
-/// the dispatch, and the barrier test below needs a parked tool to stay
-/// parked: a sleeper that returned at once would time it out on the first
-/// poll and hand the test a completion it must not see.
-struct TokioSleeper;
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, duration_ms: u64) {
-        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
-    }
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// First call: one step carrying two sibling `delegate` calls. Second call: done.
 struct TwoSpawnsThenDone {
     calls: std::sync::atomic::AtomicU32,
@@ -140,7 +119,7 @@ async fn sibling_delegate_calls_in_one_step_execute_concurrently() {
     let tools = BarrierSpawns {
         barrier: tokio::sync::Barrier::new(2),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
@@ -251,7 +230,7 @@ async fn a_mutating_call_between_spawns_keeps_its_barrier() {
     let tools = BarrierSpawnsAndEdit {
         barrier: tokio::sync::Barrier::new(2),
     };
-    let sleeper = TokioSleeper;
+    let sleeper = PausedSleeper;
     let seams = TurnCapabilities::none();
     let engine = Engine::assemble(&provider, &tools, EngineConfig::default(), &sleeper, seams);
     let mut messages = vec![
diff --git a/crates/stella-core/tests/spend_gate.rs b/crates/stella-core/tests/spend_gate.rs
index afe3dd7178..c743be30ae 100644
--- a/crates/stella-core/tests/spend_gate.rs
+++ b/crates/stella-core/tests/spend_gate.rs
@@ -33,12 +33,13 @@ use stella_core::hooks::{
     HookAction, HookExecError, HookExecResult, HookMatcher, HookRunner, Hooks,
 };
 use stella_core::ports::{FallbackResolver, ResolvedFallback, ToolExecutor};
-use stella_core::retry::{ParkPlan, Sleeper, plan_park};
+use stella_core::retry::{ParkPlan, plan_park};
 use stella_core::{Engine, EngineConfig, TurnCapabilities, TurnOutcome};
 use stella_protocol::{
     BudgetMode, CompletionMessage, CompletionRequestRef, CompletionResult, CompletionUsage,
     Provider, ProviderError, ToolCall, ToolOutput, ToolSchema,
 };
+use stella_time::test_util::NoopSleeper;
 use tokio::sync::Mutex as TokioMutex;
 use tokio::sync::mpsc;
 
@@ -58,22 +59,6 @@ struct Spend {
     cost_usd: f64,
 }
 
-/// A `Sleeper` that never waits, so a retry ladder costs no test time.
-struct NoopSleeper;
-#[async_trait]
-impl Sleeper for NoopSleeper {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// A scripted `Provider`: one entry per call, repeating the last entry once
 /// the script runs out. Counts every attempt it is handed.
 ///
diff --git a/crates/stella-core/tests/tool_wall_clock.rs b/crates/stella-core/tests/tool_wall_clock.rs
index fa5a79996d..642e737762 100644
--- a/crates/stella-core/tests/tool_wall_clock.rs
+++ b/crates/stella-core/tests/tool_wall_clock.rs
@@ -26,29 +26,14 @@ use async_trait::async_trait;
 use serde_json::Value;
 use stella_core::budget::BudgetGuard;
 use stella_core::ports::ToolExecutor;
-use stella_core::retry::Sleeper;
 use stella_core::{Engine, EngineConfig, TurnCapabilities, TurnOutcome};
 use stella_protocol::{
     AgentEvent, BudgetMode, CompletionMessage, CompletionRequestRef, CompletionResult,
     CompletionUsage, Provider, ProviderError, ToolCall, ToolOutput, ToolSchema,
 };
+use stella_time::test_util::NoopSleeper;
 use tokio::sync::mpsc;
 
-struct NoopSleeper;
-#[async_trait]
-impl Sleeper for NoopSleeper {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// First call: one long shell command. Second call: an answer. The turn only
 /// gets there because a refusal is a result it can go on from.
 struct OneShellCallThenDone {
diff --git a/crates/stella-engine/Cargo.toml b/crates/stella-engine/Cargo.toml
index 9fd69f757b..d096a32210 100644
--- a/crates/stella-engine/Cargo.toml
+++ b/crates/stella-engine/Cargo.toml
@@ -18,5 +18,7 @@ serde_json = { workspace = true }
 tokio = { workspace = true, features = ["sync"] }
 
 [dev-dependencies]
+# The shared sleeper doubles (`NoopSleeper`, `PausedSleeper`).
+stella-time = { path = "../stella-time", features = ["test-util"] }
 tokio = { workspace = true, features = ["macros", "test-util"] }
 async-trait = { workspace = true }
diff --git a/crates/stella-engine/src/tests.rs b/crates/stella-engine/src/tests.rs
index 633fa65212..a45c533e33 100644
--- a/crates/stella-engine/src/tests.rs
+++ b/crates/stella-engine/src/tests.rs
@@ -10,6 +10,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
 
 use async_trait::async_trait;
 use serde_json::Value;
+use stella_time::test_util::NoopSleeper;
 use tokio::sync::mpsc;
 
 // Everything through `crate::`, including the three completion types that used
@@ -19,26 +20,10 @@ use tokio::sync::mpsc;
 use crate::{
     AgentEvent, BudgetGuard, BudgetMode, CANCELLED_REASON, CancelToken, CompletionMessage,
     CompletionRequestRef, CompletionResult, CompletionUsage, Engine, EngineConfig, EventSender,
-    MessageRole, Provider, ProviderError, Sleeper, StepOutcome, ToolCall, ToolExecutor, ToolOutput,
+    MessageRole, Provider, ProviderError, StepOutcome, ToolCall, ToolExecutor, ToolOutput,
     ToolSchema, TurnCapabilities, TurnOutcome,
 };
 
-/// A `Sleeper` that records but never actually waits.
-struct NoopSleeper;
-#[async_trait]
-impl Sleeper for NoopSleeper {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// A scripted provider: one response per call, in order, looping the last
 /// entry once exhausted so a runaway loop fails loudly rather than panicking.
 struct ScriptedProvider {
@@ -171,9 +156,9 @@ fn event_type(event: &AgentEvent) -> String {
         .unwrap_or_default()
 }
 
-/// Context receipts are the one thing a resumed turn re-emits, because the
-/// block registry is a per-turn memo a `Checkpoint` does not
-/// carry (see `stella_core::step`). Compared separately, never mixed in.
+/// Context receipts are the one thing a resumed turn re-emits. The block
+/// registry is a per-turn memo, and a `Checkpoint` does not carry it (see
+/// `stella_core::step`). They are compared on their own, never mixed in.
 fn is_receipt(event: &AgentEvent) -> bool {
     matches!(
         event_type(event).as_str(),
@@ -432,9 +417,9 @@ async fn a_turn_resumed_from_a_checkpoint_emits_the_same_downstream_events() {
         "the downstream event stream must be indistinguishable from the un-interrupted run"
     );
 
-    // The one documented difference, asserted rather than assumed: the block
-    // registry is a per-turn memo the checkpoint drops, so a resumed turn
-    // re-registers the blocks the reference run had already seen.
+    // The one documented difference, asserted here. The block registry is a
+    // per-turn memo the checkpoint drops, so a resumed turn re-registers the
+    // blocks the reference run had already seen.
     let reference_receipts = reference[1].iter().filter(|e| is_receipt(e)).count();
     let resumed_receipts = resumed_events.iter().filter(|e| is_receipt(e)).count();
     assert!(
diff --git a/crates/stella-engine/tests/embedding.rs b/crates/stella-engine/tests/embedding.rs
index be577efde7..9315bcbe03 100644
--- a/crates/stella-engine/tests/embedding.rs
+++ b/crates/stella-engine/tests/embedding.rs
@@ -41,14 +41,15 @@ use std::sync::Arc;
 use std::sync::Mutex;
 
 use async_trait::async_trait;
+use stella_time::test_util::NoopSleeper;
 use tokio::sync::mpsc;
 
 use stella_engine::{
     AbortKind, AgentEvent, BudgetGuard, BudgetMode, CheckpointSink, CompletionMessage,
     CompletionRequestRef, CompletionResult, CompletionUsage, DispatchAdmission, DispatchGate,
-    Engine, EngineConfig, LiveService, Provider, ProviderError, RECALL_MARKER, Sleeper,
-    SteeringRequery, ToolCall, ToolContract, ToolExecutor, ToolOutput, ToolSchema,
-    TurnCapabilities, TurnOutcome, TurnSignal, WaitCall, WaitRequest, admit_dispatch,
+    Engine, EngineConfig, LiveService, Provider, ProviderError, RECALL_MARKER, SteeringRequery,
+    ToolCall, ToolContract, ToolExecutor, ToolOutput, ToolSchema, TurnCapabilities, TurnOutcome,
+    TurnSignal, WaitCall, WaitRequest, admit_dispatch,
 };
 
 /// The one tool the host advertises. Its only job is to make the model's first
@@ -304,22 +305,6 @@ async fn a_host_can_wrap_its_tool_surface_without_dropping_what_the_port_forward
     );
 }
 
-struct NoopSleeper;
-
-#[async_trait]
-impl Sleeper for NoopSleeper {
-    async fn sleep(&self, _duration_ms: u64) {}
-
-    // The floor: a test that asserts on retry timing wants no spread in it.
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, _upper: u64) -> u64 {
-        0
-    }
-}
-
 /// What one consult of the re-query port was shown, owned so the port can keep
 /// it past the borrowed [`TurnSignal`]'s lifetime.
 #[derive(Debug, Clone)]
diff --git a/crates/stella-time/README.md b/crates/stella-time/README.md
index f7430272fa..55e5c50e91 100644
--- a/crates/stella-time/README.md
+++ b/crates/stella-time/README.md
@@ -55,13 +55,12 @@ its own copy, and about thirty test files each kept a double.
 
 `test_util` lives here and not in `stella-core` for the same reason: a
 faithful sleeper double needs the Tokio timer, and `stella-core` may not
-link it. A crate that tests the engine takes this crate as a dev-dependency
-with the feature on; for `stella-core`'s integration tests that is a
-cycle, which cargo allows. That is the tokio / tokio-test shape.
-`stella-core`'s own unit tests are the one place that cannot: a lib's unit
-tests are a second build of the lib, and a dev-dependency that links the
-lib implements the trait for the first. They keep one copy, and
-`tests/one_home.rs` names it and fails on any other.
+link it. `stella-core`'s integration tests take this crate as a
+dev-dependency, which cargo allows in a cycle. That is the tokio /
+tokio-test shape. `stella-core`'s own unit tests keep one copy,
+`tests`, because a lib's unit tests are a second build of the lib
+and a dev-dependency that links the lib implements the trait for the
+first. `tests/one_home.rs` names that copy and fails on any other.
 
 ## God files — do not add lines
 
@@ -78,3 +77,4 @@ it crosses.
   extension bus from `WallClock`.
 - `stella-runtime` stamps wrapper verdicts from `WallClock`.
 - `stella-fleet` paces its CI monitor with `TokioSleeper`.
+- `stella-core` and `stella-engine` test against `test_util`.
diff --git a/crates/stella-time/tests/one_home.rs b/crates/stella-time/tests/one_home.rs
index eb629523d2..32700f0da0 100644
--- a/crates/stella-time/tests/one_home.rs
+++ b/crates/stella-time/tests/one_home.rs
@@ -5,21 +5,22 @@
 //! doubles live here and nowhere else.
 //!
 //! Three crates each kept a wall clock. Two kept a Tokio sleeper. One kept a
-//! sleeper trait of its own. This test reads the shipping tree and fails on
-//! the first copy that comes back. It reads source, which a test of "where
-//! does this live" has to do. `stella-core` would refuse that read. This
-//! crate does not.
-//!
-//! Test files are skipped here: the per-file test doubles are the next
-//! sweep, and it widens this scan to them.
+//! sleeper trait of its own. About thirty test files each wrote the same
+//! no-op double. This test reads the tree and fails on the first copy that
+//! comes back. It reads source, which a test of "where does this live" has
+//! to do. `stella-core` would refuse that read. This crate does not.
 
 use std::fs;
 use std::path::{Path, PathBuf};
 
-/// A `Sleeper` impl in shipping source outside this crate that stays on
-/// purpose, with the reason. Each one is a test double with a shape a shared
-/// double cannot have, kept in the file its one test lives in.
+/// A `Sleeper` impl outside this crate that stays on purpose, with the
+/// reason. Each one has a shape a shared double cannot have.
 const SLEEPER_IMPLS_KEPT: &[(&str, &str)] = &[
+    (
+        "crates/stella-core/src/tests.rs",
+        "the copy the compiler forces: a lib's unit tests are a second build of the lib, and a \
+         dev-dependency that links the lib implements Sleeper for the first",
+    ),
     (
         "crates/stella-core/src/retry.rs",
         "the retry tests' recording sleeper: it logs every requested delay and draws a seeded jitter",
@@ -29,24 +30,12 @@ const SLEEPER_IMPLS_KEPT: &[(&str, &str)] = &[
         "the monitor tests' advancing sleeper: a sleep moves the injected clock instead of waiting",
     ),
     (
-        "crates/stella-core/src/accounted_call.rs",
-        "two doubles beside the accounted-call tests; the test-double sweep retires them",
-    ),
-    (
-        "crates/stella-core/src/driver/dispatch.rs",
-        "a no-op double beside the dispatch tests; the test-double sweep retires it",
-    ),
-    (
-        "crates/stella-core/src/driver/drive.rs",
-        "a no-op double beside the drive tests; the test-double sweep retires it",
+        "crates/stella-core/src/driver/tests/audit_fixes.rs",
+        "the hanging sleeper: it announces its first sleep and then parks forever, for a test that drops a turn mid-backoff",
     ),
     (
-        "crates/stella-core/src/driver/restore.rs",
-        "a no-op double beside the restore tests; the test-double sweep retires it",
-    ),
-    (
-        "crates/stella-core/src/goal.rs",
-        "a no-op double beside the goal tests; the test-double sweep retires it",
+        "crates/stella-core/src/step/tests.rs",
+        "a sleeper on unpaused tokio time: the bound tests race a trickling call against a sleep that has to take time",
     ),
 ];
 
@@ -74,28 +63,20 @@ fn rust_sources(root: &Path) -> Vec<(String, String)> {
         if dir.file_name().is_some_and(|name| name == "stella-time") {
             continue;
         }
-        collect(&dir.join("src"), root, &mut out);
+        for sub in ["src", "tests"] {
+            collect(&dir.join(sub), root, &mut out);
+        }
     }
     out.sort();
     out
 }
 
-/// A test file, by the names `make core-no-io` uses: a `tests/` directory
-/// or a file called `tests.rs`.
-fn is_test_path(path: &Path) -> bool {
-    path.file_name().is_some_and(|name| name == "tests.rs")
-        || path.components().any(|part| part.as_os_str() == "tests")
-}
-
 fn collect(dir: &Path, root: &Path, out: &mut Vec<(String, String)>) {
     let Ok(entries) = fs::read_dir(dir) else {
         return;
     };
     for entry in entries {
         let path = entry.expect("a directory entry").path();
-        if is_test_path(&path) {
-            continue;
-        }
         if path.is_dir() {
             collect(&path, root, out);
         } else if path.extension().is_some_and(|ext| ext == "rs") {
diff --git a/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md b/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md
index c1aa003266..dcbfd14c3a 100644
--- a/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md
+++ b/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md
@@ -82,9 +82,9 @@ test that arms none. They live beside the real sources and not in
 `stella-core` may not link. `stella-core`'s integration tests and every
 other crate take it as a dev-dependency, which cargo allows in a cycle.
 That is the tokio / tokio-test shape. `stella-core`'s own unit tests keep
-one copy in `src/tests.rs`, because a lib's unit tests are a second build
-of the lib and a dev-dependency that links the lib implements the trait
-for the first. The compiler forces that copy, and the witness names it.
+one copy, `tests`, because a lib's unit tests are a second build of
+the lib and a dev-dependency that links the lib implements the trait for
+the first. The compiler forces that copy, and the witness names it.
 
 ## Consequences