open-multi-agent-kit 0.96.2 → 0.98.0

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 (368) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +43 -9
  3. package/dist/cli/args.d.ts +2 -3
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +13 -189
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/help.d.ts +3 -0
  8. package/dist/cli/help.d.ts.map +1 -0
  9. package/dist/cli/help.js +194 -0
  10. package/dist/cli/help.js.map +1 -0
  11. package/dist/cli/initial-message.d.ts.map +1 -1
  12. package/dist/cli/initial-message.js +2 -1
  13. package/dist/cli/initial-message.js.map +1 -1
  14. package/dist/cli/resource-paths.d.ts +4 -0
  15. package/dist/cli/resource-paths.d.ts.map +1 -0
  16. package/dist/cli/resource-paths.js +59 -0
  17. package/dist/cli/resource-paths.js.map +1 -0
  18. package/dist/commands/adaptorch-doctor-cli.d.ts +13 -0
  19. package/dist/commands/adaptorch-doctor-cli.d.ts.map +1 -0
  20. package/dist/commands/adaptorch-doctor-cli.js +115 -0
  21. package/dist/commands/adaptorch-doctor-cli.js.map +1 -0
  22. package/dist/commands/resource-doctor-cli.d.ts +4 -0
  23. package/dist/commands/resource-doctor-cli.d.ts.map +1 -1
  24. package/dist/commands/resource-doctor-cli.js +35 -5
  25. package/dist/commands/resource-doctor-cli.js.map +1 -1
  26. package/dist/core/adaptorch-bridge.d.ts +11 -13
  27. package/dist/core/adaptorch-bridge.d.ts.map +1 -1
  28. package/dist/core/adaptorch-bridge.js +11 -13
  29. package/dist/core/adaptorch-bridge.js.map +1 -1
  30. package/dist/core/agent-session-snapshot.d.ts +16 -0
  31. package/dist/core/agent-session-snapshot.d.ts.map +1 -0
  32. package/dist/core/agent-session-snapshot.js +111 -0
  33. package/dist/core/agent-session-snapshot.js.map +1 -0
  34. package/dist/core/agent-session.d.ts +14 -10
  35. package/dist/core/agent-session.d.ts.map +1 -1
  36. package/dist/core/agent-session.js +102 -217
  37. package/dist/core/agent-session.js.map +1 -1
  38. package/dist/core/agent-tool-settings.d.ts +10 -0
  39. package/dist/core/agent-tool-settings.d.ts.map +1 -1
  40. package/dist/core/agent-tool-settings.js +10 -1
  41. package/dist/core/agent-tool-settings.js.map +1 -1
  42. package/dist/core/attachment-store.d.ts +39 -0
  43. package/dist/core/attachment-store.d.ts.map +1 -0
  44. package/dist/core/attachment-store.js +81 -0
  45. package/dist/core/attachment-store.js.map +1 -0
  46. package/dist/core/command-safety.d.ts +8 -0
  47. package/dist/core/command-safety.d.ts.map +1 -1
  48. package/dist/core/command-safety.js +140 -14
  49. package/dist/core/command-safety.js.map +1 -1
  50. package/dist/core/compaction/compaction.d.ts +13 -1
  51. package/dist/core/compaction/compaction.d.ts.map +1 -1
  52. package/dist/core/compaction/compaction.js +60 -62
  53. package/dist/core/compaction/compaction.js.map +1 -1
  54. package/dist/core/compaction/index.d.ts +1 -0
  55. package/dist/core/compaction/index.d.ts.map +1 -1
  56. package/dist/core/compaction/index.js +1 -0
  57. package/dist/core/compaction/index.js.map +1 -1
  58. package/dist/core/compaction/knowledge-triage-format.d.ts +9 -0
  59. package/dist/core/compaction/knowledge-triage-format.d.ts.map +1 -0
  60. package/dist/core/compaction/knowledge-triage-format.js +71 -0
  61. package/dist/core/compaction/knowledge-triage-format.js.map +1 -0
  62. package/dist/core/compaction/knowledge-triage-types.d.ts +7 -0
  63. package/dist/core/compaction/knowledge-triage-types.d.ts.map +1 -0
  64. package/dist/core/compaction/knowledge-triage-types.js +2 -0
  65. package/dist/core/compaction/knowledge-triage-types.js.map +1 -0
  66. package/dist/core/compaction/knowledge-triage.d.ts +29 -0
  67. package/dist/core/compaction/knowledge-triage.d.ts.map +1 -0
  68. package/dist/core/compaction/knowledge-triage.js +189 -0
  69. package/dist/core/compaction/knowledge-triage.js.map +1 -0
  70. package/dist/core/compaction/transaction.d.ts.map +1 -1
  71. package/dist/core/compaction/transaction.js +1 -1
  72. package/dist/core/compaction/transaction.js.map +1 -1
  73. package/dist/core/compaction/utils.d.ts +4 -0
  74. package/dist/core/compaction/utils.d.ts.map +1 -1
  75. package/dist/core/compaction/utils.js +37 -0
  76. package/dist/core/compaction/utils.js.map +1 -1
  77. package/dist/core/completion-sound-io.d.ts +14 -0
  78. package/dist/core/completion-sound-io.d.ts.map +1 -0
  79. package/dist/core/completion-sound-io.js +64 -0
  80. package/dist/core/completion-sound-io.js.map +1 -0
  81. package/dist/core/completion-sound.d.ts +4 -10
  82. package/dist/core/completion-sound.d.ts.map +1 -1
  83. package/dist/core/completion-sound.js +28 -58
  84. package/dist/core/completion-sound.js.map +1 -1
  85. package/dist/core/context-budget-governor-v2.d.ts +1 -1
  86. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  87. package/dist/core/context-budget-governor-v2.js +1 -1
  88. package/dist/core/context-budget-governor-v2.js.map +1 -1
  89. package/dist/core/context-budget-relevance.d.ts +15 -2
  90. package/dist/core/context-budget-relevance.d.ts.map +1 -1
  91. package/dist/core/context-budget-relevance.js +21 -9
  92. package/dist/core/context-budget-relevance.js.map +1 -1
  93. package/dist/core/context-budget-system-prompt-items.d.ts +1 -1
  94. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  95. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  96. package/dist/core/context-budget-system-prompt.d.ts +1 -1
  97. package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
  98. package/dist/core/context-budget-system-prompt.js.map +1 -1
  99. package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
  100. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  101. package/dist/core/context-budget-v2-cache-keys.js +2 -0
  102. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  103. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  104. package/dist/core/context-budget-v2-plan-cache-keys.js +3 -0
  105. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  106. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  107. package/dist/core/context-budget-v2-planner.js +2 -1
  108. package/dist/core/context-budget-v2-planner.js.map +1 -1
  109. package/dist/core/context-budget-v2-scoring.d.ts +17 -0
  110. package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
  111. package/dist/core/context-budget-v2-scoring.js +25 -8
  112. package/dist/core/context-budget-v2-scoring.js.map +1 -1
  113. package/dist/core/context-budget-v2-selection.d.ts +2 -2
  114. package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
  115. package/dist/core/context-budget-v2-selection.js +18 -1
  116. package/dist/core/context-budget-v2-selection.js.map +1 -1
  117. package/dist/core/context-budget-v2-types.d.ts +17 -0
  118. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  119. package/dist/core/context-budget-v2-types.js +15 -0
  120. package/dist/core/context-budget-v2-types.js.map +1 -1
  121. package/dist/core/context-file.d.ts +23 -0
  122. package/dist/core/context-file.d.ts.map +1 -0
  123. package/dist/core/context-file.js +17 -0
  124. package/dist/core/context-file.js.map +1 -0
  125. package/dist/core/extensions/builtin/command-safety-gate.d.ts +3 -1
  126. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  127. package/dist/core/extensions/builtin/command-safety-gate.js +7 -0
  128. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  129. package/dist/core/extensions/types.d.ts +12 -1
  130. package/dist/core/extensions/types.d.ts.map +1 -1
  131. package/dist/core/extensions/types.js.map +1 -1
  132. package/dist/core/keybindings.d.ts +1 -1
  133. package/dist/core/keybindings.d.ts.map +1 -1
  134. package/dist/core/keybindings.js +6 -1
  135. package/dist/core/keybindings.js.map +1 -1
  136. package/dist/core/model-prompt-policy.d.ts +1 -1
  137. package/dist/core/model-prompt-policy.d.ts.map +1 -1
  138. package/dist/core/model-prompt-policy.js.map +1 -1
  139. package/dist/core/model-registry.d.ts +8 -3
  140. package/dist/core/model-registry.d.ts.map +1 -1
  141. package/dist/core/model-registry.js +71 -1
  142. package/dist/core/model-registry.js.map +1 -1
  143. package/dist/core/package-doctor-source-scan.d.ts +9 -0
  144. package/dist/core/package-doctor-source-scan.d.ts.map +1 -1
  145. package/dist/core/package-doctor-source-scan.js +77 -4
  146. package/dist/core/package-doctor-source-scan.js.map +1 -1
  147. package/dist/core/prompt-attachment.d.ts +51 -0
  148. package/dist/core/prompt-attachment.d.ts.map +1 -0
  149. package/dist/core/prompt-attachment.js +186 -0
  150. package/dist/core/prompt-attachment.js.map +1 -0
  151. package/dist/core/prompt-settlement.d.ts +6 -3
  152. package/dist/core/prompt-settlement.d.ts.map +1 -1
  153. package/dist/core/prompt-settlement.js +10 -14
  154. package/dist/core/prompt-settlement.js.map +1 -1
  155. package/dist/core/provider-resilience.d.ts +28 -0
  156. package/dist/core/provider-resilience.d.ts.map +1 -1
  157. package/dist/core/provider-resilience.js +50 -0
  158. package/dist/core/provider-resilience.js.map +1 -1
  159. package/dist/core/provider-retry.d.ts +1 -1
  160. package/dist/core/provider-retry.d.ts.map +1 -1
  161. package/dist/core/provider-retry.js +1 -1
  162. package/dist/core/provider-retry.js.map +1 -1
  163. package/dist/core/provider-usage.d.ts.map +1 -1
  164. package/dist/core/provider-usage.js +68 -2
  165. package/dist/core/provider-usage.js.map +1 -1
  166. package/dist/core/reasoning-router-promotion.d.ts +124 -0
  167. package/dist/core/reasoning-router-promotion.d.ts.map +1 -0
  168. package/dist/core/reasoning-router-promotion.js +121 -0
  169. package/dist/core/reasoning-router-promotion.js.map +1 -0
  170. package/dist/core/reasoning-router-replay-stability.d.ts +65 -0
  171. package/dist/core/reasoning-router-replay-stability.d.ts.map +1 -0
  172. package/dist/core/reasoning-router-replay-stability.js +67 -0
  173. package/dist/core/reasoning-router-replay-stability.js.map +1 -0
  174. package/dist/core/reasoning-router-v4-weights.d.ts +6 -6
  175. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  176. package/dist/core/reasoning-router-v4-weights.js +3 -3
  177. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  178. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  179. package/dist/core/reasoning-router-v4.js +6 -6
  180. package/dist/core/reasoning-router-v4.js.map +1 -1
  181. package/dist/core/redaction.d.ts.map +1 -1
  182. package/dist/core/redaction.js +1 -1
  183. package/dist/core/redaction.js.map +1 -1
  184. package/dist/core/resource-admission.d.ts +2 -1
  185. package/dist/core/resource-admission.d.ts.map +1 -1
  186. package/dist/core/resource-admission.js +11 -0
  187. package/dist/core/resource-admission.js.map +1 -1
  188. package/dist/core/resource-governor-format.d.ts +1 -1
  189. package/dist/core/resource-governor-format.d.ts.map +1 -1
  190. package/dist/core/resource-governor-format.js.map +1 -1
  191. package/dist/core/resource-governor-settings.d.ts +6 -12
  192. package/dist/core/resource-governor-settings.d.ts.map +1 -1
  193. package/dist/core/resource-governor-settings.js.map +1 -1
  194. package/dist/core/resource-governor-types.d.ts +24 -0
  195. package/dist/core/resource-governor-types.d.ts.map +1 -0
  196. package/dist/core/resource-governor-types.js +15 -0
  197. package/dist/core/resource-governor-types.js.map +1 -0
  198. package/dist/core/resource-loader.d.ts +1 -7
  199. package/dist/core/resource-loader.d.ts.map +1 -1
  200. package/dist/core/resource-loader.js.map +1 -1
  201. package/dist/core/resource-observation-journal.d.ts +2 -1
  202. package/dist/core/resource-observation-journal.d.ts.map +1 -1
  203. package/dist/core/resource-observation-journal.js +4 -3
  204. package/dist/core/resource-observation-journal.js.map +1 -1
  205. package/dist/core/resource-observation-report-reader.d.ts +7 -0
  206. package/dist/core/resource-observation-report-reader.d.ts.map +1 -0
  207. package/dist/core/resource-observation-report-reader.js +157 -0
  208. package/dist/core/resource-observation-report-reader.js.map +1 -0
  209. package/dist/core/resource-observation-report.d.ts +24 -0
  210. package/dist/core/resource-observation-report.d.ts.map +1 -0
  211. package/dist/core/resource-observation-report.js +101 -0
  212. package/dist/core/resource-observation-report.js.map +1 -0
  213. package/dist/core/router-feedback-collector.d.ts +1 -1
  214. package/dist/core/router-feedback-collector.d.ts.map +1 -1
  215. package/dist/core/router-feedback-collector.js.map +1 -1
  216. package/dist/core/session-bash-service.d.ts.map +1 -1
  217. package/dist/core/session-bash-service.js +4 -3
  218. package/dist/core/session-bash-service.js.map +1 -1
  219. package/dist/core/session-failure-cause.d.ts +33 -0
  220. package/dist/core/session-failure-cause.d.ts.map +1 -0
  221. package/dist/core/session-failure-cause.js +141 -0
  222. package/dist/core/session-failure-cause.js.map +1 -0
  223. package/dist/core/session-system-prompt.d.ts +1 -1
  224. package/dist/core/session-system-prompt.d.ts.map +1 -1
  225. package/dist/core/session-system-prompt.js.map +1 -1
  226. package/dist/core/session-termination.d.ts +1 -1
  227. package/dist/core/session-termination.d.ts.map +1 -1
  228. package/dist/core/session-termination.js +11 -2
  229. package/dist/core/session-termination.js.map +1 -1
  230. package/dist/core/settings-manager.d.ts +1 -1
  231. package/dist/core/settings-manager.d.ts.map +1 -1
  232. package/dist/core/settings-manager.js.map +1 -1
  233. package/dist/core/system-prompt.d.ts +1 -1
  234. package/dist/core/system-prompt.d.ts.map +1 -1
  235. package/dist/core/system-prompt.js.map +1 -1
  236. package/dist/core/tools/bash.d.ts.map +1 -1
  237. package/dist/core/tools/bash.js +10 -14
  238. package/dist/core/tools/bash.js.map +1 -1
  239. package/dist/core/tools/grep.d.ts.map +1 -1
  240. package/dist/core/tools/grep.js +13 -48
  241. package/dist/core/tools/grep.js.map +1 -1
  242. package/dist/core/tools/read.d.ts.map +1 -1
  243. package/dist/core/tools/read.js +1 -34
  244. package/dist/core/tools/read.js.map +1 -1
  245. package/dist/core/workload-classifier.d.ts +2 -10
  246. package/dist/core/workload-classifier.d.ts.map +1 -1
  247. package/dist/core/workload-classifier.js +17 -0
  248. package/dist/core/workload-classifier.js.map +1 -1
  249. package/dist/core/workload-family-matcher.d.ts +1 -1
  250. package/dist/core/workload-family-matcher.d.ts.map +1 -1
  251. package/dist/core/workload-family-matcher.js.map +1 -1
  252. package/dist/core/workload-shard-execution-types.d.ts +40 -0
  253. package/dist/core/workload-shard-execution-types.d.ts.map +1 -0
  254. package/dist/core/workload-shard-execution-types.js +14 -0
  255. package/dist/core/workload-shard-execution-types.js.map +1 -0
  256. package/dist/core/workload-shard-executor.d.ts +3 -27
  257. package/dist/core/workload-shard-executor.d.ts.map +1 -1
  258. package/dist/core/workload-shard-executor.js.map +1 -1
  259. package/dist/core/workload-shard-runner.d.ts +1 -1
  260. package/dist/core/workload-shard-runner.d.ts.map +1 -1
  261. package/dist/core/workload-shard-runner.js.map +1 -1
  262. package/dist/core/workload-types.d.ts +24 -0
  263. package/dist/core/workload-types.d.ts.map +1 -0
  264. package/dist/core/workload-types.js +15 -0
  265. package/dist/core/workload-types.js.map +1 -0
  266. package/dist/index.d.ts +1 -1
  267. package/dist/index.d.ts.map +1 -1
  268. package/dist/index.js +1 -1
  269. package/dist/index.js.map +1 -1
  270. package/dist/main.d.ts +2 -0
  271. package/dist/main.d.ts.map +1 -1
  272. package/dist/main.js +27 -47
  273. package/dist/main.js.map +1 -1
  274. package/dist/modes/interactive/components/attachment-strip.d.ts +21 -0
  275. package/dist/modes/interactive/components/attachment-strip.d.ts.map +1 -0
  276. package/dist/modes/interactive/components/attachment-strip.js +136 -0
  277. package/dist/modes/interactive/components/attachment-strip.js.map +1 -0
  278. package/dist/modes/interactive/components/footer.d.ts +6 -0
  279. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  280. package/dist/modes/interactive/components/footer.js +26 -1
  281. package/dist/modes/interactive/components/footer.js.map +1 -1
  282. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
  283. package/dist/modes/interactive/components/status-sidebar.js +12 -1
  284. package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
  285. package/dist/modes/interactive/interactive-mode.d.ts +23 -4
  286. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  287. package/dist/modes/interactive/interactive-mode.js +174 -53
  288. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  289. package/dist/modes/interactive/theme/omk-control-panel.json +1 -1
  290. package/dist/utils/clipboard-image.d.ts.map +1 -1
  291. package/dist/utils/clipboard-image.js +42 -33
  292. package/dist/utils/clipboard-image.js.map +1 -1
  293. package/dist/utils/clipboard.d.ts.map +1 -1
  294. package/dist/utils/clipboard.js +14 -2
  295. package/dist/utils/clipboard.js.map +1 -1
  296. package/docs/adaptorch-preview-spec.md +20 -18
  297. package/docs/adaptorch-preview.md +9 -9
  298. package/docs/compaction.md +25 -4
  299. package/docs/correctness-wall.md +10 -10
  300. package/docs/docs.json +12 -0
  301. package/docs/environment-variables.md +9 -2
  302. package/docs/extensions.md +12 -3
  303. package/docs/index.md +7 -1
  304. package/docs/metrics.md +74 -2
  305. package/docs/provider-resilience.md +11 -0
  306. package/docs/runtime-algorithms.md +359 -0
  307. package/docs/sdk.md +2 -2
  308. package/docs/settings.md +11 -10
  309. package/docs/usage.md +19 -3
  310. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  311. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  312. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  313. package/examples/extensions/gondolin/package-lock.json +2 -2
  314. package/examples/extensions/gondolin/package.json +1 -1
  315. package/examples/extensions/sandbox/package-lock.json +2 -2
  316. package/examples/extensions/sandbox/package.json +1 -1
  317. package/examples/extensions/with-deps/package-lock.json +2 -2
  318. package/examples/extensions/with-deps/package.json +1 -1
  319. package/npm-shrinkwrap.json +18 -18
  320. package/package.json +7 -7
  321. package/dist/core/context-budget-governor.d.ts +0 -67
  322. package/dist/core/context-budget-governor.d.ts.map +0 -1
  323. package/dist/core/context-budget-governor.js +0 -224
  324. package/dist/core/context-budget-governor.js.map +0 -1
  325. package/dist/core/lean-ctx.d.ts +0 -99
  326. package/dist/core/lean-ctx.d.ts.map +0 -1
  327. package/dist/core/lean-ctx.js +0 -225
  328. package/dist/core/lean-ctx.js.map +0 -1
  329. package/dist/core/read-anchors.d.ts +0 -110
  330. package/dist/core/read-anchors.d.ts.map +0 -1
  331. package/dist/core/read-anchors.js +0 -193
  332. package/dist/core/read-anchors.js.map +0 -1
  333. package/dist/core/recovery-checkpoint.d.ts +0 -196
  334. package/dist/core/recovery-checkpoint.d.ts.map +0 -1
  335. package/dist/core/recovery-checkpoint.js +0 -382
  336. package/dist/core/recovery-checkpoint.js.map +0 -1
  337. package/dist/core/sandbox-policy-v2.d.ts +0 -49
  338. package/dist/core/sandbox-policy-v2.d.ts.map +0 -1
  339. package/dist/core/sandbox-policy-v2.js +0 -368
  340. package/dist/core/sandbox-policy-v2.js.map +0 -1
  341. package/dist/core/tools/omp-pure-seams.d.ts +0 -41
  342. package/dist/core/tools/omp-pure-seams.d.ts.map +0 -1
  343. package/dist/core/tools/omp-pure-seams.js +0 -142
  344. package/dist/core/tools/omp-pure-seams.js.map +0 -1
  345. package/dist/core/tools/omp-seam-runtime.d.ts +0 -100
  346. package/dist/core/tools/omp-seam-runtime.d.ts.map +0 -1
  347. package/dist/core/tools/omp-seam-runtime.js +0 -40
  348. package/dist/core/tools/omp-seam-runtime.js.map +0 -1
  349. package/dist/guardrails/audit-logger.d.ts +0 -4
  350. package/dist/guardrails/audit-logger.d.ts.map +0 -1
  351. package/dist/guardrails/audit-logger.js +0 -28
  352. package/dist/guardrails/audit-logger.js.map +0 -1
  353. package/dist/guardrails/freedomd-adapter.d.ts +0 -16
  354. package/dist/guardrails/freedomd-adapter.d.ts.map +0 -1
  355. package/dist/guardrails/freedomd-adapter.js +0 -87
  356. package/dist/guardrails/freedomd-adapter.js.map +0 -1
  357. package/dist/orchestration/lane-grant-auditor.d.ts +0 -4
  358. package/dist/orchestration/lane-grant-auditor.d.ts.map +0 -1
  359. package/dist/orchestration/lane-grant-auditor.js +0 -87
  360. package/dist/orchestration/lane-grant-auditor.js.map +0 -1
  361. package/dist/types/guardrails.d.ts +0 -61
  362. package/dist/types/guardrails.d.ts.map +0 -1
  363. package/dist/types/guardrails.js +0 -9
  364. package/dist/types/guardrails.js.map +0 -1
  365. package/dist/types/lane-grant.d.ts +0 -58
  366. package/dist/types/lane-grant.d.ts.map +0 -1
  367. package/dist/types/lane-grant.js +0 -9
  368. package/dist/types/lane-grant.js.map +0 -1
