@sema-agent/client-core 0.77.0 → 0.77.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +5 -4
  3. package/dist/abortableSleep.d.ts +0 -28
  4. package/dist/abortableSleep.js +0 -28
  5. package/dist/adapt/arms.d.ts +0 -46
  6. package/dist/adapt/arms.js +9 -544
  7. package/dist/adapt/ids.d.ts +0 -58
  8. package/dist/adapt/ids.js +0 -63
  9. package/dist/adapt/instanceLedger.d.ts +0 -25
  10. package/dist/adapt/instanceLedger.js +0 -20
  11. package/dist/adapt/panelTasks.d.ts +0 -69
  12. package/dist/adapt/panelTasks.js +0 -95
  13. package/dist/adapt/textSegmentAuthority.d.ts +1 -137
  14. package/dist/adapt/textSegmentAuthority.js +0 -54
  15. package/dist/adapt/textStream.d.ts +0 -252
  16. package/dist/adapt/textStream.js +2 -281
  17. package/dist/adapt/toolCards.d.ts +0 -46
  18. package/dist/adapt/toolCards.js +0 -23
  19. package/dist/adapt/turnFlags.d.ts +0 -54
  20. package/dist/adapt/turnFlags.js +1 -17
  21. package/dist/adapt/wireShapes.d.ts +0 -92
  22. package/dist/adapt/wireShapes.js +0 -83
  23. package/dist/adapt.d.ts +0 -55
  24. package/dist/adapt.js +1 -120
  25. package/dist/adapter/activeRunSelfHeal.d.ts +22 -515
  26. package/dist/adapter/activeRunSelfHeal.js +10 -625
  27. package/dist/adapter/downstream/eventToSdkMessage.d.ts +2 -305
  28. package/dist/adapter/downstream/eventToSdkMessage.js +2 -867
  29. package/dist/adapter/downstream/terminalToSdkResult.d.ts +2 -314
  30. package/dist/adapter/downstream/terminalToSdkResult.js +13 -560
  31. package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +0 -95
  32. package/dist/adapter/downstream/turnUsageToModelUsage.js +2 -31
  33. package/dist/adapter/runStream.d.ts +0 -206
  34. package/dist/adapter/runStream.js +6 -596
  35. package/dist/adapter/types.d.ts +0 -111
  36. package/dist/adapter/types.js +0 -29
  37. package/dist/agentSession/backgroundView.d.ts +0 -106
  38. package/dist/agentSession/backgroundView.js +3 -49
  39. package/dist/agentSession/contract.d.ts +0 -98
  40. package/dist/agentSession/contract.js +0 -12
  41. package/dist/agentsWireCaps.d.ts +0 -72
  42. package/dist/agentsWireCaps.js +4 -72
  43. package/dist/approvalsStreamLiveCapability.d.ts +0 -25
  44. package/dist/approvalsStreamLiveCapability.js +0 -31
  45. package/dist/argvFlagValue.d.ts +0 -33
  46. package/dist/argvFlagValue.js +3 -35
  47. package/dist/attachmentsWireCaps.d.ts +0 -127
  48. package/dist/attachmentsWireCaps.js +0 -144
  49. package/dist/autoModeUnavailable.d.ts +0 -120
  50. package/dist/autoModeUnavailable.js +0 -144
  51. package/dist/classifierStatus.d.ts +0 -72
  52. package/dist/classifierStatus.js +0 -167
  53. package/dist/classifierVerdictWire.d.ts +0 -54
  54. package/dist/classifierVerdictWire.js +0 -133
  55. package/dist/clientContextWireCaps.d.ts +0 -37
  56. package/dist/clientContextWireCaps.js +0 -36
  57. package/dist/clientSlice.d.ts +0 -63
  58. package/dist/clientSlice.js +0 -45
  59. package/dist/cloudConfigWireCaps.d.ts +0 -86
  60. package/dist/cloudConfigWireCaps.js +2 -65
  61. package/dist/compensations.d.ts +0 -52
  62. package/dist/compensations.js +2 -63
  63. package/dist/controlRouter.d.ts +2 -208
  64. package/dist/controlRouter.js +1 -129
  65. package/dist/coreValuePorts.d.ts +0 -84
  66. package/dist/coreValuePorts.js +0 -29
  67. package/dist/decideReceipt.d.ts +0 -110
  68. package/dist/decideReceipt.js +0 -84
  69. package/dist/detachWire.d.ts +0 -130
  70. package/dist/detachWire.js +1 -130
  71. package/dist/deviceExecutorManagementCapability.d.ts +2 -35
  72. package/dist/deviceExecutorManagementCapability.js +1 -37
  73. package/dist/diagnostics.d.ts +0 -8
  74. package/dist/diagnostics.js +0 -8
  75. package/dist/diff/patch.d.ts +0 -17
  76. package/dist/diff/patch.js +0 -19
  77. package/dist/effectiveFacts.d.ts +0 -40
  78. package/dist/effectiveFacts.js +0 -22
  79. package/dist/effortWire.d.ts +0 -11
  80. package/dist/effortWire.js +0 -12
  81. package/dist/engineAgentPanelStore.d.ts +0 -169
  82. package/dist/engineAgentPanelStore.js +13 -293
  83. package/dist/engineCapReader.d.ts +0 -62
  84. package/dist/engineCapReader.js +1 -43
  85. package/dist/engineCapsCache.d.ts +0 -177
  86. package/dist/engineCapsCache.js +0 -199
  87. package/dist/engineCapsGenerationGuard.d.ts +0 -2
  88. package/dist/engineCapsGenerationGuard.js +0 -11
  89. package/dist/engineErrorCodes.d.ts +1 -316
  90. package/dist/engineErrorCodes.js +0 -427
  91. package/dist/engineHttpTools.d.ts +0 -29
  92. package/dist/engineHttpTools.js +0 -29
  93. package/dist/engineIdentity.d.ts +0 -83
  94. package/dist/engineIdentity.js +0 -88
  95. package/dist/engineInlineTaskStats.d.ts +0 -57
  96. package/dist/engineInlineTaskStats.js +1 -42
  97. package/dist/engineNoticeCodes.d.ts +0 -190
  98. package/dist/engineNoticeCodes.js +0 -182
  99. package/dist/engineSessionParam.d.ts +0 -30
  100. package/dist/engineSessionParam.js +0 -52
  101. package/dist/engineToolLabelStore.d.ts +0 -29
  102. package/dist/engineToolLabelStore.js +0 -31
  103. package/dist/engineWireSdk.d.ts +0 -90
  104. package/dist/engineWireSdk.js +0 -74
  105. package/dist/engineWireTarget.d.ts +0 -14
  106. package/dist/engineWireTarget.js +0 -39
  107. package/dist/env/localeGeo.d.ts +0 -12
  108. package/dist/env/localeGeo.js +2 -77
  109. package/dist/env/localeTag.d.ts +0 -34
  110. package/dist/env/localeTag.js +0 -32
  111. package/dist/env/uiLanguage.d.ts +0 -13
  112. package/dist/env/uiLanguage.js +0 -25
  113. package/dist/envFlag.d.ts +0 -37
  114. package/dist/envFlag.js +0 -40
  115. package/dist/executionLaneCapability.d.ts +0 -48
  116. package/dist/executionLaneCapability.js +0 -54
  117. package/dist/finalVerifyWire.d.ts +0 -67
  118. package/dist/finalVerifyWire.js +3 -43
  119. package/dist/fleet/fleetLedger.d.ts +0 -308
  120. package/dist/fleet/fleetLedger.js +10 -429
  121. package/dist/fleet/fleetProjection.d.ts +0 -240
  122. package/dist/fleet/fleetProjection.js +0 -189
  123. package/dist/fleet/fleetRowAgentType.d.ts +0 -6
  124. package/dist/fleet/fleetRowAgentType.js +1 -32
  125. package/dist/fleet/workflowSizeWarning.d.ts +0 -47
  126. package/dist/fleet/workflowSizeWarning.js +1 -46
  127. package/dist/fleetAgentPanelProjection.d.ts +0 -48
  128. package/dist/fleetAgentPanelProjection.js +8 -173
  129. package/dist/fleetTaskDesc.d.ts +0 -39
  130. package/dist/fleetTaskDesc.js +0 -69
  131. package/dist/forkWireCaps.d.ts +0 -23
  132. package/dist/forkWireCaps.js +1 -24
  133. package/dist/gateOutcome.d.ts +0 -140
  134. package/dist/gateOutcome.js +0 -85
  135. package/dist/gateVocabulary.d.ts +0 -114
  136. package/dist/gateVocabulary.js +1 -161
  137. package/dist/goalStopHook.d.ts +0 -127
  138. package/dist/goalStopHook.js +0 -178
  139. package/dist/headlessPermissionModeWire.d.ts +0 -76
  140. package/dist/headlessPermissionModeWire.js +1 -155
  141. package/dist/headlessReconnectWire.d.ts +0 -84
  142. package/dist/headlessReconnectWire.js +13 -67
  143. package/dist/hitl/approvalDecisionNoteAudit.d.ts +0 -49
  144. package/dist/hitl/approvalDecisionNoteAudit.js +0 -56
  145. package/dist/hitl/approvalOutcomeNote.d.ts +0 -2
  146. package/dist/hitl/approvalOutcomeNote.js +0 -20
  147. package/dist/hitl/approvalResolution.d.ts +0 -113
  148. package/dist/hitl/approvalResolution.js +0 -66
  149. package/dist/hitl/approvalsFeed.d.ts +0 -183
  150. package/dist/hitl/approvalsFeed.js +12 -243
  151. package/dist/hitl/armedGateRegistry.d.ts +0 -55
  152. package/dist/hitl/armedGateRegistry.js +0 -146
  153. package/dist/hitl/askGateWire.d.ts +0 -86
  154. package/dist/hitl/askGateWire.js +1 -95
  155. package/dist/hitl/askParkRowRouting.d.ts +0 -123
  156. package/dist/hitl/askParkRowRouting.js +1 -89
  157. package/dist/hitl/crashConverged.d.ts +0 -148
  158. package/dist/hitl/crashConverged.js +0 -214
  159. package/dist/hitl/editedRuleTextPrecheck.d.ts +0 -109
  160. package/dist/hitl/editedRuleTextPrecheck.js +0 -71
  161. package/dist/hitl/frameRouter.d.ts +0 -134
  162. package/dist/hitl/frameRouter.js +6 -378
  163. package/dist/hitl/gateIdentity.d.ts +0 -39
  164. package/dist/hitl/gateIdentity.js +0 -41
  165. package/dist/hitl/gateLedger.d.ts +0 -267
  166. package/dist/hitl/gateLedger.js +0 -121
  167. package/dist/hitl/hitlBridge.d.ts +7 -431
  168. package/dist/hitl/hitlBridge.js +7 -476
  169. package/dist/hitl/hitlHostSurface.d.ts +0 -150
  170. package/dist/hitl/hitlHostSurface.js +0 -169
  171. package/dist/hitl/livePendingAsk.d.ts +0 -91
  172. package/dist/hitl/livePendingAsk.js +0 -74
  173. package/dist/hitl/localAllowRule.d.ts +0 -62
  174. package/dist/hitl/localAllowRule.js +1 -33
  175. package/dist/hitl/parkOwnership.d.ts +0 -56
  176. package/dist/hitl/parkOwnership.js +0 -22
  177. package/dist/hitl/parkResolver.d.ts +3 -88
  178. package/dist/hitl/parkResolver.js +8 -349
  179. package/dist/hitl/parkRowBirthWait.d.ts +2 -26
  180. package/dist/hitl/parkRowBirthWait.js +3 -93
  181. package/dist/hitl/persistedRulesWire.d.ts +13 -358
  182. package/dist/hitl/persistedRulesWire.js +0 -310
  183. package/dist/hitl/planReviewWire.d.ts +4 -176
  184. package/dist/hitl/planReviewWire.js +9 -312
  185. package/dist/hitl/resumeRunningCard.d.ts +0 -105
  186. package/dist/hitl/resumeRunningCard.js +0 -105
  187. package/dist/hitl/sessionPolicyWire.d.ts +11 -191
  188. package/dist/hitl/sessionPolicyWire.js +0 -149
  189. package/dist/hitl/suspendedReopen.d.ts +3 -24
  190. package/dist/hitl/suspendedReopen.js +0 -14
  191. package/dist/hitl/toolApprovalWire.d.ts +5 -1308
  192. package/dist/hitl/toolApprovalWire.js +5 -945
  193. package/dist/hooksWireCaps.d.ts +0 -38
  194. package/dist/hooksWireCaps.js +0 -190
  195. package/dist/host.d.ts +0 -105
  196. package/dist/host.js +0 -33
  197. package/dist/hostEnv.d.ts +0 -14
  198. package/dist/hostEnv.js +0 -13
  199. package/dist/imagesWireCaps.d.ts +0 -21
  200. package/dist/imagesWireCaps.js +0 -22
  201. package/dist/index.d.ts +0 -134
  202. package/dist/index.js +0 -476
  203. package/dist/interactiveHalt.d.ts +5 -153
  204. package/dist/interactiveHalt.js +0 -111
  205. package/dist/interactiveToolsWire.d.ts +0 -62
  206. package/dist/interactiveToolsWire.js +1 -67
  207. package/dist/leaderConflict.d.ts +0 -59
  208. package/dist/leaderConflict.js +0 -48
  209. package/dist/limitsWire.d.ts +0 -125
  210. package/dist/limitsWire.js +2 -120
  211. package/dist/liveInitToolFace.d.ts +0 -77
  212. package/dist/liveInitToolFace.js +1 -109
  213. package/dist/liveModelCatalog.d.ts +0 -52
  214. package/dist/liveModelCatalog.js +0 -48
  215. package/dist/liveQuestionStore.d.ts +0 -87
  216. package/dist/liveQuestionStore.js +1 -56
  217. package/dist/mcpLiveness.d.ts +0 -151
  218. package/dist/mcpLiveness.js +0 -122
  219. package/dist/mcpPanel.d.ts +0 -104
  220. package/dist/mcpPanel.js +0 -49
  221. package/dist/mcpReconnect.d.ts +0 -58
  222. package/dist/mcpReconnect.js +1 -69
  223. package/dist/mcpWireCaps.d.ts +0 -55
  224. package/dist/mcpWireCaps.js +1 -13
  225. package/dist/memoryComplianceCapability.d.ts +3 -57
  226. package/dist/memoryComplianceCapability.js +0 -57
  227. package/dist/memoryEntriesWire.d.ts +6 -198
  228. package/dist/memoryEntriesWire.js +0 -144
  229. package/dist/memoryOriginCapability.d.ts +3 -53
  230. package/dist/memoryOriginCapability.js +0 -50
  231. package/dist/memorySpecWire.d.ts +0 -116
  232. package/dist/memorySpecWire.js +0 -150
  233. package/dist/model/catalog.d.ts +0 -111
  234. package/dist/model/catalog.js +0 -87
  235. package/dist/model/catalogLoader.d.ts +0 -114
  236. package/dist/model/catalogLoader.js +2 -139
  237. package/dist/model/modelSupplyRules.d.ts +1 -55
  238. package/dist/model/modelSupplyRules.js +0 -62
  239. package/dist/model/providerAuth.d.ts +0 -103
  240. package/dist/model/providerAuth.js +2 -38
  241. package/dist/model/providerCatalog.d.ts +0 -45
  242. package/dist/model/providerCatalog.js +0 -37
  243. package/dist/model/providerPresets.d.ts +0 -33
  244. package/dist/model/providerPresets.js +2 -91
  245. package/dist/model/tierVocabulary.d.ts +0 -31
  246. package/dist/model/tierVocabulary.js +0 -27
  247. package/dist/modelBudgetRule.d.ts +0 -39
  248. package/dist/modelBudgetRule.js +0 -39
  249. package/dist/modelCapabilityProbe.d.ts +0 -175
  250. package/dist/modelCapabilityProbe.js +0 -147
  251. package/dist/modelWireCaps.d.ts +0 -13
  252. package/dist/modelWireCaps.js +0 -13
  253. package/dist/notifications.d.ts +0 -233
  254. package/dist/notifications.js +14 -492
  255. package/dist/oneShotWireCaps.d.ts +0 -34
  256. package/dist/oneShotWireCaps.js +0 -35
  257. package/dist/ownKey.d.ts +0 -33
  258. package/dist/ownKey.js +0 -33
  259. package/dist/panelRunningHistory.d.ts +0 -28
  260. package/dist/panelRunningHistory.js +0 -43
  261. package/dist/peerFrames.d.ts +0 -71
  262. package/dist/peerFrames.js +0 -168
  263. package/dist/peerLaneCapability.d.ts +0 -44
  264. package/dist/peerLaneCapability.js +0 -52
  265. package/dist/permissionRuleIssue.d.ts +0 -30
  266. package/dist/permissionRuleIssue.js +0 -78
  267. package/dist/permissionRulesWriteCapability.d.ts +0 -46
  268. package/dist/permissionRulesWriteCapability.js +0 -54
  269. package/dist/permissionWireCaps.d.ts +0 -37
  270. package/dist/permissionWireCaps.js +0 -37
  271. package/dist/postureKnob.d.ts +0 -71
  272. package/dist/postureKnob.js +0 -86
  273. package/dist/principalWire.d.ts +0 -17
  274. package/dist/principalWire.js +0 -17
  275. package/dist/printToolResultFrame.d.ts +0 -100
  276. package/dist/printToolResultFrame.js +0 -33
  277. package/dist/promptProfileWireCaps.d.ts +0 -13
  278. package/dist/promptProfileWireCaps.js +0 -13
  279. package/dist/readFacePosture.d.ts +0 -43
  280. package/dist/readFacePosture.js +0 -46
  281. package/dist/request/printNotification.d.ts +0 -20
  282. package/dist/request/printNotification.js +0 -55
  283. package/dist/request/taskRequest.d.ts +0 -249
  284. package/dist/request/taskRequest.js +10 -496
  285. package/dist/resumeRefusalCopy.d.ts +0 -136
  286. package/dist/resumeRefusalCopy.js +1 -117
  287. package/dist/retainBackgroundWireCaps.d.ts +0 -48
  288. package/dist/retainBackgroundWireCaps.js +0 -48
  289. package/dist/retryStatus.d.ts +2 -221
  290. package/dist/retryStatus.js +0 -107
  291. package/dist/rewindWireCaps.d.ts +0 -27
  292. package/dist/rewindWireCaps.js +0 -24
  293. package/dist/runCancelContext.d.ts +1 -20
  294. package/dist/runCancelContext.js +0 -34
  295. package/dist/runTerminal.d.ts +0 -273
  296. package/dist/runTerminal.js +0 -177
  297. package/dist/sandboxWire.d.ts +0 -38
  298. package/dist/sandboxWire.js +0 -82
  299. package/dist/scenarioWire.d.ts +0 -60
  300. package/dist/scenarioWire.js +0 -68
  301. package/dist/scratchpadWireCaps.d.ts +0 -11
  302. package/dist/scratchpadWireCaps.js +0 -32
  303. package/dist/sdkWireTransit.d.ts +0 -38
  304. package/dist/sdkWireTransit.js +0 -30
  305. package/dist/seam.d.ts +18 -853
  306. package/dist/seam.js +0 -52
  307. package/dist/seatContract.d.ts +0 -555
  308. package/dist/seatContract.js +5 -304
  309. package/dist/selfOrchestrationDenial.d.ts +0 -189
  310. package/dist/selfOrchestrationDenial.js +0 -162
  311. package/dist/selfOrchestrationWireCaps.d.ts +0 -38
  312. package/dist/selfOrchestrationWireCaps.js +0 -38
  313. package/dist/sessionMap.d.ts +0 -60
  314. package/dist/sessionMap.js +0 -37
  315. package/dist/sessionMemoryStatus.d.ts +11 -114
  316. package/dist/sessionMemoryStatus.js +0 -112
  317. package/dist/sessionModelLatch.d.ts +0 -29
  318. package/dist/sessionModelLatch.js +0 -40
  319. package/dist/sessionPolicyCapability.d.ts +3 -53
  320. package/dist/sessionPolicyCapability.js +0 -52
  321. package/dist/sessionSlot.d.ts +0 -26
  322. package/dist/sessionSlot.js +0 -17
  323. package/dist/skillsWireCaps.d.ts +0 -87
  324. package/dist/skillsWireCaps.js +0 -38
  325. package/dist/sqlEngineCapability.d.ts +0 -118
  326. package/dist/sqlEngineCapability.js +0 -134
  327. package/dist/sseIdleTriage.d.ts +0 -79
  328. package/dist/sseIdleTriage.js +1 -80
  329. package/dist/steering.d.ts +0 -54
  330. package/dist/steering.js +0 -63
  331. package/dist/subagent/engineCompactWire.d.ts +0 -34
  332. package/dist/subagent/engineCompactWire.js +4 -134
  333. package/dist/subagent/engineDelegatedPrompt.d.ts +0 -31
  334. package/dist/subagent/engineDelegatedPrompt.js +1 -103
  335. package/dist/subagent/engineRowStopGate.d.ts +0 -20
  336. package/dist/subagent/engineRowStopGate.js +0 -49
  337. package/dist/subagent/engineSubagentOutput.d.ts +0 -7
  338. package/dist/subagent/engineSubagentOutput.js +1 -51
  339. package/dist/subagent/engineSubagentResume.d.ts +2 -279
  340. package/dist/subagent/engineSubagentResume.js +2 -129
  341. package/dist/subagent/engineSubagentSteer.d.ts +0 -26
  342. package/dist/subagent/engineSubagentSteer.js +0 -63
  343. package/dist/subagent/engineSubagentTail.d.ts +0 -38
  344. package/dist/subagent/engineSubagentTail.js +2 -110
  345. package/dist/subagent/engineTaskHandleWire.d.ts +11 -65
  346. package/dist/subagent/engineTaskHandleWire.js +20 -131
  347. package/dist/subagent/subagentOwnerAbsence.d.ts +0 -2
  348. package/dist/subagent/subagentOwnerAbsence.js +0 -25
  349. package/dist/subagentContentStore.d.ts +3 -260
  350. package/dist/subagentContentStore.js +7 -540
  351. package/dist/systemReminderTag.d.ts +0 -49
  352. package/dist/systemReminderTag.js +0 -61
  353. package/dist/toolResult.d.ts +1 -211
  354. package/dist/toolResult.js +1 -358
  355. package/dist/toolRoster.d.ts +0 -140
  356. package/dist/toolRoster.js +0 -66
  357. package/dist/typePins.d.ts +0 -46
  358. package/dist/types/engineState.d.ts +0 -71
  359. package/dist/types/engineState.js +0 -16
  360. package/dist/ultracodeWireCaps.d.ts +0 -75
  361. package/dist/ultracodeWireCaps.js +0 -92
  362. package/dist/unrefTimer.d.ts +0 -27
  363. package/dist/webSearchBackendCapability.d.ts +0 -72
  364. package/dist/webSearchBackendCapability.js +0 -79
  365. package/dist/webSearchWireCaps.d.ts +0 -37
  366. package/dist/webSearchWireCaps.js +0 -34
  367. package/dist/websearch/searchProviderPresets.d.ts +0 -81
  368. package/dist/websearch/searchProviderPresets.js +0 -27
  369. package/dist/wireErrorTriage.d.ts +4 -160
  370. package/dist/wireErrorTriage.js +0 -171
  371. package/dist/wireRefusalCopy.d.ts +0 -28
  372. package/dist/wireRefusalCopy.js +0 -28
  373. package/dist/workflow.d.ts +0 -40
  374. package/dist/workflow.js +0 -52
  375. package/dist/workflowClient.d.ts +0 -122
  376. package/dist/workflowClient.js +8 -360
  377. package/dist/workflowMonitor.d.ts +2 -72
  378. package/dist/workflowMonitor.js +0 -26
  379. package/dist/writeProtectionCapability.d.ts +0 -112
  380. package/dist/writeProtectionCapability.js +0 -109
  381. package/docs/INTEGRATION-CLIENTS.md +62 -1
  382. package/package.json +1 -1
