Skip to content

Commit 99cd9fe

Browse files
committed
fix: avoid stale skill route stop loops
1 parent 26cbe06 commit 99cd9fe

7 files changed

Lines changed: 114 additions & 13 deletions

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
"status": "PASS",
1414
"testedVersion": "codex-cli 0.145.0",
1515
"protocol": "MCP 2024-11-05",
16-
"runtimeContractDigest": "e1f1f244e0fc7cb6bb789abc18f11fe4e0e762ff611488de341277866b2142bb",
17-
"hostAdapterDigest": "b2693a7aad1643fd7d21be90b7f73ba42b6c79f1eac405d3919bbb28a316ad20",
18-
"evidenceDigest": "203c109eb4b9208035bc67c08ca632896428de795752c150773daa4f959b2b86",
19-
"evidenceRef": "E:/Worker/.devcodex/devcodex/.audit-state/skill-route-s15-codex-20260729-full-closure-production.json",
16+
"runtimeContractDigest": "4c225c1110a7a24d483c80870aca8b58c7a1d0f8864bf1b9d3a5937277e73065",
17+
"hostAdapterDigest": "10710bef81d425e6df9c09a5c059c301aff85dfb701599337698ed105fca6172",
18+
"evidenceDigest": "77a911f991b53769cdd4fb527292c74c9e7076694106e51534847abafc4e3a9f",
19+
"evidenceRef": "E:/Worker/.devcodex/devcodex/.audit-state/skill-route-s15-codex-20260729-stale-stop-fix-production-r3.json",
2020
"entrySurface": "codex exec --ephemeral",
2121
"bootstrapDelivery": "user-global hooks.json UserPromptSubmit",
2222
"defaultEligible": true