@@ -0,0 +1,157 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { RESOURCE_OBSERVATION_JOURNAL_FILE } from "./resource-observation-journal.js";
4
+ const MAX_DIRECTORY_ENTRIES = 5000;
5
+ const MAX_JOURNAL_BYTES = 2 * 1024 * 1024;
6
+ export function collectResourceJournalTexts(cwd, maxJournals) {
7
+ const root = resolveRunsRoot(cwd);
8
+ if (root.kind === "missing")
9
+ return { journals: [], diagnostics: 0, truncated: false };
10
+ if (root.kind === "invalid")
11
+ return { journals: [], diagnostics: 1, truncated: true };
12
+ const journals = [];
13
+ let diagnostics = 0;
14
+ let truncated = false;
15
+ let visited = 0;
16
+ let directory;
17
+ try {
18
+ directory = fs.opendirSync(root.path);
19
+ while (true) {
20
+ const entry = directory.readSync();
21
+ if (entry === null)
22
+ break;
23
+ visited += 1;
24
+ if (visited > MAX_DIRECTORY_ENTRIES) {
25
+ diagnostics += 1;
26
+ truncated = true;
27
+ break;
28
+ }
29
+ if (entry.isSymbolicLink()) {
30
+ diagnostics += 1;
31
+ truncated = true;
32
+ continue;
33
+ }
34
+ if (!entry.isDirectory())
35
+ continue;
36
+ const runDir = safeRealDirectory(root.path, entry.name);
37
+ if (runDir === null) {
38
+ diagnostics += 1;
39
+ truncated = true;
40
+ continue;
41
+ }
42
+ const journalPath = path.join(runDir, RESOURCE_OBSERVATION_JOURNAL_FILE);
43
+ const candidate = inspectJournalCandidate(journalPath);
44
+ if (candidate === "missing")
45
+ continue;
46
+ if (candidate === "invalid") {
47
+ diagnostics += 1;
48
+ truncated = true;
49
+ continue;
50
+ }
51
+ if (journals.length >= maxJournals) {
52
+ truncated = true;
53
+ break;
54
+ }
55
+ const read = readBoundedJournal(journalPath);
56
+ if (read.kind !== "ok" || read.text === undefined) {
57
+ diagnostics += 1;
58
+ truncated = true;
59
+ continue;
60
+ }
61
+ journals.push(read.text);
62
+ }
63
+ }
64
+ catch {
65
+ diagnostics += 1;
66
+ truncated = true;
67
+ }
68
+ finally {
69
+ directory?.closeSync();
70
+ }
71
+ return { journals, diagnostics, truncated };
72
+ }
73
+ function resolveRunsRoot(cwd) {
74
+ try {
75
+ const root = fs.realpathSync(cwd);
76
+ const omkDir = path.join(root, ".omk");
77
+ const runsDir = path.join(omkDir, "runs");
78
+ for (const candidate of [omkDir, runsDir]) {
79
+ const stat = fs.lstatSync(candidate);
80
+ if (stat.isSymbolicLink() || !stat.isDirectory())
81
+ return { kind: "invalid" };
82
+ }
83
+ const realRuns = fs.realpathSync(runsDir);
84
+ return isWithin(root, realRuns) ? { kind: "ok", path: realRuns } : { kind: "invalid" };
85
+ }
86
+ catch (error) {
87
+ return error.code === "ENOENT" ? { kind: "missing" } : { kind: "invalid" };
88
+ }
89
+ }
90
+ function safeRealDirectory(root, name) {
91
+ try {
92
+ const candidate = path.join(root, name);
93
+ const before = fs.lstatSync(candidate);
94
+ if (before.isSymbolicLink() || !before.isDirectory())
95
+ return null;
96
+ const real = fs.realpathSync(candidate);
97
+ return isWithin(root, real) ? real : null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ function inspectJournalCandidate(filePath) {
104
+ try {
105
+ const stat = fs.lstatSync(filePath);
106
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_JOURNAL_BYTES)
107
+ return "invalid";
108
+ return "valid";
109
+ }
110
+ catch (error) {
111
+ return error.code === "ENOENT" ? "missing" : "invalid";
112
+ }
113
+ }
114
+ function readBoundedJournal(filePath) {
115
+ let descriptor;
116
+ try {
117
+ const before = fs.lstatSync(filePath);
118
+ if (before.isSymbolicLink() || !before.isFile())
119
+ return { kind: "invalid" };
120
+ descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
121
+ const opened = fs.fstatSync(descriptor);
122
+ if (!opened.isFile() ||
123
+ opened.dev !== before.dev ||
124
+ opened.ino !== before.ino ||
125
+ opened.size > MAX_JOURNAL_BYTES) {
126
+ return { kind: "invalid" };
127
+ }
128
+ const buffer = Buffer.alloc(MAX_JOURNAL_BYTES + 1);
129
+ let bytesRead = 0;
130
+ while (bytesRead < buffer.length) {
131
+ const count = fs.readSync(descriptor, buffer, bytesRead, buffer.length - bytesRead, bytesRead);
132
+ if (count === 0)
133
+ break;
134
+ bytesRead += count;
135
+ }
136
+ const after = fs.fstatSync(descriptor);
137
+ if (bytesRead > MAX_JOURNAL_BYTES ||
138
+ after.dev !== opened.dev ||
139
+ after.ino !== opened.ino ||
140
+ after.size !== opened.size) {
141
+ return { kind: "invalid" };
142
+ }
143
+ return { kind: "ok", text: buffer.subarray(0, bytesRead).toString("utf8") };
144
+ }
145
+ catch (error) {
146
+ return error.code === "ENOENT" ? { kind: "missing" } : { kind: "invalid" };
147
+ }
148
+ finally {
149
+ if (descriptor !== undefined)
150
+ fs.closeSync(descriptor);
151
+ }
152
+ }
153
+ function isWithin(root, candidate) {
154
+ const relative = path.relative(root, candidate);
155
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
156
+ }
157
+ //# sourceMappingURL=resource-observation-report-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-observation-report-reader.js","sourceRoot":"","sources":["../../src/core/resource-observation-report-reader.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { RESOURCE_OBSERVATION_JOURNAL_FILE } from \"./resource-observation-journal.ts\";\n\nconst MAX_DIRECTORY_ENTRIES = 5000;\nconst MAX_JOURNAL_BYTES = 2 * 1024 * 1024;\n\nexport interface ResourceJournalCollection {\n\treadonly journals: readonly string[];\n\treadonly diagnostics: number;\n\treadonly truncated: boolean;\n}\n\ninterface JournalReadResult {\n\treadonly kind: \"missing\" | \"invalid\" | \"ok\";\n\treadonly text?: string;\n}\n\nexport function collectResourceJournalTexts(cwd: string, maxJournals: number): ResourceJournalCollection {\n\tconst root = resolveRunsRoot(cwd);\n\tif (root.kind === \"missing\") return { journals: [], diagnostics: 0, truncated: false };\n\tif (root.kind === \"invalid\") return { journals: [], diagnostics: 1, truncated: true };\n\tconst journals: string[] = [];\n\tlet diagnostics = 0;\n\tlet truncated = false;\n\tlet visited = 0;\n\tlet directory: fs.Dir | undefined;\n\ttry {\n\t\tdirectory = fs.opendirSync(root.path);\n\t\twhile (true) {\n\t\t\tconst entry = directory.readSync();\n\t\t\tif (entry === null) break;\n\t\t\tvisited += 1;\n\t\t\tif (visited > MAX_DIRECTORY_ENTRIES) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isDirectory()) continue;\n\t\t\tconst runDir = safeRealDirectory(root.path, entry.name);\n\t\t\tif (runDir === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst journalPath = path.join(runDir, RESOURCE_OBSERVATION_JOURNAL_FILE);\n\t\t\tconst candidate = inspectJournalCandidate(journalPath);\n\t\t\tif (candidate === \"missing\") continue;\n\t\t\tif (candidate === \"invalid\") {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (journals.length >= maxJournals) {\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst read = readBoundedJournal(journalPath);\n\t\t\tif (read.kind !== \"ok\" || read.text === undefined) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjournals.push(read.text);\n\t\t}\n\t} catch {\n\t\tdiagnostics += 1;\n\t\ttruncated = true;\n\t} finally {\n\t\tdirectory?.closeSync();\n\t}\n\treturn { journals, diagnostics, truncated };\n}\n\nfunction resolveRunsRoot(\n\tcwd: string,\n): { readonly kind: \"missing\" } | { readonly kind: \"invalid\" } | { readonly kind: \"ok\"; readonly path: string } {\n\ttry {\n\t\tconst root = fs.realpathSync(cwd);\n\t\tconst omkDir = path.join(root, \".omk\");\n\t\tconst runsDir = path.join(omkDir, \"runs\");\n\t\tfor (const candidate of [omkDir, runsDir]) {\n\t\t\tconst stat = fs.lstatSync(candidate);\n\t\t\tif (stat.isSymbolicLink() || !stat.isDirectory()) return { kind: \"invalid\" };\n\t\t}\n\t\tconst realRuns = fs.realpathSync(runsDir);\n\t\treturn isWithin(root, realRuns) ? { kind: \"ok\", path: realRuns } : { kind: \"invalid\" };\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"ENOENT\" ? { kind: \"missing\" } : { kind: \"invalid\" };\n\t}\n}\n\nfunction safeRealDirectory(root: string, name: string): string | null {\n\ttry {\n\t\tconst candidate = path.join(root, name);\n\t\tconst before = fs.lstatSync(candidate);\n\t\tif (before.isSymbolicLink() || !before.isDirectory()) return null;\n\t\tconst real = fs.realpathSync(candidate);\n\t\treturn isWithin(root, real) ? real : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction inspectJournalCandidate(filePath: string): \"missing\" | \"invalid\" | \"valid\" {\n\ttry {\n\t\tconst stat = fs.lstatSync(filePath);\n\t\tif (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_JOURNAL_BYTES) return \"invalid\";\n\t\treturn \"valid\";\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"ENOENT\" ? \"missing\" : \"invalid\";\n\t}\n}\n\nfunction readBoundedJournal(filePath: string): JournalReadResult {\n\tlet descriptor: number | undefined;\n\ttry {\n\t\tconst before = fs.lstatSync(filePath);\n\t\tif (before.isSymbolicLink() || !before.isFile()) return { kind: \"invalid\" };\n\t\tdescriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);\n\t\tconst opened = fs.fstatSync(descriptor);\n\t\tif (\n\t\t\t!opened.isFile() ||\n\t\t\topened.dev !== before.dev ||\n\t\t\topened.ino !== before.ino ||\n\t\t\topened.size > MAX_JOURNAL_BYTES\n\t\t) {\n\t\t\treturn { kind: \"invalid\" };\n\t\t}\n\t\tconst buffer = Buffer.alloc(MAX_JOURNAL_BYTES + 1);\n\t\tlet bytesRead = 0;\n\t\twhile (bytesRead < buffer.length) {\n\t\t\tconst count = fs.readSync(descriptor, buffer, bytesRead, buffer.length - bytesRead, bytesRead);\n\t\t\tif (count === 0) break;\n\t\t\tbytesRead += count;\n\t\t}\n\t\tconst after = fs.fstatSync(descriptor);\n\t\tif (\n\t\t\tbytesRead > MAX_JOURNAL_BYTES ||\n\t\t\tafter.dev !== opened.dev ||\n\t\t\tafter.ino !== opened.ino ||\n\t\t\tafter.size !== opened.size\n\t\t) {\n\t\t\treturn { kind: \"invalid\" };\n\t\t}\n\t\treturn { kind: \"ok\", text: buffer.subarray(0, bytesRead).toString(\"utf8\") };\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"ENOENT\" ? { kind: \"missing\" } : { kind: \"invalid\" };\n\t} finally {\n\t\tif (descriptor !== undefined) fs.closeSync(descriptor);\n\t}\n}\n\nfunction isWithin(root: string, candidate: string): boolean {\n\tconst relative = path.relative(root, candidate);\n\treturn relative === \"\" || (!relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n"]}
@@ -0,0 +1,24 @@
1
+ export declare const RESOURCE_REPORT_MIN_ADMISSIONS = 30;
2
+ export interface ResourceObservationReport {
3
+ readonly schemaVersion: 1;
4
+ readonly journalsScanned: number;
5
+ readonly admissionRecords: number;
6
+ readonly reasonRecords: number;
7
+ readonly reasonCoverageComplete: boolean;
8
+ readonly pressure: Readonly<Record<"normal" | "constrained" | "critical", number>>;
9
+ readonly actions: Readonly<Record<"allow" | "throttle" | "defer-heavy", number>>;
10
+ readonly wouldHaveThrottled: number;
11
+ readonly probePartial: number;
12
+ readonly probeTimeout: number;
13
+ readonly diagnostics: number;
14
+ readonly truncated: boolean;
15
+ readonly minimumSampleSize: number;
16
+ readonly minimumSampleMet: boolean;
17
+ readonly humanReviewRequired: true;
18
+ }
19
+ export interface ResourceObservationReportOptions {
20
+ readonly maxJournals?: number;
21
+ }
22
+ /** Aggregate bounded, identifier-free admission evidence from local run journals. */
23
+ export declare function collectResourceObservationReport(cwd: string, options?: ResourceObservationReportOptions): ResourceObservationReport;
24
+ //# sourceMappingURL=resource-observation-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-observation-report.d.ts","sourceRoot":"","sources":["../../src/core/resource-observation-report.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAKjD,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,GAAG,aAAa,GAAG,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IACnF,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,GAAG,UAAU,GAAG,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IACjF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,mBAAmB,EAAE,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,gCAAgC;IAChD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,qFAAqF;AACrF,wBAAgB,gCAAgC,CAC/C,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,gCAAqC,GAC5C,yBAAyB,CAkE3B","sourcesContent":["import { RESOURCE_REASON_CODES, type ResourceReasonCode } from \"./resource-admission.ts\";\nimport { parseResourceObservationLine, type ResourceObservationRecord } from \"./resource-observation-journal.ts\";\nimport { collectResourceJournalTexts } from \"./resource-observation-report-reader.ts\";\n\nexport const RESOURCE_REPORT_MIN_ADMISSIONS = 30;\nconst DEFAULT_MAX_JOURNALS = 500;\nconst MAX_RECORDS_PER_JOURNAL = 10_000;\nconst REASON_CODES: ReadonlySet<string> = new Set(RESOURCE_REASON_CODES);\n\nexport interface ResourceObservationReport {\n\treadonly schemaVersion: 1;\n\treadonly journalsScanned: number;\n\treadonly admissionRecords: number;\n\treadonly reasonRecords: number;\n\treadonly reasonCoverageComplete: boolean;\n\treadonly pressure: Readonly<Record<\"normal\" | \"constrained\" | \"critical\", number>>;\n\treadonly actions: Readonly<Record<\"allow\" | \"throttle\" | \"defer-heavy\", number>>;\n\treadonly wouldHaveThrottled: number;\n\treadonly probePartial: number;\n\treadonly probeTimeout: number;\n\treadonly diagnostics: number;\n\treadonly truncated: boolean;\n\treadonly minimumSampleSize: number;\n\treadonly minimumSampleMet: boolean;\n\treadonly humanReviewRequired: true;\n}\n\nexport interface ResourceObservationReportOptions {\n\treadonly maxJournals?: number;\n}\n\n/** Aggregate bounded, identifier-free admission evidence from local run journals. */\nexport function collectResourceObservationReport(\n\tcwd: string,\n\toptions: ResourceObservationReportOptions = {},\n): ResourceObservationReport {\n\tconst maxJournals = boundedMaxJournals(options.maxJournals);\n\tconst collection = collectResourceJournalTexts(cwd, maxJournals);\n\tconst pressure = { normal: 0, constrained: 0, critical: 0 };\n\tconst actions = { allow: 0, throttle: 0, \"defer-heavy\": 0 };\n\tlet admissionRecords = 0;\n\tlet reasonRecords = 0;\n\tlet wouldHaveThrottled = 0;\n\tlet probePartial = 0;\n\tlet probeTimeout = 0;\n\tlet diagnostics = collection.diagnostics;\n\tlet truncated = collection.truncated;\n\n\tfor (const raw of collection.journals) {\n\t\tlet recordsSeen = 0;\n\t\tfor (const rawLine of raw.split(\"\\n\")) {\n\t\t\tconst line = rawLine.trim();\n\t\t\tif (line === \"\") continue;\n\t\t\tif (recordsSeen >= MAX_RECORDS_PER_JOURNAL) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trecordsSeen += 1;\n\t\t\tconst record = parseResourceObservationLine(line);\n\t\t\tif (record === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (record.kind !== \"resource_admission_v1\") continue;\n\t\t\tconst admission = parseAdmissionFacts(record);\n\t\t\tif (admission === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tadmissionRecords += 1;\n\t\t\tpressure[admission.pressure] += 1;\n\t\t\tactions[admission.action] += 1;\n\t\t\tif (admission.action !== \"allow\") wouldHaveThrottled += 1;\n\t\t\tif (!admission.hasReasonEvidence) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treasonRecords += 1;\n\t\t\tif (admission.reasons.includes(\"resource.probe.partial\")) probePartial += 1;\n\t\t\tif (admission.reasons.includes(\"resource.probe.timeout\")) probeTimeout += 1;\n\t\t}\n\t}\n\n\treturn {\n\t\tschemaVersion: 1,\n\t\tjournalsScanned: collection.journals.length,\n\t\tadmissionRecords,\n\t\treasonRecords,\n\t\treasonCoverageComplete: admissionRecords > 0 && reasonRecords === admissionRecords,\n\t\tpressure,\n\t\tactions,\n\t\twouldHaveThrottled,\n\t\tprobePartial,\n\t\tprobeTimeout,\n\t\tdiagnostics,\n\t\ttruncated,\n\t\tminimumSampleSize: RESOURCE_REPORT_MIN_ADMISSIONS,\n\t\tminimumSampleMet: reasonRecords >= RESOURCE_REPORT_MIN_ADMISSIONS,\n\t\thumanReviewRequired: true,\n\t};\n}\n\nfunction boundedMaxJournals(value: number | undefined): number {\n\tif (value === undefined || !Number.isSafeInteger(value)) return DEFAULT_MAX_JOURNALS;\n\treturn Math.max(1, Math.min(DEFAULT_MAX_JOURNALS, value));\n}\n\nfunction parseAdmissionFacts(record: ResourceObservationRecord): {\n\treadonly pressure: \"normal\" | \"constrained\" | \"critical\";\n\treadonly action: \"allow\" | \"throttle\" | \"defer-heavy\";\n\treadonly reasons: readonly ResourceReasonCode[];\n\treadonly hasReasonEvidence: boolean;\n} | null {\n\tconst pressure = record.facts.pressure;\n\tconst action = record.facts.action;\n\tif (pressure !== \"normal\" && pressure !== \"constrained\" && pressure !== \"critical\") return null;\n\tif (action !== \"allow\" && action !== \"throttle\" && action !== \"defer-heavy\") return null;\n\tconst rawReasons = record.facts.reasons;\n\tif (rawReasons === undefined) return { pressure, action, reasons: [], hasReasonEvidence: false };\n\tif (typeof rawReasons !== \"string\") return null;\n\tconst reasons = rawReasons === \"\" ? [] : rawReasons.split(\",\");\n\tif (reasons.some((reason) => !REASON_CODES.has(reason))) return null;\n\treturn { pressure, action, reasons: reasons as ResourceReasonCode[], hasReasonEvidence: true };\n}\n"]}
@@ -0,0 +1,101 @@
1
+ import { RESOURCE_REASON_CODES } from "./resource-admission.js";
2
+ import { parseResourceObservationLine } from "./resource-observation-journal.js";
3
+ import { collectResourceJournalTexts } from "./resource-observation-report-reader.js";
4
+ export const RESOURCE_REPORT_MIN_ADMISSIONS = 30;
5
+ const DEFAULT_MAX_JOURNALS = 500;
6
+ const MAX_RECORDS_PER_JOURNAL = 10_000;
7
+ const REASON_CODES = new Set(RESOURCE_REASON_CODES);
8
+ /** Aggregate bounded, identifier-free admission evidence from local run journals. */
9
+ export function collectResourceObservationReport(cwd, options = {}) {
10
+ const maxJournals = boundedMaxJournals(options.maxJournals);
11
+ const collection = collectResourceJournalTexts(cwd, maxJournals);
12
+ const pressure = { normal: 0, constrained: 0, critical: 0 };
13
+ const actions = { allow: 0, throttle: 0, "defer-heavy": 0 };
14
+ let admissionRecords = 0;
15
+ let reasonRecords = 0;
16
+ let wouldHaveThrottled = 0;
17
+ let probePartial = 0;
18
+ let probeTimeout = 0;
19
+ let diagnostics = collection.diagnostics;
20
+ let truncated = collection.truncated;
21
+ for (const raw of collection.journals) {
22
+ let recordsSeen = 0;
23
+ for (const rawLine of raw.split("\n")) {
24
+ const line = rawLine.trim();
25
+ if (line === "")
26
+ continue;
27
+ if (recordsSeen >= MAX_RECORDS_PER_JOURNAL) {
28
+ diagnostics += 1;
29
+ truncated = true;
30
+ break;
31
+ }
32
+ recordsSeen += 1;
33
+ const record = parseResourceObservationLine(line);
34
+ if (record === null) {
35
+ diagnostics += 1;
36
+ continue;
37
+ }
38
+ if (record.kind !== "resource_admission_v1")
39
+ continue;
40
+ const admission = parseAdmissionFacts(record);
41
+ if (admission === null) {
42
+ diagnostics += 1;
43
+ continue;
44
+ }
45
+ admissionRecords += 1;
46
+ pressure[admission.pressure] += 1;
47
+ actions[admission.action] += 1;
48
+ if (admission.action !== "allow")
49
+ wouldHaveThrottled += 1;
50
+ if (!admission.hasReasonEvidence) {
51
+ diagnostics += 1;
52
+ continue;
53
+ }
54
+ reasonRecords += 1;
55
+ if (admission.reasons.includes("resource.probe.partial"))
56
+ probePartial += 1;
57
+ if (admission.reasons.includes("resource.probe.timeout"))
58
+ probeTimeout += 1;
59
+ }
60
+ }
61
+ return {
62
+ schemaVersion: 1,
63
+ journalsScanned: collection.journals.length,
64
+ admissionRecords,
65
+ reasonRecords,
66
+ reasonCoverageComplete: admissionRecords > 0 && reasonRecords === admissionRecords,
67
+ pressure,
68
+ actions,
69
+ wouldHaveThrottled,
70
+ probePartial,
71
+ probeTimeout,
72
+ diagnostics,
73
+ truncated,
74
+ minimumSampleSize: RESOURCE_REPORT_MIN_ADMISSIONS,
75
+ minimumSampleMet: reasonRecords >= RESOURCE_REPORT_MIN_ADMISSIONS,
76
+ humanReviewRequired: true,
77
+ };
78
+ }
79
+ function boundedMaxJournals(value) {
80
+ if (value === undefined || !Number.isSafeInteger(value))
81
+ return DEFAULT_MAX_JOURNALS;
82
+ return Math.max(1, Math.min(DEFAULT_MAX_JOURNALS, value));
83
+ }
84
+ function parseAdmissionFacts(record) {
85
+ const pressure = record.facts.pressure;
86
+ const action = record.facts.action;
87
+ if (pressure !== "normal" && pressure !== "constrained" && pressure !== "critical")
88
+ return null;
89
+ if (action !== "allow" && action !== "throttle" && action !== "defer-heavy")
90
+ return null;
91
+ const rawReasons = record.facts.reasons;
92
+ if (rawReasons === undefined)
93
+ return { pressure, action, reasons: [], hasReasonEvidence: false };
94
+ if (typeof rawReasons !== "string")
95
+ return null;
96
+ const reasons = rawReasons === "" ? [] : rawReasons.split(",");
97
+ if (reasons.some((reason) => !REASON_CODES.has(reason)))
98
+ return null;
99
+ return { pressure, action, reasons: reasons, hasReasonEvidence: true };
100
+ }
101
+ //# sourceMappingURL=resource-observation-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-observation-report.js","sourceRoot":"","sources":["../../src/core/resource-observation-report.ts"],"names":[],"mappings":"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","sourcesContent":["import { RESOURCE_REASON_CODES, type ResourceReasonCode } from \"./resource-admission.ts\";\nimport { parseResourceObservationLine, type ResourceObservationRecord } from \"./resource-observation-journal.ts\";\nimport { collectResourceJournalTexts } from \"./resource-observation-report-reader.ts\";\n\nexport const RESOURCE_REPORT_MIN_ADMISSIONS = 30;\nconst DEFAULT_MAX_JOURNALS = 500;\nconst MAX_RECORDS_PER_JOURNAL = 10_000;\nconst REASON_CODES: ReadonlySet<string> = new Set(RESOURCE_REASON_CODES);\n\nexport interface ResourceObservationReport {\n\treadonly schemaVersion: 1;\n\treadonly journalsScanned: number;\n\treadonly admissionRecords: number;\n\treadonly reasonRecords: number;\n\treadonly reasonCoverageComplete: boolean;\n\treadonly pressure: Readonly<Record<\"normal\" | \"constrained\" | \"critical\", number>>;\n\treadonly actions: Readonly<Record<\"allow\" | \"throttle\" | \"defer-heavy\", number>>;\n\treadonly wouldHaveThrottled: number;\n\treadonly probePartial: number;\n\treadonly probeTimeout: number;\n\treadonly diagnostics: number;\n\treadonly truncated: boolean;\n\treadonly minimumSampleSize: number;\n\treadonly minimumSampleMet: boolean;\n\treadonly humanReviewRequired: true;\n}\n\nexport interface ResourceObservationReportOptions {\n\treadonly maxJournals?: number;\n}\n\n/** Aggregate bounded, identifier-free admission evidence from local run journals. */\nexport function collectResourceObservationReport(\n\tcwd: string,\n\toptions: ResourceObservationReportOptions = {},\n): ResourceObservationReport {\n\tconst maxJournals = boundedMaxJournals(options.maxJournals);\n\tconst collection = collectResourceJournalTexts(cwd, maxJournals);\n\tconst pressure = { normal: 0, constrained: 0, critical: 0 };\n\tconst actions = { allow: 0, throttle: 0, \"defer-heavy\": 0 };\n\tlet admissionRecords = 0;\n\tlet reasonRecords = 0;\n\tlet wouldHaveThrottled = 0;\n\tlet probePartial = 0;\n\tlet probeTimeout = 0;\n\tlet diagnostics = collection.diagnostics;\n\tlet truncated = collection.truncated;\n\n\tfor (const raw of collection.journals) {\n\t\tlet recordsSeen = 0;\n\t\tfor (const rawLine of raw.split(\"\\n\")) {\n\t\t\tconst line = rawLine.trim();\n\t\t\tif (line === \"\") continue;\n\t\t\tif (recordsSeen >= MAX_RECORDS_PER_JOURNAL) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trecordsSeen += 1;\n\t\t\tconst record = parseResourceObservationLine(line);\n\t\t\tif (record === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (record.kind !== \"resource_admission_v1\") continue;\n\t\t\tconst admission = parseAdmissionFacts(record);\n\t\t\tif (admission === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tadmissionRecords += 1;\n\t\t\tpressure[admission.pressure] += 1;\n\t\t\tactions[admission.action] += 1;\n\t\t\tif (admission.action !== \"allow\") wouldHaveThrottled += 1;\n\t\t\tif (!admission.hasReasonEvidence) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treasonRecords += 1;\n\t\t\tif (admission.reasons.includes(\"resource.probe.partial\")) probePartial += 1;\n\t\t\tif (admission.reasons.includes(\"resource.probe.timeout\")) probeTimeout += 1;\n\t\t}\n\t}\n\n\treturn {\n\t\tschemaVersion: 1,\n\t\tjournalsScanned: collection.journals.length,\n\t\tadmissionRecords,\n\t\treasonRecords,\n\t\treasonCoverageComplete: admissionRecords > 0 && reasonRecords === admissionRecords,\n\t\tpressure,\n\t\tactions,\n\t\twouldHaveThrottled,\n\t\tprobePartial,\n\t\tprobeTimeout,\n\t\tdiagnostics,\n\t\ttruncated,\n\t\tminimumSampleSize: RESOURCE_REPORT_MIN_ADMISSIONS,\n\t\tminimumSampleMet: reasonRecords >= RESOURCE_REPORT_MIN_ADMISSIONS,\n\t\thumanReviewRequired: true,\n\t};\n}\n\nfunction boundedMaxJournals(value: number | undefined): number {\n\tif (value === undefined || !Number.isSafeInteger(value)) return DEFAULT_MAX_JOURNALS;\n\treturn Math.max(1, Math.min(DEFAULT_MAX_JOURNALS, value));\n}\n\nfunction parseAdmissionFacts(record: ResourceObservationRecord): {\n\treadonly pressure: \"normal\" | \"constrained\" | \"critical\";\n\treadonly action: \"allow\" | \"throttle\" | \"defer-heavy\";\n\treadonly reasons: readonly ResourceReasonCode[];\n\treadonly hasReasonEvidence: boolean;\n} | null {\n\tconst pressure = record.facts.pressure;\n\tconst action = record.facts.action;\n\tif (pressure !== \"normal\" && pressure !== \"constrained\" && pressure !== \"critical\") return null;\n\tif (action !== \"allow\" && action !== \"throttle\" && action !== \"defer-heavy\") return null;\n\tconst rawReasons = record.facts.reasons;\n\tif (rawReasons === undefined) return { pressure, action, reasons: [], hasReasonEvidence: false };\n\tif (typeof rawReasons !== \"string\") return null;\n\tconst reasons = rawReasons === \"\" ? [] : rawReasons.split(\",\");\n\tif (reasons.some((reason) => !REASON_CODES.has(reason))) return null;\n\treturn { pressure, action, reasons: reasons as ResourceReasonCode[], hasReasonEvidence: true };\n}\n"]}
@@ -19,7 +19,7 @@
19
19
  * an exact key-set check, not by a best-effort denylist of forbidden names.
20
20
  * ============================================================================
21
21
  */
22
- import type { ReasoningLaneTypeV4, TaskClassV4 } from "./reasoning-router-v4.ts";
22
+ import type { ReasoningLaneTypeV4, TaskClassV4 } from "./reasoning-router-v4-weights.ts";
23
23
  /** Router version eligible to tag a feedback record. */
24
24
  export type RouterFeedbackVersion = "v4";
25
25
  /** Lane type at record time, or "none" for a non-subagent turn. */
@@ -1 +1 @@
1
- {"version":3,"file":"router-feedback-collector.d.ts","sourceRoot":"","sources":["../../src/core/router-feedback-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAgBH,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEjF,wDAAwD;AACxD,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEzC,mEAAmE;AACnE,MAAM,MAAM,sBAAsB,GAAG,mBAAmB,GAAG,MAAM,CAAC;AAElE,6CAA6C;AAC7C,MAAM,MAAM,uBAAuB,GAAG,WAAW,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AAEpG,8BAA8B;AAC9B,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,WAAW,GAAG,iBAAiB,GAAG,eAAe,CAAC;AAErG,mEAAmE;AACnE,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE9G,4FAA0F;AAC1F,MAAM,MAAM,uBAAuB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEpE;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,cAAc,EAAE,uBAAuB,CAAC;IACjD,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;IAC5C,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,uBAAuB,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,wBAAwB,EAAE,SAAS,qBAAqB,EAAW,CAAC;AACjF,eAAO,MAAM,0BAA0B,EAAE,SAAS,sBAAsB,EAQvE,CAAC;AACF,eAAO,MAAM,4BAA4B,EAAE,SAAS,uBAAuB,EAQ1E,CAAC;AACF,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAQhE,CAAC;AAgBF,eAAO,MAAM,2BAA2B,EAAE,SAAS,uBAAuB,EAA6B,CAAC;AA6BxG;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAgBpF;AAED,6GAA6G;AAC7G,wBAAgB,kCAAkC,IAAI,MAAM,CAE3D;AAED,MAAM,WAAW,2BAA2B;IAC3C,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qGAAqG;IACrG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,0BAA0B,GACnC,UAAU,GACV,gBAAgB,GAChB,iBAAiB,GACjB,kBAAkB,GAClB,UAAU,CAAC;AAEd,MAAM,MAAM,0BAA0B,GACnC;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAA;CAAE,CAAC;AAgD7E;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACzC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,2BAA2B,GAClC,0BAA0B,CAwD5B","sourcesContent":["/**\n * Router feedback collector — privacy-safe learning ledger for `/think auto`.\n *\n * Default-off, privacy-safe local ledger for the v4 reasoning-router learning\n * path. This module is a pure utility; consent and wiring live in settings and\n * agent-session.ts.\n *\n * ============================================================================\n * SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)\n * ============================================================================\n * routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,\n * signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded\n * enum/number/boolean value. There is no field, in this type or in the runtime\n * validator below, that can carry raw prompt text, a prompt hash, an exact\n * prompt length, a file path, a diff/code blob, a session id, a model id, a\n * provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`\n * rejects ANY object with an unrecognized key (including all of the fields\n * just listed) — the schema is the redaction policy, enforced structurally by\n * an exact key-set check, not by a best-effort denylist of forbidden names.\n * ============================================================================\n */\n\nimport {\n\tchmodSync,\n\tcloseSync,\n\texistsSync,\n\tconstants as fsConstants,\n\tfstatSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\twriteSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.ts\";\nimport type { ReasoningLaneTypeV4, TaskClassV4 } from \"./reasoning-router-v4.ts\";\n\n/** Router version eligible to tag a feedback record. */\nexport type RouterFeedbackVersion = \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV4 | \"none\";\n\n/** Router task class; never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV4;\n\n/**\n * Reasoning ladder level, excluding \"off\". The router never *resolves* to\n * \"off\"; turning thinking off entirely is a distinct manual action outside\n * this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/** Feedback signal source. */\nexport type RouterFeedbackSignal = \"s1-override\" | \"s2-accept\" | \"s3-hook-outcome\" | \"s4-regression\";\n\n/** Coarse outcome tag; never hook stdout/stderr or tool output. */\nexport type RouterFeedbackOutcome = \"up\" | \"down\" | \"same\" | \"accepted\" | \"pass\" | \"fail\" | \"debug-follow-up\";\n\n/** floor(log2(len+1)) clamped to [0,7] — a coarse bucket, never the raw prompt length. */\nexport type RouterFeedbackLenBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n/**\n * One JSONL ledger record. Exactly these ten keys; see `isRouterFeedbackRecord`\n * for the runtime allowlist enforcement.\n */\nexport interface RouterFeedbackRecord {\n\treadonly routerVersion: RouterFeedbackVersion;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly resolvedLevel: RouterFeedbackLevel;\n\treadonly acceptedLevel: RouterFeedbackLevel;\n\treadonly signal: RouterFeedbackSignal;\n\treadonly outcome: RouterFeedbackOutcome;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport const ROUTER_FEEDBACK_VERSIONS: readonly RouterFeedbackVersion[] = [\"v4\"];\nexport const ROUTER_FEEDBACK_LANE_TYPES: readonly RouterFeedbackLaneType[] = [\n\t\"planner\",\n\t\"security\",\n\t\"explorer\",\n\t\"coder\",\n\t\"reviewer\",\n\t\"tester\",\n\t\"none\",\n];\nexport const ROUTER_FEEDBACK_TASK_CLASSES: readonly RouterFeedbackTaskClass[] = [\n\t\"trivial\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n];\nexport const ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\nconst ROUTER_FEEDBACK_SIGNALS: readonly RouterFeedbackSignal[] = [\n\t\"s1-override\",\n\t\"s2-accept\",\n\t\"s3-hook-outcome\",\n\t\"s4-regression\",\n];\nconst ROUTER_FEEDBACK_OUTCOMES: readonly RouterFeedbackOutcome[] = [\n\t\"up\",\n\t\"down\",\n\t\"same\",\n\t\"accepted\",\n\t\"pass\",\n\t\"fail\",\n\t\"debug-follow-up\",\n];\nexport const ROUTER_FEEDBACK_LEN_BUCKETS: readonly RouterFeedbackLenBucket[] = [0, 1, 2, 3, 4, 5, 6, 7];\n\n/** Exact allowed key set for `RouterFeedbackRecord`, sorted for canonical set-equality comparison. */\nconst ROUTER_FEEDBACK_RECORD_KEYS: readonly string[] = [\n\t\"routerVersion\",\n\t\"laneType\",\n\t\"predictedClass\",\n\t\"resolvedLevel\",\n\t\"acceptedLevel\",\n\t\"signal\",\n\t\"outcome\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n].sort();\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactRouterFeedbackKeySet(value: Record<string, unknown>): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== ROUTER_FEEDBACK_RECORD_KEYS.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== ROUTER_FEEDBACK_RECORD_KEYS[i]) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Strict runtime validator: exactly the ten allowed keys (no more, no fewer),\n * each within its bounded value set. Returns false for any extra key —\n * including a raw prompt, file path, diff, session id, model id, provider\n * payload, or hook/tool-output-shaped field — because ANY unrecognized key\n * fails the exact key-set check before any per-field check runs.\n */\nexport function isRouterFeedbackRecord(value: unknown): value is RouterFeedbackRecord {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactRouterFeedbackKeySet(value)) return false;\n\n\tif (!ROUTER_FEEDBACK_VERSIONS.includes(value.routerVersion as RouterFeedbackVersion)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.resolvedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.acceptedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_SIGNALS.includes(value.signal as RouterFeedbackSignal)) return false;\n\tif (!ROUTER_FEEDBACK_OUTCOMES.includes(value.outcome as RouterFeedbackOutcome)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\n\treturn true;\n}\n\n/** Legacy shared ledger path. Agent sessions use repository-scoped defaults unless explicitly overridden. */\nexport function getDefaultRouterFeedbackLedgerPath(): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"ledger.jsonl\");\n}\n\nexport interface AppendRouterFeedbackOptions {\n\t/** Consent gate. Must be exactly `true`; anything else performs zero filesystem access. */\n\treadonly enabled: boolean;\n\t/** Ledger file path override (tests / callers only); defaults to the owner-only agent-dir ledger. */\n\treadonly ledgerPath?: string;\n}\n\nexport type AppendRouterFeedbackReason =\n\t| \"disabled\"\n\t| \"invalid-schema\"\n\t| \"symlink-refused\"\n\t| \"not-regular-file\"\n\t| \"io-error\";\n\nexport type AppendRouterFeedbackResult =\n\t| { readonly appended: true }\n\t| { readonly appended: false; readonly reason: AppendRouterFeedbackReason };\n\n/** True when `path` exists and is a symlink. Uses `lstat` so it never follows the link. */\nfunction isSymlinkPath(path: string): boolean {\n\ttry {\n\t\treturn lstatSync(path).isSymbolicLink();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error\n\t\t? String((error as { code?: unknown }).code)\n\t\t: undefined;\n}\n\nfunction acquireLedgerLockSync(path: string): () => void {\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Busy-wait briefly; mirrors the auth-storage/settings-manager sync retry pattern.\n\t\t\t}\n\t\t}\n\t}\n\n\tthrow (lastError as Error) ?? new Error(\"Failed to acquire router-feedback ledger lock\");\n}\n\n/**\n * O_NOFOLLOW is undefined on some platforms (notably Windows); `?? 0` keeps\n * the flag combination valid there, and the `lstat` pre-checks above/below\n * remain the fallback symlink guard on platforms without O_NOFOLLOW.\n */\nconst LEDGER_OPEN_FLAGS =\n\tfsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0);\n\n/**\n * Append one validated feedback record as a single JSONL line.\n *\n * Requires `options.enabled === true` (checked first, before any filesystem\n * access whatsoever — disabled or omitted consent never touches disk).\n * Validates the record against the exact ten-key schema before any I/O.\n * Refuses symlinks and non-regular files where the platform allows detecting\n * them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`\n * is unavailable on some platforms, hence \"when possible\"). Reuses the same\n * `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for\n * concurrent-session safety. Never throws for expected failure modes — it\n * returns a tagged result so a caller can fail closed silently.\n */\nexport function appendRouterFeedbackRecord(\n\trecord: unknown,\n\toptions: AppendRouterFeedbackOptions,\n): AppendRouterFeedbackResult {\n\tif (options.enabled !== true) {\n\t\treturn { appended: false, reason: \"disabled\" };\n\t}\n\tif (!isRouterFeedbackRecord(record)) {\n\t\treturn { appended: false, reason: \"invalid-schema\" };\n\t}\n\n\tconst ledgerPath = options.ledgerPath ?? getDefaultRouterFeedbackLedgerPath();\n\tconst ledgerDir = dirname(ledgerPath);\n\n\ttry {\n\t\tif (isSymlinkPath(ledgerDir)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\t\tif (!existsSync(ledgerDir)) {\n\t\t\tmkdirSync(ledgerDir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = acquireLedgerLockSync(ledgerPath);\n\n\t\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t}\n\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openSync(ledgerPath, LEDGER_OPEN_FLAGS, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ELOOP\") {\n\t\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (!fstatSync(fd).isFile()) {\n\t\t\t\t\treturn { appended: false, reason: \"not-regular-file\" };\n\t\t\t\t}\n\t\t\t\twriteSync(fd, `${JSON.stringify(record)}\\n`);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tchmodSync(ledgerPath, 0o600);\n\t\t\treturn { appended: true };\n\t\t} finally {\n\t\t\tif (release) release();\n\t\t}\n\t} catch {\n\t\treturn { appended: false, reason: \"io-error\" };\n\t}\n}\n"]}
1
+ {"version":3,"file":"router-feedback-collector.d.ts","sourceRoot":"","sources":["../../src/core/router-feedback-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAmBH,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAEzF,wDAAwD;AACxD,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEzC,mEAAmE;AACnE,MAAM,MAAM,sBAAsB,GAAG,mBAAmB,GAAG,MAAM,CAAC;AAElE,6CAA6C;AAC7C,MAAM,MAAM,uBAAuB,GAAG,WAAW,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AAEpG,8BAA8B;AAC9B,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,WAAW,GAAG,iBAAiB,GAAG,eAAe,CAAC;AAErG,mEAAmE;AACnE,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE9G,4FAA0F;AAC1F,MAAM,MAAM,uBAAuB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEpE;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,cAAc,EAAE,uBAAuB,CAAC;IACjD,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;IAC5C,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,uBAAuB,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,wBAAwB,EAAE,SAAS,qBAAqB,EAAW,CAAC;AACjF,eAAO,MAAM,0BAA0B,EAAE,SAAS,sBAAsB,EAQvE,CAAC;AACF,eAAO,MAAM,4BAA4B,EAAE,SAAS,uBAAuB,EAQ1E,CAAC;AACF,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAQhE,CAAC;AAgBF,eAAO,MAAM,2BAA2B,EAAE,SAAS,uBAAuB,EAA6B,CAAC;AA6BxG;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAgBpF;AAED,6GAA6G;AAC7G,wBAAgB,kCAAkC,IAAI,MAAM,CAE3D;AAED,MAAM,WAAW,2BAA2B;IAC3C,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qGAAqG;IACrG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,0BAA0B,GACnC,UAAU,GACV,gBAAgB,GAChB,iBAAiB,GACjB,kBAAkB,GAClB,UAAU,CAAC;AAEd,MAAM,MAAM,0BAA0B,GACnC;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAA;CAAE,CAAC;AAgD7E;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACzC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,2BAA2B,GAClC,0BAA0B,CAwD5B","sourcesContent":["/**\n * Router feedback collector — privacy-safe learning ledger for `/think auto`.\n *\n * Default-off, privacy-safe local ledger for the v4 reasoning-router learning\n * path. This module is a pure utility; consent and wiring live in settings and\n * agent-session.ts.\n *\n * ============================================================================\n * SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)\n * ============================================================================\n * routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,\n * signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded\n * enum/number/boolean value. There is no field, in this type or in the runtime\n * validator below, that can carry raw prompt text, a prompt hash, an exact\n * prompt length, a file path, a diff/code blob, a session id, a model id, a\n * provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`\n * rejects ANY object with an unrecognized key (including all of the fields\n * just listed) — the schema is the redaction policy, enforced structurally by\n * an exact key-set check, not by a best-effort denylist of forbidden names.\n * ============================================================================\n */\n\nimport {\n\tchmodSync,\n\tcloseSync,\n\texistsSync,\n\tconstants as fsConstants,\n\tfstatSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\twriteSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.ts\";\n// Imported from the weights module, where both types are declared. Routing\n// through `reasoning-router-v4.ts` — which only re-exports them — made the\n// classifier and this ledger import each other.\nimport type { ReasoningLaneTypeV4, TaskClassV4 } from \"./reasoning-router-v4-weights.ts\";\n\n/** Router version eligible to tag a feedback record. */\nexport type RouterFeedbackVersion = \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV4 | \"none\";\n\n/** Router task class; never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV4;\n\n/**\n * Reasoning ladder level, excluding \"off\". The router never *resolves* to\n * \"off\"; turning thinking off entirely is a distinct manual action outside\n * this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/** Feedback signal source. */\nexport type RouterFeedbackSignal = \"s1-override\" | \"s2-accept\" | \"s3-hook-outcome\" | \"s4-regression\";\n\n/** Coarse outcome tag; never hook stdout/stderr or tool output. */\nexport type RouterFeedbackOutcome = \"up\" | \"down\" | \"same\" | \"accepted\" | \"pass\" | \"fail\" | \"debug-follow-up\";\n\n/** floor(log2(len+1)) clamped to [0,7] — a coarse bucket, never the raw prompt length. */\nexport type RouterFeedbackLenBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n/**\n * One JSONL ledger record. Exactly these ten keys; see `isRouterFeedbackRecord`\n * for the runtime allowlist enforcement.\n */\nexport interface RouterFeedbackRecord {\n\treadonly routerVersion: RouterFeedbackVersion;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly resolvedLevel: RouterFeedbackLevel;\n\treadonly acceptedLevel: RouterFeedbackLevel;\n\treadonly signal: RouterFeedbackSignal;\n\treadonly outcome: RouterFeedbackOutcome;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport const ROUTER_FEEDBACK_VERSIONS: readonly RouterFeedbackVersion[] = [\"v4\"];\nexport const ROUTER_FEEDBACK_LANE_TYPES: readonly RouterFeedbackLaneType[] = [\n\t\"planner\",\n\t\"security\",\n\t\"explorer\",\n\t\"coder\",\n\t\"reviewer\",\n\t\"tester\",\n\t\"none\",\n];\nexport const ROUTER_FEEDBACK_TASK_CLASSES: readonly RouterFeedbackTaskClass[] = [\n\t\"trivial\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n];\nexport const ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\nconst ROUTER_FEEDBACK_SIGNALS: readonly RouterFeedbackSignal[] = [\n\t\"s1-override\",\n\t\"s2-accept\",\n\t\"s3-hook-outcome\",\n\t\"s4-regression\",\n];\nconst ROUTER_FEEDBACK_OUTCOMES: readonly RouterFeedbackOutcome[] = [\n\t\"up\",\n\t\"down\",\n\t\"same\",\n\t\"accepted\",\n\t\"pass\",\n\t\"fail\",\n\t\"debug-follow-up\",\n];\nexport const ROUTER_FEEDBACK_LEN_BUCKETS: readonly RouterFeedbackLenBucket[] = [0, 1, 2, 3, 4, 5, 6, 7];\n\n/** Exact allowed key set for `RouterFeedbackRecord`, sorted for canonical set-equality comparison. */\nconst ROUTER_FEEDBACK_RECORD_KEYS: readonly string[] = [\n\t\"routerVersion\",\n\t\"laneType\",\n\t\"predictedClass\",\n\t\"resolvedLevel\",\n\t\"acceptedLevel\",\n\t\"signal\",\n\t\"outcome\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n].sort();\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactRouterFeedbackKeySet(value: Record<string, unknown>): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== ROUTER_FEEDBACK_RECORD_KEYS.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== ROUTER_FEEDBACK_RECORD_KEYS[i]) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Strict runtime validator: exactly the ten allowed keys (no more, no fewer),\n * each within its bounded value set. Returns false for any extra key —\n * including a raw prompt, file path, diff, session id, model id, provider\n * payload, or hook/tool-output-shaped field — because ANY unrecognized key\n * fails the exact key-set check before any per-field check runs.\n */\nexport function isRouterFeedbackRecord(value: unknown): value is RouterFeedbackRecord {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactRouterFeedbackKeySet(value)) return false;\n\n\tif (!ROUTER_FEEDBACK_VERSIONS.includes(value.routerVersion as RouterFeedbackVersion)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.resolvedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.acceptedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_SIGNALS.includes(value.signal as RouterFeedbackSignal)) return false;\n\tif (!ROUTER_FEEDBACK_OUTCOMES.includes(value.outcome as RouterFeedbackOutcome)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\n\treturn true;\n}\n\n/** Legacy shared ledger path. Agent sessions use repository-scoped defaults unless explicitly overridden. */\nexport function getDefaultRouterFeedbackLedgerPath(): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"ledger.jsonl\");\n}\n\nexport interface AppendRouterFeedbackOptions {\n\t/** Consent gate. Must be exactly `true`; anything else performs zero filesystem access. */\n\treadonly enabled: boolean;\n\t/** Ledger file path override (tests / callers only); defaults to the owner-only agent-dir ledger. */\n\treadonly ledgerPath?: string;\n}\n\nexport type AppendRouterFeedbackReason =\n\t| \"disabled\"\n\t| \"invalid-schema\"\n\t| \"symlink-refused\"\n\t| \"not-regular-file\"\n\t| \"io-error\";\n\nexport type AppendRouterFeedbackResult =\n\t| { readonly appended: true }\n\t| { readonly appended: false; readonly reason: AppendRouterFeedbackReason };\n\n/** True when `path` exists and is a symlink. Uses `lstat` so it never follows the link. */\nfunction isSymlinkPath(path: string): boolean {\n\ttry {\n\t\treturn lstatSync(path).isSymbolicLink();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error\n\t\t? String((error as { code?: unknown }).code)\n\t\t: undefined;\n}\n\nfunction acquireLedgerLockSync(path: string): () => void {\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Busy-wait briefly; mirrors the auth-storage/settings-manager sync retry pattern.\n\t\t\t}\n\t\t}\n\t}\n\n\tthrow (lastError as Error) ?? new Error(\"Failed to acquire router-feedback ledger lock\");\n}\n\n/**\n * O_NOFOLLOW is undefined on some platforms (notably Windows); `?? 0` keeps\n * the flag combination valid there, and the `lstat` pre-checks above/below\n * remain the fallback symlink guard on platforms without O_NOFOLLOW.\n */\nconst LEDGER_OPEN_FLAGS =\n\tfsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0);\n\n/**\n * Append one validated feedback record as a single JSONL line.\n *\n * Requires `options.enabled === true` (checked first, before any filesystem\n * access whatsoever — disabled or omitted consent never touches disk).\n * Validates the record against the exact ten-key schema before any I/O.\n * Refuses symlinks and non-regular files where the platform allows detecting\n * them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`\n * is unavailable on some platforms, hence \"when possible\"). Reuses the same\n * `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for\n * concurrent-session safety. Never throws for expected failure modes — it\n * returns a tagged result so a caller can fail closed silently.\n */\nexport function appendRouterFeedbackRecord(\n\trecord: unknown,\n\toptions: AppendRouterFeedbackOptions,\n): AppendRouterFeedbackResult {\n\tif (options.enabled !== true) {\n\t\treturn { appended: false, reason: \"disabled\" };\n\t}\n\tif (!isRouterFeedbackRecord(record)) {\n\t\treturn { appended: false, reason: \"invalid-schema\" };\n\t}\n\n\tconst ledgerPath = options.ledgerPath ?? getDefaultRouterFeedbackLedgerPath();\n\tconst ledgerDir = dirname(ledgerPath);\n\n\ttry {\n\t\tif (isSymlinkPath(ledgerDir)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\t\tif (!existsSync(ledgerDir)) {\n\t\t\tmkdirSync(ledgerDir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = acquireLedgerLockSync(ledgerPath);\n\n\t\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t}\n\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openSync(ledgerPath, LEDGER_OPEN_FLAGS, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ELOOP\") {\n\t\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (!fstatSync(fd).isFile()) {\n\t\t\t\t\treturn { appended: false, reason: \"not-regular-file\" };\n\t\t\t\t}\n\t\t\t\twriteSync(fd, `${JSON.stringify(record)}\\n`);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tchmodSync(ledgerPath, 0o600);\n\t\t\treturn { appended: true };\n\t\t} finally {\n\t\t\tif (release) release();\n\t\t}\n\t} catch {\n\t\treturn { appended: false, reason: \"io-error\" };\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"router-feedback-collector.js","sourceRoot":"","sources":["../../src/core/router-feedback-collector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Router feedback collector — privacy-safe learning ledger for `/think auto`.\n *\n * Default-off, privacy-safe local ledger for the v4 reasoning-router learning\n * path. This module is a pure utility; consent and wiring live in settings and\n * agent-session.ts.\n *\n * ============================================================================\n * SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)\n * ============================================================================\n * routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,\n * signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded\n * enum/number/boolean value. There is no field, in this type or in the runtime\n * validator below, that can carry raw prompt text, a prompt hash, an exact\n * prompt length, a file path, a diff/code blob, a session id, a model id, a\n * provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`\n * rejects ANY object with an unrecognized key (including all of the fields\n * just listed) — the schema is the redaction policy, enforced structurally by\n * an exact key-set check, not by a best-effort denylist of forbidden names.\n * ============================================================================\n */\n\nimport {\n\tchmodSync,\n\tcloseSync,\n\texistsSync,\n\tconstants as fsConstants,\n\tfstatSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\twriteSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.ts\";\nimport type { ReasoningLaneTypeV4, TaskClassV4 } from \"./reasoning-router-v4.ts\";\n\n/** Router version eligible to tag a feedback record. */\nexport type RouterFeedbackVersion = \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV4 | \"none\";\n\n/** Router task class; never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV4;\n\n/**\n * Reasoning ladder level, excluding \"off\". The router never *resolves* to\n * \"off\"; turning thinking off entirely is a distinct manual action outside\n * this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/** Feedback signal source. */\nexport type RouterFeedbackSignal = \"s1-override\" | \"s2-accept\" | \"s3-hook-outcome\" | \"s4-regression\";\n\n/** Coarse outcome tag; never hook stdout/stderr or tool output. */\nexport type RouterFeedbackOutcome = \"up\" | \"down\" | \"same\" | \"accepted\" | \"pass\" | \"fail\" | \"debug-follow-up\";\n\n/** floor(log2(len+1)) clamped to [0,7] — a coarse bucket, never the raw prompt length. */\nexport type RouterFeedbackLenBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n/**\n * One JSONL ledger record. Exactly these ten keys; see `isRouterFeedbackRecord`\n * for the runtime allowlist enforcement.\n */\nexport interface RouterFeedbackRecord {\n\treadonly routerVersion: RouterFeedbackVersion;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly resolvedLevel: RouterFeedbackLevel;\n\treadonly acceptedLevel: RouterFeedbackLevel;\n\treadonly signal: RouterFeedbackSignal;\n\treadonly outcome: RouterFeedbackOutcome;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport const ROUTER_FEEDBACK_VERSIONS: readonly RouterFeedbackVersion[] = [\"v4\"];\nexport const ROUTER_FEEDBACK_LANE_TYPES: readonly RouterFeedbackLaneType[] = [\n\t\"planner\",\n\t\"security\",\n\t\"explorer\",\n\t\"coder\",\n\t\"reviewer\",\n\t\"tester\",\n\t\"none\",\n];\nexport const ROUTER_FEEDBACK_TASK_CLASSES: readonly RouterFeedbackTaskClass[] = [\n\t\"trivial\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n];\nexport const ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\nconst ROUTER_FEEDBACK_SIGNALS: readonly RouterFeedbackSignal[] = [\n\t\"s1-override\",\n\t\"s2-accept\",\n\t\"s3-hook-outcome\",\n\t\"s4-regression\",\n];\nconst ROUTER_FEEDBACK_OUTCOMES: readonly RouterFeedbackOutcome[] = [\n\t\"up\",\n\t\"down\",\n\t\"same\",\n\t\"accepted\",\n\t\"pass\",\n\t\"fail\",\n\t\"debug-follow-up\",\n];\nexport const ROUTER_FEEDBACK_LEN_BUCKETS: readonly RouterFeedbackLenBucket[] = [0, 1, 2, 3, 4, 5, 6, 7];\n\n/** Exact allowed key set for `RouterFeedbackRecord`, sorted for canonical set-equality comparison. */\nconst ROUTER_FEEDBACK_RECORD_KEYS: readonly string[] = [\n\t\"routerVersion\",\n\t\"laneType\",\n\t\"predictedClass\",\n\t\"resolvedLevel\",\n\t\"acceptedLevel\",\n\t\"signal\",\n\t\"outcome\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n].sort();\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactRouterFeedbackKeySet(value: Record<string, unknown>): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== ROUTER_FEEDBACK_RECORD_KEYS.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== ROUTER_FEEDBACK_RECORD_KEYS[i]) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Strict runtime validator: exactly the ten allowed keys (no more, no fewer),\n * each within its bounded value set. Returns false for any extra key —\n * including a raw prompt, file path, diff, session id, model id, provider\n * payload, or hook/tool-output-shaped field — because ANY unrecognized key\n * fails the exact key-set check before any per-field check runs.\n */\nexport function isRouterFeedbackRecord(value: unknown): value is RouterFeedbackRecord {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactRouterFeedbackKeySet(value)) return false;\n\n\tif (!ROUTER_FEEDBACK_VERSIONS.includes(value.routerVersion as RouterFeedbackVersion)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.resolvedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.acceptedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_SIGNALS.includes(value.signal as RouterFeedbackSignal)) return false;\n\tif (!ROUTER_FEEDBACK_OUTCOMES.includes(value.outcome as RouterFeedbackOutcome)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\n\treturn true;\n}\n\n/** Legacy shared ledger path. Agent sessions use repository-scoped defaults unless explicitly overridden. */\nexport function getDefaultRouterFeedbackLedgerPath(): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"ledger.jsonl\");\n}\n\nexport interface AppendRouterFeedbackOptions {\n\t/** Consent gate. Must be exactly `true`; anything else performs zero filesystem access. */\n\treadonly enabled: boolean;\n\t/** Ledger file path override (tests / callers only); defaults to the owner-only agent-dir ledger. */\n\treadonly ledgerPath?: string;\n}\n\nexport type AppendRouterFeedbackReason =\n\t| \"disabled\"\n\t| \"invalid-schema\"\n\t| \"symlink-refused\"\n\t| \"not-regular-file\"\n\t| \"io-error\";\n\nexport type AppendRouterFeedbackResult =\n\t| { readonly appended: true }\n\t| { readonly appended: false; readonly reason: AppendRouterFeedbackReason };\n\n/** True when `path` exists and is a symlink. Uses `lstat` so it never follows the link. */\nfunction isSymlinkPath(path: string): boolean {\n\ttry {\n\t\treturn lstatSync(path).isSymbolicLink();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error\n\t\t? String((error as { code?: unknown }).code)\n\t\t: undefined;\n}\n\nfunction acquireLedgerLockSync(path: string): () => void {\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Busy-wait briefly; mirrors the auth-storage/settings-manager sync retry pattern.\n\t\t\t}\n\t\t}\n\t}\n\n\tthrow (lastError as Error) ?? new Error(\"Failed to acquire router-feedback ledger lock\");\n}\n\n/**\n * O_NOFOLLOW is undefined on some platforms (notably Windows); `?? 0` keeps\n * the flag combination valid there, and the `lstat` pre-checks above/below\n * remain the fallback symlink guard on platforms without O_NOFOLLOW.\n */\nconst LEDGER_OPEN_FLAGS =\n\tfsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0);\n\n/**\n * Append one validated feedback record as a single JSONL line.\n *\n * Requires `options.enabled === true` (checked first, before any filesystem\n * access whatsoever — disabled or omitted consent never touches disk).\n * Validates the record against the exact ten-key schema before any I/O.\n * Refuses symlinks and non-regular files where the platform allows detecting\n * them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`\n * is unavailable on some platforms, hence \"when possible\"). Reuses the same\n * `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for\n * concurrent-session safety. Never throws for expected failure modes — it\n * returns a tagged result so a caller can fail closed silently.\n */\nexport function appendRouterFeedbackRecord(\n\trecord: unknown,\n\toptions: AppendRouterFeedbackOptions,\n): AppendRouterFeedbackResult {\n\tif (options.enabled !== true) {\n\t\treturn { appended: false, reason: \"disabled\" };\n\t}\n\tif (!isRouterFeedbackRecord(record)) {\n\t\treturn { appended: false, reason: \"invalid-schema\" };\n\t}\n\n\tconst ledgerPath = options.ledgerPath ?? getDefaultRouterFeedbackLedgerPath();\n\tconst ledgerDir = dirname(ledgerPath);\n\n\ttry {\n\t\tif (isSymlinkPath(ledgerDir)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\t\tif (!existsSync(ledgerDir)) {\n\t\t\tmkdirSync(ledgerDir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = acquireLedgerLockSync(ledgerPath);\n\n\t\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t}\n\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openSync(ledgerPath, LEDGER_OPEN_FLAGS, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ELOOP\") {\n\t\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (!fstatSync(fd).isFile()) {\n\t\t\t\t\treturn { appended: false, reason: \"not-regular-file\" };\n\t\t\t\t}\n\t\t\t\twriteSync(fd, `${JSON.stringify(record)}\\n`);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tchmodSync(ledgerPath, 0o600);\n\t\t\treturn { appended: true };\n\t\t} finally {\n\t\t\tif (release) release();\n\t\t}\n\t} catch {\n\t\treturn { appended: false, reason: \"io-error\" };\n\t}\n}\n"]}
1
+ {"version":3,"file":"router-feedback-collector.js","sourceRoot":"","sources":["../../src/core/router-feedback-collector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Router feedback collector — privacy-safe learning ledger for `/think auto`.\n *\n * Default-off, privacy-safe local ledger for the v4 reasoning-router learning\n * path. This module is a pure utility; consent and wiring live in settings and\n * agent-session.ts.\n *\n * ============================================================================\n * SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)\n * ============================================================================\n * routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,\n * signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded\n * enum/number/boolean value. There is no field, in this type or in the runtime\n * validator below, that can carry raw prompt text, a prompt hash, an exact\n * prompt length, a file path, a diff/code blob, a session id, a model id, a\n * provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`\n * rejects ANY object with an unrecognized key (including all of the fields\n * just listed) — the schema is the redaction policy, enforced structurally by\n * an exact key-set check, not by a best-effort denylist of forbidden names.\n * ============================================================================\n */\n\nimport {\n\tchmodSync,\n\tcloseSync,\n\texistsSync,\n\tconstants as fsConstants,\n\tfstatSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\twriteSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.ts\";\n// Imported from the weights module, where both types are declared. Routing\n// through `reasoning-router-v4.ts` — which only re-exports them — made the\n// classifier and this ledger import each other.\nimport type { ReasoningLaneTypeV4, TaskClassV4 } from \"./reasoning-router-v4-weights.ts\";\n\n/** Router version eligible to tag a feedback record. */\nexport type RouterFeedbackVersion = \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV4 | \"none\";\n\n/** Router task class; never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV4;\n\n/**\n * Reasoning ladder level, excluding \"off\". The router never *resolves* to\n * \"off\"; turning thinking off entirely is a distinct manual action outside\n * this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/** Feedback signal source. */\nexport type RouterFeedbackSignal = \"s1-override\" | \"s2-accept\" | \"s3-hook-outcome\" | \"s4-regression\";\n\n/** Coarse outcome tag; never hook stdout/stderr or tool output. */\nexport type RouterFeedbackOutcome = \"up\" | \"down\" | \"same\" | \"accepted\" | \"pass\" | \"fail\" | \"debug-follow-up\";\n\n/** floor(log2(len+1)) clamped to [0,7] — a coarse bucket, never the raw prompt length. */\nexport type RouterFeedbackLenBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n/**\n * One JSONL ledger record. Exactly these ten keys; see `isRouterFeedbackRecord`\n * for the runtime allowlist enforcement.\n */\nexport interface RouterFeedbackRecord {\n\treadonly routerVersion: RouterFeedbackVersion;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly resolvedLevel: RouterFeedbackLevel;\n\treadonly acceptedLevel: RouterFeedbackLevel;\n\treadonly signal: RouterFeedbackSignal;\n\treadonly outcome: RouterFeedbackOutcome;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport const ROUTER_FEEDBACK_VERSIONS: readonly RouterFeedbackVersion[] = [\"v4\"];\nexport const ROUTER_FEEDBACK_LANE_TYPES: readonly RouterFeedbackLaneType[] = [\n\t\"planner\",\n\t\"security\",\n\t\"explorer\",\n\t\"coder\",\n\t\"reviewer\",\n\t\"tester\",\n\t\"none\",\n];\nexport const ROUTER_FEEDBACK_TASK_CLASSES: readonly RouterFeedbackTaskClass[] = [\n\t\"trivial\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n];\nexport const ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\nconst ROUTER_FEEDBACK_SIGNALS: readonly RouterFeedbackSignal[] = [\n\t\"s1-override\",\n\t\"s2-accept\",\n\t\"s3-hook-outcome\",\n\t\"s4-regression\",\n];\nconst ROUTER_FEEDBACK_OUTCOMES: readonly RouterFeedbackOutcome[] = [\n\t\"up\",\n\t\"down\",\n\t\"same\",\n\t\"accepted\",\n\t\"pass\",\n\t\"fail\",\n\t\"debug-follow-up\",\n];\nexport const ROUTER_FEEDBACK_LEN_BUCKETS: readonly RouterFeedbackLenBucket[] = [0, 1, 2, 3, 4, 5, 6, 7];\n\n/** Exact allowed key set for `RouterFeedbackRecord`, sorted for canonical set-equality comparison. */\nconst ROUTER_FEEDBACK_RECORD_KEYS: readonly string[] = [\n\t\"routerVersion\",\n\t\"laneType\",\n\t\"predictedClass\",\n\t\"resolvedLevel\",\n\t\"acceptedLevel\",\n\t\"signal\",\n\t\"outcome\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n].sort();\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactRouterFeedbackKeySet(value: Record<string, unknown>): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== ROUTER_FEEDBACK_RECORD_KEYS.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== ROUTER_FEEDBACK_RECORD_KEYS[i]) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Strict runtime validator: exactly the ten allowed keys (no more, no fewer),\n * each within its bounded value set. Returns false for any extra key —\n * including a raw prompt, file path, diff, session id, model id, provider\n * payload, or hook/tool-output-shaped field — because ANY unrecognized key\n * fails the exact key-set check before any per-field check runs.\n */\nexport function isRouterFeedbackRecord(value: unknown): value is RouterFeedbackRecord {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactRouterFeedbackKeySet(value)) return false;\n\n\tif (!ROUTER_FEEDBACK_VERSIONS.includes(value.routerVersion as RouterFeedbackVersion)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.resolvedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.acceptedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_SIGNALS.includes(value.signal as RouterFeedbackSignal)) return false;\n\tif (!ROUTER_FEEDBACK_OUTCOMES.includes(value.outcome as RouterFeedbackOutcome)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\n\treturn true;\n}\n\n/** Legacy shared ledger path. Agent sessions use repository-scoped defaults unless explicitly overridden. */\nexport function getDefaultRouterFeedbackLedgerPath(): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"ledger.jsonl\");\n}\n\nexport interface AppendRouterFeedbackOptions {\n\t/** Consent gate. Must be exactly `true`; anything else performs zero filesystem access. */\n\treadonly enabled: boolean;\n\t/** Ledger file path override (tests / callers only); defaults to the owner-only agent-dir ledger. */\n\treadonly ledgerPath?: string;\n}\n\nexport type AppendRouterFeedbackReason =\n\t| \"disabled\"\n\t| \"invalid-schema\"\n\t| \"symlink-refused\"\n\t| \"not-regular-file\"\n\t| \"io-error\";\n\nexport type AppendRouterFeedbackResult =\n\t| { readonly appended: true }\n\t| { readonly appended: false; readonly reason: AppendRouterFeedbackReason };\n\n/** True when `path` exists and is a symlink. Uses `lstat` so it never follows the link. */\nfunction isSymlinkPath(path: string): boolean {\n\ttry {\n\t\treturn lstatSync(path).isSymbolicLink();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error\n\t\t? String((error as { code?: unknown }).code)\n\t\t: undefined;\n}\n\nfunction acquireLedgerLockSync(path: string): () => void {\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Busy-wait briefly; mirrors the auth-storage/settings-manager sync retry pattern.\n\t\t\t}\n\t\t}\n\t}\n\n\tthrow (lastError as Error) ?? new Error(\"Failed to acquire router-feedback ledger lock\");\n}\n\n/**\n * O_NOFOLLOW is undefined on some platforms (notably Windows); `?? 0` keeps\n * the flag combination valid there, and the `lstat` pre-checks above/below\n * remain the fallback symlink guard on platforms without O_NOFOLLOW.\n */\nconst LEDGER_OPEN_FLAGS =\n\tfsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0);\n\n/**\n * Append one validated feedback record as a single JSONL line.\n *\n * Requires `options.enabled === true` (checked first, before any filesystem\n * access whatsoever — disabled or omitted consent never touches disk).\n * Validates the record against the exact ten-key schema before any I/O.\n * Refuses symlinks and non-regular files where the platform allows detecting\n * them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`\n * is unavailable on some platforms, hence \"when possible\"). Reuses the same\n * `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for\n * concurrent-session safety. Never throws for expected failure modes — it\n * returns a tagged result so a caller can fail closed silently.\n */\nexport function appendRouterFeedbackRecord(\n\trecord: unknown,\n\toptions: AppendRouterFeedbackOptions,\n): AppendRouterFeedbackResult {\n\tif (options.enabled !== true) {\n\t\treturn { appended: false, reason: \"disabled\" };\n\t}\n\tif (!isRouterFeedbackRecord(record)) {\n\t\treturn { appended: false, reason: \"invalid-schema\" };\n\t}\n\n\tconst ledgerPath = options.ledgerPath ?? getDefaultRouterFeedbackLedgerPath();\n\tconst ledgerDir = dirname(ledgerPath);\n\n\ttry {\n\t\tif (isSymlinkPath(ledgerDir)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\t\tif (!existsSync(ledgerDir)) {\n\t\t\tmkdirSync(ledgerDir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = acquireLedgerLockSync(ledgerPath);\n\n\t\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t}\n\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openSync(ledgerPath, LEDGER_OPEN_FLAGS, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ELOOP\") {\n\t\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (!fstatSync(fd).isFile()) {\n\t\t\t\t\treturn { appended: false, reason: \"not-regular-file\" };\n\t\t\t\t}\n\t\t\t\twriteSync(fd, `${JSON.stringify(record)}\\n`);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tchmodSync(ledgerPath, 0o600);\n\t\t\treturn { appended: true };\n\t\t} finally {\n\t\t\tif (release) release();\n\t\t}\n\t} catch {\n\t\treturn { appended: false, reason: \"io-error\" };\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"session-bash-service.d.ts","sourceRoot":"","sources":["../../src/core/session-bash-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,oBAAoB,CAAC;AAOhF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAA6B,MAAM,iBAAiB,CAAC;AAE5G,MAAM,WAAW,kBAAkB;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8FAA8F;IAC9F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,gEAAgE;IAChE,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAED,iGAA+F;AAC/F,MAAM,WAAW,uBAAuB;IACvC,yEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,aAAa,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,uBAAuB,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9F,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,QAAQ,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChE;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC5G;AAUD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAE9C,YAAY,IAAI,EAAE,sBAAsB,EAEvC;IAEK,WAAW,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAkGrB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAoBtG;IAED,mCAAmC;IACnC,SAAS,IAAI,IAAI,CAEhB;IAED,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED,IAAI,sBAAsB,IAAI,OAAO,CAEpC;IAED,+EAA+E;IAC/E,YAAY,IAAI,IAAI,CAOnB;CACD","sourcesContent":["/**\n * Session bash service: command execution, result recording, abort, and the\n * streaming-deferral queue. Extracted from AgentSession so the session class\n * coordinates lifecycle while this owns the bash surface end-to-end.\n *\n * Ordering contract (preserved from the session implementation):\n * - results recorded while the agent is streaming are queued and flushed on\n * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;\n * - `recordBashResult` is idempotent-safe to call from executeBash and from\n * extensions that run bash themselves.\n */\nimport { type BashResult, executeBashWithOperations } from \"./bash-executor.ts\";\nimport { classifyShellCommand } from \"./command-safety.ts\";\nimport {\n\tbuildBlockedBashResult,\n\tevaluateCommandGate,\n\tisCommandSafetyAssumeYesEnabled,\n} from \"./extensions/builtin/command-safety-gate.ts\";\nimport type { LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { assertLoadoutAccess, decideLoadoutAccess } from \"./loadout-access-policy.ts\";\nimport type { BashExecutionMessage } from \"./messages.ts\";\nimport type { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\n\nexport interface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\n/** Resource gate grant (roadmap §11/M3): §11.3 blocked payload, permit release, or ungated. */\nexport interface BashResourcePermitGrant {\n\t/** §11.3 structured block JSON; when set the command must not spawn. */\n\treadonly blocked?: string;\n\treadonly release?: () => void;\n}\n\nexport interface SessionBashServiceDeps {\n\treadonly settings: SettingsManager;\n\treadonly runtime: SessionBashRuntime;\n\treadonly loadoutPolicy: LoadoutAccessPolicy | undefined;\n\treadonly getCwd: () => string;\n\treadonly emit: (event: { type: \"bash_execution_update\"; id?: string; delta: string }) => void;\n\treadonly isStreaming: () => boolean;\n\treadonly pushMessage: (message: BashExecutionMessage) => void;\n\treadonly appendMessage: (message: BashExecutionMessage) => void;\n\t/**\n\t * Optional resource governor gate, invoked AFTER command safety (§9.4\n\t * order) and before any shell spawns. The abort signal cancels a queued\n\t * permit wait. Implementations must never throw.\n\t */\n\treadonly acquireResourcePermit?: (command: string, signal: AbortSignal) => Promise<BashResourcePermitGrant>;\n}\n\nfunction isSandboxDeniedError(error: unknown): error is Error {\n\treturn error instanceof Error && error.message.startsWith(\"sandbox: shell denied\");\n}\n\nfunction buildSandboxDeniedBashResult(reason: string): BashResult {\n\treturn { output: reason, exitCode: 1, cancelled: false, truncated: false };\n}\n\nexport class SessionBashService {\n\tprivate abortController: AbortController | undefined;\n\tprivate pendingBashMessages: BashExecutionMessage[] = [];\n\tprivate readonly deps: SessionBashServiceDeps;\n\n\tconstructor(deps: SessionBashServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\tconst { settings, runtime, loadoutPolicy } = this.deps;\n\t\tconst prefix = settings.getShellCommandPrefix();\n\t\tconst shellPath = settings.getShellPath();\n\t\tconst resolvedCommand = prefix ? `${prefix}\\n${command}` : command;\n\t\tif (loadoutPolicy) {\n\t\t\tassertLoadoutAccess((request) => decideLoadoutAccess(loadoutPolicy, request), {\n\t\t\t\toperation: \"execute\",\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tcommand: resolvedCommand,\n\t\t\t});\n\t\t}\n\n\t\t// Non-negotiable safety floor for headless callers (RPC bash): hard-deny\n\t\t// block-tier commands before any shell is spawned.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst floorVerdict = classifyShellCommand(command);\n\t\t\tif (floorVerdict.risk === \"block\") {\n\t\t\t\tthrow new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);\n\t\t\t}\n\t\t}\n\n\t\t// Command-safety parity for non-interactive callers (RPC bash). Interactive\n\t\t// `!`/`!!` bash is gated earlier through the user_bash extension event, which\n\t\t// keeps its prompt-based approval semantics, so it does not pass safetyGate and\n\t\t// is never double-prompted here. confirm/block-tier verdicts deny headlessly;\n\t\t// the EFFECTIVE command (after the shell command prefix) is classified.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst decision = await evaluateCommandGate(resolvedCommand, {\n\t\t\t\thasUI: false,\n\t\t\t\theadlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? \"allow\" : \"deny\",\n\t\t\t});\n\t\t\tif (decision?.deny) {\n\t\t\t\tconst blocked = buildBlockedBashResult(decision.reason);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t}\n\n\t\tthis.abortController = new AbortController();\n\t\tlet releaseResourcePermit: (() => void) | undefined;\n\n\t\ttry {\n\t\t\t// Resource safety gate (§9.4: command safety above stays authoritative;\n\t\t\t// pressure can only throttle or defer an already-allowed command).\n\t\t\tif (this.deps.acquireResourcePermit) {\n\t\t\t\tconst grant = await this.deps.acquireResourcePermit(resolvedCommand, this.abortController.signal);\n\t\t\t\tif (grant.blocked !== undefined) {\n\t\t\t\t\tconst blocked = buildBlockedBashResult(grant.blocked);\n\t\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\t\treturn blocked;\n\t\t\t\t}\n\t\t\t\treleaseResourcePermit = grant.release;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst operations =\n\t\t\t\t\toptions?.operations ??\n\t\t\t\t\tcreateLocalBashOperations({\n\t\t\t\t\t\tshellPath,\n\t\t\t\t\t\tsandboxPolicy: runtime.sandboxPreflight(options?.sandboxPolicy),\n\t\t\t\t\t});\n\t\t\t\tconst cwd = this.deps.getCwd();\n\t\t\t\tconst onChunkWrapped = (delta: string) => {\n\t\t\t\t\tonChunk?.(delta);\n\t\t\t\t\tthis.deps.emit({ type: \"bash_execution_update\", id: options?.id, delta });\n\t\t\t\t};\n\n\t\t\t\t// Default-on verified path when a session ledger exists; OMK_VERIFIED_BASH=0 rolls back.\n\t\t\t\tconst verified = await runtime.executeVerified({\n\t\t\t\t\tresolvedCommand,\n\t\t\t\t\tcwd,\n\t\t\t\t\tshellPath,\n\t\t\t\t\toperations,\n\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t});\n\t\t\t\tconst result: BashResult =\n\t\t\t\t\tverified ??\n\t\t\t\t\t(await executeBashWithOperations(resolvedCommand, cwd, operations, {\n\t\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\t}));\n\n\t\t\t\tthis.recordBashResult(command, result, options);\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tif (!isSandboxDeniedError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tconst blocked = buildSandboxDeniedBashResult(error.message);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t} finally {\n\t\t\t// §10.3 exactly-once: the pool treats a repeated release as a no-op.\n\t\t\treleaseResourcePermit?.();\n\t\t\tthis.abortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tconst bashMessage: BashExecutionMessage = {\n\t\t\trole: \"bashExecution\",\n\t\t\tcommand,\n\t\t\toutput: result.output,\n\t\t\texitCode: result.exitCode,\n\t\t\tcancelled: result.cancelled,\n\t\t\ttruncated: result.truncated,\n\t\t\tfullOutputPath: result.fullOutputPath,\n\t\t\ttimestamp: Date.now(),\n\t\t\texcludeFromContext: options?.excludeFromContext,\n\t\t};\n\n\t\t// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering\n\t\tif (this.deps.isStreaming()) {\n\t\t\tthis.pendingBashMessages.push(bashMessage);\n\t\t} else {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t}\n\n\t/** Cancel running bash command. */\n\tabortBash(): void {\n\t\tthis.abortController?.abort();\n\t}\n\n\tget isBashRunning(): boolean {\n\t\treturn this.abortController !== undefined;\n\t}\n\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this.pendingBashMessages.length > 0;\n\t}\n\n\t/** Flush queued results after the agent turn completes (ordering contract). */\n\tflushPending(): void {\n\t\tif (this.pendingBashMessages.length === 0) return;\n\t\tfor (const bashMessage of this.pendingBashMessages) {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t\tthis.pendingBashMessages = [];\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-bash-service.d.ts","sourceRoot":"","sources":["../../src/core/session-bash-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,oBAAoB,CAAC;AAQhF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAA6B,MAAM,iBAAiB,CAAC;AAE5G,MAAM,WAAW,kBAAkB;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8FAA8F;IAC9F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,gEAAgE;IAChE,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAED,iGAA+F;AAC/F,MAAM,WAAW,uBAAuB;IACvC,yEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,aAAa,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,uBAAuB,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9F,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,QAAQ,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChE;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC5G;AAUD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAE9C,YAAY,IAAI,EAAE,sBAAsB,EAEvC;IAEK,WAAW,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAmGrB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAoBtG;IAED,mCAAmC;IACnC,SAAS,IAAI,IAAI,CAEhB;IAED,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED,IAAI,sBAAsB,IAAI,OAAO,CAEpC;IAED,+EAA+E;IAC/E,YAAY,IAAI,IAAI,CAOnB;CACD","sourcesContent":["/**\n * Session bash service: command execution, result recording, abort, and the\n * streaming-deferral queue. Extracted from AgentSession so the session class\n * coordinates lifecycle while this owns the bash surface end-to-end.\n *\n * Ordering contract (preserved from the session implementation):\n * - results recorded while the agent is streaming are queued and flushed on\n * turn end (`flushPending`) so tool_use/tool_result ordering never breaks;\n * - `recordBashResult` is idempotent-safe to call from executeBash and from\n * extensions that run bash themselves.\n */\nimport { type BashResult, executeBashWithOperations } from \"./bash-executor.ts\";\nimport { classifyShellCommand } from \"./command-safety.ts\";\nimport {\n\tbuildBlockedBashResult,\n\tevaluateCommandGate,\n\tisCommandSafetyAssumeYesEnabled,\n\tisCommandSafetyDisabled,\n} from \"./extensions/builtin/command-safety-gate.ts\";\nimport type { LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { assertLoadoutAccess, decideLoadoutAccess } from \"./loadout-access-policy.ts\";\nimport type { BashExecutionMessage } from \"./messages.ts\";\nimport type { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\n\nexport interface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\n/** Resource gate grant (roadmap §11/M3): §11.3 blocked payload, permit release, or ungated. */\nexport interface BashResourcePermitGrant {\n\t/** §11.3 structured block JSON; when set the command must not spawn. */\n\treadonly blocked?: string;\n\treadonly release?: () => void;\n}\n\nexport interface SessionBashServiceDeps {\n\treadonly settings: SettingsManager;\n\treadonly runtime: SessionBashRuntime;\n\treadonly loadoutPolicy: LoadoutAccessPolicy | undefined;\n\treadonly getCwd: () => string;\n\treadonly emit: (event: { type: \"bash_execution_update\"; id?: string; delta: string }) => void;\n\treadonly isStreaming: () => boolean;\n\treadonly pushMessage: (message: BashExecutionMessage) => void;\n\treadonly appendMessage: (message: BashExecutionMessage) => void;\n\t/**\n\t * Optional resource governor gate, invoked AFTER command safety (§9.4\n\t * order) and before any shell spawns. The abort signal cancels a queued\n\t * permit wait. Implementations must never throw.\n\t */\n\treadonly acquireResourcePermit?: (command: string, signal: AbortSignal) => Promise<BashResourcePermitGrant>;\n}\n\nfunction isSandboxDeniedError(error: unknown): error is Error {\n\treturn error instanceof Error && error.message.startsWith(\"sandbox: shell denied\");\n}\n\nfunction buildSandboxDeniedBashResult(reason: string): BashResult {\n\treturn { output: reason, exitCode: 1, cancelled: false, truncated: false };\n}\n\nexport class SessionBashService {\n\tprivate abortController: AbortController | undefined;\n\tprivate pendingBashMessages: BashExecutionMessage[] = [];\n\tprivate readonly deps: SessionBashServiceDeps;\n\n\tconstructor(deps: SessionBashServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\tconst { settings, runtime, loadoutPolicy } = this.deps;\n\t\tconst prefix = settings.getShellCommandPrefix();\n\t\tconst shellPath = settings.getShellPath();\n\t\tconst resolvedCommand = prefix ? `${prefix}\\n${command}` : command;\n\t\tif (loadoutPolicy) {\n\t\t\tassertLoadoutAccess((request) => decideLoadoutAccess(loadoutPolicy, request), {\n\t\t\t\toperation: \"execute\",\n\t\t\t\ttoolName: \"bash\",\n\t\t\t\tcommand: resolvedCommand,\n\t\t\t});\n\t\t}\n\n\t\t// Non-negotiable safety floor for headless callers (RPC bash): hard-deny\n\t\t// block-tier commands before any shell is spawned. YOLO mode opts out of\n\t\t// the entire gate — the floor included — so nothing prompts or denies.\n\t\tif (options?.safetyGate === \"headless\" && !isCommandSafetyDisabled()) {\n\t\t\tconst floorVerdict = classifyShellCommand(command);\n\t\t\tif (floorVerdict.risk === \"block\") {\n\t\t\t\tthrow new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);\n\t\t\t}\n\t\t}\n\n\t\t// Command-safety parity for non-interactive callers (RPC bash). Interactive\n\t\t// `!`/`!!` bash is gated earlier through the user_bash extension event, which\n\t\t// keeps its prompt-based approval semantics, so it does not pass safetyGate and\n\t\t// is never double-prompted here. confirm/block-tier verdicts deny headlessly;\n\t\t// the EFFECTIVE command (after the shell command prefix) is classified.\n\t\tif (options?.safetyGate === \"headless\") {\n\t\t\tconst decision = await evaluateCommandGate(resolvedCommand, {\n\t\t\t\thasUI: false,\n\t\t\t\theadlessConfirmPolicy: isCommandSafetyAssumeYesEnabled() ? \"allow\" : \"deny\",\n\t\t\t});\n\t\t\tif (decision?.deny) {\n\t\t\t\tconst blocked = buildBlockedBashResult(decision.reason);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t}\n\n\t\tthis.abortController = new AbortController();\n\t\tlet releaseResourcePermit: (() => void) | undefined;\n\n\t\ttry {\n\t\t\t// Resource safety gate (§9.4: command safety above stays authoritative;\n\t\t\t// pressure can only throttle or defer an already-allowed command).\n\t\t\tif (this.deps.acquireResourcePermit) {\n\t\t\t\tconst grant = await this.deps.acquireResourcePermit(resolvedCommand, this.abortController.signal);\n\t\t\t\tif (grant.blocked !== undefined) {\n\t\t\t\t\tconst blocked = buildBlockedBashResult(grant.blocked);\n\t\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\t\treturn blocked;\n\t\t\t\t}\n\t\t\t\treleaseResourcePermit = grant.release;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst operations =\n\t\t\t\t\toptions?.operations ??\n\t\t\t\t\tcreateLocalBashOperations({\n\t\t\t\t\t\tshellPath,\n\t\t\t\t\t\tsandboxPolicy: runtime.sandboxPreflight(options?.sandboxPolicy),\n\t\t\t\t\t});\n\t\t\t\tconst cwd = this.deps.getCwd();\n\t\t\t\tconst onChunkWrapped = (delta: string) => {\n\t\t\t\t\tonChunk?.(delta);\n\t\t\t\t\tthis.deps.emit({ type: \"bash_execution_update\", id: options?.id, delta });\n\t\t\t\t};\n\n\t\t\t\t// Default-on verified path when a session ledger exists; OMK_VERIFIED_BASH=0 rolls back.\n\t\t\t\tconst verified = await runtime.executeVerified({\n\t\t\t\t\tresolvedCommand,\n\t\t\t\t\tcwd,\n\t\t\t\t\tshellPath,\n\t\t\t\t\toperations,\n\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t});\n\t\t\t\tconst result: BashResult =\n\t\t\t\t\tverified ??\n\t\t\t\t\t(await executeBashWithOperations(resolvedCommand, cwd, operations, {\n\t\t\t\t\t\tonChunk: onChunkWrapped,\n\t\t\t\t\t\tsignal: this.abortController.signal,\n\t\t\t\t\t}));\n\n\t\t\t\tthis.recordBashResult(command, result, options);\n\t\t\t\treturn result;\n\t\t\t} catch (error) {\n\t\t\t\tif (!isSandboxDeniedError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tconst blocked = buildSandboxDeniedBashResult(error.message);\n\t\t\t\tthis.recordBashResult(command, blocked, options);\n\t\t\t\treturn blocked;\n\t\t\t}\n\t\t} finally {\n\t\t\t// §10.3 exactly-once: the pool treats a repeated release as a no-op.\n\t\t\treleaseResourcePermit?.();\n\t\t\tthis.abortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tconst bashMessage: BashExecutionMessage = {\n\t\t\trole: \"bashExecution\",\n\t\t\tcommand,\n\t\t\toutput: result.output,\n\t\t\texitCode: result.exitCode,\n\t\t\tcancelled: result.cancelled,\n\t\t\ttruncated: result.truncated,\n\t\t\tfullOutputPath: result.fullOutputPath,\n\t\t\ttimestamp: Date.now(),\n\t\t\texcludeFromContext: options?.excludeFromContext,\n\t\t};\n\n\t\t// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering\n\t\tif (this.deps.isStreaming()) {\n\t\t\tthis.pendingBashMessages.push(bashMessage);\n\t\t} else {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t}\n\n\t/** Cancel running bash command. */\n\tabortBash(): void {\n\t\tthis.abortController?.abort();\n\t}\n\n\tget isBashRunning(): boolean {\n\t\treturn this.abortController !== undefined;\n\t}\n\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this.pendingBashMessages.length > 0;\n\t}\n\n\t/** Flush queued results after the agent turn completes (ordering contract). */\n\tflushPending(): void {\n\t\tif (this.pendingBashMessages.length === 0) return;\n\t\tfor (const bashMessage of this.pendingBashMessages) {\n\t\t\tthis.deps.pushMessage(bashMessage);\n\t\t\tthis.deps.appendMessage(bashMessage);\n\t\t}\n\t\tthis.pendingBashMessages = [];\n\t}\n}\n"]}
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { executeBashWithOperations } from "./bash-executor.js";
13
13
  import { classifyShellCommand } from "./command-safety.js";
14
- import { buildBlockedBashResult, evaluateCommandGate, isCommandSafetyAssumeYesEnabled, } from "./extensions/builtin/command-safety-gate.js";
14
+ import { buildBlockedBashResult, evaluateCommandGate, isCommandSafetyAssumeYesEnabled, isCommandSafetyDisabled, } from "./extensions/builtin/command-safety-gate.js";
15
15
  import { assertLoadoutAccess, decideLoadoutAccess } from "./loadout-access-policy.js";
16
16
  import { createLocalBashOperations } from "./tools/bash.js";
17
17
  function isSandboxDeniedError(error) {
@@ -40,8 +40,9 @@ export class SessionBashService {
40
40
  });
41
41
  }
42
42
  // Non-negotiable safety floor for headless callers (RPC bash): hard-deny
43
- // block-tier commands before any shell is spawned.
44
- if (options?.safetyGate === "headless") {
43
+ // block-tier commands before any shell is spawned. YOLO mode opts out of
44
+ // the entire gate — the floor included — so nothing prompts or denies.
45
+ if (options?.safetyGate === "headless" && !isCommandSafetyDisabled()) {
45
46
  const floorVerdict = classifyShellCommand(command);
46
47
  if (floorVerdict.risk === "block") {
47
48
  throw new Error(`OMK §0.1 safety floor blocked bash: [${floorVerdict.rule}] ${floorVerdict.reason}`);