@code-yeongyu/senpi 2026.8.18-3 → 2026.8.19

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 (353) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-session.d.ts +33 -4
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +293 -55
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  11. package/dist/core/compaction/branch-summarization.js +3 -0
  12. package/dist/core/compaction/branch-summarization.js.map +1 -1
  13. package/dist/core/compaction/compaction.d.ts +20 -3
  14. package/dist/core/compaction/compaction.d.ts.map +1 -1
  15. package/dist/core/compaction/compaction.js +182 -52
  16. package/dist/core/compaction/compaction.js.map +1 -1
  17. package/dist/core/extensions/builtin/claude-sdk-oauth/index.d.ts +3 -0
  18. package/dist/core/extensions/builtin/claude-sdk-oauth/index.d.ts.map +1 -1
  19. package/dist/core/extensions/builtin/claude-sdk-oauth/index.js +18 -1
  20. package/dist/core/extensions/builtin/claude-sdk-oauth/index.js.map +1 -1
  21. package/dist/core/extensions/builtin/claude-sdk-oauth/oauth-login.d.ts +1 -0
  22. package/dist/core/extensions/builtin/claude-sdk-oauth/oauth-login.d.ts.map +1 -1
  23. package/dist/core/extensions/builtin/claude-sdk-oauth/oauth-login.js +7 -1
  24. package/dist/core/extensions/builtin/claude-sdk-oauth/oauth-login.js.map +1 -1
  25. package/dist/core/extensions/builtin/claude-sdk-oauth/settings.d.ts +8 -0
  26. package/dist/core/extensions/builtin/claude-sdk-oauth/settings.d.ts.map +1 -1
  27. package/dist/core/extensions/builtin/claude-sdk-oauth/settings.js +18 -0
  28. package/dist/core/extensions/builtin/claude-sdk-oauth/settings.js.map +1 -1
  29. package/dist/core/extensions/builtin/cursor-cli-oauth/guardrails.d.ts +7 -0
  30. package/dist/core/extensions/builtin/cursor-cli-oauth/guardrails.d.ts.map +1 -1
  31. package/dist/core/extensions/builtin/cursor-cli-oauth/guardrails.js +10 -1
  32. package/dist/core/extensions/builtin/cursor-cli-oauth/guardrails.js.map +1 -1
  33. package/dist/core/extensions/builtin/cursor-cli-oauth/index.d.ts.map +1 -1
  34. package/dist/core/extensions/builtin/cursor-cli-oauth/index.js +12 -1
  35. package/dist/core/extensions/builtin/cursor-cli-oauth/index.js.map +1 -1
  36. package/dist/core/extensions/builtin/cursor-cli-oauth/oauth-login.d.ts +12 -1
  37. package/dist/core/extensions/builtin/cursor-cli-oauth/oauth-login.d.ts.map +1 -1
  38. package/dist/core/extensions/builtin/cursor-cli-oauth/oauth-login.js +20 -1
  39. package/dist/core/extensions/builtin/cursor-cli-oauth/oauth-login.js.map +1 -1
  40. package/dist/core/extensions/builtin/cursor-cli-oauth/settings.d.ts +11 -0
  41. package/dist/core/extensions/builtin/cursor-cli-oauth/settings.d.ts.map +1 -1
  42. package/dist/core/extensions/builtin/cursor-cli-oauth/settings.js +7 -2
  43. package/dist/core/extensions/builtin/cursor-cli-oauth/settings.js.map +1 -1
  44. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.d.ts.map +1 -1
  45. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js +7 -2
  46. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js.map +1 -1
  47. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  48. package/dist/core/extensions/builtin/index.js +2 -0
  49. package/dist/core/extensions/builtin/index.js.map +1 -1
  50. package/dist/core/extensions/builtin/loop/command-registration.d.ts +7 -0
  51. package/dist/core/extensions/builtin/loop/command-registration.d.ts.map +1 -0
  52. package/dist/core/extensions/builtin/loop/command-registration.js +13 -0
  53. package/dist/core/extensions/builtin/loop/command-registration.js.map +1 -0
  54. package/dist/core/extensions/builtin/loop/command.d.ts +41 -0
  55. package/dist/core/extensions/builtin/loop/command.d.ts.map +1 -0
  56. package/dist/core/extensions/builtin/loop/command.js +281 -0
  57. package/dist/core/extensions/builtin/loop/command.js.map +1 -0
  58. package/dist/core/extensions/builtin/loop/cron-planner.d.ts +23 -0
  59. package/dist/core/extensions/builtin/loop/cron-planner.d.ts.map +1 -0
  60. package/dist/core/extensions/builtin/loop/cron-planner.js +88 -0
  61. package/dist/core/extensions/builtin/loop/cron-planner.js.map +1 -0
  62. package/dist/core/extensions/builtin/loop/index.d.ts +134 -0
  63. package/dist/core/extensions/builtin/loop/index.d.ts.map +1 -0
  64. package/dist/core/extensions/builtin/loop/index.js +742 -0
  65. package/dist/core/extensions/builtin/loop/index.js.map +1 -0
  66. package/dist/core/extensions/builtin/loop/loopfile.d.ts +40 -0
  67. package/dist/core/extensions/builtin/loop/loopfile.d.ts.map +1 -0
  68. package/dist/core/extensions/builtin/loop/loopfile.js +102 -0
  69. package/dist/core/extensions/builtin/loop/loopfile.js.map +1 -0
  70. package/dist/core/extensions/builtin/loop/parse.d.ts +45 -0
  71. package/dist/core/extensions/builtin/loop/parse.d.ts.map +1 -0
  72. package/dist/core/extensions/builtin/loop/parse.js +92 -0
  73. package/dist/core/extensions/builtin/loop/parse.js.map +1 -0
  74. package/dist/core/extensions/builtin/loop/scheduler.d.ts +225 -0
  75. package/dist/core/extensions/builtin/loop/scheduler.d.ts.map +1 -0
  76. package/dist/core/extensions/builtin/loop/scheduler.js +708 -0
  77. package/dist/core/extensions/builtin/loop/scheduler.js.map +1 -0
  78. package/dist/core/extensions/builtin/loop/status.d.ts +24 -0
  79. package/dist/core/extensions/builtin/loop/status.d.ts.map +1 -0
  80. package/dist/core/extensions/builtin/loop/status.js +113 -0
  81. package/dist/core/extensions/builtin/loop/status.js.map +1 -0
  82. package/dist/core/extensions/builtin/loop/store.d.ts +49 -0
  83. package/dist/core/extensions/builtin/loop/store.d.ts.map +1 -0
  84. package/dist/core/extensions/builtin/loop/store.js +424 -0
  85. package/dist/core/extensions/builtin/loop/store.js.map +1 -0
  86. package/dist/core/extensions/builtin/loop/tick-prompt.d.ts +64 -0
  87. package/dist/core/extensions/builtin/loop/tick-prompt.d.ts.map +1 -0
  88. package/dist/core/extensions/builtin/loop/tick-prompt.js +183 -0
  89. package/dist/core/extensions/builtin/loop/tick-prompt.js.map +1 -0
  90. package/dist/core/extensions/builtin/loop/tools.d.ts +122 -0
  91. package/dist/core/extensions/builtin/loop/tools.d.ts.map +1 -0
  92. package/dist/core/extensions/builtin/loop/tools.js +155 -0
  93. package/dist/core/extensions/builtin/loop/tools.js.map +1 -0
  94. package/dist/core/extensions/builtin/loop/types.d.ts +206 -0
  95. package/dist/core/extensions/builtin/loop/types.d.ts.map +1 -0
  96. package/dist/core/extensions/builtin/loop/types.js +37 -0
  97. package/dist/core/extensions/builtin/loop/types.js.map +1 -0
  98. package/dist/core/extensions/index.d.ts +1 -1
  99. package/dist/core/extensions/index.d.ts.map +1 -1
  100. package/dist/core/extensions/index.js.map +1 -1
  101. package/dist/core/extensions/loader.d.ts.map +1 -1
  102. package/dist/core/extensions/loader.js +10 -5
  103. package/dist/core/extensions/loader.js.map +1 -1
  104. package/dist/core/extensions/types.d.ts +30 -3
  105. package/dist/core/extensions/types.d.ts.map +1 -1
  106. package/dist/core/extensions/types.js.map +1 -1
  107. package/dist/core/footer-data-provider.d.ts.map +1 -1
  108. package/dist/core/footer-data-provider.js +5 -10
  109. package/dist/core/footer-data-provider.js.map +1 -1
  110. package/dist/core/model-registry.d.ts +2 -0
  111. package/dist/core/model-registry.d.ts.map +1 -1
  112. package/dist/core/model-registry.js +4 -0
  113. package/dist/core/model-registry.js.map +1 -1
  114. package/dist/core/model-runtime.d.ts +7 -0
  115. package/dist/core/model-runtime.d.ts.map +1 -1
  116. package/dist/core/model-runtime.js +17 -0
  117. package/dist/core/model-runtime.js.map +1 -1
  118. package/dist/core/package-manager.d.ts.map +1 -1
  119. package/dist/core/package-manager.js +8 -4
  120. package/dist/core/package-manager.js.map +1 -1
  121. package/dist/core/provider-composer.d.ts +8 -0
  122. package/dist/core/provider-composer.d.ts.map +1 -1
  123. package/dist/core/provider-composer.js.map +1 -1
  124. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  125. package/dist/core/remote-catalog-provider.js +2 -1
  126. package/dist/core/remote-catalog-provider.js.map +1 -1
  127. package/dist/core/retry-fallback/chains.d.ts +1 -0
  128. package/dist/core/retry-fallback/chains.d.ts.map +1 -1
  129. package/dist/core/retry-fallback/chains.js +3 -0
  130. package/dist/core/retry-fallback/chains.js.map +1 -1
  131. package/dist/core/retry-fallback/controller.d.ts +2 -0
  132. package/dist/core/retry-fallback/controller.d.ts.map +1 -1
  133. package/dist/core/retry-fallback/controller.js.map +1 -1
  134. package/dist/core/retry-fallback/expansion.d.ts +8 -0
  135. package/dist/core/retry-fallback/expansion.d.ts.map +1 -1
  136. package/dist/core/retry-fallback/expansion.js +2 -0
  137. package/dist/core/retry-fallback/expansion.js.map +1 -1
  138. package/dist/core/settings-manager.d.ts +1 -1
  139. package/dist/core/settings-manager.d.ts.map +1 -1
  140. package/dist/core/settings-manager.js +14 -3
  141. package/dist/core/settings-manager.js.map +1 -1
  142. package/dist/core/skills.d.ts.map +1 -1
  143. package/dist/core/skills.js +51 -33
  144. package/dist/core/skills.js.map +1 -1
  145. package/dist/core/tools/edit.d.ts.map +1 -1
  146. package/dist/core/tools/edit.js +17 -2
  147. package/dist/core/tools/edit.js.map +1 -1
  148. package/dist/extensions/llama/index.d.ts.map +1 -1
  149. package/dist/extensions/llama/index.js +2 -0
  150. package/dist/extensions/llama/index.js.map +1 -1
  151. package/dist/extensions/llama/provider.d.ts.map +1 -1
  152. package/dist/extensions/llama/provider.js +6 -6
  153. package/dist/extensions/llama/provider.js.map +1 -1
  154. package/dist/index.d.ts +1 -1
  155. package/dist/index.d.ts.map +1 -1
  156. package/dist/index.js.map +1 -1
  157. package/dist/main.d.ts +4 -0
  158. package/dist/main.d.ts.map +1 -1
  159. package/dist/main.js +4 -4
  160. package/dist/main.js.map +1 -1
  161. package/dist/modes/interactive/components/settings-selector.js +1 -1
  162. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  163. package/dist/modes/interactive/editor-paste-transfer.d.ts +10 -1
  164. package/dist/modes/interactive/editor-paste-transfer.d.ts.map +1 -1
  165. package/dist/modes/interactive/editor-paste-transfer.js +27 -2
  166. package/dist/modes/interactive/editor-paste-transfer.js.map +1 -1
  167. package/dist/modes/interactive/interactive-mode.d.ts +69 -1
  168. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  169. package/dist/modes/interactive/interactive-mode.js +434 -63
  170. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  171. package/dist/modes/interactive/tips/catalog/dag-tips.d.ts +7 -0
  172. package/dist/modes/interactive/tips/catalog/dag-tips.d.ts.map +1 -0
  173. package/dist/modes/interactive/tips/catalog/dag-tips.js +57 -0
  174. package/dist/modes/interactive/tips/catalog/dag-tips.js.map +1 -0
  175. package/dist/modes/interactive/tips/catalog/memory-tips.d.ts +7 -0
  176. package/dist/modes/interactive/tips/catalog/memory-tips.d.ts.map +1 -0
  177. package/dist/modes/interactive/tips/catalog/memory-tips.js +100 -0
  178. package/dist/modes/interactive/tips/catalog/memory-tips.js.map +1 -0
  179. package/dist/modes/interactive/tips/registry.d.ts.map +1 -1
  180. package/dist/modes/interactive/tips/registry.js +4 -0
  181. package/dist/modes/interactive/tips/registry.js.map +1 -1
  182. package/dist/utils/management-http.d.ts +5 -3
  183. package/dist/utils/management-http.d.ts.map +1 -1
  184. package/dist/utils/management-http.js +14 -10
  185. package/dist/utils/management-http.js.map +1 -1
  186. package/docs/compaction.md +17 -0
  187. package/docs/custom-provider.md +5 -2
  188. package/docs/extensions.md +11 -2
  189. package/docs/llama-cpp.md +2 -0
  190. package/docs/models.md +8 -2
  191. package/docs/providers.md +5 -3
  192. package/docs/settings.md +22 -2
  193. package/docs/skills.md +4 -3
  194. package/examples/extensions/border-status-editor.ts +1 -1
  195. package/examples/extensions/git-checkpoint.ts +2 -2
  196. package/examples/extensions/notify.ts +3 -1
  197. package/examples/extensions/subagent/index.ts +6 -1
  198. package/examples/extensions/titlebar-spinner.ts +1 -1
  199. package/examples/rpc-extension-ui.ts +1 -1
  200. package/examples/sdk/06-extensions.ts +1 -1
  201. package/examples/sdk/README.md +1 -1
  202. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +12 -0
  203. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  204. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts +3 -1
  205. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  206. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +51 -17
  207. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  208. package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts +4 -2
  209. package/node_modules/@earendil-works/pi-agent-core/dist/agent.d.ts.map +1 -1
  210. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +5 -1
  211. package/node_modules/@earendil-works/pi-agent-core/dist/agent.js.map +1 -1
  212. package/node_modules/@earendil-works/pi-agent-core/dist/harness/skills.d.ts +3 -2
  213. package/node_modules/@earendil-works/pi-agent-core/dist/harness/skills.d.ts.map +1 -1
  214. package/node_modules/@earendil-works/pi-agent-core/dist/harness/skills.js +13 -3
  215. package/node_modules/@earendil-works/pi-agent-core/dist/harness/skills.js.map +1 -1
  216. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.d.ts.map +1 -1
  217. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.js +14 -1
  218. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.js.map +1 -1
  219. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  220. package/node_modules/@earendil-works/pi-ai/README.md +5 -3
  221. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts +7 -1
  222. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  223. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +21 -4
  224. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  225. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.d.ts +2 -0
  226. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.d.ts.map +1 -1
  227. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js +7 -1
  228. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js.map +1 -1
  229. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  230. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +39 -2
  231. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js.map +1 -1
  232. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/gen/agent_pb.d.ts +22 -1
  233. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/gen/agent_pb.d.ts.map +1 -1
  234. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/gen/agent_pb.js +1 -1
  235. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/gen/agent_pb.js.map +1 -1
  236. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +1 -0
  237. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  238. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +51 -2
  239. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  240. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.d.ts +2 -2
  241. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.d.ts.map +1 -1
  242. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js +14 -11
  243. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js.map +1 -1
  244. package/node_modules/@earendil-works/pi-ai/dist/api/google-shared.d.ts +5 -2
  245. package/node_modules/@earendil-works/pi-ai/dist/api/google-shared.d.ts.map +1 -1
  246. package/node_modules/@earendil-works/pi-ai/dist/api/google-shared.js +16 -0
  247. package/node_modules/@earendil-works/pi-ai/dist/api/google-shared.js.map +1 -1
  248. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.d.ts +2 -2
  249. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.d.ts.map +1 -1
  250. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js +13 -10
  251. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js.map +1 -1
  252. package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.d.ts.map +1 -1
  253. package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js +4 -1
  254. package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js.map +1 -1
  255. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts.map +1 -1
  256. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +4 -1
  257. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
  258. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts +1 -1
  259. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  260. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +45 -29
  261. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  262. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
  263. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +8 -1
  264. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
  265. package/node_modules/@earendil-works/pi-ai/dist/api/pi-messages.js +1 -1
  266. package/node_modules/@earendil-works/pi-ai/dist/api/pi-messages.js.map +1 -1
  267. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.d.ts +4 -0
  268. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.d.ts.map +1 -1
  269. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js +18 -9
  270. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js.map +1 -1
  271. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/device-code.d.ts +1 -0
  272. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/device-code.d.ts.map +1 -1
  273. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/device-code.js +1 -1
  274. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/device-code.js.map +1 -1
  275. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/github-copilot.d.ts.map +1 -1
  276. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/github-copilot.js +22 -9
  277. package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/github-copilot.js.map +1 -1
  278. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +2 -1
  279. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  280. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  281. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  282. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  283. package/node_modules/@earendil-works/pi-ai/dist/providers/data/anthropic.json +1 -1
  284. package/node_modules/@earendil-works/pi-ai/dist/providers/data/baseten.json +1 -1
  285. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  286. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  287. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  288. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  289. package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan-individual.json +1 -1
  290. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  291. package/node_modules/@earendil-works/pi-ai/dist/providers/data/xai.json +1 -1
  292. package/node_modules/@earendil-works/pi-ai/dist/providers/data/xiaomi-token-plan-ams.json +1 -1
  293. package/node_modules/@earendil-works/pi-ai/dist/providers/data/xiaomi-token-plan-cn.json +1 -1
  294. package/node_modules/@earendil-works/pi-ai/dist/providers/data/xiaomi-token-plan-sgp.json +1 -1
  295. package/node_modules/@earendil-works/pi-ai/dist/providers/data/xiaomi.json +1 -1
  296. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai-coding-cn.json +1 -1
  297. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai.json +1 -1
  298. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.d.ts +1 -1
  299. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.d.ts.map +1 -1
  300. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.js +1 -5
  301. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.js.map +1 -1
  302. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +28 -4
  303. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  304. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  305. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  306. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +15 -0
  307. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
  308. package/node_modules/@earendil-works/pi-ai/dist/utils/pi-user-agent.d.ts +2 -0
  309. package/node_modules/@earendil-works/pi-ai/dist/utils/pi-user-agent.d.ts.map +1 -1
  310. package/node_modules/@earendil-works/pi-ai/dist/utils/pi-user-agent.js +7 -0
  311. package/node_modules/@earendil-works/pi-ai/dist/utils/pi-user-agent.js.map +1 -1
  312. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.d.ts +2 -1
  313. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.d.ts.map +1 -1
  314. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.js +2 -0
  315. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.js.map +1 -1
  316. package/node_modules/@earendil-works/pi-ai/package.json +2 -3
  317. package/node_modules/@earendil-works/pi-pty/dist/loader.d.ts +6 -0
  318. package/node_modules/@earendil-works/pi-pty/dist/loader.d.ts.map +1 -1
  319. package/node_modules/@earendil-works/pi-pty/dist/loader.js +11 -0
  320. package/node_modules/@earendil-works/pi-pty/dist/loader.js.map +1 -1
  321. package/node_modules/@earendil-works/pi-pty/dist/quarantine.d.ts +15 -0
  322. package/node_modules/@earendil-works/pi-pty/dist/quarantine.d.ts.map +1 -0
  323. package/node_modules/@earendil-works/pi-pty/dist/quarantine.js +27 -0
  324. package/node_modules/@earendil-works/pi-pty/dist/quarantine.js.map +1 -0
  325. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  326. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  327. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts +73 -1
  328. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -1
  329. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +173 -17
  330. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -1
  331. package/node_modules/@earendil-works/pi-tui/dist/components/text.d.ts.map +1 -1
  332. package/node_modules/@earendil-works/pi-tui/dist/components/text.js +5 -4
  333. package/node_modules/@earendil-works/pi-tui/dist/components/text.js.map +1 -1
  334. package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts +69 -0
  335. package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts.map +1 -1
  336. package/node_modules/@earendil-works/pi-tui/dist/editor-component.js.map +1 -1
  337. package/node_modules/@earendil-works/pi-tui/dist/image-markers.d.ts +46 -0
  338. package/node_modules/@earendil-works/pi-tui/dist/image-markers.d.ts.map +1 -0
  339. package/node_modules/@earendil-works/pi-tui/dist/image-markers.js +130 -0
  340. package/node_modules/@earendil-works/pi-tui/dist/image-markers.js.map +1 -0
  341. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +1 -0
  342. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +1 -1
  343. package/node_modules/@earendil-works/pi-tui/dist/index.js +2 -0
  344. package/node_modules/@earendil-works/pi-tui/dist/index.js.map +1 -1
  345. package/node_modules/@earendil-works/pi-tui/dist/paste-markers.d.ts +3 -0
  346. package/node_modules/@earendil-works/pi-tui/dist/paste-markers.d.ts.map +1 -1
  347. package/node_modules/@earendil-works/pi-tui/dist/paste-markers.js +23 -7
  348. package/node_modules/@earendil-works/pi-tui/dist/paste-markers.js.map +1 -1
  349. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.d.ts.map +1 -1
  350. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js +5 -1
  351. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js.map +1 -1
  352. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  353. package/package.json +7 -7
