@code-yeongyu/senpi 2026.9.26 → 2026.9.27-3

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 (404) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/bun-runtime.d.ts +18 -2
  3. package/dist/bun-runtime.d.ts.map +1 -1
  4. package/dist/bun-runtime.js +60 -8
  5. package/dist/bun-runtime.js.map +1 -1
  6. package/dist/bundle/chunks/anthropic-messages-3FGAXYJS.js +8 -0
  7. package/dist/bundle/chunks/{app-server-command-ZRCKWY2X.js → app-server-command-ZVB35UIJ.js} +1 -1
  8. package/dist/bundle/chunks/azure-openai-responses-4SUOOZZ4.js +2 -0
  9. package/dist/bundle/chunks/bun-extension-importer-R4HXF3GD.js +14 -0
  10. package/dist/bundle/chunks/{chunk-KMJXSXRX.js → chunk-357I4IUU.js} +2 -2
  11. package/dist/bundle/chunks/chunk-3SIQIY57.js +8 -0
  12. package/dist/bundle/chunks/chunk-45SZOLCU.js +2 -0
  13. package/dist/bundle/chunks/{chunk-77ACEMYL.js → chunk-6J6RDMBY.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-BSOJHK4W.js → chunk-7FXXRPB3.js} +2 -1
  15. package/dist/bundle/chunks/{chunk-FD34HSJJ.js → chunk-C6LQNMRB.js} +1 -1
  16. package/dist/bundle/chunks/chunk-ELM77A26.js +2 -0
  17. package/dist/bundle/chunks/chunk-GZVCRV7I.js +3 -0
  18. package/dist/bundle/chunks/{chunk-TTY5BHCG.js → chunk-H2SGCPMD.js} +1 -1
  19. package/dist/bundle/chunks/chunk-KLBKUBDV.js +2 -0
  20. package/dist/bundle/chunks/chunk-L6QV7NZI.js +2 -0
  21. package/dist/bundle/chunks/{chunk-ZKZAATFM.js → chunk-LDB5UMH2.js} +2 -2
  22. package/dist/bundle/chunks/{chunk-HZAKOVHN.js → chunk-LGKKU4VM.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-6Q6DGME2.js → chunk-NMDALHXH.js} +1 -1
  24. package/dist/bundle/chunks/chunk-NNILBBOH.js +18 -0
  25. package/dist/bundle/chunks/{chunk-SUQDU5KM.js → chunk-OQRUK2DK.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-OZCB6BP3.js → chunk-OZN2KUPR.js} +7 -7
  27. package/dist/bundle/chunks/{chunk-X3T45623.js → chunk-QK4QKRTF.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-WM5FVFAP.js → chunk-QPZVJQC5.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-Y54V65MS.js → chunk-QYAO24GB.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-MKAM7IRH.js → chunk-RULGEHQM.js} +1 -1
  31. package/dist/bundle/chunks/{chunk-QGC46CGK.js → chunk-RYI6XR6Z.js} +63 -58
  32. package/dist/bundle/chunks/chunk-U3VOXCGP.js +2 -0
  33. package/dist/bundle/chunks/chunk-W3D3ZYDS.js +2 -0
  34. package/dist/bundle/chunks/{chunk-UBPMXBOJ.js → chunk-XKLR7C5B.js} +1 -1
  35. package/dist/bundle/chunks/{chunk-CLZVNQW3.js → chunk-Y7EIVDHZ.js} +1 -1
  36. package/dist/bundle/chunks/chunk-ZIVYE5YK.js +2 -0
  37. package/dist/bundle/chunks/cli-main-RONHLFWZ.js +3 -0
  38. package/dist/bundle/chunks/google-generative-ai-ZTGKZTGU.js +2 -0
  39. package/dist/bundle/chunks/google-vertex-CCZ376YK.js +2 -0
  40. package/dist/bundle/chunks/help-fast-path-5LRG6FCG.js +2 -0
  41. package/dist/bundle/chunks/host-command-JVPGQR74.js +2 -0
  42. package/dist/bundle/chunks/host-lifecycle-R4HLXLBM.js +3 -0
  43. package/dist/bundle/chunks/host-lifecycle.js +4 -4
  44. package/dist/bundle/chunks/interactive-host-runtime-FES2G62G.js +4 -0
  45. package/dist/bundle/chunks/interactive-mode-45EP2FVL.js +2 -0
  46. package/dist/bundle/chunks/{list-tips-EBRNBPBM.js → list-tips-XXMRDHN6.js} +1 -1
  47. package/dist/bundle/chunks/{multi-session-host-7YX2I7UK.js → multi-session-host-EFCMD5OT.js} +6 -6
  48. package/dist/bundle/chunks/{openai-completions-4CLXDDHJ.js → openai-completions-OP6VFNUR.js} +1 -1
  49. package/dist/bundle/chunks/openai-images-6U5FCN5S.js +2 -0
  50. package/dist/bundle/chunks/openai-responses-7PWMBGYX.js +2 -0
  51. package/dist/bundle/chunks/openrouter-images-QIKWQZUD.js +2 -0
  52. package/dist/bundle/chunks/{package-manager-cli-3ISYTEL2.js → package-manager-cli-5XJS3TYM.js} +1 -1
  53. package/dist/bundle/chunks/rotation-stream-KW7PTEC4.js +2 -0
  54. package/dist/bundle/chunks/rpc-mode-ZHYECU24.js +2 -0
  55. package/dist/bundle/chunks/session-picker-QHIYVQP4.js +2 -0
  56. package/dist/bundle/chunks/session-worker.js +281 -270
  57. package/dist/bundle/chunks/{state-store-Q5EWEAFG.js → state-store-D4GGGT3J.js} +1 -1
  58. package/dist/bundle/cli.js +1 -1
  59. package/dist/bundle/index.js +1 -1
  60. package/dist/bundle/rpc-entry.js +1 -1
  61. package/dist/cli/args.d.ts +1 -0
  62. package/dist/cli/args.d.ts.map +1 -1
  63. package/dist/cli/args.js +4 -0
  64. package/dist/cli/args.js.map +1 -1
  65. package/dist/cli/continue-moved.d.ts +15 -0
  66. package/dist/cli/continue-moved.d.ts.map +1 -0
  67. package/dist/cli/continue-moved.js +26 -0
  68. package/dist/cli/continue-moved.js.map +1 -0
  69. package/dist/cli/cross-project-session.d.ts +22 -0
  70. package/dist/cli/cross-project-session.d.ts.map +1 -0
  71. package/dist/cli/cross-project-session.js +38 -0
  72. package/dist/cli/cross-project-session.js.map +1 -0
  73. package/dist/core/agent-session-runtime.d.ts +3 -0
  74. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  75. package/dist/core/agent-session-runtime.js +43 -10
  76. package/dist/core/agent-session-runtime.js.map +1 -1
  77. package/dist/core/agent-session.d.ts.map +1 -1
  78. package/dist/core/agent-session.js +19 -5
  79. package/dist/core/agent-session.js.map +1 -1
  80. package/dist/core/extensions/builtin/anthropic-subscription/session-registry-pump.d.ts.map +1 -1
  81. package/dist/core/extensions/builtin/anthropic-subscription/session-registry-pump.js +5 -1
  82. package/dist/core/extensions/builtin/anthropic-subscription/session-registry-pump.js.map +1 -1
  83. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-claim.d.ts +13 -1
  84. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-claim.d.ts.map +1 -1
  85. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-claim.js +25 -2
  86. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-claim.js.map +1 -1
  87. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-types.d.ts +1 -0
  88. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-types.d.ts.map +1 -1
  89. package/dist/core/extensions/builtin/anthropic-subscription/session-turn-types.js.map +1 -1
  90. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  91. package/dist/core/extensions/builtin/index.js +2 -0
  92. package/dist/core/extensions/builtin/index.js.map +1 -1
  93. package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -0
  94. package/dist/core/extensions/builtin/mcp/catalog.d.ts.map +1 -1
  95. package/dist/core/extensions/builtin/mcp/catalog.js +9 -0
  96. package/dist/core/extensions/builtin/mcp/catalog.js.map +1 -1
  97. package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
  98. package/dist/core/extensions/builtin/mcp/connection-types.d.ts.map +1 -1
  99. package/dist/core/extensions/builtin/mcp/connection-types.js.map +1 -1
  100. package/dist/core/extensions/builtin/mcp/connection.d.ts +1 -1
  101. package/dist/core/extensions/builtin/mcp/connection.d.ts.map +1 -1
  102. package/dist/core/extensions/builtin/mcp/connection.js +3 -2
  103. package/dist/core/extensions/builtin/mcp/connection.js.map +1 -1
  104. package/dist/core/extensions/builtin/mcp/expose/session.d.ts +3 -0
  105. package/dist/core/extensions/builtin/mcp/expose/session.d.ts.map +1 -1
  106. package/dist/core/extensions/builtin/mcp/expose/session.js +9 -2
  107. package/dist/core/extensions/builtin/mcp/expose/session.js.map +1 -1
  108. package/dist/core/extensions/builtin/mcp/service-register.d.ts.map +1 -1
  109. package/dist/core/extensions/builtin/mcp/service-register.js +7 -1
  110. package/dist/core/extensions/builtin/mcp/service-register.js.map +1 -1
  111. package/dist/core/extensions/builtin/mcp/service-tools-changed.d.ts +18 -0
  112. package/dist/core/extensions/builtin/mcp/service-tools-changed.d.ts.map +1 -0
  113. package/dist/core/extensions/builtin/mcp/service-tools-changed.js +61 -0
  114. package/dist/core/extensions/builtin/mcp/service-tools-changed.js.map +1 -0
  115. package/dist/core/extensions/builtin/mcp/service-types.d.ts +13 -0
  116. package/dist/core/extensions/builtin/mcp/service-types.d.ts.map +1 -1
  117. package/dist/core/extensions/builtin/mcp/service-types.js.map +1 -1
  118. package/dist/core/extensions/builtin/mcp/service.d.ts.map +1 -1
  119. package/dist/core/extensions/builtin/mcp/service.js +4 -37
  120. package/dist/core/extensions/builtin/mcp/service.js.map +1 -1
  121. package/dist/core/extensions/builtin/mcp/shared-lease.js +1 -1
  122. package/dist/core/extensions/builtin/mcp/shared-lease.js.map +1 -1
  123. package/dist/core/extensions/builtin/mcp/startup-race.d.ts.map +1 -1
  124. package/dist/core/extensions/builtin/mcp/startup-race.js +11 -1
  125. package/dist/core/extensions/builtin/mcp/startup-race.js.map +1 -1
  126. package/dist/core/extensions/builtin/permission-system/index.d.ts.map +1 -1
  127. package/dist/core/extensions/builtin/permission-system/index.js +5 -2
  128. package/dist/core/extensions/builtin/permission-system/index.js.map +1 -1
  129. package/dist/core/extensions/builtin/permission-system/parsers.d.ts +5 -0
  130. package/dist/core/extensions/builtin/permission-system/parsers.d.ts.map +1 -1
  131. package/dist/core/extensions/builtin/permission-system/parsers.js +13 -0
  132. package/dist/core/extensions/builtin/permission-system/parsers.js.map +1 -1
  133. package/dist/core/extensions/builtin/repository-identity.d.ts +6 -0
  134. package/dist/core/extensions/builtin/repository-identity.d.ts.map +1 -0
  135. package/dist/core/extensions/builtin/repository-identity.js +33 -0
  136. package/dist/core/extensions/builtin/repository-identity.js.map +1 -0
  137. package/dist/core/extensions/bun-extension-importer.d.ts.map +1 -1
  138. package/dist/core/extensions/bun-extension-importer.js +10 -4
  139. package/dist/core/extensions/bun-extension-importer.js.map +1 -1
  140. package/dist/core/extensions/bun-extension-registry.js +6 -5
  141. package/dist/core/extensions/bun-extension-registry.js.map +1 -1
  142. package/dist/core/extensions/index.d.ts +1 -1
  143. package/dist/core/extensions/index.d.ts.map +1 -1
  144. package/dist/core/extensions/index.js.map +1 -1
  145. package/dist/core/extensions/kernel-prelude.d.ts +13 -0
  146. package/dist/core/extensions/kernel-prelude.d.ts.map +1 -0
  147. package/dist/core/extensions/kernel-prelude.js +38 -0
  148. package/dist/core/extensions/kernel-prelude.js.map +1 -0
  149. package/dist/core/extensions/types.d.ts +44 -2
  150. package/dist/core/extensions/types.d.ts.map +1 -1
  151. package/dist/core/extensions/types.js.map +1 -1
  152. package/dist/core/messages.d.ts.map +1 -1
  153. package/dist/core/messages.js +4 -2
  154. package/dist/core/messages.js.map +1 -1
  155. package/dist/core/model-resolver.js +1 -1
  156. package/dist/core/model-resolver.js.map +1 -1
  157. package/dist/core/moved-sessions.d.ts +10 -0
  158. package/dist/core/moved-sessions.d.ts.map +1 -0
  159. package/dist/core/moved-sessions.js +84 -0
  160. package/dist/core/moved-sessions.js.map +1 -0
  161. package/dist/core/process-crash-record.d.ts +62 -0
  162. package/dist/core/process-crash-record.d.ts.map +1 -0
  163. package/dist/core/process-crash-record.js +216 -0
  164. package/dist/core/process-crash-record.js.map +1 -0
  165. package/dist/core/provider-rejected-images.d.ts +18 -0
  166. package/dist/core/provider-rejected-images.d.ts.map +1 -0
  167. package/dist/core/provider-rejected-images.js +107 -0
  168. package/dist/core/provider-rejected-images.js.map +1 -0
  169. package/dist/core/repository-identity.d.ts +18 -0
  170. package/dist/core/repository-identity.d.ts.map +1 -0
  171. package/dist/core/repository-identity.js +75 -0
  172. package/dist/core/repository-identity.js.map +1 -0
  173. package/dist/core/resume-target.d.ts +20 -0
  174. package/dist/core/resume-target.d.ts.map +1 -0
  175. package/dist/core/resume-target.js +30 -0
  176. package/dist/core/resume-target.js.map +1 -0
  177. package/dist/core/session-discovery.d.ts.map +1 -1
  178. package/dist/core/session-discovery.js +1 -0
  179. package/dist/core/session-discovery.js.map +1 -1
  180. package/dist/core/session-hold-errors.d.ts +20 -0
  181. package/dist/core/session-hold-errors.d.ts.map +1 -0
  182. package/dist/core/session-hold-errors.js +28 -0
  183. package/dist/core/session-hold-errors.js.map +1 -0
  184. package/dist/core/session-holders.d.ts +22 -0
  185. package/dist/core/session-holders.d.ts.map +1 -0
  186. package/dist/core/session-holders.js +234 -0
  187. package/dist/core/session-holders.js.map +1 -0
  188. package/dist/core/session-manager.d.ts +5 -0
  189. package/dist/core/session-manager.d.ts.map +1 -1
  190. package/dist/core/session-manager.js.map +1 -1
  191. package/dist/core/session-rebind.d.ts +23 -0
  192. package/dist/core/session-rebind.d.ts.map +1 -0
  193. package/dist/core/session-rebind.js +127 -0
  194. package/dist/core/session-rebind.js.map +1 -0
  195. package/dist/core/session-summary-index-file.d.ts.map +1 -1
  196. package/dist/core/session-summary-index-file.js +5 -2
  197. package/dist/core/session-summary-index-file.js.map +1 -1
  198. package/dist/core/session-summary.d.ts +3 -0
  199. package/dist/core/session-summary.d.ts.map +1 -1
  200. package/dist/core/session-summary.js +7 -0
  201. package/dist/core/session-summary.js.map +1 -1
  202. package/dist/core/session-title-generator.d.ts.map +1 -1
  203. package/dist/core/session-title-generator.js +21 -3
  204. package/dist/core/session-title-generator.js.map +1 -1
  205. package/dist/core/session-title-reasoning.d.ts +14 -0
  206. package/dist/core/session-title-reasoning.d.ts.map +1 -0
  207. package/dist/core/session-title-reasoning.js +31 -0
  208. package/dist/core/session-title-reasoning.js.map +1 -0
  209. package/dist/core/tools/path-utils.d.ts.map +1 -1
  210. package/dist/core/tools/path-utils.js +13 -0
  211. package/dist/core/tools/path-utils.js.map +1 -1
  212. package/dist/index.d.ts +1 -1
  213. package/dist/index.d.ts.map +1 -1
  214. package/dist/index.js.map +1 -1
  215. package/dist/legacy-dir-copy.d.ts +9 -0
  216. package/dist/legacy-dir-copy.d.ts.map +1 -0
  217. package/dist/legacy-dir-copy.js +69 -0
  218. package/dist/legacy-dir-copy.js.map +1 -0
  219. package/dist/legacy-senpi-dir-migration.d.ts.map +1 -1
  220. package/dist/legacy-senpi-dir-migration.js +64 -48
  221. package/dist/legacy-senpi-dir-migration.js.map +1 -1
  222. package/dist/main.d.ts +6 -0
  223. package/dist/main.d.ts.map +1 -1
  224. package/dist/main.js +129 -22
  225. package/dist/main.js.map +1 -1
  226. package/dist/migrations-state.d.ts +1 -1
  227. package/dist/migrations-state.d.ts.map +1 -1
  228. package/dist/migrations-state.js +5 -1
  229. package/dist/migrations-state.js.map +1 -1
  230. package/dist/migrations.d.ts.map +1 -1
  231. package/dist/migrations.js +4 -0
  232. package/dist/migrations.js.map +1 -1
  233. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  234. package/dist/modes/interactive/components/session-selector.js +5 -3
  235. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  236. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  237. package/dist/modes/interactive/interactive-mode.js +10 -4
  238. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  239. package/dist/modes/interactive/resume-rebind.d.ts +12 -0
  240. package/dist/modes/interactive/resume-rebind.d.ts.map +1 -0
  241. package/dist/modes/interactive/resume-rebind.js +37 -0
  242. package/dist/modes/interactive/resume-rebind.js.map +1 -0
  243. package/dist/modes/interactive/runtime-notice-presenter.d.ts +8 -0
  244. package/dist/modes/interactive/runtime-notice-presenter.d.ts.map +1 -0
  245. package/dist/modes/interactive/runtime-notice-presenter.js +69 -0
  246. package/dist/modes/interactive/runtime-notice-presenter.js.map +1 -0
  247. package/dist/modes/interactive/runtime-notice.d.ts +50 -0
  248. package/dist/modes/interactive/runtime-notice.d.ts.map +1 -0
  249. package/dist/modes/interactive/runtime-notice.js +104 -0
  250. package/dist/modes/interactive/runtime-notice.js.map +1 -0
  251. package/dist/modes/interactive/tips/catalog/computer-tips.d.ts +7 -0
  252. package/dist/modes/interactive/tips/catalog/computer-tips.d.ts.map +1 -0
  253. package/dist/modes/interactive/tips/catalog/computer-tips.js +36 -0
  254. package/dist/modes/interactive/tips/catalog/computer-tips.js.map +1 -0
  255. package/dist/modes/interactive/tips/registry.d.ts.map +1 -1
  256. package/dist/modes/interactive/tips/registry.js +2 -0
  257. package/dist/modes/interactive/tips/registry.js.map +1 -1
  258. package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
  259. package/dist/modes/rpc/connection-handler.js +3 -0
  260. package/dist/modes/rpc/connection-handler.js.map +1 -1
  261. package/dist/modes/rpc/host-child-exit.d.ts.map +1 -1
  262. package/dist/modes/rpc/host-child-exit.js +4 -0
  263. package/dist/modes/rpc/host-child-exit.js.map +1 -1
  264. package/dist/modes/rpc/host-crash-record.d.ts +8 -0
  265. package/dist/modes/rpc/host-crash-record.d.ts.map +1 -1
  266. package/dist/modes/rpc/host-crash-record.js.map +1 -1
  267. package/dist/modes/rpc/host-crash-record.test.js +11 -0
  268. package/dist/modes/rpc/host-crash-record.test.js.map +1 -1
  269. package/dist/modes/rpc/rpc-client.d.ts +2 -2
  270. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  271. package/dist/modes/rpc/rpc-client.js +14 -6
  272. package/dist/modes/rpc/rpc-client.js.map +1 -1
  273. package/dist/modes/rpc/rpc-request-deadline.d.ts +17 -0
  274. package/dist/modes/rpc/rpc-request-deadline.d.ts.map +1 -0
  275. package/dist/modes/rpc/rpc-request-deadline.js +27 -0
  276. package/dist/modes/rpc/rpc-request-deadline.js.map +1 -0
  277. package/dist/modes/rpc/rpc-types.d.ts +9 -2
  278. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  279. package/dist/modes/rpc/rpc-types.js.map +1 -1
  280. package/dist/modes/rpc/session-command-router.d.ts +3 -6
  281. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  282. package/dist/modes/rpc/session-command-router.js +9 -22
  283. package/dist/modes/rpc/session-command-router.js.map +1 -1
  284. package/dist/modes/rpc/session-path-key.d.ts +10 -0
  285. package/dist/modes/rpc/session-path-key.d.ts.map +1 -0
  286. package/dist/modes/rpc/session-path-key.js +33 -0
  287. package/dist/modes/rpc/session-path-key.js.map +1 -0
  288. package/dist/modes/rpc/session-registry.d.ts.map +1 -1
  289. package/dist/modes/rpc/session-registry.js +6 -10
  290. package/dist/modes/rpc/session-registry.js.map +1 -1
  291. package/dist/modes/rpc/session-sweep.d.ts +18 -0
  292. package/dist/modes/rpc/session-sweep.d.ts.map +1 -0
  293. package/dist/modes/rpc/session-sweep.js +32 -0
  294. package/dist/modes/rpc/session-sweep.js.map +1 -0
  295. package/dist/pi-dir-restore.d.ts +4 -0
  296. package/dist/pi-dir-restore.d.ts.map +1 -0
  297. package/dist/pi-dir-restore.js +92 -0
  298. package/dist/pi-dir-restore.js.map +1 -0
  299. package/docs/environment-variables.md +2 -0
  300. package/docs/extensions.md +27 -1
  301. package/docs/quickstart.md +7 -1
  302. package/docs/rpc.md +8 -3
  303. package/docs/sdk.md +29 -0
  304. package/docs/sessions.md +13 -0
  305. package/docs/usage.md +1 -0
  306. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +38 -0
  307. package/node_modules/@code-yeongyu/senpi-codemode/README.md +7 -0
  308. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  309. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +9 -0
  310. package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/schema-bridge.ts +2 -0
  311. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/kernel-preludes.ts +30 -0
  312. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +15 -1
  313. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +1 -1
  314. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/local-module-loader.ts +16 -2
  315. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel.ts +6 -1
  316. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/prelude.py +15 -2
  317. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +7 -1
  318. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/kernel-prelude-plan.ts +19 -0
  319. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt-template.ts +2 -1
  320. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +4 -0
  321. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool-options.ts +11 -1
  322. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +1 -0
  323. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/run-eval-cell.ts +1 -0
  324. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +3 -1
  325. package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts +2 -2
  326. package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
  327. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +7 -0
  328. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
  329. package/node_modules/@earendil-works/pi-agent-core/dist/assistant-terminal-state.d.ts.map +1 -1
  330. package/node_modules/@earendil-works/pi-agent-core/dist/assistant-terminal-state.js +3 -1
  331. package/node_modules/@earendil-works/pi-agent-core/dist/assistant-terminal-state.js.map +1 -1
  332. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts +3 -1
  333. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts.map +1 -1
  334. package/node_modules/@earendil-works/pi-agent-core/dist/types.js.map +1 -1
  335. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  336. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  337. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +10 -7
  338. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  339. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  340. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +9 -1
  341. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  342. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +1 -0
  343. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  344. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  345. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  346. package/node_modules/@earendil-works/pi-ai/dist/provider-diagnostic.d.ts +33 -0
  347. package/node_modules/@earendil-works/pi-ai/dist/provider-diagnostic.d.ts.map +1 -0
  348. package/node_modules/@earendil-works/pi-ai/dist/provider-diagnostic.js +56 -0
  349. package/node_modules/@earendil-works/pi-ai/dist/provider-diagnostic.js.map +1 -0
  350. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  351. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  352. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  353. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  354. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  355. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  356. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +3 -0
  357. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  358. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  359. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-carrier.d.ts +5 -0
  360. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-carrier.d.ts.map +1 -0
  361. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-carrier.js +18 -0
  362. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-carrier.js.map +1 -0
  363. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-sources.d.ts +26 -0
  364. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-sources.d.ts.map +1 -0
  365. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-sources.js +103 -0
  366. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-sources.js.map +1 -0
  367. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-vocabulary.d.ts +36 -0
  368. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-vocabulary.d.ts.map +1 -0
  369. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-vocabulary.js +129 -0
  370. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-diagnostic-vocabulary.js.map +1 -0
  371. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-retry.d.ts.map +1 -1
  372. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-retry.js +5 -4
  373. package/node_modules/@earendil-works/pi-ai/dist/utils/provider-retry.js.map +1 -1
  374. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  375. package/node_modules/@earendil-works/pi-pty/native/prebuilds/darwin-arm64/senpi_pty.darwin-arm64.node +0 -0
  376. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  377. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  378. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  379. package/package.json +7 -7
  380. package/dist/bundle/chunks/anthropic-messages-UYDVRFAS.js +0 -8
  381. package/dist/bundle/chunks/azure-openai-responses-DODWR4W6.js +0 -2
  382. package/dist/bundle/chunks/bun-extension-importer-H4IJCWWQ.js +0 -14
  383. package/dist/bundle/chunks/chunk-34HMVB33.js +0 -2
  384. package/dist/bundle/chunks/chunk-3WBSBDBX.js +0 -13
  385. package/dist/bundle/chunks/chunk-L6JO4M6J.js +0 -3
  386. package/dist/bundle/chunks/chunk-SFYHCFRO.js +0 -2
  387. package/dist/bundle/chunks/chunk-T3JBT2IK.js +0 -8
  388. package/dist/bundle/chunks/chunk-TTHMOL5G.js +0 -2
  389. package/dist/bundle/chunks/chunk-UYCGOK6M.js +0 -2
  390. package/dist/bundle/chunks/chunk-ZTVQY5V6.js +0 -2
  391. package/dist/bundle/chunks/cli-main-H6JAXAXV.js +0 -3
  392. package/dist/bundle/chunks/google-generative-ai-VIEFECKJ.js +0 -2
  393. package/dist/bundle/chunks/google-vertex-Q64CKFID.js +0 -2
  394. package/dist/bundle/chunks/help-fast-path-ZGKPSDFO.js +0 -2
  395. package/dist/bundle/chunks/host-command-BMHJ4YUD.js +0 -2
  396. package/dist/bundle/chunks/host-lifecycle-2GZAGSGW.js +0 -3
  397. package/dist/bundle/chunks/interactive-host-runtime-YI75GFD5.js +0 -4
  398. package/dist/bundle/chunks/interactive-mode-HM7UQYRC.js +0 -2
  399. package/dist/bundle/chunks/openai-images-JA5N5DUU.js +0 -2
  400. package/dist/bundle/chunks/openai-responses-IBFGBAJP.js +0 -2
  401. package/dist/bundle/chunks/openrouter-images-MW5TXV7L.js +0 -2
  402. package/dist/bundle/chunks/rotation-stream-NATO6IMN.js +0 -2
  403. package/dist/bundle/chunks/rpc-mode-6PY4TN5F.js +0 -2
  404. package/dist/bundle/chunks/session-picker-BP7THCWF.js +0 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moved-sessions.js","sourceRoot":"","sources":["../../src/core/moved-sessions.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { getSessionsDir } from \"../config.ts\";\nimport { normalizePath, resolvePath } from \"../utils/paths.ts\";\nimport { compareRepositoryIdentities, type RepositoryIdentity, readRepositoryIdentity } from \"./repository-identity.ts\";\nimport { listSessionsFromDir } from \"./session-discovery.ts\";\nimport { getDefaultSessionDir, type SessionInfo } from \"./session-manager.ts\";\nimport { readFileLines } from \"./session-summary.ts\";\n\n// A session is \"moved\" when it belongs to the current repository (by its recorded identity) and was\n// recorded at a path that no longer exists: the repository moved away from it. A live second checkout\n// is not moved, and an unrecorded or different repository never matches.\n\nexport interface MovedSessionOptions {\n\treadonly sessionDir?: string;\n\treadonly readIdentity?: (dir: string) => Promise<RepositoryIdentity | undefined>;\n}\n\nfunction isMoved(session: SessionInfo, cwd: string, current: RepositoryIdentity): boolean {\n\tif (!session.cwd || resolvePath(session.cwd) === cwd || existsSync(session.cwd)) return false;\n\treturn compareRepositoryIdentities(session.repositoryIdentity, current) === \"same\";\n}\n\nfunction newestFirst(a: SessionInfo, b: SessionInfo): number {\n\treturn b.modified.getTime() - a.modified.getTime();\n}\n\nasync function recordedCwd(dir: string): Promise<string | undefined> {\n\tconst names = (await readdir(dir)).filter((name) => name.endsWith(\".jsonl\"));\n\tfor (const name of names) {\n\t\tlet firstLine: string | undefined;\n\t\ttry {\n\t\t\tawait readFileLines(join(dir, name), (line) => {\n\t\t\t\tfirstLine = line;\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\tconst header: unknown = JSON.parse(firstLine ?? \"\");\n\t\t\tif (typeof header === \"object\" && header !== null && \"cwd\" in header && typeof header.cwd === \"string\") {\n\t\t\t\treturn header.cwd;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error)) throw error;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n// Each project directory holds one cwd, so one header decides whether its sessions can be moved ones;\n// only directories whose recorded path is gone are listed in full.\nasync function sessionsAtVanishedPaths(cwd: string): Promise<SessionInfo[]> {\n\tconst root = getSessionsDir();\n\tif (!existsSync(root)) return [];\n\tconst own = normalizePath(getDefaultSessionDir(cwd));\n\tconst sessions: SessionInfo[] = [];\n\tfor (const entry of await readdir(root, { withFileTypes: true })) {\n\t\tif (!entry.isDirectory()) continue;\n\t\tconst dir = join(root, entry.name);\n\t\tif (normalizePath(dir) === own) continue;\n\t\tconst recorded = await recordedCwd(dir);\n\t\tif (recorded === undefined || existsSync(recorded)) continue;\n\t\tsessions.push(...(await listSessionsFromDir(dir)));\n\t}\n\treturn sessions;\n}\n\nexport async function listMovedSessions(cwd: string, options: MovedSessionOptions = {}): Promise<SessionInfo[]> {\n\tconst here = resolvePath(cwd);\n\tconst current = await (options.readIdentity ?? readRepositoryIdentity)(here);\n\tif (current === undefined) return [];\n\tconst candidates = options.sessionDir\n\t\t? await listSessionsFromDir(normalizePath(options.sessionDir))\n\t\t: await sessionsAtVanishedPaths(here);\n\treturn candidates\n\t\t.filter((session) => isMoved(session, here, current))\n\t\t.map((session) => ({ ...session, moved: true }))\n\t\t.sort(newestFirst);\n}\n\nexport async function markMovedSessions(\n\tsessions: readonly SessionInfo[],\n\tcwd: string,\n\toptions: Pick<MovedSessionOptions, \"readIdentity\"> = {},\n): Promise<SessionInfo[]> {\n\tconst here = resolvePath(cwd);\n\tconst current = await (options.readIdentity ?? readRepositoryIdentity)(here);\n\tif (current === undefined) return [...sessions];\n\treturn sessions.map((session) => (isMoved(session, here, current) ? { ...session, moved: true } : session));\n}\n\nexport async function withMovedSessions(\n\tlocal: Promise<SessionInfo[]>,\n\tcwd: string,\n\toptions: MovedSessionOptions = {},\n): Promise<SessionInfo[]> {\n\tconst [own, moved] = await Promise.all([local, listMovedSessions(cwd, options)]);\n\treturn [...own, ...moved].sort(newestFirst);\n}\n"]}