‎hooks/_runtime/lifecycle.cjs‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,7 +1722,8 @@ async function main() {
17221722
if (eventName === 'Stop' && state.contextAcquisition?.contextEpoch) {
17231723
try {
17241724
const {
1725-
evaluateProgressiveSkillRouteStop
1725+
evaluateProgressiveSkillRouteStop,
1726+
shouldEnforceProgressiveSkillRouteStop
17261727
} = require('./skill-route-tool.cjs')
17271728
const routeStop = evaluateProgressiveSkillRouteStop({
17281729
project: state.contextAcquisition.project,
@@ -1736,9 +1737,10 @@ async function main() {
17361737
const explicitRoutePending =
17371738
state.progressiveSkillRoute?.bootstrap?.explicitStatus === 'ready'
17381739
if (
1739-
routeStop.present &&
1740-
!routeStop.complete &&
1741-
(routeStop.errorCode !== 'PLAN_NOT_COMMITTED' || explicitRoutePending)
1740+
shouldEnforceProgressiveSkillRouteStop(
1741+
routeStop,
1742+
explicitRoutePending
1743+
)
17421744
) {
17431745
const enforceCount = Number(state.progressiveSkillRouteStopCount || 0)
17441746
if (enforceCount < 2) {

‎hooks/_runtime/skill-route-tool.cjs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,25 @@ function evaluateProgressiveSkillRouteStop (input, options = {}) {
11721172
}
11731173
}
11741174

1175+
const NON_RECOVERABLE_ROUTE_IDENTITY_ERRORS = new Set([
1176+
'MODE_CAPABILITY_STALE',
1177+
'RUNTIME_CONTRACT_STALE'
1178+
])
1179+
1180+
function shouldEnforceProgressiveSkillRouteStop (routeStop, explicitRoutePending) {
1181+
if (!routeStop?.present || routeStop.complete) return false
1182+
if (routeStop.errorCode === 'PLAN_NOT_COMMITTED') {
1183+
return explicitRoutePending === true
1184+
}
1185+
if (NON_RECOVERABLE_ROUTE_IDENTITY_ERRORS.has(routeStop.errorCode)) {
1186+
// A runtime/capability refresh invalidates the frozen turn identity. A
1187+
// non-explicit route has no body obligation, so fail closed without
1188+
// trapping Stop in a continuation that cannot repair the old envelope.
1189+
return explicitRoutePending === true
1190+
}
1191+
return true
1192+
}
1193+
11751194
function handleSkillRoute (input, options = {}) {
11761195
const shapeError = validateRequestShape(input)
11771196
if (shapeError) return makeToolError(input?.op || 'unknown', shapeError)
@@ -1266,6 +1285,7 @@ module.exports = {
12661285
finalizeResponse,
12671286
handleSkillRoute,
12681287
evaluateProgressiveSkillRouteStop,
1288+
shouldEnforceProgressiveSkillRouteStop,
12691289
formatSkillRouteBootstrapInjection,
12701290
bootstrapSkillRouteForTurn,
12711291
summarizePlan,

‎scripts/fixtures/skill-route-closure-trace.v1.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"S10": ["C06"],
3535
"S11": ["P02"],
3636
"S12": ["C07", "P10", "M05"],
37-
"S13": ["M02", "M05"],
37+
"S13": ["M02", "M05", "M08a"],
3838
"S14": ["M01", "M03"],
3939
"S15": ["M05"],
4040
"S16": ["C01", "C03"],
@@ -90,6 +90,7 @@
9090
"M06": {"owner": "scripts/test-global-install-smoke.js", "anchor": "real global install"},
9191
"M07": {"owner": "README.md", "anchor": "global-adapters apply"},
9292
"M08": {"owner": "scripts/test-skill-route-state.js", "anchor": "MODE_CAPABILITY_STALE"},
93+
"M08a": {"owner": "scripts/test-skill-route-lifecycle.js", "anchor": "MODE_CAPABILITY_STALE"},
9394
"M09": {"owner": "scripts/test-global-install-smoke.js", "anchor": "host-native .agents/skills root"},
9495
"M10": {"owner": "scripts/test-skill-route-state.js", "anchor": "Acceptance T01a / T07 / M10"}
9596
}

‎scripts/test-skill-route-closure.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const expectedTests = [
5757
'M02',
5858
'M02a',
5959
...range('M0', 3, 9),
60+
'M08a',
6061
'M10'
6162
]
6263

‎scripts/test-skill-route-lifecycle.js‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const { spawnSync } = require('child_process')
99
const {
1010
createSkillRouteFixture
1111
} = require('./lib/skill-route-test-fixture')
12+
const {
13+
shouldEnforceProgressiveSkillRouteStop
14+
} = require('../hooks/_runtime/skill-route-tool.cjs')
1215

1316
const RUNTIME = path.resolve(__dirname, '..', 'hooks', '_runtime', 'lifecycle.cjs')
1417

@@ -106,6 +109,64 @@ function runLifecycle (fixture, payload = {}, env = {}) {
106109
childBefore.contextAcquisition.contextEpoch
107110
)
108111
assert.strictEqual(childAfter.progressiveSkillRouteStopCount || 0, 0)
112+
113+
assert.strictEqual(
114+
shouldEnforceProgressiveSkillRouteStop({
115+
present: true,
116+
complete: false,
117+
errorCode: 'MODE_CAPABILITY_STALE'
118+
}, false),
119+
false
120+
)
121+
assert.strictEqual(
122+
shouldEnforceProgressiveSkillRouteStop({
123+
present: true,
124+
complete: false,
125+
errorCode: 'MODE_CAPABILITY_STALE'
126+
}, true),
127+
true
128+
)
129+
130+
const staleSession = 'non-explicit-runtime-refresh'
131+
runLifecycle(fixture, {
132+
session_id: staleSession,
133+
prompt: 'Summarize the current implementation status'
134+
})
135+
const alternateSkillsRoot = path.join(fixture.root, 'alternate-skills')
136+
fs.mkdirSync(path.join(alternateSkillsRoot, '_schemas'), { recursive: true })
137+
fs.copyFileSync(
138+
path.join(fixture.packageRoot, 'skills', 'portfolio.json'),
139+
path.join(alternateSkillsRoot, 'portfolio.json')
140+
)
141+
for (const schema of [
142+
'skill-intent.v1.schema.json',
143+
'workflow-root-registry.v1.schema.json',
144+
'progressive-skill-route.v1.schema.json'
145+
]) {
146+
fs.copyFileSync(
147+
path.join(fixture.packageRoot, 'skills', '_schemas', schema),
148+
path.join(alternateSkillsRoot, '_schemas', schema)
149+
)
150+
}
151+
fs.appendFileSync(
152+
path.join(alternateSkillsRoot, '_schemas', 'skill-intent.v1.schema.json'),
153+
'\n',
154+
'utf8'
155+
)
156+
const staleStop = runLifecycle(fixture, {
157+
hookEventName: 'Stop',
158+
session_id: staleSession,
159+
lastAssistantMessage: 'Implementation status summarized.'
160+
}, {
161+
DEVCODEX_GLOBAL_SKILLS_RUNTIME: alternateSkillsRoot
162+
})
163+
assert.doesNotMatch(staleStop.text, /Progressive Skill route is incomplete/)
164+
const staleState = JSON.parse(fs.readFileSync(sessionFile(staleSession), 'utf8'))
165+
assert.strictEqual(
166+
staleState.progressiveSkillRouteStop.errorCode,
167+
'RUNTIME_CONTRACT_STALE'
168+
)
169+
assert.strictEqual(staleState.progressiveSkillRouteStopCount || 0, 0)
109170
} finally {
110171
fixture.cleanup()
111172
}

‎skills/portfolio.json‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"pluginDigest": "705fe7c86c7225b67756749be5bb3b14a1ee3bffa386483a9ab61dc971f419ae",
1212
"portfolioEvidence": "skills/portfolio-evidence.json",
1313
"portfolioEvidenceDigest": "8364a73c66ed2ec9787cdb9300223278d128a45b689a2b8deda0d1f03189f42b",
14-
"consumerInventoryFileCount": 482,
15-
"consumerInventoryDigest": "4e64bbe16c22c73b0b4510c7ca628e0a3b32dad24b45f74ee6864990c3fc3ddd",
16-
"consumerProjectionDigest": "2cf58088304bebaf95c778266bb663dda2b6dc934f08810e70247a9665604065",
17-
"portfolioInputDigest": "02e808b3092e5f8e37a8f4168e5581e2918429cb839c97930718af3eef813cf6"
14+
"consumerInventoryFileCount": 484,
15+
"consumerInventoryDigest": "b41848c7674010854bda6ef9503b8a0ad3d725b7beabe021c9b7cc618bdcb727",
16+
"consumerProjectionDigest": "536f496032116a06bc8598c46deb39d8d06b7794de0589c39ed2e23b13b90ea0",
17+
"portfolioInputDigest": "388dfc88d475e57d9274319ee838caa8fab7b7ba66b0c1f83d54eafb5eb1689f"
1818
},
1919
"ordering": "skills.id asc; graph edges from/to asc",
2020
"summary": {
@@ -10760,6 +10760,10 @@
1076010760
"path": "scripts/fixtures/host-parity/unaligned-ledger.v1.json",
1076110761
"role": "current"
1076210762
},
10763+
{
10764+
"path": "scripts/fixtures/skill-route-closure-trace.v1.json",
10765+
"role": "current"
10766+
},
1076310767
{
1076410768
"path": "scripts/generate-skill-intents.js",
1076510769
"role": "current"
@@ -10940,10 +10944,18 @@
1094010944
"path": "scripts/test-skill-resolve.js",
1094110945
"role": "current"
1094210946
},
10947+
{
10948+
"path": "scripts/test-skill-route-closure.js",
10949+
"role": "current"
10950+
},
1094310951
{
1094410952
"path": "scripts/test-skill-route-contracts.js",
1094510953
"role": "current"
1094610954
},
10955+
{
10956+
"path": "scripts/test-skill-route-lifecycle.js",
10957+
"role": "current"
10958+
},
1094710959
{
1094810960
"path": "scripts/test-skill-route-state.js",
1094910961
"role": "current"
@@ -10987,7 +10999,9 @@
1098710999
"scripts/test-skill-intents.js",
1098811000
"scripts/test-skill-portfolio.js",
1098911001
"scripts/test-skill-resolve.js",
11002+
"scripts/test-skill-route-closure.js",
1099011003
"scripts/test-skill-route-contracts.js",
11004+
"scripts/test-skill-route-lifecycle.js",
1099111005
"scripts/test-skill-route-state.js",
1099211006
"scripts/test-visible-output-contract.js",
1099311007
"scripts/test-workflow-completion-contract.js",
@@ -11077,7 +11091,9 @@
1107711091
"scripts/test-skill-intents.js",
1107811092
"scripts/test-skill-portfolio.js",
1107911093
"scripts/test-skill-resolve.js",
11094+
"scripts/test-skill-route-closure.js",
1108011095
"scripts/test-skill-route-contracts.js",
11096+
"scripts/test-skill-route-lifecycle.js",
1108111097
"scripts/test-skill-route-state.js",
1108211098
"scripts/test-visible-output-contract.js",
1108311099
"scripts/test-workflow-completion-contract.js",

0 commit comments

Comments
 (0)