@@ -3,23 +3,8 @@ import { eventSeq } from '../adapter/types.js';
3
3
  import { hostLog } from '../host.js';
4
4
  import { abortableSleep } from '../abortableSleep.js';
5
5
  import { suspendedReopenOf } from './suspendedReopen.js';
6
- // ── deny/plan-review 归因的 wire 窄化(0.28.0 发版扫描 F1/F2/F3 收编;单源,三条腿共用)──────
7
- //
8
- // server 对 `/decide` 与 plan-review 两条腿的 `reason` 同限 `MAX_APPROVAL_REASON_CHARS`(4096 字符,
9
- // 超限 **413 `reason_too_large`**;server 侧 reason 入签后无法替发送方截 —— 超长原样发出去,丢掉的
10
- // 不是归因而是**整次决断**:413 ⇒ 决断没送达 ⇒ run 留 suspended)。所以三条 reason 出口(卡 deny /
11
- // canUseTool deny / plan-review)全部经本窄化器,绝不裸发。
12
- /** durable `/decide` 腿的既有缺省拒因(不带归因时逐字不变 —— 0.27.0 及之前的 wire 字节)。 */
13
6
  export const DEFAULT_DENY_REASON = 'The user rejected this tool use';
14
- /** server 两条腿共用的 reason 字符上限(超限 413,决断被打回)。 */
15
7
  const MAX_DENY_REASON_CHARS = 4096;
