@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,742 @@
1
+ /**
2
+ * `/loop` extension factory and lifecycle wiring.
3
+ *
4
+ * This module owns the only impure parts of the feature: real timers, the session store,
5
+ * the extension event wiring, and message dispatch. Every decision it makes is delegated
6
+ * to the pure modules beside it (`scheduler.ts`, `tick-prompt.ts`, `cron-planner.ts`,
7
+ * `loopfile.ts`, `status.ts`), so the runtime here is a thin, testable adapter.
8
+ *
9
+ * Load-bearing rules implemented here:
10
+ * - A tick NEVER steers. Idle dispatch goes through `sendUserMessage(..., {
11
+ * expandPromptTemplates: true })` so a slash payload reaches the real command path;
12
+ * a busy session receives the tick as a follow-up instead.
13
+ * - Shutdown SUSPENDS: timers are cancelled, the ticker disposed, and the snapshot is
14
+ * persisted without a terminal reason (every senpi shutdown reason is resumable).
15
+ * - Keepalive is applied only to an attributed dynamic iteration, never after an ordinary
16
+ * user turn, and never after a user abort (an abort PAUSES the loop instead).
17
+ * - A store failure is not swallowed: the affected loops end with `error` and the user is
18
+ * told, because a schedule that cannot be persisted must not keep running.
19
+ */
20
+ import { homedir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { getAgentDir } from "../../../../config.js";
23
+ import { noticeEntryRenderer } from "../../notice/index.js";
24
+ import { registerLoopCommand } from "./command-registration.js";
25
+ import { describeCron, normalizeInterval } from "./cron-planner.js";
26
+ import { nodeFs, resolveLoopFile as resolveLoopFileDefault } from "./loopfile.js";
27
+ import { createLoopScheduler, } from "./scheduler.js";
28
+ import { formatLoopStatus, formatNoopFold, LoopStatusTicker } from "./status.js";
29
+ import { readLoopState, writeLoopState } from "./store.js";
30
+ import { buildTickMessage } from "./tick-prompt.js";
31
+ import { registerLoopTools } from "./tools.js";
32
+ /** Custom entry type carrying one dispatched tick, used for attribution and folding. */
33
+ export const LOOP_TICK_ENTRY_TYPE = "loop-tick";
34
+ export const renderLoopTickEntry = noticeEntryRenderer((entry) => {
35
+ const data = entry.data;
36
+ if (data === undefined)
37
+ return undefined;
38
+ const folded = data.folded ? formatNoopFold(data.noopStreak) : "";
39
+ if (folded.length > 0) {
40
+ return {
41
+ title: folded,
42
+ why: "Consecutive loop ticks reported no actionable change.",
43
+ };
44
+ }
45
+ return {
46
+ title: `↻ loop tick (${data.mode}) · ${data.delivery}`,
47
+ why: "A /loop schedule dispatched this tick.",
48
+ };
49
+ });
50
+ /**
51
+ * Real timer port: exactly one live timeout per key, replaced on re-arm.
52
+ *
53
+ * Follows the generation-counter discipline of `cache-keepalive/index.ts`: cancelling or
54
+ * re-arming a key bumps its generation, so a callback whose generation is stale returns
55
+ * without firing even if it was already scheduled on the event loop.
56
+ */
57
+ export function createNodeTimerPort() {
58
+ const handles = new Map();
59
+ const generations = new Map();
60
+ function cancel(key) {
61
+ const handle = handles.get(key);
62
+ if (handle !== undefined)
63
+ clearTimeout(handle);
64
+ handles.delete(key);
65
+ generations.set(key, (generations.get(key) ?? 0) + 1);
66
+ }
67
+ return {
68
+ arm(key, dueAt, callback) {
69
+ cancel(key);
70
+ const generation = generations.get(key) ?? 0;
71
+ const delayMs = Math.max(0, dueAt - Date.now());
72
+ const handle = setTimeout(() => {
73
+ handles.delete(key);
74
+ if (generations.get(key) !== generation)
75
+ return;
76
+ callback();
77
+ }, delayMs);
78
+ handle.unref?.();
79
+ handles.set(key, handle);
80
+ },
81
+ cancel,
82
+ cancelAll() {
83
+ for (const key of [...handles.keys()])
84
+ cancel(key);
85
+ },
86
+ };
87
+ }
88
+ function defaultIds() {
89
+ let sequence = 0;
90
+ const mint = (prefix) => {
91
+ sequence += 1;
92
+ return `${prefix}-${Date.now().toString(36)}-${sequence.toString(36)}`;
93
+ };
94
+ return {
95
+ loopId: () => mint("loop"),
96
+ wakeupId: () => mint("wakeup"),
97
+ deliveryId: () => mint("delivery"),
98
+ };
99
+ }
100
+ function defaultStoreRef(ctx) {
101
+ const sessionFile = ctx.sessionManager.getSessionFile();
102
+ const baseDir = sessionFile === undefined
103
+ ? join(getAgentDir(), "extensions", "loop", "no-session")
104
+ : join(ctx.sessionManager.getSessionDir(), "extensions", "loop");
105
+ return { baseDir, sessionId: ctx.sessionManager.getSessionId() };
106
+ }
107
+ function emptyDeliveryState() {
108
+ return { autonomousPreambleDelivered: false, lastLoopFileDelivered: null, forceFullDelivery: false };
109
+ }
110
+ function loopFileSnapshot(result) {
111
+ if (!result.found)
112
+ return { present: false };
113
+ return {
114
+ present: true,
115
+ path: result.path,
116
+ content: result.content,
117
+ mtimeMs: result.fingerprint.mtimeMs,
118
+ size: result.fingerprint.size,
119
+ contentHash: result.fingerprint.contentHash,
120
+ };
121
+ }
122
+ function sentinelFor(mode, loopFilePresent) {
123
+ if (loopFilePresent)
124
+ return mode === "fixed" ? "<<loop.md>>" : "<<loop.md-dynamic>>";
125
+ return mode === "fixed" ? "<<autonomous-loop>>" : "<<autonomous-loop-dynamic>>";
126
+ }
127
+ function modeOf(entry) {
128
+ return entry.kind === "fixed" ? "fixed" : "dynamic";
129
+ }
130
+ /** Reads the tick payload of a `loop-tick` entry, whichever entry kind carries it. */
131
+ function anchorPayloadOf(entry) {
132
+ if (entry.type === "custom") {
133
+ return entry.customType === LOOP_TICK_ENTRY_TYPE ? entry.data : undefined;
134
+ }
135
+ if (entry.type === "custom_message") {
136
+ return entry.customType === LOOP_TICK_ENTRY_TYPE ? entry.details : undefined;
137
+ }
138
+ return undefined;
139
+ }
140
+ /**
141
+ * True when the compaction-aware context still contains the delivery that anchored a full
142
+ * sentinel block. Compaction can drop it, and a reminder's "established earlier in this
143
+ * conversation" phrasing must never dangle, so a missing anchor forces the next tick back
144
+ * to full. When no specific anchor id was recorded (the autonomous preamble carries none),
145
+ * any surviving tick delivery for that loop counts as the anchor.
146
+ */
147
+ function anchorPresent(ctx, loopId, deliveryId) {
148
+ return ctx.sessionManager.buildContextEntries().some((entry) => {
149
+ const payload = anchorPayloadOf(entry);
150
+ if (payload === undefined)
151
+ return false;
152
+ if (payload.loopId !== undefined && payload.loopId !== loopId)
153
+ return false;
154
+ return deliveryId === undefined ? true : payload.deliveryId === deliveryId;
155
+ });
156
+ }
157
+ export function createLoopExtension(deps = {}) {
158
+ const clock = deps.clock ?? { now: () => Date.now() };
159
+ const makeTimers = deps.timers ?? createNodeTimerPort;
160
+ const ids = deps.ids ?? defaultIds();
161
+ const storeRefOf = deps.storeRef ?? defaultStoreRef;
162
+ const resolveLoopFile = deps.resolveLoopFile ??
163
+ ((options) => resolveLoopFileDefault({ cwd: options.cwd, homeDir: options.homeDir, fs: nodeFs, path: { join } }));
164
+ const readState = deps.readState ?? readLoopState;
165
+ const writeState = deps.writeState ?? writeLoopState;
166
+ return (pi) => {
167
+ let ctx;
168
+ let scheduler;
169
+ let storeRef;
170
+ let timers = makeTimers();
171
+ let storeFailure;
172
+ let sessionNamed = false;
173
+ /** Loop the currently running iteration is attributed to, for keepalive + tools. */
174
+ let attributedLoopId;
175
+ /** Delivery id of the tick that started the attributed iteration. */
176
+ let attributedDeliveryId;
177
+ /** True once the attributed iteration rescheduled or stopped itself. */
178
+ let attributionResolved = false;
179
+ /** Ticks held back because their slash payload cannot be queued while streaming. */
180
+ const deferredDispatches = [];
181
+ const deliveryStates = new Map();
182
+ /** Loops retired with `error` because their state could not be read or written. */
183
+ const endedWithError = new Set();
184
+ /** True once this session has state worth persisting (loaded or created). */
185
+ let storeTouched = false;
186
+ let persistTail = Promise.resolve();
187
+ const ticker = new LoopStatusTicker({
188
+ render: (key, text) => {
189
+ try {
190
+ ctx?.ui.setStatus(key, text);
191
+ }
192
+ catch {
193
+ // A stale context after session replacement must not break the loop runtime.
194
+ }
195
+ },
196
+ now: () => clock.now(),
197
+ });
198
+ pi.registerEntryRenderer(LOOP_TICK_ENTRY_TYPE, renderLoopTickEntry);
199
+ registerLoopTools(pi, {
200
+ scheduler: {
201
+ getWakeupTarget: () => {
202
+ if (scheduler === undefined || attributedLoopId === undefined)
203
+ return null;
204
+ const entry = scheduler.getState().entries[attributedLoopId];
205
+ if (entry === undefined || entry.phase === "ended")
206
+ return null;
207
+ return { kind: entry.kind, loopId: entry.id };
208
+ },
209
+ scheduleWakeup: async (request) => {
210
+ const active = requireScheduler();
211
+ const result = active.onScheduleWakeup(request);
212
+ if (!result.ok)
213
+ throw new Error(`schedule_wakeup rejected: ${result.reason}`);
214
+ attributionResolved = true;
215
+ await persist();
216
+ refreshStatus();
217
+ return {
218
+ wakeupId: result.wakeupId,
219
+ ...(result.replacedWakeupId === undefined ? {} : { replacedWakeupId: result.replacedWakeupId }),
220
+ dueAt: result.dueAt,
221
+ noopStreak: result.noopStreak,
222
+ };
223
+ },
224
+ stopDynamicLoop: async (request) => {
225
+ const active = requireScheduler();
226
+ active.stop(request.loopId, "model-stop");
227
+ attributionResolved = true;
228
+ await persist();
229
+ refreshStatus();
230
+ return { endedAt: clock.now() };
231
+ },
232
+ },
233
+ });
234
+ function requireScheduler() {
235
+ if (scheduler === undefined)
236
+ throw new Error("loop extension has no active session");
237
+ return scheduler;
238
+ }
239
+ /** Merges extension-owned sentinel delivery state into the scheduler's snapshot. */
240
+ function withDeliveryOverlay(state) {
241
+ const entries = {};
242
+ for (const [id, entry] of Object.entries(state.entries)) {
243
+ const delivery = deliveryStates.get(id);
244
+ entries[id] = delivery === undefined ? entry : { ...entry, sentinelDelivery: delivery };
245
+ }
246
+ return { ...state, entries };
247
+ }
248
+ /** Serializes persistence so a timer, tool, and command write cannot interleave. */
249
+ function persist() {
250
+ const ref = storeRef;
251
+ const active = scheduler;
252
+ if (ref === undefined || active === undefined)
253
+ return Promise.resolve();
254
+ const snapshot = withDeliveryOverlay(active.getState());
255
+ // The extension is registered for every session, so a session that never ran a loop
256
+ // must not leave a sidecar behind.
257
+ if (!storeTouched && Object.keys(snapshot.entries).length === 0)
258
+ return Promise.resolve();
259
+ storeTouched = true;
260
+ persistTail = persistTail.then(async () => {
261
+ try {
262
+ await writeState(ref, snapshot);
263
+ }
264
+ catch (error) {
265
+ failStore(error, Object.keys(snapshot.entries));
266
+ }
267
+ });
268
+ return persistTail;
269
+ }
270
+ /**
271
+ * A schedule that cannot be read or written must not keep running: end the affected
272
+ * loops with `error` and tell the user, rather than silently losing the schedule. The
273
+ * terminal record is kept in memory because the store that would hold it is exactly the
274
+ * thing that just failed; what matters is that nothing stays armed and the user knows.
275
+ */
276
+ function failStore(error, loopIds) {
277
+ const message = error instanceof Error ? error.message : String(error);
278
+ timers.cancelAll();
279
+ for (const loopId of loopIds)
280
+ endedWithError.add(loopId);
281
+ storeFailure = {
282
+ endReason: "error",
283
+ message,
284
+ loopIds,
285
+ };
286
+ ticker.dispose();
287
+ ctx?.ui.notify(`/loop state could not be used and the affected loops were ended: ${message}`, "error");
288
+ }
289
+ function refreshStatus() {
290
+ if (scheduler === undefined)
291
+ return;
292
+ const state = scheduler.getState();
293
+ // The ticker owns a repeating render timer, so it only runs while a loop is armed;
294
+ // a session that never used /loop must not carry one.
295
+ if (Object.values(state.entries).every((entry) => entry.phase === "ended")) {
296
+ if (ticker.running)
297
+ ticker.dispose();
298
+ return;
299
+ }
300
+ ticker.sync(state);
301
+ }
302
+ function deliveryStateOf(loopId) {
303
+ return deliveryStates.get(loopId) ?? emptyDeliveryState();
304
+ }
305
+ function markForceFullDelivery() {
306
+ for (const [id, state] of deliveryStates) {
307
+ deliveryStates.set(id, { ...state, forceFullDelivery: true });
308
+ }
309
+ const active = scheduler;
310
+ if (active === undefined)
311
+ return;
312
+ for (const entry of Object.values(active.getState().entries)) {
313
+ if (entry.phase === "ended")
314
+ continue;
315
+ if (deliveryStates.has(entry.id))
316
+ continue;
317
+ deliveryStates.set(entry.id, { ...entry.sentinelDelivery, forceFullDelivery: true });
318
+ }
319
+ }
320
+ function isExtensionCommandPayload(text) {
321
+ if (!text.startsWith("/"))
322
+ return false;
323
+ const name = text.slice(1).split(/\s+/)[0];
324
+ return pi.getCommands().some((command) => command.name === name);
325
+ }
326
+ /**
327
+ * Delivers one tick. Idle dispatch expands prompt templates so a slash payload
328
+ * reaches its real command handler; a busy session gets a follow-up instead of
329
+ * steering. A slash payload cannot be queued at all, so it waits for the turn to
330
+ * settle rather than being rewritten into plain text.
331
+ */
332
+ async function dispatchTick(tick) {
333
+ const active = requireScheduler();
334
+ const entry = active.getState().entries[tick.loopId];
335
+ if (entry === undefined || entry.phase === "ended")
336
+ return;
337
+ if (endedWithError.has(tick.loopId))
338
+ return;
339
+ const currentCtx = ctx;
340
+ if (currentCtx === undefined)
341
+ return;
342
+ const mode = modeOf(entry);
343
+ const loopFile = entry.payload.type === "sentinel" ? await resolveLoopFileSafely(currentCtx) : { found: false };
344
+ const built = buildTickMessage({
345
+ loopId: entry.id,
346
+ deliveryId: tick.deliveryId,
347
+ mode,
348
+ payload: entry.payload,
349
+ reentryPrompt: entry.reentryPrompt,
350
+ deliveryState: deliveryStateOf(entry.id),
351
+ loopFile: loopFileSnapshot(loopFile),
352
+ });
353
+ deliveryStates.set(entry.id, built.deliveryState);
354
+ const busy = !currentCtx.isIdle() || currentCtx.hasPendingMessages();
355
+ if (busy && isExtensionCommandPayload(built.text)) {
356
+ // Extension commands cannot be queued; hold this delivery until the turn settles.
357
+ deferredDispatches.push(tick);
358
+ return;
359
+ }
360
+ const noopStreak = entry.noopStreak;
361
+ pi.appendEntry(LOOP_TICK_ENTRY_TYPE, {
362
+ loopId: entry.id,
363
+ deliveryId: tick.deliveryId,
364
+ scheduledForAt: tick.scheduledForAt,
365
+ mode,
366
+ delivery: built.delivery,
367
+ ...(built.details.sentinel === undefined ? {} : { sentinel: built.details.sentinel }),
368
+ noopStreak,
369
+ folded: noopStreak >= 2,
370
+ });
371
+ attributedLoopId = entry.id;
372
+ attributedDeliveryId = tick.deliveryId;
373
+ attributionResolved = false;
374
+ pi.sendUserMessage(built.text, {
375
+ expandPromptTemplates: true,
376
+ ...(busy ? { deliverAs: "followUp" } : {}),
377
+ });
378
+ await persist();
379
+ refreshStatus();
380
+ }
381
+ async function resolveLoopFileSafely(currentCtx) {
382
+ try {
383
+ return await resolveLoopFile({ cwd: currentCtx.cwd, homeDir: homedir() });
384
+ }
385
+ catch {
386
+ // A loop file that cannot be read is treated as absent for this tick; the loop
387
+ // stays alive and picks the file up again once it is readable.
388
+ return { found: false };
389
+ }
390
+ }
391
+ async function drainDeferred() {
392
+ if (deferredDispatches.length === 0)
393
+ return;
394
+ const pending = deferredDispatches.splice(0, deferredDispatches.length);
395
+ for (const tick of pending)
396
+ await dispatchTick(tick);
397
+ }
398
+ async function fireDue(loopId) {
399
+ const active = scheduler;
400
+ const currentCtx = ctx;
401
+ if (active === undefined || currentCtx === undefined)
402
+ return;
403
+ const busy = !currentCtx.isIdle() || currentCtx.hasPendingMessages();
404
+ const decision = active.onDue(loopId, clock.now(), busy);
405
+ await persist();
406
+ if (decision.action === "dispatch") {
407
+ await dispatchTick(decision.tick);
408
+ return;
409
+ }
410
+ if (decision.action === "expire") {
411
+ ctx?.ui.notify("Loop expired after 7 days and is no longer armed.", "info");
412
+ }
413
+ refreshStatus();
414
+ }
415
+ /**
416
+ * Timer callbacks are synchronous, so the async due step is scheduled and its failure is
417
+ * contained: a rejected dispatch must never take down the timer that owns the loop.
418
+ */
419
+ function handleTimerFire(loopId) {
420
+ void fireDue(loopId).catch((error) => {
421
+ ctx?.ui.notify(`Loop tick failed: ${error instanceof Error ? error.message : String(error)}`, "error");
422
+ });
423
+ }
424
+ function payloadForPrompt(prompt) {
425
+ return { type: "prompt", prompt };
426
+ }
427
+ async function startFixed(request) {
428
+ const active = requireScheduler();
429
+ const effective = normalizeInterval(request.requestedInterval);
430
+ const created = active.createFixed({
431
+ originalArgs: request.originalArgs,
432
+ reentryPrompt: `/loop ${request.originalArgs}`.trimEnd(),
433
+ payload: payloadForPrompt(request.prompt),
434
+ requestedInterval: request.requestedInterval,
435
+ effectiveInterval: effective,
436
+ cronExpression: describeCron(effective),
437
+ intervalMs: effective.intervalMs,
438
+ });
439
+ if (!created.ok)
440
+ return { ok: false, message: created.message };
441
+ deliveryStates.set(created.loopId, emptyDeliveryState());
442
+ await persist();
443
+ nameSessionAfterLoop(request.prompt);
444
+ await fireDue(created.loopId);
445
+ refreshStatus();
446
+ return {
447
+ ok: true,
448
+ loopId: created.loopId,
449
+ ...(effective.roundingNotice === undefined ? {} : { roundingNotice: effective.roundingNotice }),
450
+ cronExpression: describeCron(effective),
451
+ effectiveCadence: effective.human,
452
+ expiresAt: active.getState().entries[created.loopId]?.expiresAt,
453
+ };
454
+ }
455
+ async function startDynamic(request) {
456
+ const active = requireScheduler();
457
+ const created = active.createDynamic({
458
+ originalArgs: request.originalArgs,
459
+ reentryPrompt: `/loop ${request.originalArgs}`.trimEnd(),
460
+ payload: payloadForPrompt(request.prompt),
461
+ });
462
+ if (!created.ok)
463
+ return { ok: false, message: created.message };
464
+ deliveryStates.set(created.loopId, emptyDeliveryState());
465
+ await persist();
466
+ nameSessionAfterLoop(request.prompt);
467
+ await fireDue(created.loopId);
468
+ refreshStatus();
469
+ return {
470
+ ok: true,
471
+ loopId: created.loopId,
472
+ ...(created.supersededLoopId === undefined ? {} : { supersededLoopId: created.supersededLoopId }),
473
+ expiresAt: active.getState().entries[created.loopId]?.expiresAt,
474
+ };
475
+ }
476
+ async function startBare(request) {
477
+ const active = requireScheduler();
478
+ const currentCtx = ctx;
479
+ if (currentCtx === undefined)
480
+ return { ok: false, message: "loop extension has no active session" };
481
+ const loopFile = await resolveLoopFileSafely(currentCtx);
482
+ const mode = request.interval === undefined ? "dynamic" : "fixed";
483
+ const payload = { type: "sentinel", sentinel: sentinelFor(mode, loopFile.found) };
484
+ const reentryPrompt = request.originalArgs.trim() === "" ? "/loop" : `/loop ${request.originalArgs.trim()}`;
485
+ const created = request.interval === undefined
486
+ ? active.createDynamic({ originalArgs: request.originalArgs, reentryPrompt, payload })
487
+ : (() => {
488
+ const effective = normalizeInterval(request.interval);
489
+ return active.createFixed({
490
+ originalArgs: request.originalArgs,
491
+ reentryPrompt,
492
+ payload,
493
+ requestedInterval: request.interval,
494
+ effectiveInterval: effective,
495
+ cronExpression: describeCron(effective),
496
+ intervalMs: effective.intervalMs,
497
+ });
498
+ })();
499
+ if (!created.ok)
500
+ return { ok: false, message: created.message };
501
+ deliveryStates.set(created.loopId, emptyDeliveryState());
502
+ await persist();
503
+ nameSessionAfterLoop(reentryPrompt);
504
+ await fireDue(created.loopId);
505
+ refreshStatus();
506
+ return {
507
+ ok: true,
508
+ loopId: created.loopId,
509
+ expiresAt: active.getState().entries[created.loopId]?.expiresAt,
510
+ };
511
+ }
512
+ function nameSessionAfterLoop(prompt) {
513
+ if (sessionNamed)
514
+ return;
515
+ sessionNamed = true;
516
+ if (pi.getSessionName() !== undefined)
517
+ return;
518
+ pi.setSessionName(`loop: ${prompt}`.trim());
519
+ }
520
+ const controller = {
521
+ startFixed,
522
+ startDynamic,
523
+ startBare,
524
+ fireDue,
525
+ scheduleWakeup: async (request) => {
526
+ const active = requireScheduler();
527
+ const result = active.onScheduleWakeup(request);
528
+ if (result.ok)
529
+ attributionResolved = true;
530
+ await persist();
531
+ refreshStatus();
532
+ },
533
+ stop: async (target, detail) => {
534
+ const active = requireScheduler();
535
+ const result = active.stop(target, detail);
536
+ await persist();
537
+ refreshStatus();
538
+ return result.affectedLoopIds;
539
+ },
540
+ pause: async (target) => {
541
+ const active = requireScheduler();
542
+ const result = active.pause(target);
543
+ await persist();
544
+ refreshStatus();
545
+ return result.affectedLoopIds;
546
+ },
547
+ resume: async (target) => {
548
+ const active = requireScheduler();
549
+ const result = active.resume(target);
550
+ await persist();
551
+ refreshStatus();
552
+ return result.affectedLoopIds;
553
+ },
554
+ getState: () => requireScheduler().getState(),
555
+ statusLine: () => formatLoopStatus(requireScheduler().getState(), clock.now()),
556
+ lastStoreFailure: () => storeFailure,
557
+ isEndedWithError: (loopId) => endedWithError.has(loopId),
558
+ };
559
+ deps.onControllerReady?.(controller);
560
+ registerLoopCommand(pi, { controller });
561
+ pi.on("session_start", async (_event, nextCtx) => {
562
+ ctx = nextCtx;
563
+ timers.cancelAll();
564
+ timers = makeTimers();
565
+ storeFailure = undefined;
566
+ endedWithError.clear();
567
+ storeTouched = false;
568
+ sessionNamed = false;
569
+ attributedLoopId = undefined;
570
+ attributedDeliveryId = undefined;
571
+ attributionResolved = false;
572
+ deferredDispatches.length = 0;
573
+ deliveryStates.clear();
574
+ storeRef = storeRefOf(nextCtx);
575
+ let initialState;
576
+ try {
577
+ initialState = (await readState(storeRef)) ?? undefined;
578
+ }
579
+ catch (error) {
580
+ // Fail closed: arm nothing, end nothing silently, and tell the user.
581
+ failStore(error, []);
582
+ scheduler = createLoopScheduler({
583
+ sessionId: nextCtx.sessionManager.getSessionId(),
584
+ clock,
585
+ timers,
586
+ ids,
587
+ onFire: handleTimerFire,
588
+ });
589
+ return;
590
+ }
591
+ scheduler = createLoopScheduler({
592
+ sessionId: nextCtx.sessionManager.getSessionId(),
593
+ clock,
594
+ timers,
595
+ ids,
596
+ onFire: handleTimerFire,
597
+ ...(initialState === undefined ? {} : { initialState }),
598
+ });
599
+ storeTouched = initialState !== undefined;
600
+ for (const entry of Object.values(scheduler.getState().entries)) {
601
+ const anchorDeliveryId = entry.sentinelDelivery.lastLoopFileDelivered?.anchorDeliveryId;
602
+ const anchored = anchorPresent(nextCtx, entry.id, anchorDeliveryId);
603
+ const needsFull = !anchored && hasAnchoredDelivery(entry.sentinelDelivery);
604
+ deliveryStates.set(entry.id, {
605
+ ...entry.sentinelDelivery,
606
+ forceFullDelivery: entry.sentinelDelivery.forceFullDelivery || needsFull,
607
+ });
608
+ }
609
+ const restored = scheduler.restore(clock.now());
610
+ await persist();
611
+ refreshStatus();
612
+ for (const tick of restored.recoveryTicks)
613
+ await dispatchTick(tick);
614
+ if (restored.expiredLoopIds.length > 0) {
615
+ nextCtx.ui.notify(`Loop expired after 7 days: ${restored.expiredLoopIds.join(", ")}`, "info");
616
+ }
617
+ });
618
+ pi.on("session_compact", async (event) => {
619
+ // Accepted compaction may have dropped the anchor the reminders point at, so the
620
+ // next sentinel tick must carry the full instructions again. Timers and phase are
621
+ // deliberately untouched.
622
+ if (event.accepted !== true)
623
+ return;
624
+ markForceFullDelivery();
625
+ await persist();
626
+ });
627
+ pi.on("input", async (event) => {
628
+ // A user turn is never an attributed loop iteration, so keepalive must not see it.
629
+ // A tick dispatched by this extension arrives with source "extension" and MUST keep
630
+ // its attribution, or no dynamic iteration would ever be judged.
631
+ if (event.source === "extension")
632
+ return;
633
+ attributedLoopId = undefined;
634
+ attributedDeliveryId = undefined;
635
+ attributionResolved = false;
636
+ });
637
+ pi.on("agent_end", async (event) => {
638
+ // A retrying turn is still in progress: neither settle it nor judge its liveness.
639
+ if (event.willRetry === true)
640
+ return;
641
+ if (event.aborted === true && event.abortSource === "user") {
642
+ await pauseAttributedLoop();
643
+ return;
644
+ }
645
+ await settleAttributedTick(event.aborted === true ? "error" : "completed");
646
+ });
647
+ pi.on("agent_settled", async () => {
648
+ await settleAttributedTick("completed");
649
+ await drainDeferred();
650
+ refreshStatus();
651
+ });
652
+ pi.on("session_abort", async () => {
653
+ await pauseAttributedLoop();
654
+ });
655
+ pi.on("session_shutdown", async () => {
656
+ const active = scheduler;
657
+ if (active !== undefined) {
658
+ // Shutdown SUSPENDS: every senpi shutdown reason leaves the session resumable,
659
+ // so the snapshot must carry no terminal reason.
660
+ active.onShutdown();
661
+ await persist();
662
+ }
663
+ timers.cancelAll();
664
+ deferredDispatches.length = 0;
665
+ ticker.dispose();
666
+ attributedLoopId = undefined;
667
+ attributedDeliveryId = undefined;
668
+ attributionResolved = false;
669
+ ctx = undefined;
670
+ });
671
+ async function pauseAttributedLoop() {
672
+ const active = scheduler;
673
+ if (active === undefined)
674
+ return;
675
+ const targets = attributedLoopId === undefined
676
+ ? Object.values(active.getState().entries)
677
+ .filter((entry) => entry.phase !== "ended")
678
+ .map((entry) => entry.id)
679
+ : [attributedLoopId];
680
+ const paused = [];
681
+ for (const loopId of targets) {
682
+ if (active.onUserAbort(loopId).action === "paused")
683
+ paused.push(loopId);
684
+ }
685
+ attributedLoopId = undefined;
686
+ attributedDeliveryId = undefined;
687
+ attributionResolved = false;
688
+ if (paused.length === 0)
689
+ return;
690
+ await persist();
691
+ refreshStatus();
692
+ ctx?.ui.notify("loop paused - /loop resume or /loop stop", "info");
693
+ }
694
+ /**
695
+ * Settles the tick this turn was attributed to, drains a coalesced occurrence, and
696
+ * applies the two-strike keepalive to a dynamic iteration that ended without calling
697
+ * `schedule_wakeup`. An ordinary user turn clears the attribution first, so keepalive
698
+ * can never be charged to it, and a user abort takes the pause path instead.
699
+ */
700
+ async function settleAttributedTick(outcome) {
701
+ const active = scheduler;
702
+ const loopId = attributedLoopId;
703
+ const deliveryId = attributedDeliveryId;
704
+ if (active === undefined || loopId === undefined)
705
+ return;
706
+ const entry = active.getState().entries[loopId];
707
+ if (entry === undefined)
708
+ return;
709
+ const resolved = attributionResolved;
710
+ attributedLoopId = undefined;
711
+ attributedDeliveryId = undefined;
712
+ attributionResolved = false;
713
+ const settled = deliveryId === undefined ? undefined : active.onTickSettled({ loopId, deliveryId, outcome });
714
+ await persist();
715
+ if (!resolved && entry.kind === "dynamic") {
716
+ const keepalive = active.onTurnEndedWithoutSchedule(loopId);
717
+ await persist();
718
+ refreshStatus();
719
+ if (keepalive.action === "ended") {
720
+ ctx?.ui.notify(keepalive.endReason === "keepalive_exhausted"
721
+ ? "Loop ended: the model stopped scheduling wakeups."
722
+ : "Loop expired after 7 days and is no longer armed.", "info");
723
+ }
724
+ return;
725
+ }
726
+ if (settled?.action === "dispatch") {
727
+ // Exactly one delivery drains a coalesced occurrence, however many were missed.
728
+ await dispatchTick(settled.tick);
729
+ }
730
+ else if (settled?.action === "ended") {
731
+ ctx?.ui.notify(`Loop ended: ${settled.endReason}.`, "info");
732
+ }
733
+ refreshStatus();
734
+ }
735
+ };
736
+ }
737
+ /** True when a reminder would refer back to a full delivery that must still exist. */
738
+ function hasAnchoredDelivery(state) {
739
+ return state.autonomousPreambleDelivered || state.lastLoopFileDelivered !== null;
740
+ }
741
+ export default createLoopExtension();
742
+ //# sourceMappingURL=index.js.map