@@ -1687,6 +1687,101 @@ function handleStatus (input, target, options) {
16871687 return finalizeResponse ( response , 16 * 1024 )
16881688}
16891689
1690+ const ROUTE_RETIREMENT_IDENTITY_ERRORS = new Set ( [
1691+ 'PROJECT_BINDING_MISMATCH' ,
1692+ 'CONTEXT_BINDING_MISMATCH' ,
1693+ 'MODE_CAPABILITY_STALE' ,
1694+ 'RUNTIME_CONTRACT_STALE' ,
1695+ 'TURN_EXPIRED'
1696+ ] )
1697+
1698+ const TRUSTED_BUSINESS_RETIREMENT_ERRORS = new Set ( [
1699+ 'MODE_CAPABILITY_STALE' ,
1700+ 'RUNTIME_CONTRACT_STALE'
1701+ ] )
1702+
1703+ /**
1704+ * Summarize immutable process obligations and the selected business reply for
1705+ * Stop evaluation without mutating the durable route envelope.
1706+ *
1707+ * @param {object } state persisted route state
1708+ * @param {object } input lifecycle Stop input
1709+ * @param {object } [options] trust controls for a misbound envelope
1710+ * @returns {{pendingStageIds: string[], business: object|null, businessSatisfied: boolean} }
1711+ */
1712+ function summarizeStopObligations ( state , input , options = { } ) {
1713+ const requiredStageIds = state . obligationLedger ?. requiredStageIds || [ ]
1714+ const stageProgress = state . stageProgress || { }
1715+ const pendingStageIds = requiredStageIds . filter ( stageId =>
1716+ stageProgress [ stageId ] ?. status !== 'loaded'
1717+ )
1718+ const business = options . trustBusiness === false
1719+ ? null
1720+ : ( state . obligationLedger ?. items ?. find ( item =>
1721+ item . skillId === state . obligationLedger . selectedBusinessSkillId
1722+ ) || null )
1723+ const mustReplyCore = String ( business ?. mustReplyCore || '' )
1724+ const businessSatisfied = ! business || (
1725+ mustReplyCore . length > 0 &&
1726+ String ( input . assistantText || '' ) . includes ( mustReplyCore )
1727+ )
1728+ return { pendingStageIds, business, businessSatisfied }
1729+ }
1730+
1731+ /**
1732+ * Project a route that cannot execute under the current identity as retired.
1733+ * Retirement preserves incomplete process evidence; it only ends obligations
1734+ * that no current-runtime operation can satisfy.
1735+ *
1736+ * @param {object } state persisted route state
1737+ * @param {object } input lifecycle Stop input
1738+ * @param {string } turnBinding active turn binding
1739+ * @param {Error } error binding failure
1740+ * @returns {object|null } retired Stop projection or null for other failures
1741+ */
1742+ function buildRetiredRouteStop ( state , input , turnBinding , error ) {
1743+ const errorCode = error . code || 'SKILL_ROUTE_STOP_BINDING_FAILED'
1744+ if ( ! ROUTE_RETIREMENT_IDENTITY_ERRORS . has ( errorCode ) ) return null
1745+ const trustBusiness = TRUSTED_BUSINESS_RETIREMENT_ERRORS . has ( errorCode )
1746+ const { pendingStageIds, business, businessSatisfied } = summarizeStopObligations (
1747+ state ,
1748+ input ,
1749+ { trustBusiness }
1750+ )
1751+ const businessActionRequired = businessSatisfied === false
1752+ return {
1753+ schemaVersion : 'ProgressiveSkillRouteStopV1' ,
1754+ present : true ,
1755+ complete : ! businessActionRequired ,
1756+ turnBinding,
1757+ contextEpoch : state . contextEpoch ,
1758+ planDigest : state . plan ?. planDigest || null ,
1759+ processComplete : false ,
1760+ retired : true ,
1761+ retirementReason : errorCode ,
1762+ completionDisposition : 'retired-stale-identity' ,
1763+ pendingStageIds,
1764+ selectedBusinessSkillId : business ?. skillId || null ,
1765+ mustReplyCore : business ?. mustReplyCore || null ,
1766+ businessSatisfied,
1767+ businessEvaluation : trustBusiness
1768+ ? 'trusted-bound-route'
1769+ : 'not-applicable-untrusted-route-identity' ,
1770+ errorCode,
1771+ nextOp : businessActionRequired ? 'satisfy_business' : null ,
1772+ nextCall : null ,
1773+ recovery : {
1774+ schemaVersion : 'SkillRouteRetirementRecoveryV1' ,
1775+ automatic : ! businessActionRequired ,
1776+ action : businessActionRequired
1777+ ? 'reply-selected-business-core'
1778+ : 'retire-and-allow-stop' ,
1779+ mustReplyCore : business ?. mustReplyCore || null ,
1780+ rebootstrapOnNextUserPrompt : true
1781+ }
1782+ }
1783+ }
1784+
16901785function evaluateProgressiveSkillRouteStop ( input , options = { } ) {
16911786 const target = resolveProjectTarget (
16921787 options . inputRoot || input . cwd || process . cwd ( ) ,
@@ -1709,6 +1804,23 @@ function evaluateProgressiveSkillRouteStop (input, options = {}) {
17091804 turnBinding
17101805 }
17111806 }
1807+ if ( error . code === 'TURN_EXPIRED' && error . routeEnvelope ?. state ) {
1808+ const expiredState = error . routeEnvelope . state
1809+ const requestedHostSessionId = String ( input . hostSessionId || '' )
1810+ if ( requestedHostSessionId &&
1811+ expiredState . hostSessionId &&
1812+ requestedHostSessionId !== expiredState . hostSessionId ) {
1813+ return {
1814+ schemaVersion : 'ProgressiveSkillRouteStopV1' ,
1815+ present : false ,
1816+ complete : true ,
1817+ turnBinding,
1818+ ignoredReason : 'HOST_SESSION_MISMATCH'
1819+ }
1820+ }
1821+ const retired = buildRetiredRouteStop ( expiredState , input , turnBinding , error )
1822+ if ( retired ) return retired
1823+ }
17121824 return {
17131825 schemaVersion : 'ProgressiveSkillRouteStopV1' ,
17141826 present : true ,
@@ -1718,16 +1830,30 @@ function evaluateProgressiveSkillRouteStop (input, options = {}) {
17181830 }
17191831 }
17201832 const state = envelope . state
1833+ const requestedHostSessionId = String ( input . hostSessionId || '' )
1834+ if ( requestedHostSessionId &&
1835+ state . hostSessionId &&
1836+ requestedHostSessionId !== state . hostSessionId ) {
1837+ return {
1838+ schemaVersion : 'ProgressiveSkillRouteStopV1' ,
1839+ present : false ,
1840+ complete : true ,
1841+ turnBinding,
1842+ ignoredReason : 'HOST_SESSION_MISMATCH'
1843+ }
1844+ }
17211845 try {
17221846 assertEnvelopeBinding ( state , {
17231847 project : target . project ,
17241848 turnBinding,
17251849 contextEpoch : input . contextEpoch
17261850 } , target , options )
17271851 } catch ( error ) {
1728- const requiredStageIds = state . obligationLedger ?. requiredStageIds || [ ]
1729- const pendingStageIds = requiredStageIds . filter ( stageId =>
1730- state . stageProgress [ stageId ] ?. status !== 'loaded'
1852+ const retired = buildRetiredRouteStop ( state , input , turnBinding , error )
1853+ if ( retired ) return retired
1854+ const { pendingStageIds, business, businessSatisfied } = summarizeStopObligations (
1855+ state ,
1856+ input
17311857 )
17321858 return {
17331859 schemaVersion : 'ProgressiveSkillRouteStopV1' ,
@@ -1738,33 +1864,19 @@ function evaluateProgressiveSkillRouteStop (input, options = {}) {
17381864 planDigest : state . plan ?. planDigest || null ,
17391865 processComplete : false ,
17401866 pendingStageIds,
1741- businessSatisfied : true ,
1867+ selectedBusinessSkillId : business ?. skillId || null ,
1868+ mustReplyCore : business ?. mustReplyCore || null ,
1869+ businessSatisfied,
17421870 errorCode : error . code || 'SKILL_ROUTE_STOP_BINDING_FAILED' ,
17431871 nextOp : null ,
1744- nextCall : null
1745- }
1746- }
1747- const requestedHostSessionId = String ( input . hostSessionId || '' )
1748- if ( requestedHostSessionId &&
1749- state . hostSessionId &&
1750- requestedHostSessionId !== state . hostSessionId ) {
1751- return {
1752- schemaVersion : 'ProgressiveSkillRouteStopV1' ,
1753- present : false ,
1754- complete : true ,
1755- turnBinding,
1756- ignoredReason : 'HOST_SESSION_MISMATCH'
1872+ nextCall : null ,
1873+ recovery : null
17571874 }
17581875 }
1759- const requiredStageIds = state . obligationLedger ?. requiredStageIds || [ ]
1760- const pendingStageIds = requiredStageIds . filter ( stageId =>
1761- state . stageProgress [ stageId ] ?. status !== 'loaded'
1876+ const { pendingStageIds , business , businessSatisfied } = summarizeStopObligations (
1877+ state ,
1878+ input
17621879 )
1763- const business = state . obligationLedger ?. items ?. find ( item =>
1764- item . skillId === state . obligationLedger . selectedBusinessSkillId
1765- ) || null
1766- const businessSatisfied = ! business ||
1767- String ( input . assistantText || '' ) . includes ( business . mustReplyCore )
17681880 let contextErrorCode = null
17691881 let contextRecovery = null
17701882 if ( state . plan && pendingStageIds . length ) {
@@ -1828,20 +1940,17 @@ function evaluateProgressiveSkillRouteStop (input, options = {}) {
18281940 }
18291941}
18301942
1831- const NON_RECOVERABLE_ROUTE_IDENTITY_ERRORS = new Set ( [
1832- 'MODE_CAPABILITY_STALE' ,
1833- 'RUNTIME_CONTRACT_STALE'
1834- ] )
1835-
18361943function shouldEnforceProgressiveSkillRouteStop ( routeStop , explicitRoutePending ) {
18371944 if ( ! routeStop ?. present || routeStop . complete ) return false
1945+ if ( routeStop . retired === true ) {
1946+ return routeStop . businessSatisfied === false
1947+ }
18381948 if ( routeStop . errorCode === 'PLAN_NOT_COMMITTED' ) {
18391949 return explicitRoutePending === true
18401950 }
1841- if ( NON_RECOVERABLE_ROUTE_IDENTITY_ERRORS . has ( routeStop . errorCode ) ) {
1842- // Once a plan exists, its required stages are obligations regardless of
1843- // whether selection was explicit or model-free. Runtime/capability drift
1844- // must surface a recovery boundary instead of silently discarding them.
1951+ if ( ROUTE_RETIREMENT_IDENTITY_ERRORS . has ( routeStop . errorCode ) ) {
1952+ // Legacy projections without an explicit retirement disposition remain
1953+ // fail-closed. Only the current evaluator may retire a bound route.
18451954 return Boolean (
18461955 routeStop . planDigest ||
18471956 ( routeStop . pendingStageIds || [ ] ) . length
0 commit comments