16
- /**
17
- * 归因原文 → 可上 wire 的形。缺席/非串/纯空白 ⇒ `undefined`(调用方自定回落:deny 腿落
18
- * {@link DEFAULT_DENY_REASON},plan-review 腿键不落);超上限 ⇒ 截到上限并 debug 留痕。
19
- * 🔴 截断**边界安全**:切点落在代理对中间会产出孤高代理 —— server 长度检查放行,但 UTF-8
20
- * 落库/入签时末尾退化成 U+FFFD,两侧字节不一致还会毒化签名对账。末码元是高代理就再退一位。
21
- * 🔴 只做归因,不参与裁决:任何返回值都不改变这次决断本身。
22
- */
23
8
  export function denyReasonForWire(reason, tag) {
24
9
  if (typeof reason !== 'string')
25
10
  return undefined;
@@ -31,66 +16,25 @@ export function denyReasonForWire(reason, tag) {
31
16
  let cut = trimmed.slice(0, MAX_DENY_REASON_CHARS);
32
17
  const last = cut.charCodeAt(cut.length - 1);
33
18
  if (last >= 0xd800 && last <= 0xdbff)
34
- cut = cut.slice(0, -1); // 孤高代理:退一位,绝不发半个字符
19
+ cut = cut.slice(0, -1);
35
20
  hostLog('debug', `hitlBridge: reason for ${tag} truncated ${trimmed.length}→${cut.length} chars ` +
36
21
  `(server caps approval reason at ${MAX_DENY_REASON_CHARS}; over-cap is a 413 reason_too_large that would drop the DECISION, not just the reason)`);
37
22
  return cut;
38
23
  }
39
- /** 契约 §4c `permissionModeAfter` 的闭集两词(`acceptEdits` = 工作目录内的写不再逐次征询;`default` = 每次写照常征询)。 */
40
24
  export const PLAN_REVIEW_MODE_AFTER_WORDS = Object.freeze(['default', 'acceptEdits']);
41
- /** 这个值是不是闭集里的词(词表外 —— 含 `plan` / `bypassPermissions` / `auto` —— 引擎 400:一次批准不许顺带关掉征询面)。 */
42
25
  export function isPlanReviewModeAfter(v) {
43
26
  return v !== undefined && PLAN_REVIEW_MODE_AFTER_WORDS.includes(v);
44
27
  }
45
- /** A safety stop: a contract law was about to be violated (e.g. a binding mismatch, a wrong-gate route).
46
- * The caller MUST re-present to the human or surface the error — NEVER silently retry or auto-decide
47
- * (contract/04 §9.1 fail-closed law). */
48
28
  export class HitlSafetyError extends Error {
49
29
  code;
50
- constructor(message,
51
- /** A stable code the shell can branch on (REF-CC-036,2026-08-02:闭集联合,不再是开集 string —
52
- * 与同族兄弟 `controlRouter.ts` 的 `ControlSafetyError.code` 同形)。 */
53
- /** 🔴 闭集(FIX② 2026-08-07 加 `empty_answer`,第 5 员)。加员必须**同批**改齐
54
- * `parkResolver.GATE_FAILURE_CODES` 白名单 —— 那道白名单把不认得的码降成 `undefined`,
55
- * 一边加一边不改另一边 = 新码在消费端被静默吞掉(比不加还坏:本地拦住了,判词却丢了)。 */
56
- code) {
30
+ constructor(message, code) {
57
31
  super(message);
58
32
  this.code = code;
59
33
  this.name = 'HitlSafetyError';
60
34
  }
61
35
  }
62
- /**
63
- * 🔴 **编译期负控**(对抗复审 [medium] 采纳,0.48.0):三位必须**全 `readonly`**。
64
- *
65
- * 0.47.0 的自铸形三位逐个写着 `readonly`,而上游 `ApprovalStaleCurrentPending` 的成员是**可写**的
66
- * —— 首版别名(`NonNullable<…>`,不带 `Readonly<>`)把这条已发布的类型契约悄悄弱了一档:
67
- * 消费方从此可以直接改写 `boundCallId` / `boundInputHash`,而那两位的契约恰恰是
68
- * **逐字回显、绝不本地重算**(§9.1;本地重算一次就会把一次合法审批 fail-close 掉)。
69
- * 🔴 这类退化**没有任何行为门看得见**(它不改变任何运行期字节)—— 名字在、字段在、行为门全绿。
70
- * 所以钉必须下在编译期:`Readonly<>` 哪天被顺手摘掉,这一行当场编译错。
71
- */
72
36
  const _gateCurrentPendingReadonlyPin = true;
73
37
  void _gateCurrentPendingReadonlyPin;
74
- /**
75
- * 从一次 decide 失败(任意抛出物)里读出 {@link GateCurrentPending} —— 读不出即 `undefined`。
76
- *
77
- * 🔴 **结构视图读,不 `instanceof`**(两条理由,都是承重的):
78
- * ① **供给面**:本包的 decide 出站走的是宿主注入的 `HitlClientLike`(见该型)。抛出来的到底是不是
79
- * SDK 的 `ApprovalStaleError` 由**宿主**决定 —— web/desktop 完全可以注入自己的传输层。拿
80
- * `instanceof` 当闸,等于让「宿主用的是哪一份 SDK 实例」决定一个纯读值的位在不在
81
- * (跨 realm / 双实例 = `HitlSafetyError` 判型契约踩过的同一个坑,见 §4e)。
82
- * ② **类型面**(0.48.0 就地订正):0.47.0 这条写的是「`ApprovalStaleError.currentPending` 只在
83
- * sdk ≥7.3.0 上声明,而本包地板是 7.2.0」——**地板已抬到 `>=7.4.0`,那半条不再成立**。
84
- * 但本条的**结论不变**,因为承重的从来是 ① 那半:即使类型面现在够得着,抛出来的对象仍由宿主
85
- * 的传输层决定,`instanceof` 依旧是错的判据。⇒ 保留宽读,只订正理由。
86
- * 🔴 **判据锚在「值本身合不合形」,不锚它骑在哪个类/哪个 errorCode 上**
87
- * ([anchor-on-the-deciding-quantity]):真正决定「壳能不能一跳重定位」的量,是**这个三元组的
88
- * 两件必填位在不在且是非空串** —— 而不是错误的类名。宽读在这里是**方向安全**的:本位不参与
89
- * 任何裁决(见 {@link GateCurrentPending} 顶注),多读到一次的代价 = 多一条指路;漏读的代价 =
90
- * 退回整队重拉。
91
- * 🔴 **两件必填位缺一即整只不铸**:半个三元组重定位不了(`decide` 要的是 boundCallId 本体),
92
- * 铸一个残形只会让消费点以为「有指路」。`boundInputHash` 是**行上有才带**的可选位,缺席照铸。
93
- */
94
38
  export function readDecideCurrentPending(e) {
95
39
  const raw = e?.currentPending;
96
40
  if (typeof raw !== 'object' || raw === null)
@@ -108,132 +52,23 @@ export function readDecideCurrentPending(e) {
108
52
  : {}),
109
53
  };
110
54
  }
111
- /**
112
- * 从一次 decide / 取件失败(任意抛出物)里读出**引擎的机器码** —— 读不出即 `undefined`。
113
- *
114
- * 🔴 **为什么这一位非透出不可**(0.60.0;上游候包件 ②,与 0.47.0 补 `currentPending` **同形**):
115
- * 本包把 typed 错**拍平成 `reason` 串**交给消费点(`FsApprovalOutcome.failed.reason`),于是
116
- * 「只读 outcome 形」的消费者(壳的重开链、迟到决断代理)对机读码**全盲** —— 要分辨
117
- * `resume.preflight_rejected` / `approval_stale` / 传输类失败,只能去正则匹配那句拍平文案,
118
- * 而那句文案里嵌的是 `String(e)`(上游一改措辞就静默空转)。上游为此在壳里绕了一整圈
119
- * (把 client 套进代理、站在拍平**之前**读)——那圈本该由这一位省掉。
120
- * 🔴 **结构视图读,不 `instanceof`**:理由与 {@link readDecideCurrentPending} 逐字同一条 ——
121
- * 抛出来的是不是 SDK 的错误类由**宿主注入的传输层**决定(跨 realm / 双实例)。
122
- * 🔴 **键位只认 `errorCode`**([2055] 死键纪律,退役的 `code` 槽不做兼容);非空串才算数,
123
- * 其余一律诚实缺席。
124
- * 🔴 **只搬运不解释**:本读口不判任何码属于哪一族(词表属主在 server/core,开集)——消费点分支
125
- * 已知码、永远带 `default`。
126
- * 🔴 **绝不把它当凭证面**:`errorCode` 是机器码,不是错误正文;错误正文(可能含凭据/内部路径)
127
- * 照旧**不**经本位外流。
128
- */
129
55
  export function readWireErrorCode(e) {
130
56
  const raw = e?.errorCode;
131
57
  return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
132
58
  }
133
- // ── decide 出站的瞬断分诊与有界重试(Inkglow-1085 P0a,2026-08-19;[4664] server 定谳后对表)────
134
- //
135
- // 病形(案卷 INKGLOW-1085 车1 全链):`approvals.decide` 是**非 submit POST** —— SDK transport 对它
136
- // `maxAttempts = 1`(「a decide must not double-act」),且每发带显式 `AbortSignal.timeout(timeoutMs)`
137
- // (缺省 60s;per-call opts 只有 `signal`,调用方 signal 与之**合流取先**,只能收短、不能放长 ——
138
- // timeoutMs 是 client 构造级旋钮,本包对已构造的注入 client 不可配)。于是一次网络瞬断/超时 =
139
- // decide 单发即死 → 上层拼 `failed` → parkResolver 合成 `hitl_unanswered` 终帧 → ask 死局。
140
- //
141
- // 🔴 **口径换代(#318 件②,2026-08-21):超时从「常态」变「真异常」** —— [4664] 的长调用口径作废。
142
- // server #316([4687] 点名 BREAKING,随 7.37.0 发车)把任务级 decide 的 200 体从**终局形**改成
143
- // **受理回执** `{taskId, sessionId, status:"resuming", bindingEnforced:true}`:受理点设在 core
144
- // `resumeStream` 解析之后,一切会变成拒绝的判定(lease 429 / markResuming CAS 409 / core pre-CAS
145
- // 守卫 / 绑定不符 409 / 卡不在 404)**仍同步发生**,受理后只剩模型段异步跑。实测受理即回 ≈23ms。
146
- // ⇒ 对 ≥7.37 的部署,一次 60s 超时**不再是**「server 还在跑 resume」的常态,而是**真异常**
147
- // (网络路径断在半途 / server 病态卡死 / 见下两条残余形)。口径随之收窄:
148
- // · **超时类**(TimeoutError = SDK per-attempt 帽掐断)⇒ 仍带退避重试(重试环结构不动),但总窗
149
- // 从 10 分钟收到 {@link DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS}(十秒级,见该常量的取值推导);
150
- // · **网络断类**(ECONNREFUSED / fetch failed 等:连语义答复都没拿到,引擎多半真死)⇒ 重试
151
- // **恰一次**,让真死尽快显形;
152
- // · 语义答复类(带 HTTP status 的 4xx/5xx —— 引擎收到并回答了)⇒ 零重试,原样上抛;
153
- // · `HitlSafetyError`(binding mismatch 等安全信号)⇒ 恒零重试(§9.1 铁律,原有行为不动);
154
- // · 调用方已中止(用户 Esc)⇒ 零重试(AbortError 是人按的,不是瞬断)。
155
- //
156
- // 🔴 为什么重试不违反「decide 绝不 double-act」:①[4664] 原话 —— **重复 decide 不双跑,server CAS
157
- // 保证**(重试幂等安全,放心重试);②D-1 绑定回显(boundCallId+boundInputHash 逐字回显,server
158
- // fail-closed on mismatch)—— 第一发其实送达时,重发只能撞 4xx(approval not found / conflict),
159
- // 绝不可能批掉**另一件**事;那个 4xx 原样上抛,parkResolver 的「已解决重放救回」判据照认。
160
- // 重试耗尽(超时类窗尽 / 网络断类第二发仍断)⇒ 抛 {@link DecideTransportRetryExhaustedError}
161
- // (typed 判别位)—— 消费方(toolApprovalWire / parkResolver)据此走**重呈臂**而不是把 turn 判死。
162
- // 引擎真死时失败也会尽快显形:重呈臂的下一步(approvals.list / runs.events)对死引擎当场失败,
163
- // 走既有诚实红。
164
- // 🔴 **两条残余的同步形如实登记**(收窄不是「长调用消失了」,[4687] 逐字):终局形仍存在于
165
- // ① **pre-7.37 的 server**(所有 decide 都是终局形 = 真长调用);② **≥7.37 但没有 durable run 行
166
- // 可跟的部署**(那里提前受理 = 把结果扔掉,所以腿如实保持同步)。本包是三端共用件,面向的是
167
- // 任意部署 —— 所以重试环**保留**、总窗**不设 0**。这两形上窗尽的代价是可接受的:耗尽走的是
168
- // {@link DecideTransportRetryExhaustedError} → **重呈臂**(re-attach ⇒ durable 流重放 park ⇒
169
- // 同一张卡重交用户),而不是把 turn 判死;若第一发其实已送达,重呈的下一步会撞
170
- // `isAlreadyResolvedGateReason` 的已解决判据被救回。
171
- // 🔴 请托半场(候协作板):SDK decide 若开 per-call timeoutMs(或对 HITL 面单列长缺省),本层的
172
- // 超时类重试环可整段收敛成一发长等待。
173
- /** 瞬断重试的起始退避(×2 递增,封顶 {@link DECIDE_RETRY_BACKOFF_MAX_MS};别把 decide 打成连发)。 */
174
59
  const DECIDE_TRANSPORT_RETRY_BACKOFF_MS = 750;
175
60
  const DECIDE_RETRY_BACKOFF_MAX_MS = 5_000;
176
- /**
177
- * SDK 的 **per-attempt 超时帽**(`AbortSignal.timeout(timeoutMs)`)的**缺省值**。
178
- *
179
- * 🔴 **它是「观察到的缺省」,不是本包能保证的量**(对抗复审 [medium] 采纳,2026-08-21)。
180
- * per-call opts 只有 `signal`(与之合流取先,只能收短不能放长),但 `timeoutMs` 是 **client
181
- * 构造级**旋钮,而本桥吃的是**宿主注入的** client(`HitlClientLike` 根本不暴露它)⇒ 一个 web/桌面
182
- * 宿主完全可以用 30s 或 120s 的 client 构造本桥。所以任何「总窗 ÷ 帽 = 发数」的推导都只在缺省值
183
- * 上成立,**不能当成本共用件的性质**。
184
- * ⇒ 本常量只用来推导下面那个**墙钟上界**;「至少重试一次」这条**性质**改由
185
- * {@link DECIDE_TIMEOUT_MIN_ATTEMPTS} 用**发数**保证,与宿主的 timeoutMs 无关。
186
- */
187
61
  const DECIDE_ATTEMPT_TIMEOUT_CAP_MS = 60_000;
188
- /**
189
- * 超时类**最少发数**(首发 + 至少一次重试)—— 与墙钟窗**两个独立的界**,不是第二层节奏。
190
- *
191
- * 🔴 为什么必须有它([4687] 登记的两条残余同步形是承重理由):终局形仍存在于 pre-7.37 的 server
192
- * 与「≥7.37 但没有 durable run 行可跟」的部署 —— 那两形上一次超时**极可能是真的还在跑**,
193
- * 至少给一次重试是这条腿唯一的补救。若只用墙钟窗判,宿主拿 120s 的 client 构造本桥时首发超时
194
- * 那一刻 elapsed 已经 ≥ 窗 ⇒ **一次重试都没有**,而这件事在代码里毫无痕迹(注释还写着「恰一次重试」)。
195
- * 🔴 这**不是** [4675] 说的「第二层节奏叠乘」:没有新增任何定时器/退避层,退避仍是同一条
196
- * `backoff` 链;这只是同一个循环上的第二个**终止条件**(发数尽 ∧ 窗尽,两者都满足才停)。
197
- */
198
62
  const DECIDE_TIMEOUT_MIN_ATTEMPTS = 2;
199
- /**
200
- * 超时类重试的总窗(#318 件② 收窄:`10 * 60_000` → 本值)。
201
- *
202
- * ── 取值推导(锚在 {@link DECIDE_ATTEMPT_TIMEOUT_CAP_MS} 上,不是拍脑袋的整数)────────────────
203
- * 窗的语义是**不再起新发**(见下),而每一发超时类失败**本身**就要吃满一个 per-attempt 帽 ⇒
204
- * 实际发数由「总窗 ÷ 帽」决定,且量化得很粗:
205
- * · 窗 ≤ 1 帽 ⇒ 窗判本身当场耗尽;
206
- * · 1 帽 < 窗 ≤ 2 帽 ⇒ 窗判允许恰一次重试(共 2 发);
207
- * · > 2 帽 ⇒ 3 发起步,一路回到分钟级。
208
- * 受理形下超时是**真异常**(不是「还在跑」),所以取**恰一次重试**那一档:一次重试足够吃掉单次
209
- * 网络抖动,再多就是对着一个病态 server 空等。取 90s = 1.5 帽,**刻意落在区间中部**而不是边界
210
- * (120s 恰等于 2 帽 + 退避,会让发数悬在退避时序的一根头发上)。
211
- * ⇒ 在缺省帽上可预算的最坏墙钟 ≈ 60s(首发)+ 0.75s(退避)+ 60s(重发)≈ 121s,而不是旧口径的 10 分钟。
212
- *
213
- * 🔴 **本窗只是墙钟上界,不承诺发数**(复审 [medium] 采纳):宿主可以用非缺省 `timeoutMs` 构造
214
- * client(见 {@link DECIDE_ATTEMPT_TIMEOUT_CAP_MS}),那时「窗 ÷ 帽」得出的发数与这里写的不同。
215
- * 与宿主无关的那条性质(**至少重试一次**)由 {@link DECIDE_TIMEOUT_MIN_ATTEMPTS} 单独保证 ——
216
- * 两个界合取:**发数达标 ∧ 窗尽** 才停。所以在 120s client 上是「2 发、~240s」,在 30s client 上
217
- * 是「3 发、~92s」,在缺省 60s 上是「2 发、~121s」—— 三者都有界,且都拿得到那一次重试。
218
- *
219
- * 🔴 窗的语义是**不再起新发**,刻意不掐在飞那一发(对抗复审议题,驳回后成文):给一发可能已被
220
- * server 受理的 decide 塞截止 signal 换不来任何安全 —— server 侧照跑,客户端只多制造一个「送达
221
- * 未知」。
222
- * 🔴 **写成「帽 × 系数」而不是裸 90_000**:上面那段推导只有在两者绑在一起时才会随 SDK 改帽自动
223
- * 跟手;写裸整数的话,SDK 哪天把帽改成 30s,注释里的「恰一次重试」当天变成假话而代码全绿。
224
- */
225
63
  const DECIDE_TIMEOUT_RETRY_BUDGET_ATTEMPT_MULTIPLE = 1.5;
226
64
  const DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS = DECIDE_ATTEMPT_TIMEOUT_CAP_MS * DECIDE_TIMEOUT_RETRY_BUDGET_ATTEMPT_MULTIPLE;
227
65
  let decideTimeoutRetryBudgetOverrideMs;
228
- /** 测试钩:把超时类重试总窗调小(传 undefined 还原缺省)。 */
229
66
  export function __setDecideTimeoutRetryBudgetForTests(ms) {
230
67
  decideTimeoutRetryBudgetOverrideMs = ms;
231
68
  }
232
69
  function decideTimeoutRetryBudgetMs() {
233
70
  return decideTimeoutRetryBudgetOverrideMs ?? DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS;
234
71
  }
235
- /** 超时类判别(自身或 cause 链上的 TimeoutError —— SDK per-attempt `AbortSignal.timeout` 的产物;
236
- * [4664] 口径下它多半意味着 server 仍在同步跑 resume,不是引擎死了)。 */
237
72
  function isDecideAttemptTimeout(e, depth = 0) {
238
73
  if (depth > 3 || e === null || typeof e !== 'object')
239
74
  return false;
@@ -242,17 +77,11 @@ function isDecideAttemptTimeout(e, depth = 0) {
242
77
  return true;
243
78
  return isDecideAttemptTimeout(o.cause, depth + 1);
244
79
  }
245
- /** 瞬断类网络错误码(自身或 cause 链上;undici 的 fetch failed 把真因挂在 cause)。 */
246
80
  const TRANSIENT_NETWORK_CODES = new Set([
247
81
  'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN',
248
82
  'ENETUNREACH', 'EHOSTUNREACH', 'ECONNABORTED',
249
83
  'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_SOCKET',
250
84
  ]);
251
- /**
252
- * decide 出站失败的瞬断判别(module 私有 —— 唯一消费点是 {@link HitlBridge} 的 decide choke point)。
253
- * 🔴 方向:判不出一律**非瞬断**(fail 到「不重试」侧 —— 语义拒绝被误判成瞬断才是真事故:那会把
254
- * 一次已被拒绝的决断再发一遍)。带数字 `status` 的错误 = 引擎**答了**,无论 4xx/5xx 都不是瞬断。
255
- */
256
85
  function isTransientDecideTransportFailure(e, depth = 0) {
257
86
  if (depth > 3 || e === null || typeof e !== 'object')
258
87
  return false;
@@ -269,96 +98,16 @@ function isTransientDecideTransportFailure(e, depth = 0) {
269
98
  return true;
270
99
  return isTransientDecideTransportFailure(o.cause, depth + 1);
271
100
  }
272
- /**
273
- * decide 出站在瞬断类失败上重试一次**仍未送达**(两发都没拿到引擎的语义答复)。
274
- *
275
- * 🔴 判别位契约:消费方(`toolApprovalWire` 的 catch 臂 / `parkResolver.surfaceGateAndDecide`)
276
- * 据 `instanceof` 在 outcome 上 stamp `retryExhausted: true`,parkResolver 对该位走**重呈臂**
277
- * (re-attach ⇒ durable 流重放 park ⇒ 同一张卡重新交给用户),不再合成 `hitl_unanswered` 把 turn
278
- * 判死 —— run 仍 parked、pending 行仍可决,判死是三条出路里唯一不可逆的那条。
279
- * 🔴 message 刻意避开 `isAlreadyResolvedGateReason` 的词表(no pending checkpoint / resolved /
280
- * already / not found):被那把兜底尺误判成「已解决」会走成静默 reattach,判别位就白铸了 ——
281
- * pure 门(hitl-gate-honesty F6-d)有常驻负控钉着这一条。
282
- */
283
101
  export class DecideTransportRetryExhaustedError extends Error {
284
102
  attempts;
285
- constructor(
286
- /** 实际发出的次数(网络断类 = 2:首发 + 单次重试;超时类 = 总窗内发出的全部)。 */
287
- attempts, lastFailureText) {
103
+ constructor(attempts, lastFailureText) {
288
104
  super(`decide did not reach the engine after ${attempts} attempts (transient transport failure): ${lastFailureText}`);
289
105
  this.attempts = attempts;
290
106
  this.name = 'DecideTransportRetryExhaustedError';
291
107
  }
292
108
  }
293
- /**
294
- * 「本层对工具族没有意见」的谓词(`HitlBridge.pendingForDecide` 用)。桥不是 wire 模块:它决断的
295
- * 就是人刚刚看过的那张门,族属由那两条 wire 在**取件时**说了算。写成具名常量而不是就地 `() => true`
296
- * —— 一个匿名恒真谓词读起来像「忘了填」,而这里的恒真是**有理由**的(理由即本注)。
297
- */
298
109
  const ANY_TOOL_FAMILY = () => true;
299
- /**
300
- * The single source for "which `PendingCheckpoint` row is the human about to decide on", used by the two
301
- * decision wires (`toolApprovalWire.surfaceFsApprovalAndDecide` / `askGateWire.surfaceGateAndDecide`)
302
- * BEFORE they know which specific call/question they're presenting — that is the FIRST fetch, the one
303
- * that decides what gets rendered on the card/overlay.
304
- *
305
- * REF-CC-029(2026-08-02,消 hitl2-08/hitl2-01 双病根):this used to be three near-identical copies
306
- * (this file's own `HitlBridge.findPending` plus one each in `toolApprovalWire.ts` / `askGateWire.ts`),
307
- * and only `HitlBridge.findPending`'s copy had a `?? pending[0]` last-resort fallback (REF-CC-022) that
308
- * could resolve a DIFFERENT session's or a background subagent lane's pending row once this taskId's own
309
- * rows were gone — they all share the same `/v1/approvals` queue. Single-sourcing this lookup here means
310
- * that divergence can't recur, and passing the resolved row straight into `decideTool`/`answerQuestion`'s
311
- * `preResolvedPending` param closes the TOCTOU window between "the row the human saw" (this fetch) and
312
- * "the row decide resolves" (previously a SECOND independent fetch inside the bridge).
313
- *
314
- * Match by `taskId` + a caller-supplied predicate over the row's `toolName` (the tool family this wire
315
- * cares about, e.g. fs-write vs AskUserQuestion) first; fall back to ANY row for this `taskId` (a gate
316
- * whose toolName the predicate doesn't recognize, but which still belongs to this run). Never `pending[0]`
317
- * — a typed failure when nothing for this taskId is queued.
318
- *
319
- * F4947-1(2026-08-22,0.41.0):`callId` 是**更强的身份腿**,在场时先于上面两条跑。它来自
320
- * `HitlBridge.decideTool`/`answerQuestion` 的 `toolUseID`(CC 的 tool_use id)—— 那两处此前走的是
321
- * 本类里一个**同名私有** `findPending`,REF-CC-029 的收编没覆盖到它、P-30 的门种闸也没加到它,
322
- * 于是真实生产入口(`makeHitlCanUseTool` → `decideTool`,从不传 `preResolvedPending`)一直在一条
323
- * **没有闸**的复刻上跑(test 线 P0-KPI 复现)。收口方式是把私有件删掉改调本函数,而不是给它补第二
324
- * 份过滤 —— 复刻正是这条缺陷的成因。身份腿同样只在**过完门种闸的行**里找:一个 `plan_review` 行
325
- * 哪怕 callId 逐字命中也不该被工具审批 decide 掉(server 那边照样 409 `gate_not_tool_approval`)。
326
- *
327
- * P-30(2026-08-14):both legs only consider rows whose `gateKind` belongs to the ask/approval family
328
- * ({@link askParkForeignGateKind} — the same single-source guard `classifyAskParkRows` uses). Without it
329
- * the ANY-row fallback would hand a `plan_review`/`resource_limit` row to the tool-approval wire — an
330
- * empty-toolName card whose decide then 409s (`gate_not_tool_approval`). A foreign-kind row is filtered
331
- * out, not a rejection of the whole queue: a genuine approval row for the same task still resolves.
332
- * Rows with no `gateKind` (pre-`gate_kind` history) keep the old behaviour — no stronger signal exists.
333
- */
334
- export async function findPendingForTask(client, taskId, matches, opts,
335
- /**
336
- * F4947-1:调用方手上的**更强身份** —— CC 的 `toolUseID`(`HitlBridge`),或这张 park 的
337
- * wire 身份(server ≥7.41.0,两条决断腿)。在场 ⇒ 先在**本 taskId** 的行里按
338
- * `boundCallId`/`toolCallId` 逐字命中(仍在门种闸之内);不命中再走下面两条 taskId 腿。
339
- *
340
- * 🔴 **刻意不 fail-closed**(异源复审二审 finding① 的部分驳回,理由成文):身份在场而队列里
341
- * 没有那一行时**照旧回落**,不铸「身份不匹配」的硬拒。两条理由:
342
- * · 回落的失效面 = 与本参数出现之前**逐字节相同**(那时压根没有身份腿);fail-closed 的失效面
343
- * = **每一次审批都决断不了**(用户点了 Yes 也过不去)。代价严重不对称;
344
- * · 这个 join(checkpoint 的 `pendingAction.toolCallId` ↔ `/v1/approvals` 富行的
345
- * `toolCallId`/`boundCallId`)本仓**没有对真 server 的实证**,只有上游帖面的口径。拿一个
346
- * 没实证的键去关整条 HITL 的门,是把「可能对不上」赌成「一定对得上」。
347
- * 跨 run 那一半的风险由上面的 taskId 约束关掉;「决断落在同 task 的另一行」这一格与本参数出现
348
- * 之前同形,且下游 `dropHeldForDecidedPark` 对这一格自带保守判(身份对不上 ⇒ 一帧都不摘)。
349
- */
350
- callId,
351
- /**
352
- * 身份腿的**族闸**(整行可读)。缺省 = 退回 `matches(toolName)`。
353
- *
354
- * 🔴 为什么必须能读**整行**而不是只读 toolName(五审 finding①):工具审批族有**两条腿** ——
355
- * 一等 `kind === 'tool_approval'`(放行**任意** toolName)与 fs 写 / shell 名字腿。而 wire 模块
356
- * 传下来的 `matches` 只有名字腿那半(它服务的是「同族第一行」那条 leg 2)。拿它当身份闸,一个
357
- * **合法的 kind-only 行**即使 taskId+callId 逐字命中也会被判出局 ⇒ 回落挑了同 task 的另一行,
358
- * 卡面与决断全落在别的审批上、真 gate 继续 suspended。⇒ 调用方按自己那条腿的**完整**族判据
359
- * 供给本闸(fs 腿 = `isToolApprovalGate({kind: row.gateKind, toolName})`,ask 腿 = `isAskTool`)。
360
- */
361
- identityAccepts = (r) => matches(typeof r.toolName === 'string' ? r.toolName : undefined)) {
110
+ export async function findPendingForTask(client, taskId, matches, opts, callId, identityAccepts = (r) => matches(typeof r.toolName === 'string' ? r.toolName : undefined)) {
362
111
  let rows;
363
112
  try {
364
113
  rows = (await client.approvals.list(opts)).pending;
@@ -367,29 +116,10 @@ identityAccepts = (r) => matches(typeof r.toolName === 'string' ? r.toolName : u
367
116
  return { ok: false, reason: `approvals.list failed: ${String(e)}` };
368
117
  }
369
118
  const decidable = rows.filter((r) => askParkForeignGateKind(r) === null);
370
- // 🔴 身份腿**约束在本 taskId 之内**(异源复审二审 finding①):`/v1/approvals` 是 scope 级单队列,
371
- // 别的 run / 后台子代 lane 的行同在表里 —— 一个撞名或被复用的 callId 就能把**别人会话**的 pending
372
- // 行交给这次 decide。REF-CC-022 拆掉 `?? pending[0]` 防的正是这件事;身份腿是「更强的身份」,
373
- // 不是「可以跨 run 的身份」,不能自带一个新的绕行口。
374
119
  const identityRow = callId !== undefined
375
120
  ? decidable.find((r) => r.taskId === taskId && (r.boundCallId === callId || r.toolCallId === callId))
376
121
  : undefined;
377
- // 🔴 身份命中还要过**族闸**(四审 finding② / 五审 finding①,闸读整行见 `identityAccepts`):
378
- // 门种闸只挡 plan_review/resource_limit 这类外族,AskUserQuestion 与工具审批**同在** `decidable` 里。
379
- // 一个陈旧/错配/被复用的 park 身份否则就能让 fs 腿选中一行 ask(反之亦然),而那一行随后原样当
380
- // `preResolvedPending` 去呈卡 + decide —— 卡面全错、决断打在另一个 checkpoint 上。
381
122
  const identityConflicts = identityRow !== undefined && !identityAccepts(identityRow);
382
- // 🔴 身份证据与本腿族属**冲突**时,这一轮的候选池整体收紧到族闸放行的行(六审 finding① +
383
- // 七审 finding①)。两步演进的理由要写清楚:
384
- // · 只拦身份腿是自相矛盾的 —— 最后那条「同 taskId 任意行」兜底会把**同一行**重新捞出来
385
- // (队列里只剩它时必然发生),刚判定「这一行不是本腿的」,转头又拿它去呈卡 + 决断;
386
- // · 只剔除命中的**那一个对象**也不够 —— 同 task 排着两条反族行时,删掉第一条,兜底照样捡起
387
- // 第二条(七审实撞)。
388
- // 冲突 = wire 数据自相矛盾(park 说 gate 是 A 族、身份却指着 B 族的行),这一轮**整体**保守:
389
- // 只在族闸放行的行里选;一条都不剩 ⇒ 诚实 `no_pending`(调用方走既有 fail-soft / 重呈路)。
390
- // 🔴 收紧**只发生在冲突路径上**:身份缺席(绝大多数路径)时候选池一个字节不动 —— 那条
391
- // 「任意行」兜底本来就是给 kind-only / 名字腿认不得的合法行留的(见上方 leg 说明),不能被
392
- // 一条与它无关的规则顺手削掉。
393
123
  const pool = identityConflicts ? decidable.filter((r) => identityAccepts(r)) : decidable;
394
124
  const pending = (identityConflicts ? undefined : identityRow) ??
395
125
  pool.find((r) => r.taskId === taskId && matches(typeof r.toolName === 'string' ? r.toolName : undefined)) ??
@@ -398,25 +128,17 @@ identityAccepts = (r) => matches(typeof r.toolName === 'string' ? r.toolName : u
398
128
  return { ok: false, reason: 'no pending checkpoint for this run (resolved/expired?)', code: 'no_pending' };
399
129
  return { ok: true, pending, gatedCallId: pending.toolCallId ?? pending.boundCallId ?? undefined };
400
130
  }
401
- /**
402
- * The bridge state. One per run/session. The shell feeds it `suspended` events (observe) and the human's
403
- * outcome (decide / answer / planReview); it owns the `approvals.list()` join + the verbatim binding echo.
404
- */
405
131
  export class HitlBridge {
406
132
  client;
407
133
  taskId;
408
134
  active = null;
409
- constructor(client,
410
- /** The suspended run's taskId — the handle plan_review/resume address (RunReceipt.taskId). */
411
- taskId) {
135
+ constructor(client, taskId) {
412
136
  this.client = client;
413
137
  this.taskId = taskId;
414
138
  }
415
- /** Observe a downstream event; latch the gate on `suspended`, clear it on a resuming/terminal arm. */
416
139
  observe(ev) {
417
140
  switch (ev.type) {
418
141
  case 'suspended':
419
- // gate may be null (Event_suspended 'null' arm) → a generic "needs you" pause (contract/08 CS-9).
420
142
  if (ev.gate) {
421
143
  this.active = { gate: ev.gate, seq: eventSeq(ev), reopened: suspendedReopenOf(ev) };
422
144
  }
@@ -429,63 +151,26 @@ export class HitlBridge {
429
151
  this.active = null;
430
152
  break;
431
153
  default:
432
- // running again (text / tool_start / turn_end / …) → the gate cleared when decide resumed the stream.
433
154
  if (this.active && (ev.type === 'text' || ev.type === 'tool_start' || ev.type === 'turn_end')) {
434
155
  this.active = null;
435
156
  }
436
157
  break;
437
158
  }
438
159
  }
439
- /** The gate currently awaiting a human, if any. The shell branches on `gate.kind` to pick the chrome. */
440
160
  currentGate() {
441
161
  return this.active?.gate ?? null;
442
162
  }
443
- /**
444
- * 0.74.3 CC-73:当前挂起是不是一次 reopen(`suspended.reopened` 三态);没挂起 ⇒ `null`。
445
- * 消费方据此把「真 reopen 成功、重试会重放」那半句说出来:`reopened` 在场码 ⇒ 卡仍 pending、决定没被消费、可再决;
446
- * `not_reopened` / `unstated` ⇒ 普通挂起,一个字都不多说。
447
- */
448
163
  currentGateReopen() {
449
164
  return this.active?.reopened ?? null;
450
165
  }
451
- /**
452
- * Fetch the `PendingCheckpoint` this decide/answer is about to resolve, for the callers that do NOT
453
- * already have one in hand (see `decideTool`/`answerQuestion`'s `preResolvedPending` param — the wire
454
- * modules that surface a card/overlay BEFORE deciding fetch the row themselves and pass it straight
455
- * through, closing the TOCTOU window between "the row the human saw" and "the row decide resolves",
456
- * REF-CC-029/hitl2-08).
457
- *
458
- * 🔴 F4947-1(2026-08-22,0.41.0):本方法**没有自己的查找逻辑** —— 它是 {@link findPendingForTask}
459
- * 的一层出参映射,一个字节的行过滤都不在这里。上一版是一个与那个单源导出**撞名**的私有复刻
460
- * (`findPending`),REF-CC-029 的三合一没覆盖到它、P-30 的门种闸也只加在导出那一份上,于是
461
- * `makeHitlCanUseTool` → `decideTool`(真实用户点 Yes/No 那条路,从不传 `preResolvedPending`)
462
- * 一直在没有闸的老复刻上跑:同 task 停着 `plan_review` 行时,它被当成「这个 run 的 pending」
463
- * 交给工具审批 decide(空 toolName 卡 / 409 `gate_not_tool_approval`)。
464
- * 名字也一并换掉:撞名正是「看起来只有一份实现」的成因。
465
- *
466
- * 谓词传 {@link ANY_TOOL_FAMILY}:桥这一层对工具族**没有意见**(人刚刚决断的是哪张门就是哪张),
467
- * 与旧私有件的 taskId 兜底逐字同语义;身份腿(`toolUseID`)由单源函数的 `callId` 参数承接。
468
- */
469
166
  async pendingForDecide(toolUseID, opts) {
470
167
  const found = await findPendingForTask(this.client, this.taskId, ANY_TOOL_FAMILY, opts, toolUseID);
471
168
  if (found.ok)
472
169
  return found.pending;
473
- // 🔴 `no_pending`(良性缺席)才是「没有行可决」;`approvals.list` 自身失败必须照旧**抛**出去。
474
- // 旧私有件让 list 的错原样逃逸,而下游 `parkResolver` 对 `no_pending` 的处置是「这张 gate
475
- // 早就被解决了 ⇒ 继续读流」—— 把一次网络失败降级成那个码 = 谎报已解决(#110 烧会话的路径)。
476
170
  if (found.code === 'no_pending')
477
171
  return null;
478
172
  throw new Error(found.reason);
479
173
  }
480
- /**
481
- * Build the D-1 binding off the pending row the human SAW. Echo VERBATIM — NEVER recompute the hash
482
- * (contract/04 §2.2). `checkpointToken` is DELIBERATELY never set (deprecated legacy path;
483
- * compliant clients send ONLY the two-tuple, approvals.ts:23-27).
484
- *
485
- * pre-D-1 的 `{}` 回落臂已删(2026-08-03,#117 提货 / [2356]#2):server 5.x 的 decide 三键必填、
486
- * 缺键 400,「发一个注定 400 的请求」不如在本地响亮失败——pending 行缺绑定键只可能来自
487
- * 不再支持的旧 worker 或坏数据,fail-loud 带坐标好过静默烧一轮 HTTP。
488
- */
489
174
  bindingOf(p) {
490
175
  if (p.boundCallId === undefined || p.boundInputHash === undefined) {
491
176
  throw new HitlSafetyError(`pending checkpoint ${p.id ?? '<no-id>'} is missing D-1 binding fields ` +
@@ -494,41 +179,7 @@ export class HitlBridge {
494
179
  }
495
180
  return { boundCallId: p.boundCallId, boundInputHash: p.boundInputHash };
496
181
  }
497
- // ── §2. Permission gate: approve / deny a tool ──────────────────────────────
498
- /**
499
- * Resolve a `human` / `irreversible_ask` permission gate (contract/04 §2, §6.2; 08 IH-4/IH-10).
500
- *
501
- * - `approve` → `{decision:"approve", boundCallId, boundInputHash}`; `updatedInput` rides along for
502
- * approve-with-edit (applied AFTER the binding check — the hash still binds the ORIGINAL input).
503
- * - `deny` → `{decision:"deny", reason}`.
504
- *
505
- * 🔴 **§2.4「DENY-abort」撤稿(0.42.0;server [4833] 明请,契约成文 `4631a0f` 随 7.39 出)**。
506
- * 本段原文写的是:「CANCEL a suspended run by DENYING, never by `runs.cancel` (**which 409s on a
507
- * suspended run** — contract/04 §2.4); the deny-and-abort `interrupt:true` EFFECT is *the run ends
508
- * after the deny*」。**三句话里有两句已被上游证伪,逐条**:
509
- * · **「deny 用来 cancel 一条 run」** —— `ASSISTANT-WIRE-CONTRACT.md` §4a 逐字反过来说:
510
- * **DENY is a TOOL-level answer, NEVER a run kill**;客户端不得把用户的拒绝译成 cancel。
511
- * 两个动词的 wire 判别式是 `cancelled`(真取消)vs `gate.batch_halted`(裸拒的兄弟结算)。
512
- * · **「`runs.cancel` 对 suspended run 回 409」** —— 自 server [868] 起**就地取消**:
513
- * `runs.js` 的 cancel 腿对 SUSPENDED/needs_review 先结算 pending checkpoint(CAS expire)再
514
- * 终态化,`cancelSuspended` 有实体,409 只剩 `conflict.approval_settled` 一条
515
- * (本仓 `docs/fresh-scan-client-core-2026-08-08.md` B型-5 已按真字节证伪,当时未跟修注释)。
516
- * · **仍然成立的那一句**:deny 之后 run 是否结束由**后端编排**决定,不是 wire 上的旗标
517
- * (裸拒 ⇒ `gate.batch_halted` + `haltedOnUserRejection`;带留言拒 ⇒ run 续跑)。
518
- * ⇒ 本方法的**行为一字未改**(它本来走的就是 §4a 说的那条 TOOL 级 decide 通路);改的是这段
519
- * 引用错权威、并把一条早已失效的 409 断言当理由的散文。
520
- *
521
- * The resumed run continues its SAME durable stream. A binding mismatch (409) is re-raised as a
522
- * `HitlSafetyError('binding_mismatch')` — the caller re-presents, NEVER auto-retries.
523
- */
524
- async decideTool(outcome, toolUseID, opts,
525
- /** REF-CC-029(2026-08-02):the pending row the caller ALREADY fetched (via `findPendingForTask`,
526
- * below) to render the card/overlay the human just decided on. When supplied, this method skips
527
- * its own `approvals.list()` — closing the TOCTOU window between "the row the human saw" and "the
528
- * row decide resolves" (two independent fetches could otherwise land on different rows if the
529
- * first one got resolved/expired in between, hitl2-01/hitl2-08). Omit only when the caller has no
530
- * such row in hand(缺省回落 `pendingForDecide` — 见其头注,那是单源 `findPendingForTask` 的一层出参映射). */
531
- preResolvedPending) {
182
+ async decideTool(outcome, toolUseID, opts, preResolvedPending) {
532
183
  const pending = preResolvedPending ?? (await this.pendingForDecide(toolUseID, opts));
533
184
  if (!pending) {
534
185
  throw new HitlSafetyError('no pending checkpoint to decide (resolved/expired under the human) — refetch + re-present', 'no_pending');
@@ -537,12 +188,7 @@ export class HitlBridge {
537
188
  ? {
538
189
  decision: 'approve',
539
190
  ...this.bindingOf(pending),
540
- // updatedInput is applied AFTER the binding check; binding still binds the ORIGINAL input.
541
191
  ...(outcome.updatedInput !== undefined ? { updatedInput: outcome.updatedInput } : {}),
542
- // accept-session 的 durable-park 半场(server 1.191 decide 契约 `remember:"session"` →
543
- // approvalExemptionStore.grant(sessionId, toolName)= 同 session 该工具后续 ask 免 park。
544
- // 需要 binding(上面已 echo)。调用方只在三选第 2 项才带;老 server 收到未知键按契约 400,
545
- // 故带 remember 的 decide 失败时调用方要能回退纯 approve(见 liveToolApprovalWire)。
546
192
  ...(outcome.remember !== undefined ? { remember: outcome.remember } : {}),
547
193
  }
548
194
  : {
@@ -552,30 +198,7 @@ export class HitlBridge {
552
198
  };
553
199
  return this.decideRaw(pending.sessionId, decision, opts);
554
200
  }
555
- // ── §3. AskUserQuestion: answer through the same gate ───────────────────────
556
- /**
557
- * Answer an `AskUserQuestion` gate (contract/04 §4; 08 CS-16). Same suspended → decide loop as a
558
- * permission gate, but the answer rides `ApprovalDecision.answer` as the worker-validated
559
- * `{ answers: [{ header, selected, note? }] }` shape. The D-1 binding STILL binds (it is still a
560
- * checkpoint). Each answer's `header` MUST match a question header (the worker 400s on mismatch).
561
- *
562
- * 🔴 FIX②(2026-08-07)—— **本地 fail-loud**:此前本方法对 `answers: []` 与「某条 `selected: []`」
563
- * 零校验,一条注定 400 的请求照样出门。这不是「多一次往返」而已 ——
564
- * · 用户面只看到「作答失败」,而失败的原因(空作答)在壳侧一个字都没说;
565
- * · `ToolPermissionDecision.answer` 的头注自己写着「a silently emptied answer is the user's
566
- * choice being thrown away without anyone noticing」—— 那条纪律此前只写在类型注释里,
567
- * 没有任何一层真的执行它;
568
- * · `{answers:[]}` 在 wire 上另有确切含义(question 域 **deny** 的 NO_HUMAN 形),把它当成一次
569
- * approve 发出去 = 拿 deny 的载荷冒充 approve。
570
- * 三形一律抛 `HitlSafetyError('empty_answer')`,一次 decide 都不发:空 `answers[]` /
571
- * 任一条 `selected[]` 为空 / 任一条 `header` 为空串。
572
- */
573
- async answerQuestion(answers, toolUseID, opts,
574
- /** REF-CC-029(2026-08-02):see `decideTool`'s same-named param — the pending row the caller already
575
- * fetched via `findPendingForTask`. */
576
- preResolvedPending) {
577
- // 🔴 校验在取件**之前**:入参坏了跟 pending 行在不在场无关,而先 list 再拒会把
578
- // 「你的作答是空的」这条真判词换成「no_pending」那条假判词(行恰好过期时)。
201
+ async answerQuestion(answers, toolUseID, opts, preResolvedPending) {
579
202
  assertAnswersSendable(answers);
580
203
  const pending = preResolvedPending ?? (await this.pendingForDecide(toolUseID, opts));
581
204
  if (!pending) {
@@ -588,28 +211,11 @@ export class HitlBridge {
588
211
  };
589
212
  return this.decideRaw(pending.sessionId, decision, opts);
590
213
  }
591
- // ── §4. plan_review: the distinct wire ──────────────────────────────────────
592
- /**
593
- * Resolve a plan-mode gate (contract/04 §3; 08 IH-6). Routed to `assistant.planReview`, NOT `decide`/
594
- * `resume`. Also guards the `editedPlan` invariant (REQUIRED iff `decision==="edit"`, FORBIDDEN
595
- * otherwise → 400) so the bad call never leaves the shell — that check is unconditional (no
596
- * `observe()` dependency). The gate-kind guard below is host-optional (see its own comment); the
597
- * server's 409 `gate_not_plan_review` is the real backstop either way.
598
- */
599
214
  async reviewPlan(outcome, opts) {
600
- // Gate-kind guard (contract/04 §3.1): plan_review parks as needs_review / plan_review, distinct from the
601
- // approval family. REF-CC-028(2026-08-02,评注现势):this.active only latches when the host calls
602
- // `observe(ev)` on the downstream stream — BOTH production construction points
603
- // (askGateWire.ts / toolApprovalWire.ts) build a fresh `HitlBridge` per decide and never call
604
- // `observe`, so `this.active` is null and this guard is a no-op there today. It is a host-optional
605
- // auxiliary: a host that DOES drive `observe()` gets a local refuse instead of a round-trip; a host
606
- // that doesn't (today's shell) relies entirely on the server's 409 `gate_not_plan_review` fail-closed
607
- // path — the safety property holds either way, this guard just saves the round-trip when wired.
608
215
  const kind = this.active?.gate.kind;
609
216
  if (kind && kind !== 'plan_review' && kind !== 'needs_review') {
610
217
  throw new HitlSafetyError(`cannot plan-review a "${kind}" gate — a plan decision must never resolve a tool approval`, 'wrong_gate');
611
218
  }
612
- // editedPlan invariant (contract/04 §3.1): REQUIRED iff edit, FORBIDDEN otherwise.
613
219
  if (outcome.decision === 'edit') {
614
220
  if (!outcome.editedPlan || outcome.editedPlan.length === 0) {
615
221
  throw new HitlSafetyError('plan-review "edit" requires a non-empty editedPlan', 'bad_plan_edit');
@@ -618,8 +224,6 @@ export class HitlBridge {
618
224
  else if (outcome.editedPlan !== undefined) {
619
225
  throw new HitlSafetyError(`plan-review "${outcome.decision}" must NOT carry editedPlan`, 'bad_plan_edit');
620
226
  }
621
- // 0.72.13 CC-46:permissionModeAfter invariant(契约 §4c)—— 只配 approve、闭集两词;坏形本地拦,一个字节都不出门
622
- // (server 会 400 field_conflict / field_invalid,而那次决断就整个丢了)。拒文不回显调用方给的值。
623
227
  const modeAfter = outcome.permissionModeAfter;
624
228
  if (modeAfter !== undefined) {
625
229
  if (outcome.decision !== 'approve') {
@@ -629,16 +233,11 @@ export class HitlBridge {
629
233
  throw new HitlSafetyError('plan-review permissionModeAfter must be "default" or "acceptEdits"', 'bad_plan_mode');
630
234
  }
631
235
  }
632
- // sdk 9.6.0 的 PlanReviewRequest 尚未声明 permissionModeAfter ⇒ 交集型加一可选位(不断言、不 any);缺席 = 键不落。
633
236
  const req = { decision: outcome.decision };
634
237
  if (modeAfter !== undefined && isPlanReviewModeAfter(modeAfter))
635
238
  req.permissionModeAfter = modeAfter;
636
- // eopt:上面的 invariant 检查已经保证 decision==='edit' ⇒ editedPlan 非空,但那道保证隔着一个
637
- // 已经关闭的 if 块,TS 不跨块记账——这里重新直接判一次 undefined,narrowing 与赋值同一表达式内完成
638
- // (PlanReviewRequest 是 SDK 外部 wire 型,不能改;诚实缺席 = 键不落,不传 `editedPlan: undefined`)。
639
239
  if (outcome.decision === 'edit' && outcome.editedPlan !== undefined)
640
240
  req.editedPlan = outcome.editedPlan;
641
- // 0.28.0 发版扫描 F2:reason 经窄化器(超 4096 截断防 413 丢整次计划决断);空白 ⇒ 键不落。
642
241
  if (outcome.reason !== undefined) {
643
242
  const r = denyReasonForWire(outcome.reason, `plan-review ${this.taskId}`);
644
243
  if (r !== undefined)
@@ -646,15 +245,6 @@ export class HitlBridge {
646
245
  }
647
246
  return this.client.assistant.planReview(this.taskId, req, opts);
648
247
  }
649
- // ── §5. resource_limit: continue ────────────────────────────────────────────
650
- /**
651
- * Continue a `resource_limit` slice pause (contract/04 §6.1; 08 IH-9). Bodyless `assistant.resume` — NOT
652
- * `decide`. A `human`/`irreversible_ask` gate routed here would 409 `gate_not_resumable` (a "continue"
653
- * must NEVER bypass a human approval). REF-CC-028(2026-08-02,评注现势):the local guard below is
654
- * host-optional — it only fires when the host drives `observe(ev)` (neither production call site does
655
- * today, see `reviewPlan`'s comment); the server's 409 is the fail-closed backstop that actually holds
656
- * this invariant.
657
- */
658
248
  async resumeResourceLimit(opts) {
659
249
  const kind = this.active?.gate.kind;
660
250
  if (kind && kind !== 'resource_limit') {
@@ -662,13 +252,7 @@ export class HitlBridge {
662
252
  }
663
253
  return this.client.assistant.resume(this.taskId, opts);
664
254
  }
665
- // ── decide transport (the single choke point; maps 409 binding mismatch → safety stop) ──
666
255
  async decideRaw(sessionId, decision, opts) {
667
- // Inkglow-1085 P0a([4664] 对表后形):瞬断类失败带退避有界重试 —— 超时类按长调用总窗,
668
- // 网络断类恰一次;为什么重试不违反「decide 绝不 double-act」、哪些失败绝不重试,见文件上方
669
- // DECIDE_TRANSPORT_RETRY 段总注。
670
- // 🔴 经函数读 aborted(waitForClaimRelease.isAborted 同注):它在 await 两侧会变,直接读两次
671
- // 会被 tsc 控制流分析把第二次窄成恒假比较(TS2367)。
672
256
  const callerAborted = () => opts?.signal?.aborted === true;
673
257
  const startedAt = Date.now();
674
258
  let attempts = 0;
@@ -678,45 +262,20 @@ export class HitlBridge {
678
262
  attempts++;
679
263
  try {
680
264
  const r = await this.client.approvals.decide(sessionId, decision, opts);
681
- // 🔴 **B-070(0.65.0):200 是投递受理,不是「门已解决」——所以这里不再清 `active`。**
682
- // sdk README §9.0.0 逐字:「workflow 车道的 200 只是**投递受理** —— 子代的 checkpoint 仍
683
- // pending,`/v1/approvals` 上那张卡**可能还在**。据 200 立刻把卡从 UI 抹掉,用户会看到
684
- // 一张『批过了却还在』的幽灵卡」;core `runner/contracts.d.ts:1492` 同向(这一步
685
- // **launches the run that re-invokes**)。修前这一行正上方的注释自己写着「the gate clears
686
- // on the next running arm」—— 而代码在同一拍就把它清了,注释与码互相矛盾,矛盾的那一半
687
- // 是码。
688
- // ⇒ 判据锚换到**真正决定结果的量**上:`active` 由 {@link HitlBridge.observe} 在**流上的下一条
689
- // running 臂 / 终态臂**上清([anchor-on-the-deciding-quantity])。那条腿修前就在
690
- // (`observe` 的 `text`/`tool_start`/`turn_end`/`done`/`failed` 五臂),本批只是把抢跑的
691
- // 那一行拿掉,没有新增任何清除路径。
692
- // ⚠️ 影响面如实说:`active` 今天只在宿主真的驱动 `observe()` 时才 latch(两处生产调用点都
693
- // 不驱动,见 `reviewPlan` 顶注),所以这一行的现实效果 = 对已驱动 observe 的宿主
694
- // (desktop/web 的座位层)不再谎报「门没了」;不驱动的宿主逐字节不变。
695
265
  return r;
696
266
  }
697
267
  catch (e) {
698
268
  if (isBindingMismatch(e)) {
699
- // contract/04 §2.2 / §9.1: a binding mismatch means the pending action changed under the human.
700
- // SAFETY signal — re-present to the human, NEVER auto-retry / auto-re-decide.
701
269
  throw new HitlSafetyError('approval_binding_mismatch — the pending action changed under the human; refetch + re-present', 'binding_mismatch');
702
270
  }
703
- // 调用方已中止(用户 Esc)⇒ 这个 AbortError 是人按的,不是瞬断 —— 原错上抛,零重试。
704
271
  if (!isTransientDecideTransportFailure(e) || callerAborted())
705
272
  throw e;
706
273
  if (isDecideAttemptTimeout(e)) {
707
- // #318 件② 后口径:受理形(server ≥7.37)下 decide 受理即回,一次 per-attempt 帽掐断
708
- // **是真异常**,不再是「server 还在跑 resume」的常态 —— 所以总窗只留恰一次重试的量
709
- // (推导见 DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS)。重发仍然安全:重复 decide 由 server CAS
710
- // 保证不双跑,首发其实送达时下一发只会撞 4xx(conflict/not-found ⇒ 上抛,已解决判据接手)。
711
- // 🔴 **两个界合取**(复审 [medium] 采纳):发数没达标就一定再发一次(与宿主的
712
- // per-attempt timeoutMs 无关),达标之后才由墙钟窗决定还发不发。少了前半句,
713
- // 120s client 的宿主一次重试都拿不到;少了后半句,30s client 会一路重试到分钟级。
714
274
  if (attempts >= DECIDE_TIMEOUT_MIN_ATTEMPTS && Date.now() - startedAt >= decideTimeoutRetryBudgetMs()) {
715
275
  throw new DecideTransportRetryExhaustedError(attempts, String(e));
716
276
  }
717
277
  }
718
278
  else if (netRetriesUsed >= 1) {
719
- // 网络断类(连语义答复都没有)重试恰一次 —— 引擎真死时让失败尽快显形。
720
279
  throw new DecideTransportRetryExhaustedError(attempts, String(e));
721
280
  }
722
281
  else {
@@ -727,51 +286,30 @@ export class HitlBridge {
727
286
  'at-most-once-effective: a landed first shot turns the retry into a 4xx, never a second act)');
728
287
  await abortableSleep(backoff, opts?.signal ?? new AbortController().signal);
729
288
  backoff = Math.min(backoff * 2, DECIDE_RETRY_BACKOFF_MAX_MS);
730
- // 退避期间被中止 ⇒ 不再补发(abortableSleep 对 abort 是提前 resolve,不抛)。
731
289
  if (callerAborted())
732
290
  throw e;
733
291
  }
734
292
  }
735
293
  }
736
294
  }
737
- /**
738
- * Build a `CanUseToolFn` backed by the bridge. CC's loop calls it before any guarded tool; it surfaces the
739
- * gate via `prompt`, routes the human's decision to `approvals.decide` (binding echoed verbatim), and
740
- * returns the same `PermissionDecision` to unblock the loop. A `forceDecision` (CC's pre-resolved path)
741
- * short-circuits the prompt but STILL drives the decide verb so the backend resolves the checkpoint.
742
- */
743
295
  export function makeHitlCanUseTool(bridge, prompt) {
744
296
  return async (tool, input, _toolUseContext, _assistantMessage, toolUseID, forceDecision) => {
745
297
  const gate = bridge.currentGate();
746
298
  const decision = forceDecision ??
747
299
  (await prompt({ toolName: tool.name, input, toolUseID, gate }));
748
300
  if (decision.behavior === 'deny') {
749
- // Deny → 一次 **TOOL 级**的 deny 应答(server `ASSISTANT-WIRE-CONTRACT.md` §4a;0.42.0 撤稿:
750
- // 原文写的是「cancel-by-deny (contract/04 §2.4)」,而 §4a 逐字反对把拒绝读成 run kill ——
751
- // 详见 `decideTool` 头注的撤稿段)。The deny message rides `reason`.
752
- // 0.28.0 发版扫描 F1(P2):message 逐字嵌原始命令(壳侧 bashPermissions 无上限)——必须与
753
- // 卡腿同门经窄化器截到 4096,否则 server 413 丢的是整次 deny(run 留 suspended)。
754
301
  await bridge.decideTool({ decision: 'deny', reason: denyReasonForWire(decision.message, `canUseTool ${toolUseID}`) ?? DEFAULT_DENY_REASON }, toolUseID);
755
302
  return decision;
756
303
  }
757
304
  if (decision.behavior === 'allow') {
758
- // Approve (+ approve-with-edit via updatedInput, applied after the binding check).
759
305
  await bridge.decideTool(decision.updatedInput !== undefined
760
306
  ? { decision: 'approve', updatedInput: decision.updatedInput }
761
307
  : { decision: 'approve' }, toolUseID);
762
308
  return decision;
763
309
  }
764
- // behavior === 'ask': not a wire decision — re-surface (the shell loops the prompt). Returned as-is so
765
- // CC's loop re-asks; the bridge does NOT decide on an `ask`.
766
310
  return decision;
767
311
  };
768
312
  }
769
- // ── helpers ───────────────────────────────────────────────────────────────────
770
- /**
771
- * FIX②(2026-08-07):作答**出门前**的本地闭集校验(fail-closed;详见 `answerQuestion` 头注)。
772
- * 只判「这批作答在 wire 上有没有意义」,不判 header 与题面对不对得上 —— 后者的真源在 worker
773
- * (它持有题面),壳侧猜一份题面副本只会造出第二个真源。
774
- */
775
313
  function assertAnswersSendable(answers) {
776
314
  if (!Array.isArray(answers) || answers.length === 0) {
777
315
  throw new HitlSafetyError('refusing to send an EMPTY AskUserQuestion answer set — `{answers:[]}` is the wire\'s DENY/NO_HUMAN form, ' +
@@ -787,8 +325,6 @@ function assertAnswersSendable(answers) {
787
325
  }
788
326
  }
789
327
  }
790
- /** Marshal one answer into the worker-validated wire entry (contract/04 §4.1). Drops undefined `note`.
791
- * REF-CC-038(2026-08-02):return 类型是 `AskAnswer` 本身(唯一源),不再是一份重写的内联匿名形。 */
792
328
  function marshalAnswer(a) {
793
329
  const entry = {
794
330
  header: a.header,
@@ -798,22 +334,17 @@ function marshalAnswer(a) {
798
334
  entry.note = a.note;
799
335
  return entry;
800
336
  }
801
- /** Detect a 409 `approval_binding_mismatch` across the SDK error shape variants (errorCode / code / status). */
802
337
  function isBindingMismatch(e) {
803
338
  if (!e || typeof e !== 'object')
804
339
  return false;
805
340
  const o = e;
806
- // [1912] errorCode 恒在(server ≥1.302 canonical;全端引擎达标线 ≥1.303)——`?? o.code` 老兜底已清。
807
341
  const code = o.errorCode;
808
342
  if (code === 'approval_binding_mismatch')
809
343
  return true;
810
- // SDK throws a named ApprovalBindingMismatchError (index.ts:19).
811
344
  if (typeof o.name === 'string' && o.name === 'ApprovalBindingMismatchError')
812
345
  return true;
813
346
  return false;
814
347
  }
815
- /** Build a CC deny `PermissionDecision` from a backend-driven denial (e.g. an SLA / policy auto-deny).
816
- * The decisionReason is `asyncAgent` — the denial originated upstream, not from a local rule/mode. */
817
348
  export function backendDeny(reason) {
818
349
  return {
819
350
  behavior: 'deny',