@@ -0,0 +1,708 @@
1
+ /**
2
+ * Pure state machine behind the `/loop` extension.
3
+ *
4
+ * Everything time-related or timer-related is injected: a {@link LoopClock} supplies
5
+ * `now`, and a {@link LoopTimerPort} owns the single armed timeout per loop. This module
6
+ * performs NO I/O, never calls `setTimeout`/`Date.now`, and never persists anything
7
+ * itself — the caller mirrors {@link LoopScheduler.getState} into the sidecar store. That
8
+ * keeps every invariant below testable with a fake clock and zero real waiting.
9
+ *
10
+ * The load-bearing invariants:
11
+ * - At most ONE queued or running tick per loop. A fire that lands while one is in flight
12
+ * sets `coalescedFirePending` instead of enqueuing a second delivery, so a sleeping
13
+ * laptop or a long turn can never produce a tick storm.
14
+ * - `nextFireAt` is ALWAYS recomputed from `now`, never from the stale due time, so all
15
+ * occurrences missed during sleep collapse into exactly one catch-up tick.
16
+ * - 7-day expiry from `createdAt` is checked at arm, fire, re-entry and restore, is never
17
+ * extended by a new wakeup, and no tick fires at or after it.
18
+ * - At most {@link MAX_ACTIVE_LOOPS} active loops; a further creation returns a typed
19
+ * rejection and leaves existing loops armed.
20
+ * - A dispatched-tick budget (default {@link DEFAULT_MAX_TICKS}) ends the loop with
21
+ * `tick_budget_exhausted` so a forgotten fast loop cannot spend without bound.
22
+ * - Keepalive is a two-strike device on dynamic loops only, and a user abort PAUSES a
23
+ * loop instead of ending it (and is never counted as a keepalive omission).
24
+ * - Shutdown SUSPENDS: every senpi shutdown reason leaves the session resumable, so there
25
+ * is deliberately no `session_closed` terminal reason.
26
+ * - A provider/turn error is never terminal.
27
+ */
28
+ import { LOOP_STATE_VERSION } from "./types.js";
29
+ /** Absolute lifetime of a loop, measured from `createdAt`. */
30
+ export const LOOP_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
31
+ /** Hard cap on simultaneously active loops per session. */
32
+ export const MAX_ACTIVE_LOOPS = 5;
33
+ /** Fallback wakeup length when a dynamic iteration forgets to reschedule. */
34
+ export const DEFAULT_KEEPALIVE_SECONDS = 1200;
35
+ export const MIN_KEEPALIVE_SECONDS = 60;
36
+ export const MAX_KEEPALIVE_SECONDS = 3600;
37
+ export const KEEPALIVE_SECONDS_ENV = "SENPI_LOOP_KEEPALIVE_SECONDS";
38
+ /** Dispatched-tick budget per loop. */
39
+ export const DEFAULT_MAX_TICKS = 2000;
40
+ export const MIN_MAX_TICKS = 1;
41
+ export const MAX_TICKS_ENV = "SENPI_LOOP_MAX_TICKS";
42
+ /** Phases in which a loop already has a delivery in flight. */
43
+ function isInFlight(entry) {
44
+ return entry.phase === "queued" || entry.phase === "running";
45
+ }
46
+ function isActive(entry) {
47
+ return entry.phase !== "ended";
48
+ }
49
+ /** Armed means the loop wants its timer running: not ended, not paused. */
50
+ function isArmable(entry) {
51
+ return entry.phase !== "ended" && entry.phase !== "suspended";
52
+ }
53
+ /**
54
+ * THE coalescing rule: the next occurrence is always derived from the CURRENT time, never
55
+ * from the stale due time. That is what makes every occurrence missed during laptop sleep,
56
+ * a long turn, or a pause collapse into exactly one catch-up tick.
57
+ */
58
+ function nextOccurrenceAfter(nowMs, intervalMs) {
59
+ return nowMs + intervalMs;
60
+ }
61
+ /**
62
+ * Rebuilds an entry in a non-terminal phase, dropping any terminal fields. Spreading an
63
+ * `ended` entry and overriding `phase` would leave `endedAt`/`endReason` behind, which the
64
+ * `LoopLifecycle` union forbids and the store parser rejects, so every non-terminal
65
+ * transition goes through here.
66
+ */
67
+ function withPhase(entry, phase, patch = {}) {
68
+ const { endedAt: _endedAt, endReason: _endReason, endDetail: _endDetail, ...rest } = entry;
69
+ return rest.kind === "fixed"
70
+ ? { ...rest, ...patch, kind: "fixed", phase }
71
+ : { ...rest, ...patch, kind: "dynamic", phase };
72
+ }
73
+ /** Non-terminal transition of a fixed loop, including its recomputed next occurrence. */
74
+ function fixedWithPhase(entry, phase, patch = {}) {
75
+ const { endedAt: _endedAt, endReason: _endReason, endDetail: _endDetail, ...rest } = entry;
76
+ return { ...rest, ...patch, kind: "fixed", phase };
77
+ }
78
+ /** Non-terminal transition of a dynamic loop, including its wakeup and keepalive fields. */
79
+ function dynamicWithPhase(entry, phase, patch = {}) {
80
+ const { endedAt: _endedAt, endReason: _endReason, endDetail: _endDetail, ...rest } = entry;
81
+ return { ...rest, ...patch, kind: "dynamic", phase };
82
+ }
83
+ function readIntEnv(env, name, fallback, min, max) {
84
+ const raw = env[name];
85
+ if (raw === undefined || raw.trim().length === 0)
86
+ return fallback;
87
+ const parsed = Number(raw.trim());
88
+ if (!Number.isInteger(parsed))
89
+ return fallback;
90
+ return Math.min(max, Math.max(min, parsed));
91
+ }
92
+ export function createLoopScheduler(deps) {
93
+ const env = deps.env ?? process.env;
94
+ const keepaliveSeconds = readIntEnv(env, KEEPALIVE_SECONDS_ENV, DEFAULT_KEEPALIVE_SECONDS, MIN_KEEPALIVE_SECONDS, MAX_KEEPALIVE_SECONDS);
95
+ const maxTicks = readIntEnv(env, MAX_TICKS_ENV, DEFAULT_MAX_TICKS, MIN_MAX_TICKS, Number.MAX_SAFE_INTEGER);
96
+ let state = deps.initialState ?? {
97
+ version: LOOP_STATE_VERSION,
98
+ sessionId: deps.sessionId,
99
+ entries: {},
100
+ activeDynamicId: null,
101
+ updatedAt: 0,
102
+ };
103
+ let mutationCount = 0;
104
+ /** In-memory only: which delivery each loop currently has in flight. */
105
+ const inFlightDeliveries = new Map();
106
+ /**
107
+ * Applies one atomic state revision. Every transition in this module funnels through
108
+ * here, so a multi-entry change (e.g. supersede + create) is a single revision and the
109
+ * persistence callback observes no intermediate state.
110
+ */
111
+ function commit(update) {
112
+ const entries = { ...state.entries };
113
+ const nextDynamicId = update(entries);
114
+ state = {
115
+ version: LOOP_STATE_VERSION,
116
+ sessionId: state.sessionId,
117
+ entries,
118
+ activeDynamicId: nextDynamicId === undefined ? state.activeDynamicId : nextDynamicId,
119
+ updatedAt: deps.clock.now(),
120
+ };
121
+ mutationCount += 1;
122
+ deps.onStateChanged?.(state);
123
+ }
124
+ function entryOf(loopId) {
125
+ return state.entries[loopId];
126
+ }
127
+ function activeIds() {
128
+ return Object.values(state.entries)
129
+ .filter(isActive)
130
+ .map((entry) => entry.id);
131
+ }
132
+ /** Cancels the timer and clears any pending wakeup, then marks the loop ended. */
133
+ function endEntry(entries, entry, nowMs, endReason, endDetail) {
134
+ deps.timers.cancel(entry.id);
135
+ inFlightDeliveries.delete(entry.id);
136
+ const terminal = {
137
+ phase: "ended",
138
+ endedAt: nowMs,
139
+ endReason,
140
+ ...(endDetail === undefined ? {} : { endDetail }),
141
+ };
142
+ entries[entry.id] =
143
+ entry.kind === "fixed" ? { ...entry, ...terminal } : { ...entry, ...terminal, pendingWakeup: null };
144
+ }
145
+ /**
146
+ * Re-arms a loop's single timer, or ends it as `expired` when the next occurrence would
147
+ * land at or after expiry. Returns the reason the loop ended, if it did.
148
+ */
149
+ function armEntry(entries, entry, nowMs, onFire) {
150
+ if (nowMs >= entry.expiresAt) {
151
+ endEntry(entries, entry, nowMs, "expired");
152
+ return "expired";
153
+ }
154
+ const dueAt = entry.kind === "fixed" ? entry.nextFireAt : entry.pendingWakeup?.dueAt;
155
+ if (dueAt === undefined) {
156
+ deps.timers.cancel(entry.id);
157
+ return undefined;
158
+ }
159
+ if (dueAt >= entry.expiresAt) {
160
+ // A tick may never fire at or after expiry, so retire the loop now rather than
161
+ // arming a timer that could only produce an illegal delivery.
162
+ endEntry(entries, entry, nowMs, "expired");
163
+ return "expired";
164
+ }
165
+ deps.timers.arm(entry.id, dueAt, () => onFire(entry.id));
166
+ return undefined;
167
+ }
168
+ /** Fire callback the timer port invokes; the extension re-enters through `onDue`. */
169
+ let fireHandler = () => { };
170
+ function onFire(loopId) {
171
+ fireHandler(loopId);
172
+ }
173
+ function baseFields(nowMs, id, request) {
174
+ return {
175
+ id,
176
+ originalArgs: request.originalArgs,
177
+ reentryPrompt: request.reentryPrompt,
178
+ payload: request.payload,
179
+ createdAt: nowMs,
180
+ lastFiredAt: null,
181
+ expiresAt: nowMs + LOOP_EXPIRY_MS,
182
+ lastScheduledForAt: null,
183
+ coalescedFirePending: false,
184
+ queuedForAt: null,
185
+ noopStreak: 0,
186
+ tickCount: 0,
187
+ sentinelDelivery: {
188
+ autonomousPreambleDelivered: false,
189
+ lastLoopFileDelivered: null,
190
+ forceFullDelivery: false,
191
+ },
192
+ wakeSources: [],
193
+ };
194
+ }
195
+ function capRejection() {
196
+ const active = activeIds();
197
+ return {
198
+ ok: false,
199
+ reason: "active_loop_cap",
200
+ cap: MAX_ACTIVE_LOOPS,
201
+ activeLoopIds: active,
202
+ message: `At most ${MAX_ACTIVE_LOOPS} loops can be active at once. Active: ${active.join(", ")}. ` +
203
+ "Stop one with `/loop stop <id>` or `/loop stop all` first.",
204
+ };
205
+ }
206
+ function createFixed(request) {
207
+ if (activeIds().length >= MAX_ACTIVE_LOOPS)
208
+ return capRejection();
209
+ const nowMs = deps.clock.now();
210
+ const id = deps.ids.loopId();
211
+ commit((entries) => {
212
+ const entry = {
213
+ ...baseFields(nowMs, id, request),
214
+ kind: "fixed",
215
+ phase: "waiting",
216
+ requestedInterval: request.requestedInterval,
217
+ effectiveInterval: request.effectiveInterval,
218
+ cronExpression: request.cronExpression,
219
+ nextFireAt: nextOccurrenceAfter(nowMs, request.intervalMs),
220
+ intervalMs: request.intervalMs,
221
+ };
222
+ entries[id] = entry;
223
+ // Arm before the caller dispatches the immediate first tick, so a failing first
224
+ // turn cannot silently lose the recurring schedule.
225
+ armEntry(entries, entry, nowMs, onFire);
226
+ return undefined;
227
+ });
228
+ return { ok: true, loopId: id };
229
+ }
230
+ function createDynamic(request) {
231
+ const existingDynamicId = state.activeDynamicId;
232
+ const existing = existingDynamicId === null ? undefined : entryOf(existingDynamicId);
233
+ const supersedes = existing !== undefined && isActive(existing);
234
+ // A superseded loop frees its slot, so it must not count against the cap.
235
+ const activeAfterSupersede = activeIds().filter((id) => !(supersedes && id === existingDynamicId));
236
+ if (activeAfterSupersede.length >= MAX_ACTIVE_LOOPS)
237
+ return capRejection();
238
+ const nowMs = deps.clock.now();
239
+ const id = deps.ids.loopId();
240
+ commit((entries) => {
241
+ if (supersedes && existing !== undefined) {
242
+ endEntry(entries, existing, nowMs, "stopped", "superseded");
243
+ }
244
+ const entry = {
245
+ ...baseFields(nowMs, id, request),
246
+ kind: "dynamic",
247
+ phase: "waiting",
248
+ pendingWakeup: null,
249
+ keepaliveCredit: 1,
250
+ };
251
+ entries[id] = entry;
252
+ return id;
253
+ });
254
+ return supersedes && existingDynamicId !== null
255
+ ? { ok: true, loopId: id, supersededLoopId: existingDynamicId }
256
+ : { ok: true, loopId: id };
257
+ }
258
+ function onDue(loopId, nowMs, sessionBusy) {
259
+ const entry = entryOf(loopId);
260
+ if (entry === undefined)
261
+ return { action: "coalesce" };
262
+ if (nowMs >= entry.expiresAt) {
263
+ if (entry.phase === "ended")
264
+ return { action: "coalesce" };
265
+ commit((entries) => {
266
+ endEntry(entries, entry, nowMs, "expired");
267
+ return undefined;
268
+ });
269
+ return { action: "expire" };
270
+ }
271
+ if (entry.phase === "ended" || entry.phase === "suspended")
272
+ return { action: "coalesce" };
273
+ if (entry.tickCount >= maxTicks)
274
+ return { action: "coalesce" };
275
+ if (isInFlight(entry)) {
276
+ // Invariant: one queued/running tick per loop. Extra occurrences collapse into a
277
+ // single pending bit and the schedule still moves forward from `now`.
278
+ commit((entries) => {
279
+ const coalesced = entry.kind === "fixed"
280
+ ? fixedWithPhase(entry, entry.phase, {
281
+ coalescedFirePending: true,
282
+ nextFireAt: nextOccurrenceAfter(nowMs, entry.intervalMs),
283
+ })
284
+ : dynamicWithPhase(entry, entry.phase, { coalescedFirePending: true });
285
+ entries[loopId] = coalesced;
286
+ armEntry(entries, coalesced, nowMs, onFire);
287
+ return undefined;
288
+ });
289
+ return { action: "coalesce" };
290
+ }
291
+ const deliveryId = deps.ids.deliveryId();
292
+ const tick = {
293
+ loopId,
294
+ deliveryId,
295
+ scheduledForAt: nowMs,
296
+ deliverAs: "followUp",
297
+ coalesced: entry.coalescedFirePending,
298
+ };
299
+ let expired = false;
300
+ commit((entries) => {
301
+ const dispatched = {
302
+ lastFiredAt: nowMs,
303
+ lastScheduledForAt: nowMs,
304
+ queuedForAt: nowMs,
305
+ coalescedFirePending: false,
306
+ tickCount: entry.tickCount + 1,
307
+ };
308
+ const phase = sessionBusy ? "queued" : "running";
309
+ const next = entry.kind === "fixed"
310
+ ? fixedWithPhase(entry, phase, {
311
+ ...dispatched,
312
+ nextFireAt: nextOccurrenceAfter(nowMs, entry.intervalMs),
313
+ })
314
+ : dynamicWithPhase(entry, phase, { ...dispatched, pendingWakeup: null });
315
+ entries[loopId] = next;
316
+ // A dynamic loop has no schedule of its own; its next timer comes from
317
+ // schedule_wakeup or the keepalive fallback.
318
+ if (next.kind === "fixed") {
319
+ expired = armEntry(entries, next, nowMs, onFire) === "expired";
320
+ }
321
+ else {
322
+ deps.timers.cancel(loopId);
323
+ }
324
+ return undefined;
325
+ });
326
+ if (!expired)
327
+ inFlightDeliveries.set(loopId, deliveryId);
328
+ return { action: "dispatch", tick };
329
+ }
330
+ function onTickSettled(input) {
331
+ const entry = entryOf(input.loopId);
332
+ if (entry === undefined)
333
+ return { action: "ignored" };
334
+ if (input.outcome === "retrying")
335
+ return { action: "in_progress" };
336
+ if (inFlightDeliveries.get(input.loopId) !== input.deliveryId)
337
+ return { action: "ignored" };
338
+ inFlightDeliveries.delete(input.loopId);
339
+ if (entry.phase === "ended")
340
+ return { action: "ignored" };
341
+ // A user abort already paused this loop; settling must not resurrect it.
342
+ if (entry.phase === "suspended")
343
+ return { action: "idle" };
344
+ const nowMs = deps.clock.now();
345
+ if (nowMs >= entry.expiresAt) {
346
+ commit((entries) => {
347
+ endEntry(entries, entry, nowMs, "expired");
348
+ return undefined;
349
+ });
350
+ return { action: "ended", endReason: "expired" };
351
+ }
352
+ if (entry.tickCount >= maxTicks) {
353
+ // The budget bounds spend, not wall clock: expiry alone would let a `/loop 1m`
354
+ // job reach thousands of model turns.
355
+ commit((entries) => {
356
+ endEntry(entries, entry, nowMs, "tick_budget_exhausted");
357
+ return undefined;
358
+ });
359
+ return { action: "ended", endReason: "tick_budget_exhausted" };
360
+ }
361
+ if (entry.coalescedFirePending) {
362
+ // Exactly one delivery drains the coalesced occurrence, however many were missed.
363
+ commit((entries) => {
364
+ entries[input.loopId] = withPhase(entry, "waiting", { coalescedFirePending: false, queuedForAt: null });
365
+ return undefined;
366
+ });
367
+ return onDue(input.loopId, nowMs, false);
368
+ }
369
+ let expired = false;
370
+ commit((entries) => {
371
+ const settled = withPhase(entry, "waiting", { queuedForAt: null });
372
+ entries[input.loopId] = settled;
373
+ if (settled.kind === "fixed") {
374
+ expired = armEntry(entries, settled, nowMs, onFire) === "expired";
375
+ }
376
+ return undefined;
377
+ });
378
+ return expired ? { action: "ended", endReason: "expired" } : { action: "idle" };
379
+ }
380
+ function onScheduleWakeup(request) {
381
+ const entry = entryOf(request.loopId);
382
+ if (entry === undefined)
383
+ return { ok: false, reason: "unknown_loop" };
384
+ if (entry.kind !== "dynamic")
385
+ return { ok: false, reason: "not_dynamic" };
386
+ if (entry.phase === "ended")
387
+ return { ok: false, reason: "ended" };
388
+ const nowMs = deps.clock.now();
389
+ if (nowMs >= entry.expiresAt) {
390
+ commit((entries) => {
391
+ endEntry(entries, entry, nowMs, "expired");
392
+ return undefined;
393
+ });
394
+ return { ok: false, reason: "expired" };
395
+ }
396
+ const wakeupId = deps.ids.wakeupId();
397
+ const replacedWakeupId = entry.pendingWakeup?.id;
398
+ // Expiry is absolute: a new wakeup never extends `expiresAt`.
399
+ const dueAt = nowMs + request.delaySeconds * 1000;
400
+ const wakeup = {
401
+ id: wakeupId,
402
+ loopId: entry.id,
403
+ kind: "dynamic",
404
+ source: "model",
405
+ requestedDelaySeconds: request.requestedDelaySeconds,
406
+ delaySeconds: request.delaySeconds,
407
+ dueAt,
408
+ reason: request.reason,
409
+ prompt: request.prompt,
410
+ noop: request.noop,
411
+ createdAt: nowMs,
412
+ };
413
+ const noopStreak = request.noop ? entry.noopStreak + 1 : 0;
414
+ commit((entries) => {
415
+ const scheduled = dynamicWithPhase(entry, "waiting", {
416
+ queuedForAt: null,
417
+ pendingWakeup: wakeup,
418
+ // A successful model schedule restores the one-omission recovery budget.
419
+ keepaliveCredit: 1,
420
+ noopStreak,
421
+ });
422
+ entries[entry.id] = scheduled;
423
+ armEntry(entries, scheduled, nowMs, onFire);
424
+ return undefined;
425
+ });
426
+ return replacedWakeupId === undefined
427
+ ? { ok: true, wakeupId, dueAt, noopStreak }
428
+ : { ok: true, wakeupId, replacedWakeupId, dueAt, noopStreak };
429
+ }
430
+ function onTurnEndedWithoutSchedule(loopId) {
431
+ const entry = entryOf(loopId);
432
+ if (entry === undefined || entry.kind !== "dynamic")
433
+ return { action: "ignored" };
434
+ // Fixed loops re-arm from their own schedule, and a paused or ended loop has no
435
+ // liveness to protect. A user abort lands here as `suspended`, which is exactly why
436
+ // an abort can never be converted into a keepalive omission.
437
+ if (entry.phase === "ended" || entry.phase === "suspended")
438
+ return { action: "ignored" };
439
+ const nowMs = deps.clock.now();
440
+ if (nowMs >= entry.expiresAt) {
441
+ commit((entries) => {
442
+ endEntry(entries, entry, nowMs, "expired");
443
+ return undefined;
444
+ });
445
+ return { action: "ended", endReason: "expired" };
446
+ }
447
+ if (entry.keepaliveCredit === 0) {
448
+ commit((entries) => {
449
+ endEntry(entries, entry, nowMs, "keepalive_exhausted");
450
+ return undefined;
451
+ });
452
+ return { action: "ended", endReason: "keepalive_exhausted" };
453
+ }
454
+ const wakeupId = deps.ids.wakeupId();
455
+ const dueAt = nowMs + keepaliveSeconds * 1000;
456
+ const wakeup = {
457
+ id: wakeupId,
458
+ loopId: entry.id,
459
+ kind: "dynamic",
460
+ source: "keepalive",
461
+ requestedDelaySeconds: keepaliveSeconds,
462
+ delaySeconds: keepaliveSeconds,
463
+ dueAt,
464
+ reason: "keepalive armed (model did not reschedule)",
465
+ prompt: entry.reentryPrompt,
466
+ noop: false,
467
+ createdAt: nowMs,
468
+ };
469
+ let expired = false;
470
+ commit((entries) => {
471
+ const armed = dynamicWithPhase(entry, "waiting", {
472
+ queuedForAt: null,
473
+ pendingWakeup: wakeup,
474
+ keepaliveCredit: 0,
475
+ });
476
+ entries[entry.id] = armed;
477
+ expired = armEntry(entries, armed, nowMs, onFire) === "expired";
478
+ return undefined;
479
+ });
480
+ return expired
481
+ ? { action: "ended", endReason: "expired" }
482
+ : { action: "keepalive_armed", wakeupId, delaySeconds: keepaliveSeconds, dueAt };
483
+ }
484
+ function pauseEntries(ids) {
485
+ const affected = [];
486
+ const pausable = ids
487
+ .map((id) => entryOf(id))
488
+ .filter((entry) => entry !== undefined && isArmable(entry));
489
+ if (pausable.length === 0)
490
+ return affected;
491
+ commit((entries) => {
492
+ for (const entry of pausable) {
493
+ deps.timers.cancel(entry.id);
494
+ inFlightDeliveries.delete(entry.id);
495
+ entries[entry.id] = withPhase(entry, "suspended", { queuedForAt: null });
496
+ affected.push(entry.id);
497
+ }
498
+ return undefined;
499
+ });
500
+ return affected;
501
+ }
502
+ function resolveTargets(target) {
503
+ if (target !== "all")
504
+ return [target];
505
+ return Object.values(state.entries).map((entry) => entry.id);
506
+ }
507
+ function pause(target) {
508
+ return { affectedLoopIds: pauseEntries(resolveTargets(target)) };
509
+ }
510
+ function onUserAbort(loopId) {
511
+ // Esc is also how a user interrupts to steer, so an abort PAUSES the loop; the
512
+ // schedule survives for `/loop resume` and is never a terminal state.
513
+ const affected = pauseEntries([loopId]);
514
+ return { action: affected.length > 0 ? "paused" : "ignored" };
515
+ }
516
+ function resume(target) {
517
+ const nowMs = deps.clock.now();
518
+ const resumable = resolveTargets(target)
519
+ .map((id) => entryOf(id))
520
+ .filter((entry) => entry !== undefined && entry.phase === "suspended");
521
+ if (resumable.length === 0)
522
+ return { affectedLoopIds: [] };
523
+ const affected = [];
524
+ commit((entries) => {
525
+ for (const entry of resumable) {
526
+ // The occurrence missed while paused is not replayed: the next fire is
527
+ // recomputed from now, exactly as after a sleep.
528
+ const rearmed = entry.kind === "fixed"
529
+ ? fixedWithPhase(entry, "waiting", {
530
+ queuedForAt: null,
531
+ nextFireAt: nextOccurrenceAfter(nowMs, entry.intervalMs),
532
+ })
533
+ : dynamicWithPhase(entry, "waiting", {
534
+ queuedForAt: null,
535
+ pendingWakeup: entry.pendingWakeup === null
536
+ ? null
537
+ : { ...entry.pendingWakeup, dueAt: Math.max(entry.pendingWakeup.dueAt, nowMs) },
538
+ });
539
+ entries[entry.id] = rearmed;
540
+ armEntry(entries, rearmed, nowMs, onFire);
541
+ affected.push(entry.id);
542
+ }
543
+ return undefined;
544
+ });
545
+ return { affectedLoopIds: affected };
546
+ }
547
+ function stop(target, detail) {
548
+ const nowMs = deps.clock.now();
549
+ const stoppable = resolveTargets(target)
550
+ .map((id) => entryOf(id))
551
+ .filter((entry) => entry !== undefined && isActive(entry));
552
+ if (stoppable.length === 0)
553
+ return { affectedLoopIds: [] };
554
+ const affected = [];
555
+ let clearDynamic = false;
556
+ commit((entries) => {
557
+ for (const entry of stoppable) {
558
+ endEntry(entries, entry, nowMs, "stopped", detail);
559
+ if (entry.id === state.activeDynamicId)
560
+ clearDynamic = true;
561
+ affected.push(entry.id);
562
+ }
563
+ return clearDynamic ? null : undefined;
564
+ });
565
+ return { affectedLoopIds: affected };
566
+ }
567
+ function onShutdown() {
568
+ // Every senpi shutdown reason (quit|reload|new|resume|fork) leaves the session
569
+ // resumable, so shutdown SUSPENDS and emits no terminal reason.
570
+ const suspendable = Object.values(state.entries).filter(isArmable);
571
+ deps.timers.cancelAll();
572
+ if (suspendable.length === 0)
573
+ return { suspendedLoopIds: [] };
574
+ const suspended = [];
575
+ commit((entries) => {
576
+ for (const entry of suspendable) {
577
+ entries[entry.id] = withPhase(entry, "suspended", { queuedForAt: null });
578
+ suspended.push(entry.id);
579
+ }
580
+ return undefined;
581
+ });
582
+ inFlightDeliveries.clear();
583
+ return { suspendedLoopIds: suspended };
584
+ }
585
+ function restore(nowMs, options = {}) {
586
+ const recoveryTicks = [];
587
+ const expiredLoopIds = [];
588
+ const rearmedLoopIds = [];
589
+ const stillPausedLoopIds = [];
590
+ const userPaused = new Set(options.userPausedLoopIds ?? []);
591
+ // A delivery this process still has in flight is NOT a crash remnant; only state
592
+ // whose owning process is gone needs recovering.
593
+ const liveDeliveries = new Set(inFlightDeliveries.keys());
594
+ deps.timers.cancelAll();
595
+ commit((entries) => {
596
+ for (const entry of Object.values(state.entries)) {
597
+ if (entry.phase === "ended")
598
+ continue;
599
+ // A delivery this process still owns is in flight, not a crash remnant: leave it
600
+ // alone so restore can never duplicate a tick that is already running.
601
+ if (liveDeliveries.has(entry.id))
602
+ continue;
603
+ if (userPaused.has(entry.id)) {
604
+ // An explicit `/loop pause` outlives a restart: no timer, no recovery tick.
605
+ entries[entry.id] = withPhase(entry, "suspended", { queuedForAt: null });
606
+ stillPausedLoopIds.push(entry.id);
607
+ continue;
608
+ }
609
+ if (nowMs >= entry.expiresAt) {
610
+ endEntry(entries, entry, nowMs, "expired");
611
+ expiredLoopIds.push(entry.id);
612
+ continue;
613
+ }
614
+ // A crashed `starting`/`queued`/`running` remnant, a pending coalesced fire, and
615
+ // an overdue occurrence all collapse into ONE recovery delivery per job.
616
+ const dueAt = entry.kind === "fixed" ? entry.nextFireAt : entry.pendingWakeup?.dueAt;
617
+ const overdue = dueAt !== undefined && dueAt <= nowMs;
618
+ const crashRemnant = isInFlight(entry) || entry.phase === "starting";
619
+ const needsRecovery = (overdue || crashRemnant || entry.coalescedFirePending) && entry.tickCount < maxTicks;
620
+ const recovered = entry.kind === "fixed"
621
+ ? fixedWithPhase(entry, "waiting", {
622
+ queuedForAt: null,
623
+ coalescedFirePending: false,
624
+ nextFireAt: nextOccurrenceAfter(nowMs, entry.intervalMs),
625
+ })
626
+ : dynamicWithPhase(entry, "waiting", {
627
+ queuedForAt: null,
628
+ coalescedFirePending: false,
629
+ // The one-shot wakeup is consumed by the recovery delivery.
630
+ pendingWakeup: overdue ? null : entry.pendingWakeup,
631
+ });
632
+ entries[entry.id] = recovered;
633
+ const ended = armEntry(entries, recovered, nowMs, onFire);
634
+ if (ended === "expired") {
635
+ expiredLoopIds.push(entry.id);
636
+ continue;
637
+ }
638
+ if (needsRecovery) {
639
+ const deliveryId = deps.ids.deliveryId();
640
+ recoveryTicks.push({
641
+ loopId: entry.id,
642
+ deliveryId,
643
+ scheduledForAt: nowMs,
644
+ deliverAs: "followUp",
645
+ coalesced: true,
646
+ });
647
+ }
648
+ rearmedLoopIds.push(entry.id);
649
+ }
650
+ return undefined;
651
+ });
652
+ // Recovery deliveries count as dispatched ticks and occupy the single in-flight slot.
653
+ // One revision for all of them, so a persisting caller never sees a partial recovery.
654
+ if (recoveryTicks.length > 0) {
655
+ commit((entries) => {
656
+ for (const tick of recoveryTicks) {
657
+ const entry = entries[tick.loopId];
658
+ if (entry === undefined || entry.phase === "ended")
659
+ continue;
660
+ entries[tick.loopId] = withPhase(entry, "queued", {
661
+ lastFiredAt: nowMs,
662
+ lastScheduledForAt: nowMs,
663
+ queuedForAt: nowMs,
664
+ tickCount: entry.tickCount + 1,
665
+ });
666
+ inFlightDeliveries.set(tick.loopId, tick.deliveryId);
667
+ }
668
+ return undefined;
669
+ });
670
+ }
671
+ return { recoveryTicks, expiredLoopIds, rearmedLoopIds, stillPausedLoopIds };
672
+ }
673
+ const scheduler = {
674
+ getState: () => state,
675
+ get mutationCount() {
676
+ return mutationCount;
677
+ },
678
+ maxTicks,
679
+ keepaliveSeconds,
680
+ currentDeliveryId: (loopId) => inFlightDeliveries.get(loopId),
681
+ createFixed,
682
+ createDynamic,
683
+ onDue,
684
+ onTickSettled,
685
+ onScheduleWakeup,
686
+ onTurnEndedWithoutSchedule,
687
+ onUserAbort,
688
+ pause,
689
+ resume,
690
+ stop,
691
+ onShutdown,
692
+ restore,
693
+ };
694
+ // The timer port fires with a loop id; the due decision is always re-derived from the
695
+ // clock so a stale callback cannot dispatch a tick the state machine would refuse.
696
+ // When an owner supplies `onFire` it performs the whole due step (persist + dispatch), so
697
+ // calling `onDue` here as well would consume the occurrence and drop the delivery.
698
+ fireHandler = (loopId) => {
699
+ const owner = deps.onFire;
700
+ if (owner !== undefined) {
701
+ owner(loopId);
702
+ return;
703
+ }
704
+ scheduler.onDue(loopId, deps.clock.now(), false);
705
+ };
706
+ return scheduler;
707
+ }
708
+ //# sourceMappingURL=scheduler.js.map