@@ -0,0 +1,62 @@
1
+ /** Crash records kept per agent dir. A crash loop rewrites the file down to this many. */
2
+ export declare const PROCESS_CRASH_RECORD_LIMIT = 50;
3
+ /** How often a live process refreshes its marker, which bounds the error of a recorded uptime. */
4
+ export declare const PROCESS_MARKER_HEARTBEAT_MS = 60000;
5
+ export interface ProcessCrashPaths {
6
+ readonly dir: string;
7
+ readonly liveDir: string;
8
+ readonly recordsFile: string;
9
+ }
10
+ export declare function processCrashPaths(agentDir: string): ProcessCrashPaths;
11
+ /** What a live process says about itself; the crash record is derived from it. */
12
+ export interface ProcessLifetimeMarker {
13
+ readonly kind: string;
14
+ readonly pid: number;
15
+ readonly startedAt: number;
16
+ readonly bunVersion?: string;
17
+ readonly senpiVersion: string;
18
+ /** The branded product's version (omo's), present only when a brand names one. */
19
+ readonly productVersion?: string;
20
+ }
21
+ export interface ProcessCrashRecord {
22
+ /** ISO-8601 instant of the last heartbeat before the death: the latest moment it was known alive. */
23
+ readonly at: string;
24
+ /** Which kind of process died: the app mode (`interactive`, `print`, `json`). */
25
+ readonly kind: string;
26
+ /** A marker left behind proves the death but not its signal. */
27
+ readonly detection: "unclean_exit";
28
+ /** Start to last heartbeat, in milliseconds; short by at most one heartbeat interval. */
29
+ readonly uptimeMs: number;
30
+ readonly bunVersion?: string;
31
+ readonly senpiVersion: string;
32
+ readonly productVersion?: string;
33
+ }
34
+ /** Runtime identity every record carries; the product version only when a brand sets one. */
35
+ export declare function crashingRuntimeVersions(): {
36
+ bunVersion?: string;
37
+ senpiVersion: string;
38
+ productVersion?: string;
39
+ };
40
+ export interface RecordProcessLifetimeOptions {
41
+ readonly pid?: number;
42
+ readonly now?: () => number;
43
+ readonly isAlive?: (pid: number) => boolean;
44
+ readonly heartbeatMs?: number;
45
+ }
46
+ /**
47
+ * The launch seam: turn every dead peer's marker into a crash record, then, for a process no parent
48
+ * supervises, start this process's own marker. Returns the disposer of this process's marker.
49
+ */
50
+ export declare function recordProcessLifetime(agentDir: string, kind: string, options: RecordProcessLifetimeOptions & {
51
+ readonly supervised: boolean;
52
+ }): () => void;
53
+ /** Write this process's marker, keep it fresh, and remove it on every observable exit. */
54
+ export declare function installProcessLifetimeMarker(agentDir: string, kind: string, options?: RecordProcessLifetimeOptions): () => void;
55
+ /**
56
+ * Record one crash per marker whose process is gone. A marker is CLAIMED by renaming it before it is
57
+ * recorded, so two processes starting at once cannot both record the same death.
58
+ */
59
+ export declare function sweepDeadProcessMarkers(agentDir: string, options?: RecordProcessLifetimeOptions): number;
60
+ /** Every record currently held, oldest first. Unparseable lines are skipped, never thrown on. */
61
+ export declare function readProcessCrashRecords(agentDir: string): readonly ProcessCrashRecord[];
62
+ //# sourceMappingURL=process-crash-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-crash-record.d.ts","sourceRoot":"","sources":["../../src/core/process-crash-record.ts"],"names":[],"mappings":"AAmCA,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C,kGAAkG;AAClG,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAMlD,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAGrE;AAED,kFAAkF;AAClF,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IAClC,qGAAqG;IACrG,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,6FAA6F;AAC7F,wBAAgB,uBAAuB,IAAI;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,CAMhH;AAED,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,4BAA4B,GAAG;IAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GACtE,MAAM,IAAI,CAGZ;AAED,0FAA0F;AAC1F,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,4BAAiC,GACxC,MAAM,IAAI,CA4CZ;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,4BAAiC,GAAG,MAAM,CAkC5G;AAED,iGAAiG;AACjG,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,kBAAkB,EAAE,CAkBvF"}
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Durable evidence that a senpi process nobody supervises DIED rather than exited.
3
+ *
4
+ * The shared RPC host has a supervisor that sees its child's exit signal and writes
5
+ * `rpc-host-daemon/<endpoint>/crashes.jsonl` (`modes/rpc/host-crash-record.ts`). An interactive or
6
+ * print process has no such parent: the omo launcher `execve`s into it on POSIX, so a native crash
7
+ * (a JSC heap corruption, a Bun panic) leaves nothing behind but an OS crash report.
8
+ *
9
+ * Such a process can still leave evidence of its own death by what it FAILS to do. At boot it writes
10
+ * a lifetime marker, `process-crashes/live/<pid>.json`, keeps the marker's mtime fresh as a
11
+ * heartbeat, and removes it on every exit JavaScript can observe - `process.exit`, a natural end, a
12
+ * handled or default-fatal signal (a plain `exit` listener plus `signal-exit`, which every senpi
13
+ * process already loads for `proper-lockfile`, so no new signal behaviour is installed). A native crash or SIGKILL runs none of
14
+ * that. The next senpi start finds the marker of a dead pid and turns it into one crash record in
15
+ * `process-crashes/crashes.jsonl`. The signal is unknowable from outside, so the record says
16
+ * `detection: "unclean_exit"` instead of guessing one.
17
+ *
18
+ * Every step is best-effort and never throws: crash evidence must not be able to delay or break the
19
+ * launch it only annotates.
20
+ */
21
+ import { appendFileSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, utimesSync, writeFileSync, } from "node:fs";
22
+ import { join } from "node:path";
23
+ import onExit from "signal-exit";
24
+ import { DISPLAY_VERSION, VERSION } from "../config.js";
25
+ /** Crash records kept per agent dir. A crash loop rewrites the file down to this many. */
26
+ export const PROCESS_CRASH_RECORD_LIMIT = 50;
27
+ /** How often a live process refreshes its marker, which bounds the error of a recorded uptime. */
28
+ export const PROCESS_MARKER_HEARTBEAT_MS = 60_000;
29
+ const FILE_MODE = 0o600;
30
+ const DIR_MODE = 0o700;
31
+ const MARKER_SUFFIX = ".json";
32
+ export function processCrashPaths(agentDir) {
33
+ const dir = join(agentDir, "process-crashes");
34
+ return { dir, liveDir: join(dir, "live"), recordsFile: join(dir, "crashes.jsonl") };
35
+ }
36
+ /** Runtime identity every record carries; the product version only when a brand sets one. */
37
+ export function crashingRuntimeVersions() {
38
+ return {
39
+ ...(process.versions.bun === undefined ? {} : { bunVersion: process.versions.bun }),
40
+ senpiVersion: VERSION,
41
+ ...(DISPLAY_VERSION === VERSION ? {} : { productVersion: DISPLAY_VERSION }),
42
+ };
43
+ }
44
+ /**
45
+ * The launch seam: turn every dead peer's marker into a crash record, then, for a process no parent
46
+ * supervises, start this process's own marker. Returns the disposer of this process's marker.
47
+ */
48
+ export function recordProcessLifetime(agentDir, kind, options) {
49
+ sweepDeadProcessMarkers(agentDir, options);
50
+ return options.supervised ? () => { } : installProcessLifetimeMarker(agentDir, kind, options);
51
+ }
52
+ /** Write this process's marker, keep it fresh, and remove it on every observable exit. */
53
+ export function installProcessLifetimeMarker(agentDir, kind, options = {}) {
54
+ const pid = options.pid ?? process.pid;
55
+ const now = options.now ?? Date.now;
56
+ const file = join(processCrashPaths(agentDir).liveDir, `${pid}${MARKER_SUFFIX}`);
57
+ const marker = {
58
+ kind,
59
+ pid,
60
+ startedAt: now(),
61
+ ...crashingRuntimeVersions(),
62
+ };
63
+ try {
64
+ mkdirSync(processCrashPaths(agentDir).liveDir, { recursive: true, mode: DIR_MODE });
65
+ writeFileSync(file, `${JSON.stringify(marker)}\n`, { mode: FILE_MODE });
66
+ }
67
+ catch {
68
+ return () => { };
69
+ }
70
+ const heartbeat = setInterval(() => {
71
+ try {
72
+ const at = new Date(now());
73
+ utimesSync(file, at, at);
74
+ }
75
+ catch {
76
+ // A missing or unwritable marker only costs the uptime precision of a later crash.
77
+ }
78
+ }, options.heartbeatMs ?? PROCESS_MARKER_HEARTBEAT_MS);
79
+ heartbeat.unref();
80
+ let disposed = false;
81
+ const dispose = () => {
82
+ if (disposed)
83
+ return;
84
+ disposed = true;
85
+ clearInterval(heartbeat);
86
+ process.off("exit", onProcessExit);
87
+ removeSignalHook();
88
+ rmSync(file, { force: true });
89
+ };
90
+ const onProcessExit = () => {
91
+ try {
92
+ dispose();
93
+ }
94
+ catch { }
95
+ };
96
+ // Bun emits a natural exit's `exit` event without the `process.emit` that signal-exit patches, so
97
+ // the plain listener covers every exit and signal-exit only covers the default-fatal signals.
98
+ process.on("exit", onProcessExit);
99
+ const removeSignalHook = onExit(onProcessExit);
100
+ return dispose;
101
+ }
102
+ /**
103
+ * Record one crash per marker whose process is gone. A marker is CLAIMED by renaming it before it is
104
+ * recorded, so two processes starting at once cannot both record the same death.
105
+ */
106
+ export function sweepDeadProcessMarkers(agentDir, options = {}) {
107
+ const paths = processCrashPaths(agentDir);
108
+ const selfPid = options.pid ?? process.pid;
109
+ const isAlive = options.isAlive ?? isProcessAlive;
110
+ let names;
111
+ try {
112
+ names = readdirSync(paths.liveDir);
113
+ }
114
+ catch {
115
+ return 0;
116
+ }
117
+ let recorded = 0;
118
+ for (const name of names) {
119
+ if (!name.endsWith(MARKER_SUFFIX))
120
+ continue;
121
+ const pid = Number(name.slice(0, -MARKER_SUFFIX.length));
122
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid === selfPid || isAlive(pid))
123
+ continue;
124
+ const claimed = join(paths.liveDir, `${name}.${selfPid}.sweep`);
125
+ try {
126
+ renameSync(join(paths.liveDir, name), claimed);
127
+ }
128
+ catch {
129
+ continue; // Another process claimed it first, or it was removed by its own exit.
130
+ }
131
+ try {
132
+ const record = crashRecordFromMarker(readFileSync(claimed, "utf8"), statSync(claimed).mtimeMs);
133
+ if (record !== undefined) {
134
+ appendProcessCrashRecord(paths, record);
135
+ recorded += 1;
136
+ }
137
+ }
138
+ catch {
139
+ // An unreadable marker is not evidence of anything; it is discarded below.
140
+ }
141
+ finally {
142
+ rmSync(claimed, { force: true });
143
+ }
144
+ }
145
+ return recorded;
146
+ }
147
+ /** Every record currently held, oldest first. Unparseable lines are skipped, never thrown on. */
148
+ export function readProcessCrashRecords(agentDir) {
149
+ let raw;
150
+ try {
151
+ raw = readFileSync(processCrashPaths(agentDir).recordsFile, "utf8");
152
+ }
153
+ catch {
154
+ return [];
155
+ }
156
+ const records = [];
157
+ for (const line of raw.split("\n")) {
158
+ if (line.trim() === "")
159
+ continue;
160
+ try {
161
+ const value = JSON.parse(line);
162
+ if (typeof value === "object" && value !== null && typeof Reflect.get(value, "at") === "string") {
163
+ records.push(value);
164
+ }
165
+ }
166
+ catch { }
167
+ }
168
+ return records;
169
+ }
170
+ /** Signal 0 probes existence; EPERM means the pid exists under another user, so it is alive. */
171
+ function isProcessAlive(pid) {
172
+ try {
173
+ process.kill(pid, 0);
174
+ return true;
175
+ }
176
+ catch (error) {
177
+ return error.code === "EPERM";
178
+ }
179
+ }
180
+ function crashRecordFromMarker(raw, lastHeartbeatMs) {
181
+ const value = JSON.parse(raw);
182
+ if (typeof value !== "object" || value === null)
183
+ return undefined;
184
+ const kind = Reflect.get(value, "kind");
185
+ const startedAt = Reflect.get(value, "startedAt");
186
+ const senpiVersion = Reflect.get(value, "senpiVersion");
187
+ const bunVersion = Reflect.get(value, "bunVersion");
188
+ const productVersion = Reflect.get(value, "productVersion");
189
+ if (typeof kind !== "string" || typeof startedAt !== "number" || typeof senpiVersion !== "string")
190
+ return undefined;
191
+ return {
192
+ at: new Date(lastHeartbeatMs).toISOString(),
193
+ kind,
194
+ detection: "unclean_exit",
195
+ uptimeMs: Math.max(0, Math.round(lastHeartbeatMs - startedAt)),
196
+ ...(typeof bunVersion === "string" ? { bunVersion } : {}),
197
+ senpiVersion,
198
+ ...(typeof productVersion === "string" ? { productVersion } : {}),
199
+ };
200
+ }
201
+ function appendProcessCrashRecord(paths, record) {
202
+ mkdirSync(paths.dir, { recursive: true, mode: DIR_MODE });
203
+ appendFileSync(paths.recordsFile, `${JSON.stringify(record)}\n`, { mode: FILE_MODE });
204
+ const records = readProcessCrashRecordsFrom(paths.recordsFile);
205
+ if (records.length <= PROCESS_CRASH_RECORD_LIMIT)
206
+ return;
207
+ const kept = records.slice(records.length - PROCESS_CRASH_RECORD_LIMIT);
208
+ writeFileSync(paths.recordsFile, `${kept.join("\n")}\n`, { mode: FILE_MODE });
209
+ }
210
+ /** Raw non-empty lines, so pruning keeps each surviving record byte-identical. */
211
+ function readProcessCrashRecordsFrom(file) {
212
+ return readFileSync(file, "utf8")
213
+ .split("\n")
214
+ .filter((line) => line.trim() !== "");
215
+ }
216
+ //# sourceMappingURL=process-crash-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-crash-record.js","sourceRoot":"","sources":["../../src/core/process-crash-record.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable evidence that a senpi process nobody supervises DIED rather than exited.\n *\n * The shared RPC host has a supervisor that sees its child's exit signal and writes\n * `rpc-host-daemon/<endpoint>/crashes.jsonl` (`modes/rpc/host-crash-record.ts`). An interactive or\n * print process has no such parent: the omo launcher `execve`s into it on POSIX, so a native crash\n * (a JSC heap corruption, a Bun panic) leaves nothing behind but an OS crash report.\n *\n * Such a process can still leave evidence of its own death by what it FAILS to do. At boot it writes\n * a lifetime marker, `process-crashes/live/<pid>.json`, keeps the marker's mtime fresh as a\n * heartbeat, and removes it on every exit JavaScript can observe - `process.exit`, a natural end, a\n * handled or default-fatal signal (a plain `exit` listener plus `signal-exit`, which every senpi\n * process already loads for `proper-lockfile`, so no new signal behaviour is installed). A native crash or SIGKILL runs none of\n * that. The next senpi start finds the marker of a dead pid and turns it into one crash record in\n * `process-crashes/crashes.jsonl`. The signal is unknowable from outside, so the record says\n * `detection: \"unclean_exit\"` instead of guessing one.\n *\n * Every step is best-effort and never throws: crash evidence must not be able to delay or break the\n * launch it only annotates.\n */\nimport {\n\tappendFileSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n\tutimesSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport onExit from \"signal-exit\";\nimport { DISPLAY_VERSION, VERSION } from \"../config.ts\";\n\n/** Crash records kept per agent dir. A crash loop rewrites the file down to this many. */\nexport const PROCESS_CRASH_RECORD_LIMIT = 50;\n\n/** How often a live process refreshes its marker, which bounds the error of a recorded uptime. */\nexport const PROCESS_MARKER_HEARTBEAT_MS = 60_000;\n\nconst FILE_MODE = 0o600;\nconst DIR_MODE = 0o700;\nconst MARKER_SUFFIX = \".json\";\n\nexport interface ProcessCrashPaths {\n\treadonly dir: string;\n\treadonly liveDir: string;\n\treadonly recordsFile: string;\n}\n\nexport function processCrashPaths(agentDir: string): ProcessCrashPaths {\n\tconst dir = join(agentDir, \"process-crashes\");\n\treturn { dir, liveDir: join(dir, \"live\"), recordsFile: join(dir, \"crashes.jsonl\") };\n}\n\n/** What a live process says about itself; the crash record is derived from it. */\nexport interface ProcessLifetimeMarker {\n\treadonly kind: string;\n\treadonly pid: number;\n\treadonly startedAt: number;\n\treadonly bunVersion?: string;\n\treadonly senpiVersion: string;\n\t/** The branded product's version (omo's), present only when a brand names one. */\n\treadonly productVersion?: string;\n}\n\nexport interface ProcessCrashRecord {\n\t/** ISO-8601 instant of the last heartbeat before the death: the latest moment it was known alive. */\n\treadonly at: string;\n\t/** Which kind of process died: the app mode (`interactive`, `print`, `json`). */\n\treadonly kind: string;\n\t/** A marker left behind proves the death but not its signal. */\n\treadonly detection: \"unclean_exit\";\n\t/** Start to last heartbeat, in milliseconds; short by at most one heartbeat interval. */\n\treadonly uptimeMs: number;\n\treadonly bunVersion?: string;\n\treadonly senpiVersion: string;\n\treadonly productVersion?: string;\n}\n\n/** Runtime identity every record carries; the product version only when a brand sets one. */\nexport function crashingRuntimeVersions(): { bunVersion?: string; senpiVersion: string; productVersion?: string } {\n\treturn {\n\t\t...(process.versions.bun === undefined ? {} : { bunVersion: process.versions.bun }),\n\t\tsenpiVersion: VERSION,\n\t\t...(DISPLAY_VERSION === VERSION ? {} : { productVersion: DISPLAY_VERSION }),\n\t};\n}\n\nexport interface RecordProcessLifetimeOptions {\n\treadonly pid?: number;\n\treadonly now?: () => number;\n\treadonly isAlive?: (pid: number) => boolean;\n\treadonly heartbeatMs?: number;\n}\n\n/**\n * The launch seam: turn every dead peer's marker into a crash record, then, for a process no parent\n * supervises, start this process's own marker. Returns the disposer of this process's marker.\n */\nexport function recordProcessLifetime(\n\tagentDir: string,\n\tkind: string,\n\toptions: RecordProcessLifetimeOptions & { readonly supervised: boolean },\n): () => void {\n\tsweepDeadProcessMarkers(agentDir, options);\n\treturn options.supervised ? () => {} : installProcessLifetimeMarker(agentDir, kind, options);\n}\n\n/** Write this process's marker, keep it fresh, and remove it on every observable exit. */\nexport function installProcessLifetimeMarker(\n\tagentDir: string,\n\tkind: string,\n\toptions: RecordProcessLifetimeOptions = {},\n): () => void {\n\tconst pid = options.pid ?? process.pid;\n\tconst now = options.now ?? Date.now;\n\tconst file = join(processCrashPaths(agentDir).liveDir, `${pid}${MARKER_SUFFIX}`);\n\tconst marker: ProcessLifetimeMarker = {\n\t\tkind,\n\t\tpid,\n\t\tstartedAt: now(),\n\t\t...crashingRuntimeVersions(),\n\t};\n\ttry {\n\t\tmkdirSync(processCrashPaths(agentDir).liveDir, { recursive: true, mode: DIR_MODE });\n\t\twriteFileSync(file, `${JSON.stringify(marker)}\\n`, { mode: FILE_MODE });\n\t} catch {\n\t\treturn () => {};\n\t}\n\tconst heartbeat = setInterval(() => {\n\t\ttry {\n\t\t\tconst at = new Date(now());\n\t\t\tutimesSync(file, at, at);\n\t\t} catch {\n\t\t\t// A missing or unwritable marker only costs the uptime precision of a later crash.\n\t\t}\n\t}, options.heartbeatMs ?? PROCESS_MARKER_HEARTBEAT_MS);\n\theartbeat.unref();\n\tlet disposed = false;\n\tconst dispose = (): void => {\n\t\tif (disposed) return;\n\t\tdisposed = true;\n\t\tclearInterval(heartbeat);\n\t\tprocess.off(\"exit\", onProcessExit);\n\t\tremoveSignalHook();\n\t\trmSync(file, { force: true });\n\t};\n\tconst onProcessExit = (): void => {\n\t\ttry {\n\t\t\tdispose();\n\t\t} catch {}\n\t};\n\t// Bun emits a natural exit's `exit` event without the `process.emit` that signal-exit patches, so\n\t// the plain listener covers every exit and signal-exit only covers the default-fatal signals.\n\tprocess.on(\"exit\", onProcessExit);\n\tconst removeSignalHook = onExit(onProcessExit);\n\treturn dispose;\n}\n\n/**\n * Record one crash per marker whose process is gone. A marker is CLAIMED by renaming it before it is\n * recorded, so two processes starting at once cannot both record the same death.\n */\nexport function sweepDeadProcessMarkers(agentDir: string, options: RecordProcessLifetimeOptions = {}): number {\n\tconst paths = processCrashPaths(agentDir);\n\tconst selfPid = options.pid ?? process.pid;\n\tconst isAlive = options.isAlive ?? isProcessAlive;\n\tlet names: string[];\n\ttry {\n\t\tnames = readdirSync(paths.liveDir);\n\t} catch {\n\t\treturn 0;\n\t}\n\tlet recorded = 0;\n\tfor (const name of names) {\n\t\tif (!name.endsWith(MARKER_SUFFIX)) continue;\n\t\tconst pid = Number(name.slice(0, -MARKER_SUFFIX.length));\n\t\tif (!Number.isSafeInteger(pid) || pid <= 0 || pid === selfPid || isAlive(pid)) continue;\n\t\tconst claimed = join(paths.liveDir, `${name}.${selfPid}.sweep`);\n\t\ttry {\n\t\t\trenameSync(join(paths.liveDir, name), claimed);\n\t\t} catch {\n\t\t\tcontinue; // Another process claimed it first, or it was removed by its own exit.\n\t\t}\n\t\ttry {\n\t\t\tconst record = crashRecordFromMarker(readFileSync(claimed, \"utf8\"), statSync(claimed).mtimeMs);\n\t\t\tif (record !== undefined) {\n\t\t\t\tappendProcessCrashRecord(paths, record);\n\t\t\t\trecorded += 1;\n\t\t\t}\n\t\t} catch {\n\t\t\t// An unreadable marker is not evidence of anything; it is discarded below.\n\t\t} finally {\n\t\t\trmSync(claimed, { force: true });\n\t\t}\n\t}\n\treturn recorded;\n}\n\n/** Every record currently held, oldest first. Unparseable lines are skipped, never thrown on. */\nexport function readProcessCrashRecords(agentDir: string): readonly ProcessCrashRecord[] {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(processCrashPaths(agentDir).recordsFile, \"utf8\");\n\t} catch {\n\t\treturn [];\n\t}\n\tconst records: ProcessCrashRecord[] = [];\n\tfor (const line of raw.split(\"\\n\")) {\n\t\tif (line.trim() === \"\") continue;\n\t\ttry {\n\t\t\tconst value: unknown = JSON.parse(line);\n\t\t\tif (typeof value === \"object\" && value !== null && typeof Reflect.get(value, \"at\") === \"string\") {\n\t\t\t\trecords.push(value as ProcessCrashRecord);\n\t\t\t}\n\t\t} catch {}\n\t}\n\treturn records;\n}\n\n/** Signal 0 probes existence; EPERM means the pid exists under another user, so it is alive. */\nfunction isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nfunction crashRecordFromMarker(raw: string, lastHeartbeatMs: number): ProcessCrashRecord | undefined {\n\tconst value: unknown = JSON.parse(raw);\n\tif (typeof value !== \"object\" || value === null) return undefined;\n\tconst kind: unknown = Reflect.get(value, \"kind\");\n\tconst startedAt: unknown = Reflect.get(value, \"startedAt\");\n\tconst senpiVersion: unknown = Reflect.get(value, \"senpiVersion\");\n\tconst bunVersion: unknown = Reflect.get(value, \"bunVersion\");\n\tconst productVersion: unknown = Reflect.get(value, \"productVersion\");\n\tif (typeof kind !== \"string\" || typeof startedAt !== \"number\" || typeof senpiVersion !== \"string\") return undefined;\n\treturn {\n\t\tat: new Date(lastHeartbeatMs).toISOString(),\n\t\tkind,\n\t\tdetection: \"unclean_exit\",\n\t\tuptimeMs: Math.max(0, Math.round(lastHeartbeatMs - startedAt)),\n\t\t...(typeof bunVersion === \"string\" ? { bunVersion } : {}),\n\t\tsenpiVersion,\n\t\t...(typeof productVersion === \"string\" ? { productVersion } : {}),\n\t};\n}\n\nfunction appendProcessCrashRecord(paths: ProcessCrashPaths, record: ProcessCrashRecord): void {\n\tmkdirSync(paths.dir, { recursive: true, mode: DIR_MODE });\n\tappendFileSync(paths.recordsFile, `${JSON.stringify(record)}\\n`, { mode: FILE_MODE });\n\tconst records = readProcessCrashRecordsFrom(paths.recordsFile);\n\tif (records.length <= PROCESS_CRASH_RECORD_LIMIT) return;\n\tconst kept = records.slice(records.length - PROCESS_CRASH_RECORD_LIMIT);\n\twriteFileSync(paths.recordsFile, `${kept.join(\"\\n\")}\\n`, { mode: FILE_MODE });\n}\n\n/** Raw non-empty lines, so pruning keeps each surviving record byte-identical. */\nfunction readProcessCrashRecordsFrom(file: string): string[] {\n\treturn readFileSync(file, \"utf8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim() !== \"\");\n}\n"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Provider image rejections (senpi#2170). A provider that rejects an image
3
+ * rejects every later request that replays it, and `dropFailedAssistantTurns`
4
+ * removes the failed turn but not the image, so one bad image bricked the
5
+ * session. The failed assistant turn is persisted with its error, so the
6
+ * rejection is derived from history on every request (and after a restart):
7
+ * images no successful response had accepted before an image-rejection failure
8
+ * are replaced by a placeholder naming their source.
9
+ */
10
+ interface TurnMessage {
11
+ role: string;
12
+ }
13
+ export declare function isProviderImageRejection(errorMessage: string | undefined): boolean;
14
+ export declare function rejectedImagePlaceholder(source: string): string;
15
+ export declare function rejectedImageSources(messages: readonly TurnMessage[], failedTurn: TurnMessage): string[];
16
+ export declare function omitProviderRejectedImages<T extends TurnMessage>(messages: T[]): T[];
17
+ export {};
18
+ //# sourceMappingURL=provider-rejected-images.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-rejected-images.d.ts","sourceRoot":"","sources":["../../src/core/provider-rejected-images.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,UAAU,WAAW;IACpB,IAAI,EAAE,MAAM,CAAC;CACb;AAQD,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAElF;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE/D;AAiED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,EAAE,UAAU,EAAE,WAAW,GAAG,MAAM,EAAE,CAGxG;AAED,wBAAgB,0BAA0B,CAAC,CAAC,SAAS,WAAW,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAmBpF"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Provider image rejections (senpi#2170). A provider that rejects an image
3
+ * rejects every later request that replays it, and `dropFailedAssistantTurns`
4
+ * removes the failed turn but not the image, so one bad image bricked the
5
+ * session. The failed assistant turn is persisted with its error, so the
6
+ * rejection is derived from history on every request (and after a restart):
7
+ * images no successful response had accepted before an image-rejection failure
8
+ * are replaced by a placeholder naming their source.
9
+ */
10
+ const PROVIDER_IMAGE_REJECTION_PATTERN = /does not represent a valid image|image_parse_error|invalid_image|unsupported image|could not process image|unable to process input image|image_url'?\.?[^\n]*invalid base64|expected a base64-encoded data url|image was specified using the image\/[a-z]+ media type|unsupported media type for base64 image|invalid data url for image/i;
11
+ export function isProviderImageRejection(errorMessage) {
12
+ return errorMessage !== undefined && PROVIDER_IMAGE_REJECTION_PATTERN.test(errorMessage);
13
+ }
14
+ export function rejectedImagePlaceholder(source) {
15
+ return `[Image omitted: the provider rejected this image (${source}) as invalid, so it is no longer sent with the conversation.]`;
16
+ }
17
+ function field(message, key) {
18
+ return key in message ? message[key] : undefined;
19
+ }
20
+ function contentBlocks(message) {
21
+ const content = field(message, "content");
22
+ return Array.isArray(content) ? content : [];
23
+ }
24
+ function toolCallSource(block) {
25
+ const args = block.arguments;
26
+ if (typeof args !== "object" || args === null)
27
+ return undefined;
28
+ const record = args;
29
+ for (const key of ["path", "file_path", "filePath"]) {
30
+ const value = record[key];
31
+ if (typeof value === "string" && value.trim().length > 0)
32
+ return value;
33
+ }
34
+ return undefined;
35
+ }
36
+ function imageSource(message, toolCallSources) {
37
+ if (message.role !== "toolResult")
38
+ return "an attached image";
39
+ const toolCallId = field(message, "toolCallId");
40
+ const known = typeof toolCallId === "string" ? toolCallSources.get(toolCallId) : undefined;
41
+ if (known)
42
+ return known;
43
+ const toolName = field(message, "toolName");
44
+ return typeof toolName === "string" ? `${toolName} tool result` : "a tool result";
45
+ }
46
+ /** Rejected images keyed by the index of the failed assistant turn that rejected them. */
47
+ function scanRejectedImages(messages) {
48
+ const toolCallSources = new Map();
49
+ const rejections = new Map();
50
+ let unconfirmed = [];
51
+ messages.forEach((message, messageIndex) => {
52
+ if (message.role === "assistant") {
53
+ for (const block of contentBlocks(message)) {
54
+ const source = block.type === "toolCall" ? toolCallSource(block) : undefined;
55
+ if (source && typeof block.id === "string")
56
+ toolCallSources.set(block.id, source);
57
+ }
58
+ const stopReason = field(message, "stopReason");
59
+ const errorMessage = field(message, "errorMessage");
60
+ if (stopReason === "error" &&
61
+ isProviderImageRejection(typeof errorMessage === "string" ? errorMessage : undefined)) {
62
+ if (unconfirmed.length > 0)
63
+ rejections.set(messageIndex, unconfirmed);
64
+ unconfirmed = [];
65
+ }
66
+ else if (stopReason !== "error" && stopReason !== "aborted") {
67
+ unconfirmed = [];
68
+ }
69
+ return;
70
+ }
71
+ if (message.role !== "user" && message.role !== "toolResult")
72
+ return;
73
+ contentBlocks(message).forEach((block, blockIndex) => {
74
+ if (block.type === "image") {
75
+ unconfirmed.push({ messageIndex, blockIndex, source: imageSource(message, toolCallSources) });
76
+ }
77
+ });
78
+ });
79
+ return rejections;
80
+ }
81
+ export function rejectedImageSources(messages, failedTurn) {
82
+ const failedIndex = messages.lastIndexOf(failedTurn);
83
+ return (scanRejectedImages(messages).get(failedIndex) ?? []).map((image) => image.source);
84
+ }
85
+ export function omitProviderRejectedImages(messages) {
86
+ const rejected = new Map();
87
+ for (const images of scanRejectedImages(messages).values()) {
88
+ for (const image of images) {
89
+ const blocks = rejected.get(image.messageIndex) ?? new Map();
90
+ blocks.set(image.blockIndex, image.source);
91
+ rejected.set(image.messageIndex, blocks);
92
+ }
93
+ }
94
+ if (rejected.size === 0)
95
+ return messages;
96
+ return messages.map((message, messageIndex) => {
97
+ const blocks = rejected.get(messageIndex);
98
+ if (!blocks)
99
+ return message;
100
+ const content = contentBlocks(message).map((block, blockIndex) => {
101
+ const source = blocks.get(blockIndex);
102
+ return source === undefined ? block : { type: "text", text: rejectedImagePlaceholder(source) };
103
+ });
104
+ return { ...message, content };
105
+ });
106
+ }
107
+ //# sourceMappingURL=provider-rejected-images.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-rejected-images.js","sourceRoot":"","sources":["../../src/core/provider-rejected-images.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider image rejections (senpi#2170). A provider that rejects an image\n * rejects every later request that replays it, and `dropFailedAssistantTurns`\n * removes the failed turn but not the image, so one bad image bricked the\n * session. The failed assistant turn is persisted with its error, so the\n * rejection is derived from history on every request (and after a restart):\n * images no successful response had accepted before an image-rejection failure\n * are replaced by a placeholder naming their source.\n */\n\nconst PROVIDER_IMAGE_REJECTION_PATTERN =\n\t/does not represent a valid image|image_parse_error|invalid_image|unsupported image|could not process image|unable to process input image|image_url'?\\.?[^\\n]*invalid base64|expected a base64-encoded data url|image was specified using the image\\/[a-z]+ media type|unsupported media type for base64 image|invalid data url for image/i;\n\ninterface TurnBlock {\n\ttype: string;\n\tid?: unknown;\n\targuments?: unknown;\n}\n\ninterface TurnMessage {\n\trole: string;\n}\n\ninterface RejectedImage {\n\tmessageIndex: number;\n\tblockIndex: number;\n\tsource: string;\n}\n\nexport function isProviderImageRejection(errorMessage: string | undefined): boolean {\n\treturn errorMessage !== undefined && PROVIDER_IMAGE_REJECTION_PATTERN.test(errorMessage);\n}\n\nexport function rejectedImagePlaceholder(source: string): string {\n\treturn `[Image omitted: the provider rejected this image (${source}) as invalid, so it is no longer sent with the conversation.]`;\n}\n\nfunction field(message: TurnMessage, key: string): unknown {\n\treturn key in message ? (message as unknown as Record<string, unknown>)[key] : undefined;\n}\n\nfunction contentBlocks(message: TurnMessage): readonly TurnBlock[] {\n\tconst content = field(message, \"content\");\n\treturn Array.isArray(content) ? (content as TurnBlock[]) : [];\n}\n\nfunction toolCallSource(block: TurnBlock): string | undefined {\n\tconst args = block.arguments;\n\tif (typeof args !== \"object\" || args === null) return undefined;\n\tconst record = args as Record<string, unknown>;\n\tfor (const key of [\"path\", \"file_path\", \"filePath\"]) {\n\t\tconst value = record[key];\n\t\tif (typeof value === \"string\" && value.trim().length > 0) return value;\n\t}\n\treturn undefined;\n}\n\nfunction imageSource(message: TurnMessage, toolCallSources: ReadonlyMap<string, string>): string {\n\tif (message.role !== \"toolResult\") return \"an attached image\";\n\tconst toolCallId = field(message, \"toolCallId\");\n\tconst known = typeof toolCallId === \"string\" ? toolCallSources.get(toolCallId) : undefined;\n\tif (known) return known;\n\tconst toolName = field(message, \"toolName\");\n\treturn typeof toolName === \"string\" ? `${toolName} tool result` : \"a tool result\";\n}\n\n/** Rejected images keyed by the index of the failed assistant turn that rejected them. */\nfunction scanRejectedImages(messages: readonly TurnMessage[]): Map<number, RejectedImage[]> {\n\tconst toolCallSources = new Map<string, string>();\n\tconst rejections = new Map<number, RejectedImage[]>();\n\tlet unconfirmed: RejectedImage[] = [];\n\tmessages.forEach((message, messageIndex) => {\n\t\tif (message.role === \"assistant\") {\n\t\t\tfor (const block of contentBlocks(message)) {\n\t\t\t\tconst source = block.type === \"toolCall\" ? toolCallSource(block) : undefined;\n\t\t\t\tif (source && typeof block.id === \"string\") toolCallSources.set(block.id, source);\n\t\t\t}\n\t\t\tconst stopReason = field(message, \"stopReason\");\n\t\t\tconst errorMessage = field(message, \"errorMessage\");\n\t\t\tif (\n\t\t\t\tstopReason === \"error\" &&\n\t\t\t\tisProviderImageRejection(typeof errorMessage === \"string\" ? errorMessage : undefined)\n\t\t\t) {\n\t\t\t\tif (unconfirmed.length > 0) rejections.set(messageIndex, unconfirmed);\n\t\t\t\tunconfirmed = [];\n\t\t\t} else if (stopReason !== \"error\" && stopReason !== \"aborted\") {\n\t\t\t\tunconfirmed = [];\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (message.role !== \"user\" && message.role !== \"toolResult\") return;\n\t\tcontentBlocks(message).forEach((block, blockIndex) => {\n\t\t\tif (block.type === \"image\") {\n\t\t\t\tunconfirmed.push({ messageIndex, blockIndex, source: imageSource(message, toolCallSources) });\n\t\t\t}\n\t\t});\n\t});\n\treturn rejections;\n}\n\nexport function rejectedImageSources(messages: readonly TurnMessage[], failedTurn: TurnMessage): string[] {\n\tconst failedIndex = messages.lastIndexOf(failedTurn);\n\treturn (scanRejectedImages(messages).get(failedIndex) ?? []).map((image) => image.source);\n}\n\nexport function omitProviderRejectedImages<T extends TurnMessage>(messages: T[]): T[] {\n\tconst rejected = new Map<number, Map<number, string>>();\n\tfor (const images of scanRejectedImages(messages).values()) {\n\t\tfor (const image of images) {\n\t\t\tconst blocks = rejected.get(image.messageIndex) ?? new Map<number, string>();\n\t\t\tblocks.set(image.blockIndex, image.source);\n\t\t\trejected.set(image.messageIndex, blocks);\n\t\t}\n\t}\n\tif (rejected.size === 0) return messages;\n\treturn messages.map((message, messageIndex) => {\n\t\tconst blocks = rejected.get(messageIndex);\n\t\tif (!blocks) return message;\n\t\tconst content = contentBlocks(message).map((block, blockIndex) => {\n\t\t\tconst source = blocks.get(blockIndex);\n\t\t\treturn source === undefined ? block : { type: \"text\", text: rejectedImagePlaceholder(source) };\n\t\t});\n\t\treturn { ...message, content } as T;\n\t});\n}\n"]}
@@ -0,0 +1,18 @@
1
+ export declare const REPOSITORY_IDENTITY_ENTRY_TYPE = "repository-identity";
2
+ export interface RepositoryIdentity {
3
+ readonly rootCommits: readonly string[];
4
+ readonly originUrl?: string;
5
+ }
6
+ export type RepositoryMatch = "same" | "different" | "unknown";
7
+ export type GitRunner = (dir: string, args: readonly string[]) => Promise<string | undefined>;
8
+ export declare function readRepositoryIdentity(dir: string, run?: GitRunner): Promise<RepositoryIdentity | undefined>;
9
+ /** `git@host:owner/repo.git`, `ssh://git@host:22/owner/repo`, and `https://host/owner/repo` all become `host/owner/repo`. */
10
+ export declare function normalizeRemoteUrl(url: string): string;
11
+ /**
12
+ * Two checkouts are the same repository when their histories share a root commit. Without commits on
13
+ * either side the origin remote decides. Anything less is "unknown", never a guess.
14
+ */
15
+ export declare function compareRepositoryIdentities(a: RepositoryIdentity | undefined, b: RepositoryIdentity | undefined): RepositoryMatch;
16
+ export declare function parseRepositoryIdentity(value: unknown): RepositoryIdentity | undefined;
17
+ export declare function sameRepositoryIdentity(a: RepositoryIdentity | undefined, b: RepositoryIdentity | undefined): boolean;
18
+ //# sourceMappingURL=repository-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository-identity.d.ts","sourceRoot":"","sources":["../../src/core/repository-identity.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,8BAA8B,wBAAwB,CAAC;AAOpE,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;AAE/D,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAU9F,wBAAsB,sBAAsB,CAC3C,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,SAAkB,GACrB,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAYzC;AAED,6HAA6H;AAC7H,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAatD;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAC1C,CAAC,EAAE,kBAAkB,GAAG,SAAS,EACjC,CAAC,EAAE,kBAAkB,GAAG,SAAS,GAC/B,eAAe,CASjB;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,GAAG,SAAS,CAQtF;AAED,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,kBAAkB,GAAG,SAAS,EAAE,CAAC,EAAE,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAGpH"}
@@ -0,0 +1,75 @@
1
+ import { execFile } from "node:child_process";
2
+ export const REPOSITORY_IDENTITY_ENTRY_TYPE = "repository-identity";
3
+ const GIT_TIMEOUT_MS = 5_000;
4
+ const COMMIT_HASH = /^[0-9a-f]{40,64}$/;
5
+ // An inherited repository location would make every directory report that one repository.
6
+ const GIT_LOCATION_ENV = new Set(["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"]);
7
+ const runGit = (dir, args) => new Promise((resolve) => {
8
+ const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !GIT_LOCATION_ENV.has(name)));
9
+ execFile("git", ["-C", dir, ...args], { env, timeout: GIT_TIMEOUT_MS, windowsHide: true }, (error, stdout) => {
10
+ resolve(error ? undefined : stdout);
11
+ });
12
+ });
13
+ export async function readRepositoryIdentity(dir, run = runGit) {
14
+ const [roots, origin] = await Promise.all([
15
+ run(dir, ["rev-list", "--max-parents=0", "HEAD"]),
16
+ run(dir, ["config", "--get", "remote.origin.url"]),
17
+ ]);
18
+ const rootCommits = (roots ?? "")
19
+ .split(/\s+/)
20
+ .filter((line) => COMMIT_HASH.test(line))
21
+ .sort();
22
+ const originUrl = origin?.trim() ? normalizeRemoteUrl(origin) : undefined;
23
+ if (rootCommits.length === 0 && originUrl === undefined)
24
+ return undefined;
25
+ return originUrl === undefined ? { rootCommits } : { rootCommits, originUrl };
26
+ }
27
+ /** `git@host:owner/repo.git`, `ssh://git@host:22/owner/repo`, and `https://host/owner/repo` all become `host/owner/repo`. */
28
+ export function normalizeRemoteUrl(url) {
29
+ const trimmed = url.trim();
30
+ const scpLike = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(.+)$/.exec(trimmed);
31
+ const withoutScheme = scpLike
32
+ ? `${scpLike[1]}/${scpLike[2]}`
33
+ : trimmed
34
+ .replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
35
+ .replace(/^[^@/]+@/, "")
36
+ .replace(/^([^/:]+):\d+\//, "$1/");
37
+ return withoutScheme
38
+ .replace(/\/+$/, "")
39
+ .replace(/\.git$/i, "")
40
+ .toLowerCase();
41
+ }
42
+ /**
43
+ * Two checkouts are the same repository when their histories share a root commit. Without commits on
44
+ * either side the origin remote decides. Anything less is "unknown", never a guess.
45
+ */
46
+ export function compareRepositoryIdentities(a, b) {
47
+ if (a === undefined || b === undefined)
48
+ return "unknown";
49
+ if (a.rootCommits.length > 0 && b.rootCommits.length > 0) {
50
+ return a.rootCommits.some((commit) => b.rootCommits.includes(commit)) ? "same" : "different";
51
+ }
52
+ if (a.originUrl !== undefined && b.originUrl !== undefined) {
53
+ return a.originUrl === b.originUrl ? "same" : "different";
54
+ }
55
+ return "unknown";
56
+ }
57
+ export function parseRepositoryIdentity(value) {
58
+ if (typeof value !== "object" || value === null)
59
+ return undefined;
60
+ const { rootCommits, originUrl } = value;
61
+ if (!Array.isArray(rootCommits) || !rootCommits.every((commit) => typeof commit === "string"))
62
+ return undefined;
63
+ if (originUrl !== undefined && typeof originUrl !== "string")
64
+ return undefined;
65
+ const commits = rootCommits.filter((commit) => COMMIT_HASH.test(commit)).sort();
66
+ if (commits.length === 0 && originUrl === undefined)
67
+ return undefined;
68
+ return originUrl === undefined ? { rootCommits: commits } : { rootCommits: commits, originUrl };
69
+ }
70
+ export function sameRepositoryIdentity(a, b) {
71
+ if (a === undefined || b === undefined)
72
+ return a === b;
73
+ return a.originUrl === b.originUrl && a.rootCommits.join(",") === b.rootCommits.join(",");
74
+ }
75
+ //# sourceMappingURL=repository-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository-identity.js","sourceRoot":"","sources":["../../src/core/repository-identity.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"node:child_process\";\n\nexport const REPOSITORY_IDENTITY_ENTRY_TYPE = \"repository-identity\";\n\nconst GIT_TIMEOUT_MS = 5_000;\nconst COMMIT_HASH = /^[0-9a-f]{40,64}$/;\n// An inherited repository location would make every directory report that one repository.\nconst GIT_LOCATION_ENV = new Set([\"GIT_DIR\", \"GIT_WORK_TREE\", \"GIT_COMMON_DIR\", \"GIT_INDEX_FILE\"]);\n\nexport interface RepositoryIdentity {\n\treadonly rootCommits: readonly string[];\n\treadonly originUrl?: string;\n}\n\nexport type RepositoryMatch = \"same\" | \"different\" | \"unknown\";\n\nexport type GitRunner = (dir: string, args: readonly string[]) => Promise<string | undefined>;\n\nconst runGit: GitRunner = (dir, args) =>\n\tnew Promise((resolve) => {\n\t\tconst env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !GIT_LOCATION_ENV.has(name)));\n\t\texecFile(\"git\", [\"-C\", dir, ...args], { env, timeout: GIT_TIMEOUT_MS, windowsHide: true }, (error, stdout) => {\n\t\t\tresolve(error ? undefined : stdout);\n\t\t});\n\t});\n\nexport async function readRepositoryIdentity(\n\tdir: string,\n\trun: GitRunner = runGit,\n): Promise<RepositoryIdentity | undefined> {\n\tconst [roots, origin] = await Promise.all([\n\t\trun(dir, [\"rev-list\", \"--max-parents=0\", \"HEAD\"]),\n\t\trun(dir, [\"config\", \"--get\", \"remote.origin.url\"]),\n\t]);\n\tconst rootCommits = (roots ?? \"\")\n\t\t.split(/\\s+/)\n\t\t.filter((line) => COMMIT_HASH.test(line))\n\t\t.sort();\n\tconst originUrl = origin?.trim() ? normalizeRemoteUrl(origin) : undefined;\n\tif (rootCommits.length === 0 && originUrl === undefined) return undefined;\n\treturn originUrl === undefined ? { rootCommits } : { rootCommits, originUrl };\n}\n\n/** `git@host:owner/repo.git`, `ssh://git@host:22/owner/repo`, and `https://host/owner/repo` all become `host/owner/repo`. */\nexport function normalizeRemoteUrl(url: string): string {\n\tconst trimmed = url.trim();\n\tconst scpLike = /^(?:[^@/\\s]+@)?([^:/\\s]+):(?!\\/)(.+)$/.exec(trimmed);\n\tconst withoutScheme = scpLike\n\t\t? `${scpLike[1]}/${scpLike[2]}`\n\t\t: trimmed\n\t\t\t\t.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, \"\")\n\t\t\t\t.replace(/^[^@/]+@/, \"\")\n\t\t\t\t.replace(/^([^/:]+):\\d+\\//, \"$1/\");\n\treturn withoutScheme\n\t\t.replace(/\\/+$/, \"\")\n\t\t.replace(/\\.git$/i, \"\")\n\t\t.toLowerCase();\n}\n\n/**\n * Two checkouts are the same repository when their histories share a root commit. Without commits on\n * either side the origin remote decides. Anything less is \"unknown\", never a guess.\n */\nexport function compareRepositoryIdentities(\n\ta: RepositoryIdentity | undefined,\n\tb: RepositoryIdentity | undefined,\n): RepositoryMatch {\n\tif (a === undefined || b === undefined) return \"unknown\";\n\tif (a.rootCommits.length > 0 && b.rootCommits.length > 0) {\n\t\treturn a.rootCommits.some((commit) => b.rootCommits.includes(commit)) ? \"same\" : \"different\";\n\t}\n\tif (a.originUrl !== undefined && b.originUrl !== undefined) {\n\t\treturn a.originUrl === b.originUrl ? \"same\" : \"different\";\n\t}\n\treturn \"unknown\";\n}\n\nexport function parseRepositoryIdentity(value: unknown): RepositoryIdentity | undefined {\n\tif (typeof value !== \"object\" || value === null) return undefined;\n\tconst { rootCommits, originUrl } = value as { rootCommits?: unknown; originUrl?: unknown };\n\tif (!Array.isArray(rootCommits) || !rootCommits.every((commit) => typeof commit === \"string\")) return undefined;\n\tif (originUrl !== undefined && typeof originUrl !== \"string\") return undefined;\n\tconst commits = rootCommits.filter((commit) => COMMIT_HASH.test(commit)).sort();\n\tif (commits.length === 0 && originUrl === undefined) return undefined;\n\treturn originUrl === undefined ? { rootCommits: commits } : { rootCommits: commits, originUrl };\n}\n\nexport function sameRepositoryIdentity(a: RepositoryIdentity | undefined, b: RepositoryIdentity | undefined): boolean {\n\tif (a === undefined || b === undefined) return a === b;\n\treturn a.originUrl === b.originUrl && a.rootCommits.join(\",\") === b.rootCommits.join(\",\");\n}\n"]}