@fastagent-sh/fastagent 0.21.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +9 -12
  2. package/dist/agent.d.ts +24 -37
  3. package/dist/agent.js +7 -15
  4. package/dist/atomic-write.d.ts +13 -17
  5. package/dist/atomic-write.js +18 -29
  6. package/dist/bind.d.ts +12 -22
  7. package/dist/bind.js +18 -27
  8. package/dist/channel.d.ts +11 -33
  9. package/dist/channels/agentcore-limits.d.ts +2 -13
  10. package/dist/channels/agentcore-limits.js +2 -13
  11. package/dist/channels/agentcore-protocol.d.ts +67 -58
  12. package/dist/channels/agentcore-protocol.js +23 -15
  13. package/dist/channels/agentcore-service.d.ts +19 -33
  14. package/dist/channels/agentcore-service.js +176 -59
  15. package/dist/channels/agentcore.d.ts +39 -33
  16. package/dist/channels/agentcore.js +90 -224
  17. package/dist/channels/body.d.ts +15 -2
  18. package/dist/channels/body.js +24 -2
  19. package/dist/channels/busy.d.ts +1 -21
  20. package/dist/channels/busy.js +2 -39
  21. package/dist/channels/control.d.ts +17 -62
  22. package/dist/channels/control.js +74 -182
  23. package/dist/channels/define-channel.d.ts +39 -0
  24. package/dist/channels/define-channel.js +9 -0
  25. package/dist/channels/discover.d.ts +10 -31
  26. package/dist/channels/discover.js +37 -31
  27. package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
  28. package/dist/channels/feishu/bootstrap-token.js +7 -23
  29. package/dist/channels/feishu/card.d.ts +14 -28
  30. package/dist/channels/feishu/card.js +16 -30
  31. package/dist/channels/feishu/cloud.d.ts +1 -5
  32. package/dist/channels/feishu/context-buffer.d.ts +4 -23
  33. package/dist/channels/feishu/context-buffer.js +8 -31
  34. package/dist/channels/feishu/crypto.d.ts +10 -5
  35. package/dist/channels/feishu/crypto.js +12 -19
  36. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  37. package/dist/channels/feishu/feishu-api.js +5 -4
  38. package/dist/channels/feishu/feishu.d.ts +4 -10
  39. package/dist/channels/feishu/feishu.js +113 -227
  40. package/dist/channels/feishu/invoke-turn.d.ts +5 -4
  41. package/dist/channels/feishu/invoke-turn.js +36 -54
  42. package/dist/channels/feishu/model.d.ts +4 -21
  43. package/dist/channels/feishu/model.js +1 -8
  44. package/dist/channels/feishu/normalize.d.ts +2 -10
  45. package/dist/channels/feishu/normalize.js +7 -42
  46. package/dist/channels/feishu/parse.d.ts +9 -29
  47. package/dist/channels/feishu/parse.js +8 -26
  48. package/dist/channels/feishu/preview.d.ts +12 -14
  49. package/dist/channels/feishu/preview.js +108 -207
  50. package/dist/channels/feishu/register-app.d.ts +8 -23
  51. package/dist/channels/feishu/register-app.js +7 -20
  52. package/dist/channels/feishu/register-webhook.d.ts +2 -9
  53. package/dist/channels/feishu/register-webhook.js +9 -33
  54. package/dist/channels/feishu/scaffold/channel.ts +27 -19
  55. package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
  56. package/dist/channels/feishu/setup-mode.d.ts +11 -36
  57. package/dist/channels/feishu/setup-mode.js +7 -26
  58. package/dist/channels/feishu/ws-ingress.d.ts +1 -2
  59. package/dist/channels/feishu/ws-ingress.js +6 -8
  60. package/dist/channels/http.d.ts +15 -15
  61. package/dist/channels/http.js +19 -9
  62. package/dist/channels/kit/attachment-path.d.ts +4 -5
  63. package/dist/channels/kit/attachment-path.js +10 -34
  64. package/dist/channels/kit/context-buffer.d.ts +14 -21
  65. package/dist/channels/kit/context-buffer.js +25 -38
  66. package/dist/channels/kit/delivery.d.ts +35 -0
  67. package/dist/channels/kit/delivery.js +129 -0
  68. package/dist/channels/kit/event-stream.d.ts +5 -0
  69. package/dist/channels/kit/event-stream.js +29 -0
  70. package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
  71. package/dist/channels/kit/invoke-turn-kit.js +89 -32
  72. package/dist/channels/kit/preview-kit.d.ts +18 -72
  73. package/dist/channels/kit/preview-kit.js +22 -88
  74. package/dist/channels/kit/signature.d.ts +3 -9
  75. package/dist/channels/kit/signature.js +3 -9
  76. package/dist/channels/kit/state.d.ts +1 -2
  77. package/dist/channels/kit/state.js +5 -14
  78. package/dist/channels/kit/stop-command.d.ts +1 -2
  79. package/dist/channels/kit/stop-command.js +6 -9
  80. package/dist/channels/kit/tasks.d.ts +1 -1
  81. package/dist/channels/kit/tasks.js +8 -21
  82. package/dist/channels/kit/text.d.ts +2 -16
  83. package/dist/channels/kit/text.js +23 -31
  84. package/dist/channels/kit/thread-participants.d.ts +4 -17
  85. package/dist/channels/kit/thread-participants.js +8 -55
  86. package/dist/channels/kit/transport.d.ts +26 -0
  87. package/dist/channels/kit/transport.js +20 -0
  88. package/dist/channels/kit/turn-queue.d.ts +7 -11
  89. package/dist/channels/kit/turn-queue.js +21 -39
  90. package/dist/channels/kit/turn-runner.d.ts +32 -18
  91. package/dist/channels/kit/turn-runner.js +98 -53
  92. package/dist/channels/kit/turn-store.d.ts +32 -32
  93. package/dist/channels/kit/turn-store.js +45 -88
  94. package/dist/channels/lark/lark.d.ts +1 -5
  95. package/dist/channels/lark/onboard.d.ts +3 -12
  96. package/dist/channels/lark/onboard.js +3 -13
  97. package/dist/channels/lark/scaffold/channel.ts +27 -19
  98. package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
  99. package/dist/channels/registration.d.ts +9 -35
  100. package/dist/channels/registration.js +6 -37
  101. package/dist/channels/secret.js +2 -5
  102. package/dist/channels/serve.d.ts +47 -44
  103. package/dist/channels/serve.js +165 -101
  104. package/dist/channels/slack/config-api.d.ts +4 -27
  105. package/dist/channels/slack/config-api.js +5 -34
  106. package/dist/channels/slack/context-buffer.d.ts +2 -3
  107. package/dist/channels/slack/context-buffer.js +5 -5
  108. package/dist/channels/slack/invoke-turn.d.ts +3 -1
  109. package/dist/channels/slack/invoke-turn.js +25 -34
  110. package/dist/channels/slack/manifest.js +1 -3
  111. package/dist/channels/slack/model.d.ts +1 -2
  112. package/dist/channels/slack/model.js +1 -1
  113. package/dist/channels/slack/onboard.d.ts +4 -3
  114. package/dist/channels/slack/onboard.js +8 -11
  115. package/dist/channels/slack/onboarding-state.d.ts +3 -5
  116. package/dist/channels/slack/onboarding-state.js +9 -6
  117. package/dist/channels/slack/parse.d.ts +4 -13
  118. package/dist/channels/slack/parse.js +7 -28
  119. package/dist/channels/slack/preview.d.ts +12 -2
  120. package/dist/channels/slack/preview.js +292 -269
  121. package/dist/channels/slack/reaction.d.ts +11 -3
  122. package/dist/channels/slack/reaction.js +19 -8
  123. package/dist/channels/slack/register-webhook.js +4 -7
  124. package/dist/channels/slack/scaffold/channel.ts +20 -13
  125. package/dist/channels/slack/scaffold/slack-send.ts +2 -2
  126. package/dist/channels/slack/setup-server.d.ts +1 -5
  127. package/dist/channels/slack/setup-server.js +1 -5
  128. package/dist/channels/slack/shared-api.d.ts +1 -4
  129. package/dist/channels/slack/shared-api.js +2 -13
  130. package/dist/channels/slack/slack-api.d.ts +2 -1
  131. package/dist/channels/slack/slack-api.js +6 -4
  132. package/dist/channels/slack/slack.d.ts +11 -14
  133. package/dist/channels/slack/slack.js +121 -122
  134. package/dist/channels/sse.d.ts +1 -1
  135. package/dist/channels/sse.js +1 -1
  136. package/dist/channels/telegram/context-buffer.d.ts +7 -22
  137. package/dist/channels/telegram/context-buffer.js +6 -23
  138. package/dist/channels/telegram/invoke-turn.d.ts +7 -7
  139. package/dist/channels/telegram/invoke-turn.js +24 -43
  140. package/dist/channels/telegram/parse.d.ts +24 -51
  141. package/dist/channels/telegram/parse.js +32 -84
  142. package/dist/channels/telegram/preview.d.ts +10 -1
  143. package/dist/channels/telegram/preview.js +79 -100
  144. package/dist/channels/telegram/register-webhook.js +5 -23
  145. package/dist/channels/telegram/scaffold/channel.ts +25 -18
  146. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
  147. package/dist/channels/telegram/telegram-api.d.ts +4 -3
  148. package/dist/channels/telegram/telegram-api.js +9 -7
  149. package/dist/channels/telegram/telegram.d.ts +4 -18
  150. package/dist/channels/telegram/telegram.js +38 -94
  151. package/dist/channels/telegram/turn-store.d.ts +11 -14
  152. package/dist/channels/telegram/turn-store.js +8 -6
  153. package/dist/channels/wait-health.d.ts +4 -6
  154. package/dist/channels/wait-health.js +10 -14
  155. package/dist/cli/add-feishu.d.ts +2 -12
  156. package/dist/cli/add-feishu.js +24 -71
  157. package/dist/cli/add-slack.d.ts +5 -3
  158. package/dist/cli/add-slack.js +53 -76
  159. package/dist/cli/auth-view.d.ts +2 -16
  160. package/dist/cli/auth-view.js +2 -16
  161. package/dist/cli/commands/add.js +25 -40
  162. package/dist/cli/commands/chat.d.ts +0 -1
  163. package/dist/cli/commands/chat.js +5 -6
  164. package/dist/cli/commands/deploy/agentcore.js +60 -65
  165. package/dist/cli/commands/deploy/docker.js +50 -23
  166. package/dist/cli/commands/deploy/fly.js +23 -61
  167. package/dist/cli/commands/deploy/railway.js +23 -18
  168. package/dist/cli/commands/deploy/shared.d.ts +38 -57
  169. package/dist/cli/commands/deploy/shared.js +40 -59
  170. package/dist/cli/commands/deploy.d.ts +10 -27
  171. package/dist/cli/commands/deploy.js +38 -60
  172. package/dist/cli/commands/destroy.d.ts +4 -0
  173. package/dist/cli/commands/destroy.js +56 -0
  174. package/dist/cli/commands/dev.d.ts +2 -1
  175. package/dist/cli/commands/dev.js +20 -23
  176. package/dist/cli/commands/info.d.ts +0 -2
  177. package/dist/cli/commands/info.js +83 -49
  178. package/dist/cli/commands/init.d.ts +1 -2
  179. package/dist/cli/commands/init.js +16 -55
  180. package/dist/cli/commands/invoke.d.ts +0 -1
  181. package/dist/cli/commands/invoke.js +5 -7
  182. package/dist/cli/commands/login.d.ts +2 -1
  183. package/dist/cli/commands/login.js +33 -38
  184. package/dist/cli/commands/logs.js +3 -4
  185. package/dist/cli/commands/models.js +1 -5
  186. package/dist/cli/commands/routine-run.d.ts +6 -0
  187. package/dist/cli/commands/routine-run.js +47 -0
  188. package/dist/cli/commands/routine.d.ts +25 -0
  189. package/dist/cli/commands/routine.js +122 -0
  190. package/dist/cli/commands/start.d.ts +31 -3
  191. package/dist/cli/commands/start.js +131 -81
  192. package/dist/cli/commands/tool.js +26 -12
  193. package/dist/cli/fail.d.ts +13 -23
  194. package/dist/cli/fail.js +20 -23
  195. package/dist/cli/invoke-stream.d.ts +1 -8
  196. package/dist/cli/kernel.d.ts +6 -38
  197. package/dist/cli/kernel.js +14 -77
  198. package/dist/cli/models-view.d.ts +1 -9
  199. package/dist/cli/models-view.js +11 -22
  200. package/dist/cli/program.d.ts +5 -6
  201. package/dist/cli/program.js +120 -157
  202. package/dist/cli/serve.d.ts +57 -41
  203. package/dist/cli/serve.js +94 -121
  204. package/dist/cli/shared.d.ts +15 -61
  205. package/dist/cli/shared.js +55 -181
  206. package/dist/cli.js +1 -8
  207. package/dist/collect.d.ts +6 -0
  208. package/dist/collect.js +6 -0
  209. package/dist/core.d.ts +4 -1
  210. package/dist/core.js +9 -7
  211. package/dist/declared-secrets.d.ts +55 -0
  212. package/dist/declared-secrets.js +88 -0
  213. package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
  214. package/dist/deploy/agentcore/aws-cli.js +64 -0
  215. package/dist/deploy/agentcore/destroy.d.ts +51 -0
  216. package/dist/deploy/agentcore/destroy.js +273 -0
  217. package/dist/deploy/agentcore/forwarder.js +28 -79
  218. package/dist/deploy/agentcore/logs.d.ts +2 -18
  219. package/dist/deploy/agentcore/logs.js +27 -51
  220. package/dist/deploy/agentcore/plan.d.ts +85 -105
  221. package/dist/deploy/agentcore/plan.js +152 -314
  222. package/dist/deploy/agentcore/run.d.ts +28 -28
  223. package/dist/deploy/agentcore/run.js +227 -306
  224. package/dist/deploy/agentcore/zip.d.ts +2 -15
  225. package/dist/deploy/agentcore/zip.js +3 -17
  226. package/dist/deploy/build-context.d.ts +21 -0
  227. package/dist/deploy/build-context.js +137 -0
  228. package/dist/deploy/channel-ingress.d.ts +26 -39
  229. package/dist/deploy/channel-ingress.js +57 -28
  230. package/dist/deploy/container.d.ts +20 -34
  231. package/dist/deploy/container.js +49 -100
  232. package/dist/deploy/docker/plan.d.ts +12 -11
  233. package/dist/deploy/docker/plan.js +43 -24
  234. package/dist/deploy/docker/run.d.ts +22 -25
  235. package/dist/deploy/docker/run.js +39 -37
  236. package/dist/deploy/fly/plan.d.ts +15 -44
  237. package/dist/deploy/fly/plan.js +41 -73
  238. package/dist/deploy/fly/run.d.ts +16 -51
  239. package/dist/deploy/fly/run.js +42 -95
  240. package/dist/deploy/hosts.d.ts +24 -3
  241. package/dist/deploy/hosts.js +24 -3
  242. package/dist/deploy/preflight.d.ts +47 -41
  243. package/dist/deploy/preflight.js +282 -383
  244. package/dist/deploy/railway/plan.d.ts +16 -42
  245. package/dist/deploy/railway/plan.js +42 -60
  246. package/dist/deploy/railway/run.d.ts +30 -68
  247. package/dist/deploy/railway/run.js +86 -100
  248. package/dist/deploy/registration-gate.d.ts +6 -13
  249. package/dist/deploy/residency.d.ts +37 -0
  250. package/dist/deploy/residency.js +24 -0
  251. package/dist/deploy/runner.d.ts +10 -9
  252. package/dist/deploy/runner.js +7 -18
  253. package/dist/deploy/secrets.d.ts +40 -41
  254. package/dist/deploy/secrets.js +117 -103
  255. package/dist/deploy/workspace.d.ts +50 -0
  256. package/dist/deploy/workspace.js +197 -0
  257. package/dist/dev-supervisor.d.ts +1 -9
  258. package/dist/dev-supervisor.js +21 -64
  259. package/dist/effect-port.d.ts +50 -0
  260. package/dist/effect-port.js +77 -0
  261. package/dist/engines/pi/agent-session-factory.d.ts +50 -69
  262. package/dist/engines/pi/agent-session-factory.js +249 -146
  263. package/dist/engines/pi/auth.d.ts +15 -11
  264. package/dist/engines/pi/auth.js +96 -99
  265. package/dist/engines/pi/chat.d.ts +1 -5
  266. package/dist/engines/pi/chat.js +2 -14
  267. package/dist/engines/pi/config.d.ts +53 -51
  268. package/dist/engines/pi/config.js +83 -92
  269. package/dist/engines/pi/create.d.ts +60 -75
  270. package/dist/engines/pi/create.js +108 -141
  271. package/dist/engines/pi/definition.d.ts +14 -58
  272. package/dist/engines/pi/definition.js +21 -85
  273. package/dist/engines/pi/invoke-session.d.ts +5 -25
  274. package/dist/engines/pi/invoke-session.js +209 -244
  275. package/dist/engines/pi/login.d.ts +95 -35
  276. package/dist/engines/pi/login.js +259 -107
  277. package/dist/engines/pi/machine.d.ts +31 -0
  278. package/dist/engines/pi/machine.js +106 -0
  279. package/dist/engines/pi/models.d.ts +70 -89
  280. package/dist/engines/pi/models.js +188 -81
  281. package/dist/engines/pi/open.d.ts +65 -53
  282. package/dist/engines/pi/open.js +129 -69
  283. package/dist/engines/pi/report.d.ts +3 -12
  284. package/dist/engines/pi/report.js +2 -16
  285. package/dist/engines/pi/search-tools.d.ts +1 -9
  286. package/dist/engines/pi/search-tools.js +26 -57
  287. package/dist/engines/pi/service.d.ts +2 -16
  288. package/dist/engines/pi/service.js +2 -16
  289. package/dist/engines/pi/session-builder.d.ts +5 -9
  290. package/dist/engines/pi/session-builder.js +44 -82
  291. package/dist/engines/pi/session-control.js +286 -269
  292. package/dist/engines/pi/session-effects.d.ts +14 -0
  293. package/dist/engines/pi/session-effects.js +30 -0
  294. package/dist/engines/pi/session-inheritance.d.ts +9 -18
  295. package/dist/engines/pi/session-inheritance.js +103 -160
  296. package/dist/engines/pi/session-markers.d.ts +31 -30
  297. package/dist/engines/pi/session-markers.js +26 -25
  298. package/dist/engines/pi/session-settings.d.ts +6 -22
  299. package/dist/engines/pi/session-settings.js +10 -21
  300. package/dist/engines/pi/session-store.d.ts +26 -92
  301. package/dist/engines/pi/session-store.js +96 -310
  302. package/dist/engines/pi/tool-context.d.ts +17 -45
  303. package/dist/engines/pi/tool-context.js +7 -26
  304. package/dist/engines/pi/tool.d.ts +45 -36
  305. package/dist/engines/pi/tool.js +43 -57
  306. package/dist/engines/pi/turn-kit.d.ts +14 -52
  307. package/dist/engines/pi/turn-kit.js +18 -67
  308. package/dist/engines/pi/wake-tool.d.ts +2 -10
  309. package/dist/engines/pi/wake-tool.js +7 -27
  310. package/dist/env.d.ts +27 -28
  311. package/dist/env.js +45 -50
  312. package/dist/index.js +0 -2
  313. package/dist/lark.d.ts +1 -2
  314. package/dist/lark.js +1 -2
  315. package/dist/loader.d.ts +14 -55
  316. package/dist/loader.js +24 -43
  317. package/dist/log.d.ts +2 -12
  318. package/dist/log.js +3 -24
  319. package/dist/node.d.ts +2 -15
  320. package/dist/node.js +2 -18
  321. package/dist/observe.d.ts +2 -8
  322. package/dist/open-url.d.ts +1 -1
  323. package/dist/open-url.js +1 -1
  324. package/dist/paths.d.ts +70 -134
  325. package/dist/paths.js +165 -232
  326. package/dist/pi.d.ts +4 -6
  327. package/dist/pi.js +3 -3
  328. package/dist/proxy.d.ts +16 -6
  329. package/dist/proxy.js +52 -16
  330. package/dist/runtime.d.ts +18 -12
  331. package/dist/runtime.js +18 -11
  332. package/dist/scaffold/add-channel.d.ts +14 -31
  333. package/dist/scaffold/add-channel.js +48 -112
  334. package/dist/scaffold/init.d.ts +9 -31
  335. package/dist/scaffold/init.js +45 -138
  336. package/dist/scaffold/templates/env.example +6 -4
  337. package/dist/scaffold/templates/fastagent.config.ts +36 -0
  338. package/dist/scaffold/templates.d.ts +3 -3
  339. package/dist/scaffold/templates.js +5 -8
  340. package/dist/scaffold/vendor-skill.d.ts +2 -5
  341. package/dist/scaffold/vendor-skill.js +19 -29
  342. package/dist/schedule/cron.d.ts +9 -5
  343. package/dist/schedule/cron.js +9 -11
  344. package/dist/schedule/discover.d.ts +12 -12
  345. package/dist/schedule/discover.js +43 -34
  346. package/dist/schedule/routine.d.ts +43 -0
  347. package/dist/schedule/routine.js +32 -0
  348. package/dist/schedule/run.d.ts +55 -0
  349. package/dist/schedule/run.js +146 -0
  350. package/dist/schedule/scheduler.d.ts +44 -55
  351. package/dist/schedule/scheduler.js +291 -265
  352. package/dist/schedule/state.d.ts +105 -10
  353. package/dist/schedule/state.js +293 -19
  354. package/dist/schedule/wake-alarm.d.ts +10 -23
  355. package/dist/schedule/wake-alarm.js +91 -121
  356. package/dist/schedule/wakeups.d.ts +23 -39
  357. package/dist/schedule/wakeups.js +33 -75
  358. package/dist/secrets-gate.d.ts +32 -0
  359. package/dist/secrets-gate.js +33 -0
  360. package/dist/service.d.ts +137 -98
  361. package/dist/service.js +250 -252
  362. package/dist/session-remote.d.ts +33 -44
  363. package/dist/session-remote.js +203 -192
  364. package/dist/session.d.ts +153 -228
  365. package/dist/session.js +33 -55
  366. package/dist/tunnel.d.ts +8 -36
  367. package/dist/tunnel.js +24 -65
  368. package/dist/version.d.ts +2 -2
  369. package/dist/version.js +2 -2
  370. package/package.json +10 -16
  371. package/dist/channels/agentcore-state.d.ts +0 -77
  372. package/dist/channels/agentcore-state.js +0 -276
  373. package/dist/channels/github/github.d.ts +0 -31
  374. package/dist/channels/github/github.js +0 -89
  375. package/dist/channels/github/scaffold/channel.ts +0 -25
  376. package/dist/cli/commands/attach.d.ts +0 -108
  377. package/dist/cli/commands/attach.js +0 -637
  378. package/dist/cli/commands/fire.d.ts +0 -7
  379. package/dist/cli/commands/fire.js +0 -38
  380. package/dist/cli/commands/schedule.d.ts +0 -12
  381. package/dist/cli/commands/schedule.js +0 -87
  382. package/dist/github.d.ts +0 -3
  383. package/dist/github.js +0 -2
  384. package/dist/scaffold/templates/fastagent.config.mjs +0 -23
  385. package/dist/schedule/audit.d.ts +0 -18
  386. package/dist/schedule/audit.js +0 -57
  387. package/dist/schedule/schedule.d.ts +0 -35
  388. package/dist/schedule/schedule.js +0 -23
@@ -0,0 +1,14 @@
1
+ /** Scoped acquisition of the two things a pi turn or control write owns: the session lease and the
2
+ * bound session. The Promise crossing itself is `src/effect-port.ts`. */
3
+ import * as Effect from "effect/Effect";
4
+ import type { PiAgentSessionFactory } from "./invoke-session.ts";
5
+ import type { SessionInheritance } from "./session-inheritance.ts";
6
+ import type { Lease } from "./turn-kit.ts";
7
+ /** Admission was refused: another turn (or another write) holds this session. Its own tag because it
8
+ * is CONTROL FLOW — a caller answers it with `session_busy` and a retry — not a port failure. */
9
+ export declare class SessionBusy extends Error {
10
+ readonly _tag = "SessionBusy";
11
+ constructor();
12
+ }
13
+ export declare function acquireSessionLease(lease: Lease, session: string): Effect.Effect<import("./turn-kit.ts").Release, SessionBusy, import("effect/Scope").Scope>;
14
+ export declare function acquireSession(factory: PiAgentSessionFactory, session: string, inherit?: SessionInheritance): Effect.Effect<import("@earendil-works/pi-coding-agent").AgentSession, import("../../effect-port.ts").PortFailure, import("effect/Scope").Scope>;
@@ -0,0 +1,30 @@
1
+ /** Scoped acquisition of the two things a pi turn or control write owns: the session lease and the
2
+ * bound session. The Promise crossing itself is `src/effect-port.ts`. */
3
+ import * as Effect from "effect/Effect";
4
+ import { port, portCleanup } from "../../effect-port.js";
5
+ /** Admission was refused: another turn (or another write) holds this session. Its own tag because it
6
+ * is CONTROL FLOW — a caller answers it with `session_busy` and a retry — not a port failure. */
7
+ export class SessionBusy extends Error {
8
+ _tag = "SessionBusy";
9
+ constructor() {
10
+ super("session busy: a turn or another write is already in flight");
11
+ }
12
+ }
13
+ export function acquireSessionLease(lease, session) {
14
+ return Effect.acquireRelease(Effect.suspend(() => {
15
+ const release = lease.tryAcquire(session);
16
+ return release ? Effect.succeed(release) : Effect.fail(new SessionBusy());
17
+ }), (release) => portCleanup("lease release", release));
18
+ }
19
+ export function acquireSession(factory, session, inherit) {
20
+ // Acquisition stays uninterruptible: a late factory may still publish durable state under the lease.
21
+ // The extension instances are this session's alone, so their end is its end: pi's `dispose()` does not tell them.
22
+ return Effect.acquireRelease(port(() => factory(session, inherit)), (bound) => portCleanup("dispose", async () => {
23
+ try {
24
+ await bound.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
25
+ }
26
+ finally {
27
+ bound.dispose();
28
+ }
29
+ }));
30
+ }
@@ -1,30 +1,21 @@
1
- import type { SessionManager } from "@earendil-works/pi-coding-agent";
1
+ import { type SessionManager } from "@earendil-works/pi-coding-agent";
2
2
  /** What a Caller names when a new session should start from an existing one. */
3
3
  export interface SessionInheritance {
4
- /** The session to inherit from. Missing or unreadable → start empty, with a warn: context is not
5
- * the ask, and losing it must not cost the turn. */
6
4
  parentSession: string;
7
- /** Opaque markers that MAY locate the branch point on the parent's active path (searched in
8
- * message content, first hit wins, most recent occurrence). No match → the parent's present. */
5
+ /**
6
+ * Opaque markers that MAY locate the branch point on the parent's active path (searched in message content, first
7
+ * hit wins, most recent occurrence).
8
+ */
9
9
  branchHints?: string[];
10
10
  }
11
- /**
12
- * The branch point this inheritance should copy up to, and the parent's path — the decision half,
13
- * shared by both backends because WHERE a thread branches is policy, not storage.
14
- */
11
+ /** The branch point this inheritance should copy up to, and the parent's path. */
15
12
  export declare function inheritanceCut(parent: SessionManager, branchHints?: string[]): {
16
13
  at: string;
17
14
  } | undefined;
18
15
  /**
19
- * Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to
20
- * fork has to do instead. Kinds pi models as facts about an entry rather than positions (labels,
21
- * the session name) are not copied: they describe the parent's record, not the thread's history.
22
- *
23
- * The COPY only. Bounding what the child's model sees is {@link markInheritanceWindow}, which only
24
- * {@link copyBranchForInheritance} applies: a lifecycle fork that marked a window would hide the
25
- * exact entries its user forked to keep.
16
+ * Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to fork has to do
17
+ * instead.
26
18
  */
27
19
  export declare function copyBranchInto(parent: SessionManager, child: SessionManager, at: string): void;
28
- /** {@link copyBranchInto} plus the inheritance window — what a new THREAD gets and a fork does not.
29
- * Both backends call THIS one, so neither can drift on where a child's context begins. */
20
+ /** {@link copyBranchInto} plus the inheritance window — what a new THREAD gets and a fork does not. */
30
21
  export declare function copyBranchForInheritance(parent: SessionManager, child: SessionManager, at: string): void;
@@ -1,83 +1,20 @@
1
+ import { estimateTokens } from "@earendil-works/pi-coding-agent";
1
2
  import { log } from "../../log.js";
2
- import { isPlaneMarker } from "./session-markers.js";
3
+ import { isConversationMessage, isPlaneMarker } from "./session-markers.js";
3
4
  /** Inheritance window: at most this many exchanges of the parent reach the child's model context. */
4
5
  const INHERIT_MAX_EXCHANGES = 50;
5
- /** …and at most roughly this many tokens (~1/4 of a 200K context: generous, not everything). Both
6
- * limits govern how far the window EXTENDS into older history — the newest exchange is a FLOOR,
7
- * kept whole even when it alone exceeds the budget: the mark's boundary is entry-granular, and an
8
- * inheritance that drops the exchange the thread branched off would be no inheritance at all. */
6
+ /** …and at most roughly this many tokens (~1/4 of a 200K context: generous, not everything). */
9
7
  const INHERIT_MAX_TOKENS = 50_000;
10
- /** Branch hints are IDS, not payloads: each one costs a scan over the parent's serialized path, and
11
- * the wire accepts arbitrary arrays — so the engine caps them where the cost lives. */
8
+ /**
9
+ * Branch hints are IDS, not payloads: each one costs a scan over the parent's serialized path, and the wire accepts
10
+ * arbitrary arrays.
11
+ */
12
12
  const MAX_BRANCH_HINTS = 16;
13
13
  const MAX_BRANCH_HINT_CHARS = 128;
14
- /** A vision image is priced FLAT — what a provider bills for a resized image, roughly — because its
15
- * base64 length (~1M chars for a photo) measures storage, not context: pricing it by chars would
16
- * let one photo evict the whole text window. */
17
- const INHERIT_IMAGE_TOKENS = 1_600;
18
14
  function isUserMessage(entry) {
19
15
  return entry?.type === "message" && entry.message?.role === "user";
20
16
  }
21
- /** Rough token estimate for windowing — text at chars/4, images flat. Precision is not the point:
22
- * the window is a budget, and being 20% off moves a boundary by an exchange, not correctness. */
23
- function estimateContentTokens(content) {
24
- if (typeof content === "string")
25
- return Math.ceil(content.length / 4);
26
- if (!Array.isArray(content))
27
- return 0;
28
- let tokens = 0;
29
- for (const block of content) {
30
- if (block.type === "image")
31
- tokens += INHERIT_IMAGE_TOKENS;
32
- else if (typeof block.text === "string")
33
- tokens += Math.ceil(block.text.length / 4);
34
- else
35
- tokens += Math.ceil(JSON.stringify(block).length / 4);
36
- }
37
- return tokens;
38
- }
39
- /** Both entry kinds pi projects into model context from a copied path: a `custom_message` is an
40
- * extension's injection INTO the conversation, so it charges the budget like any message. */
41
- function estimateEntryTokens(entry) {
42
- if (entry.type === "message")
43
- return estimateContentTokens(entry.message?.content);
44
- if (entry.type === "custom_message")
45
- return estimateContentTokens(entry.content);
46
- return 0;
47
- }
48
- /** A compaction entry's summary AND its retained tail — the entries from `firstKeptEntryId` up to
49
- * the compaction — DO reach the model. They are the floor under every window that starts above the
50
- * compaction, so the budget must count them; the tail is read off the path, since pi-coding-agent's
51
- * SessionManager stores it as a POINTER and not as messages on the entry (0.84: `appendCompaction`
52
- * writes `firstKeptEntryId`, and both of AgentSession's compaction paths go through it).
53
- *
54
- * pi-agent-core's harness writes the newer self-contained form instead — `retainedTail` on the entry,
55
- * no pointer. If pi-coding-agent adopts it the pointer stops resolving, and this function prices the
56
- * tail at zero while {@link copyBranchInto} drops it. Adopting it the way pi-agent-core did, by
57
- * REPLACING the pointer, fails to typecheck at `Entry.firstKeptEntryId` rather than running quietly;
58
- * a `retainedTail` added BESIDE the pointer would not, and is the case to watch on the next bump.
59
- * Either way this is the place to change — not a reason to read both shapes today. */
60
- function estimateCompactionTokens(path, compactionIdx) {
61
- const compaction = path[compactionIdx];
62
- if (compaction?.type !== "compaction")
63
- return 0;
64
- let tokens = Math.ceil((compaction.summary ?? "").length / 4);
65
- const firstKept = path.findIndex((entry) => entry.id === compaction.firstKeptEntryId);
66
- if (firstKept < 0)
67
- return tokens; // an unresolvable pointer keeps nothing — pi's own reading
68
- for (let i = firstKept; i < compactionIdx; i++) {
69
- const entry = path[i];
70
- if (entry)
71
- tokens += estimateEntryTokens(entry);
72
- }
73
- return tokens;
74
- }
75
- /**
76
- * Find the fork target on the parent's active path: the LAST message whose content carries a hint
77
- * (the most recent turn that talked about that message), extended forward to the end of its exchange
78
- * — forking mid-exchange would inherit a question without its answer. Hints are tried in caller
79
- * order; the first that matches anywhere wins. No match → undefined (the caller forks the present).
80
- */
17
+ /** Find the fork target on the parent's active path. */
81
18
  function locateBranchPoint(path, hints) {
82
19
  const usable = hints
83
20
  .filter((hint) => hint.length > 0 && hint.length <= MAX_BRANCH_HINT_CHARS)
@@ -87,10 +24,10 @@ function locateBranchPoint(path, hints) {
87
24
  }
88
25
  if (usable.length === 0)
89
26
  return undefined;
90
- // Serialize each message ONCE — the scan is hints × entries, and stringify must not sit in the
91
- // inner loop. The whole message, not just content: shape-agnostic, and a hint is a platform id —
92
- // a false positive would need the id to appear outside content, which is where ids live anyway.
93
- const serialized = path.map((entry) => (entry.type === "message" ? JSON.stringify(entry.message) : ""));
27
+ // Serialize each conversation message ONCE — the scan is hints × entries, and stringify must not sit in the inner
28
+ // loop. A hint is something a participant said, so the engine's own system entries are not searched: see
29
+ // `isConversationMessage` for what a hint landing in the assembled prompt would otherwise do here.
30
+ const serialized = path.map((entry) => (isConversationMessage(entry) ? JSON.stringify(entry.message) : ""));
94
31
  for (const hint of usable) {
95
32
  for (let i = path.length - 1; i >= 0; i--) {
96
33
  if (!serialized[i]?.includes(hint))
@@ -104,60 +41,70 @@ function locateBranchPoint(path, hints) {
104
41
  return undefined;
105
42
  }
106
43
  /**
107
- * Bound what the child's MODEL CONTEXT starts with: keep the newest exchange unconditionally, extend
108
- * older while both window limits hold, and mark the boundary with a mechanical compaction entry.
109
- * Entries above the parent's own last compaction are already outside model context and need no mark;
110
- * a child whose visible history fits the window gets no mark at all.
44
+ * Bound what the child's MODEL CONTEXT starts with. Measured on pi's projection, what the model will actually see:
45
+ * a `context_edit` omission (an abandoned retry or overflow attempt) costs nothing, and the engine's own `system`
46
+ * entries are not history. The newest copied compaction (its summary, then the tail it retained) reaches the model
47
+ * only while no window is cut: pi reads the NEWEST compaction alone, so a mark placed after it drops that tail. Its
48
+ * summary, a few hundred tokens covering everything older, is carried into the mark instead.
111
49
  */
112
50
  function markInheritanceWindow(child) {
113
51
  const path = child.getBranch();
114
- let scanFrom = 0;
115
- for (let i = path.length - 1; i >= 0; i--) {
116
- if (path[i]?.type === "compaction") {
117
- scanFrom = i + 1;
118
- break;
52
+ const position = new Map(path.map((entry, i) => [entry.id, i]));
53
+ const latest = path.map((entry) => entry.type).lastIndexOf("compaction");
54
+ const previous = latest >= 0 ? path[latest] : undefined;
55
+ let summaryTokens = 0;
56
+ let tailTokens = 0;
57
+ let tailExchanges = 0;
58
+ const scanned = [];
59
+ for (const { sourceEntry, messages } of child.buildSessionProjection().entries) {
60
+ const tokens = messages.reduce((sum, message) => (message.role === "system" ? sum : sum + estimateTokens(message)), 0);
61
+ const startsExchange = sourceEntry.type === "message" && sourceEntry.message.role === "user" && messages.length > 0;
62
+ if (sourceEntry.id === previous?.id)
63
+ summaryTokens += tokens;
64
+ else if ((position.get(sourceEntry.id) ?? -1) < latest) {
65
+ // The retained tail: journal entries BEFORE the compaction that pi still projects.
66
+ tailTokens += tokens;
67
+ if (startsExchange)
68
+ tailExchanges++;
119
69
  }
70
+ else
71
+ scanned.push({ id: sourceEntry.id, tokens, startsExchange });
120
72
  }
121
- const scanned = path.slice(scanFrom);
122
- // The compaction's own summary + retained tail reach the model regardless of where the window
123
- // lands, so they charge the budget as a base cost — not estimating them would over-admit.
124
- const baseTokens = estimateCompactionTokens(path, scanFrom - 1);
125
73
  const starts = [];
126
74
  scanned.forEach((entry, i) => {
127
- if (isUserMessage(entry))
75
+ if (entry.startsExchange)
128
76
  starts.push(i);
129
77
  });
130
- if (starts.length <= 1)
131
- return; // zero or one visible exchange — nothing to cut
78
+ if (starts.length === 0)
79
+ return; // no exchange after the compaction to place a mark at
132
80
  const suffixTokens = new Array(scanned.length + 1).fill(0);
133
81
  for (let i = scanned.length - 1; i >= 0; i--) {
134
- const entry = scanned[i];
135
- suffixTokens[i] = (suffixTokens[i + 1] ?? 0) + (entry ? estimateEntryTokens(entry) : 0);
82
+ suffixTokens[i] = (suffixTokens[i + 1] ?? 0) + (scanned[i]?.tokens ?? 0);
136
83
  }
84
+ const fits = (exchanges, tokens) => exchanges <= INHERIT_MAX_EXCHANGES && tokens <= INHERIT_MAX_TOKENS;
85
+ const everything = summaryTokens + tailTokens + (suffixTokens[0] ?? 0);
86
+ if (fits(tailExchanges + starts.length, everything))
87
+ return; // the whole visible history fits the window
88
+ // Cut: the tail goes, the summary stays (carried below). The newest exchange is the floor.
137
89
  let chosen = starts.length - 1;
138
90
  for (let k = starts.length - 2; k >= 0; k--) {
139
- const exchanges = starts.length - k;
140
- const startIdx = starts[k];
141
- if (startIdx === undefined)
142
- break;
143
- if (exchanges > INHERIT_MAX_EXCHANGES || baseTokens + (suffixTokens[startIdx] ?? 0) > INHERIT_MAX_TOKENS)
91
+ if (!fits(starts.length - k, summaryTokens + (suffixTokens[starts[k] ?? 0] ?? 0)))
144
92
  break;
145
93
  chosen = k;
146
94
  }
147
- if (chosen === 0)
148
- return; // the whole visible history fits the window
149
- const boundaryIdx = starts[chosen];
150
- if (boundaryIdx === undefined)
95
+ // Without a compaction, a mark at the first exchange would hide nothing that costs tokens.
96
+ if (!previous && chosen === 0)
151
97
  return;
152
- const boundary = scanned[boundaryIdx];
98
+ const boundary = scanned[starts[chosen] ?? 0];
153
99
  if (boundary === undefined)
154
100
  return;
155
- child.appendCompaction(`Inherited from the parent conversation; ${chosen} earlier exchange(s) are not shown.`, boundary.id, (suffixTokens[0] ?? 0) - (suffixTokens[boundaryIdx] ?? 0));
101
+ const hidden = tailExchanges + chosen;
102
+ const note = hidden > 0
103
+ ? `Inherited from the parent conversation; ${hidden} earlier exchange(s) are not shown.`
104
+ : "Inherited from the parent conversation; earlier messages are not shown.";
105
+ child.appendCompaction(previous ? `${previous.summary}\n\n${note}` : note, boundary.id, everything);
156
106
  }
157
- /**
158
- * The branch point this inheritance should copy up to, and the parent's path — the decision half,
159
- * shared by both backends because WHERE a thread branches is policy, not storage.
160
- */
107
+ /** The branch point this inheritance should copy up to, and the parent's path. */
161
108
  export function inheritanceCut(parent, branchHints) {
162
109
  const path = parent.getBranch();
163
110
  const leaf = path[path.length - 1];
@@ -171,22 +118,16 @@ export function inheritanceCut(parent, branchHints) {
171
118
  return { at: at ?? leaf.id };
172
119
  }
173
120
  /**
174
- * Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to
175
- * fork has to do instead. Kinds pi models as facts about an entry rather than positions (labels,
176
- * the session name) are not copied: they describe the parent's record, not the thread's history.
177
- *
178
- * The COPY only. Bounding what the child's model sees is {@link markInheritanceWindow}, which only
179
- * {@link copyBranchForInheritance} applies: a lifecycle fork that marked a window would hide the
180
- * exact entries its user forked to keep.
121
+ * Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to fork has to do
122
+ * instead.
181
123
  */
182
124
  export function copyBranchInto(parent, child, at) {
183
- /** Parent entry id → the child's id for that entry: the copy mints its own, and a compaction
184
- * points BACK into the path it was appended to. */
125
+ /**
126
+ * Parent entry id → the child's id for that entry: the copy mints its own, and a compaction points BACK into the
127
+ * path it was appended to.
128
+ */
185
129
  const copied = new Map();
186
- /** Ids of entries this copy did NOT append. A compaction's `firstKeptEntryId` routinely names one:
187
- * pi walks the cut point backwards onto the metadata entries adjacent to it, which carry no
188
- * context. They resolve to the next entry that survived — the retained tail starts there — so a
189
- * dropped anchor moves the boundary by an invisible entry instead of erasing the whole tail. */
130
+ /** Ids of entries this copy did NOT append. */
190
131
  let unanchored = [];
191
132
  const record = (parentId, childId) => {
192
133
  for (const id of unanchored)
@@ -194,50 +135,57 @@ export function copyBranchInto(parent, child, at) {
194
135
  unanchored = [];
195
136
  copied.set(parentId, childId);
196
137
  };
197
- for (const raw of parent.getBranch(at)) {
198
- const entry = raw;
138
+ // Typed by pi's own union, and exhaustive: a pi release that adds an entry type fails typecheck here instead of
139
+ // being dropped from every copied branch without a word.
140
+ for (const entry of parent.getBranch(at)) {
199
141
  let childId;
200
142
  switch (entry.type) {
201
143
  case "message":
202
- if (entry.message) {
203
- childId = child.appendMessage(entry.message);
204
- }
144
+ // Every message, INCLUDING the engine's system entries: the child starts from the prompt the parent was
145
+ // running under, and pi diffs its own sections against it, so the thread's first request carries that
146
+ // prompt once rather than twice.
147
+ childId = child.appendMessage(entry.message);
205
148
  break;
206
149
  case "custom_message":
207
- // Model-visible history, unlike the `custom` entries below it: an extension injected it INTO
208
- // the conversation, and the assistant messages answering it are being copied.
209
- childId = child.appendCustomMessageEntry(entry.customType ?? "", entry.content, entry.display ?? false, entry.details);
150
+ // Model-visible history, unlike the `custom` entries below it: an extension injected it INTO the
151
+ // conversation, and the assistant messages answering it are being copied.
152
+ childId = child.appendCustomMessageEntry(entry.customType, entry.content, entry.display, entry.details);
210
153
  break;
211
154
  case "compaction":
212
- // `firstKeptEntryId` is where the RETAINED TAIL starts — the entries pi did not summarize,
213
- // which still reach the model. Translated through the copy rather than pinned to the child's
214
- // leaf: pinning kept exactly ONE entry, and when that entry was a toolResult (a compaction
215
- // lands wherever the turn ended) the child's first request opened with a tool result whose
216
- // call had been summarized away, which every provider rejects. An id the copy never saw
217
- // keeps nothing, which is what pi itself does with a pointer it cannot resolve.
218
- childId = child.appendCompaction(entry.summary ?? "", copied.get(entry.firstKeptEntryId ?? "") ?? "", entry.tokensBefore ?? 0, entry.details);
155
+ // `firstKeptEntryId` is where the RETAINED TAIL starts — the entries pi did not summarize, which still reach
156
+ // the model.
157
+ childId = child.appendCompaction(entry.summary, copied.get(entry.firstKeptEntryId) ?? "", entry.tokensBefore, entry.details);
219
158
  break;
159
+ case "context_edit": {
160
+ // pi omits an abandoned retry/overflow attempt from the model's context this way; dropping the edit would
161
+ // hand that attempt back to the copy. Its target is a message, which the copy always anchors.
162
+ const target = copied.get(entry.targetId);
163
+ if (target)
164
+ childId = child.appendContextEdit(target, entry.replacement);
165
+ break;
166
+ }
220
167
  case "model_change":
221
- if (entry.provider && entry.modelId)
222
- childId = child.appendModelChange(entry.provider, entry.modelId);
168
+ childId = child.appendModelChange(entry.provider, entry.modelId);
223
169
  break;
224
170
  case "thinking_level_change":
225
- if (entry.thinkingLevel)
226
- childId = child.appendThinkingLevelChange(entry.thinkingLevel);
171
+ childId = child.appendThinkingLevelChange(entry.thinkingLevel);
227
172
  break;
228
173
  case "custom":
229
- // The plane's markers describe the parent's RECORD, not the thread's history: a copied
230
- // provenance would make a fork of a fork claim its grandparent's branch point (the
231
- // idempotency check reads that value), and a copied leaf anchor would pin the child's head
232
- // to a position its own history never chose. Every other custom entry is history and travels
233
- // — the engine's tool-activation delta above all, since the copied assistant messages call
234
- // the tools it records.
235
- if (entry.customType && !isPlaneMarker(entry)) {
174
+ // The plane's markers describe the parent's RECORD, not the thread's history.
175
+ if (!isPlaneMarker(entry))
236
176
  childId = child.appendCustomEntry(entry.customType, entry.data);
237
- }
238
177
  break;
239
- default:
240
- break; // label / session_info / branch_summary: the parent's facts, not the thread's history
178
+ case "label":
179
+ case "session_info":
180
+ case "branch_summary":
181
+ case "usage":
182
+ break; // the parent's facts, not the thread's history
183
+ default: {
184
+ // Unreachable by type; a record written by a newer pi can still carry one, and refusing to start the thread
185
+ // over it would be worse than naming it.
186
+ const unhandled = entry;
187
+ log.warn(`[fastagent] inheritance skipped a session entry of unknown type "${unhandled.type}"`);
188
+ }
241
189
  }
242
190
  if (childId === undefined)
243
191
  unanchored.push(entry.id);
@@ -245,17 +193,12 @@ export function copyBranchInto(parent, child, at) {
245
193
  record(entry.id, childId);
246
194
  }
247
195
  }
248
- /** {@link copyBranchInto} plus the inheritance window — what a new THREAD gets and a fork does not.
249
- * Both backends call THIS one, so neither can drift on where a child's context begins. */
196
+ /** {@link copyBranchInto} plus the inheritance window — what a new THREAD gets and a fork does not. */
250
197
  export function copyBranchForInheritance(parent, child, at) {
251
198
  copyBranchInto(parent, child, at);
252
199
  markInheritanceWindow(child);
253
200
  }
254
- /*
255
- * There is deliberately NO file-level fork here. pi can copy a path into a new file
256
- * (`createBranchedSession` + `forkFrom`), but that pair writes the intermediate only when the copied
257
- * path contains an ASSISTANT message — so forking at a user entry hands `forkFrom` a path that does
258
- * not exist, and the failure reads as retryable for a condition no retry can change. Copying entries
259
- * is what a backend with no file to fork has to do anyway, so both share these functions and one
260
- * semantics; a fork is not hot enough to buy a second path back.
261
- */
201
+ // There is deliberately NO file-level fork here. pi can copy a path into a new file (`createBranchedSession` +
202
+ // `forkFrom`), but that pair writes the intermediate only when the copied path contains an ASSISTANT message — so
203
+ // forking at a user entry hands `forkFrom` a path that does not exist, and the failure reads as retryable for a
204
+ // condition no retry can change.
@@ -1,48 +1,49 @@
1
1
  /**
2
- * Which journal entries are POSITIONS and which are the control plane's own bookkeeping.
3
- *
4
- * pi's journal has one shape for everything, so the plane writes what it needs to remember into the
5
- * same log the conversation lives in: what fork a record is, and the anchor that makes a leaf move
6
- * survive a reopen. Both the record store and the history copier have to agree on which is which —
7
- * one publishes and navigates, the other copies — and a disagreement is invisible until a fork comes
8
- * back missing something. Hence one module, imported by both, depending on neither.
2
+ * Which journal entries are POSITIONS, which are the control plane's own bookkeeping, and which are turns in the
3
+ * CONVERSATION. pi's journal has one shape for everything, so the plane writes what it needs to remember into the
4
+ * same log the conversation lives in (what fork a record is, and the anchor that makes a leaf move survive a
5
+ * reopen) — and since 0.86 the ENGINE writes its own bookkeeping there too, as the `system` entries carrying the
6
+ * assembled prompt. Three questions, one file: a reader of the journal answers them here or it answers them alone,
7
+ * which is how one reader came to count the prompt as history.
9
8
  */
10
9
  import type { SessionManager } from "@earendil-works/pi-coding-agent";
11
- /** What pins a leaf move to disk. pi's `branch()` writes nothing — the leaf is runtime state, and
12
- * `open()` puts it back on the file's last entry — so a move that no other write follows is
13
- * forgotten. Carries no data: its PARENT is the position, which is the whole point of appending it. */
10
+ /** What pins a leaf move to disk. pi's `branch()` writes nothing. */
14
11
  export declare const LEAF_ANCHOR = "fastagent.leaf";
15
12
  /**
16
- * The CONTROL PLANE's bookkeeping — not a place in a conversation, so never published, never
17
- * navigable, and never copied by a fork: these describe THIS record, not the history it holds.
18
- *
19
- * An EXACT list, not a `fastagent` prefix. The prefix was tried and was wrong for one entry, at a
20
- * cost worth remembering: `fastagent:tool-activation` is written by the ENGINE and is thread history
21
- * (which deferred tools this conversation discovered), so matching it here dropped it from every
22
- * fork and every inherited thread — while the assistant messages that call those tools came along.
23
- * A new marker joins this list deliberately, which is the point of it being a list.
13
+ * The CONTROL PLANE's bookkeeping — not a place in a conversation, so never published, never navigable, and never
14
+ * copied by a fork.
24
15
  */
25
16
  export declare function isPlaneMarker(entry: {
26
17
  type?: string;
27
18
  customType?: string;
28
19
  }): boolean;
29
- /** Every position a client may move the branch head to, and everything `entries()` publishes — ONE
30
- * predicate, so "anything published is navigable" holds by construction. `label` is metadata ABOUT
31
- * an entry rather than a place; {@link isPlaneMarker} is ours rather than the conversation's. */
32
- export declare function isNavigable(entry: {
20
+ /**
21
+ * An entry that is a TURN IN THE CONVERSATION — what a reader counting, previewing or searching the history means
22
+ * by "a message".
23
+ *
24
+ * The line it draws is the ENGINE's prompt state. Since pi 0.86 pi writes `system` messages into the same log the
25
+ * conversation lives in, carrying the assembled prompt (persona, project context, skill and tool descriptions)
26
+ * plus one more per prompt or tool-set change. They are bookkeeping, not something anyone said, and every reader
27
+ * of the journal has to decide about them. Deciding once, here, is the point: the first reader that forgot cut
28
+ * inheritance above every exchange and handed a new thread an empty history, with no diagnostic.
29
+ */
30
+ export declare function isConversationMessage(entry: {
33
31
  type?: string;
34
- customType?: string;
32
+ message?: {
33
+ role?: string;
34
+ };
35
35
  }): boolean;
36
36
  /**
37
- * The head a client SEES: the last publishable entry on the active path.
38
- *
39
- * Not pi's leaf, which may be a marker — a solo leaf move anchors itself with one — so reporting
40
- * pi's answer would hand back an id the client did not ask for and cannot find in `entries()`.
37
+ * Every position a client may move the branch head to, and everything `entries()` publishes — ONE predicate, so
38
+ * "anything published is navigable" holds by construction.
41
39
  */
40
+ export declare function isNavigable(entry: {
41
+ type?: string;
42
+ customType?: string;
43
+ }): boolean;
44
+ /** The head a client SEES: the last publishable entry on the active path. */
42
45
  export declare function publishedLeaf(record: SessionManager): string | undefined;
43
46
  /** Stamp a fresh fork with where it came from. */
44
47
  export declare function stampProvenance(record: SessionManager, provenance: string): void;
45
- /** What fork this record IS, or undefined for a record that was not forked. The LAST stamp wins — a
46
- * fork of a fork carries its own — and the whole journal is read rather than the active path, so a
47
- * later leaf move cannot make a fork stop being one. */
48
+ /** What fork this record IS, or undefined for a record that was not forked. */
48
49
  export declare function forkProvenance(record: SessionManager): string | undefined;
@@ -1,35 +1,38 @@
1
- /** "This record is a fork of X at Y." Its own entry rather than pi's header `parentSession`, which
2
- * pi fills from its own fork path and names a FILE, not the branch point idempotency needs. */
1
+ /**
2
+ * "This record is a fork of X at Y." Its own entry rather than pi's header `parentSession`, which pi fills from its
3
+ * own fork path and names a FILE, not the branch point idempotency needs.
4
+ */
3
5
  const FORK_PROVENANCE = "fastagent.fork";
4
- /** What pins a leaf move to disk. pi's `branch()` writes nothing — the leaf is runtime state, and
5
- * `open()` puts it back on the file's last entry — so a move that no other write follows is
6
- * forgotten. Carries no data: its PARENT is the position, which is the whole point of appending it. */
6
+ /** What pins a leaf move to disk. pi's `branch()` writes nothing. */
7
7
  export const LEAF_ANCHOR = "fastagent.leaf";
8
8
  /**
9
- * The CONTROL PLANE's bookkeeping — not a place in a conversation, so never published, never
10
- * navigable, and never copied by a fork: these describe THIS record, not the history it holds.
11
- *
12
- * An EXACT list, not a `fastagent` prefix. The prefix was tried and was wrong for one entry, at a
13
- * cost worth remembering: `fastagent:tool-activation` is written by the ENGINE and is thread history
14
- * (which deferred tools this conversation discovered), so matching it here dropped it from every
15
- * fork and every inherited thread — while the assistant messages that call those tools came along.
16
- * A new marker joins this list deliberately, which is the point of it being a list.
9
+ * The CONTROL PLANE's bookkeeping — not a place in a conversation, so never published, never navigable, and never
10
+ * copied by a fork.
17
11
  */
18
12
  export function isPlaneMarker(entry) {
19
13
  return entry.type === "custom" && (entry.customType === FORK_PROVENANCE || entry.customType === LEAF_ANCHOR);
20
14
  }
21
- /** Every position a client may move the branch head to, and everything `entries()` publishes — ONE
22
- * predicate, so "anything published is navigable" holds by construction. `label` is metadata ABOUT
23
- * an entry rather than a place; {@link isPlaneMarker} is ours rather than the conversation's. */
24
- export function isNavigable(entry) {
25
- return entry.type !== "label" && !isPlaneMarker(entry);
26
- }
27
15
  /**
28
- * The head a client SEES: the last publishable entry on the active path.
16
+ * An entry that is a TURN IN THE CONVERSATION — what a reader counting, previewing or searching the history means
17
+ * by "a message".
29
18
  *
30
- * Not pi's leaf, which may be a marker — a solo leaf move anchors itself with one — so reporting
31
- * pi's answer would hand back an id the client did not ask for and cannot find in `entries()`.
19
+ * The line it draws is the ENGINE's prompt state. Since pi 0.86 pi writes `system` messages into the same log the
20
+ * conversation lives in, carrying the assembled prompt (persona, project context, skill and tool descriptions)
21
+ * plus one more per prompt or tool-set change. They are bookkeeping, not something anyone said, and every reader
22
+ * of the journal has to decide about them. Deciding once, here, is the point: the first reader that forgot cut
23
+ * inheritance above every exchange and handed a new thread an empty history, with no diagnostic.
32
24
  */
25
+ export function isConversationMessage(entry) {
26
+ return entry.type === "message" && entry.message?.role !== "system";
27
+ }
28
+ /**
29
+ * Every position a client may move the branch head to, and everything `entries()` publishes — ONE predicate, so
30
+ * "anything published is navigable" holds by construction.
31
+ */
32
+ export function isNavigable(entry) {
33
+ return entry.type !== "label" && !isPlaneMarker(entry);
34
+ }
35
+ /** The head a client SEES: the last publishable entry on the active path. */
33
36
  export function publishedLeaf(record) {
34
37
  const path = record.getBranch();
35
38
  for (let i = path.length - 1; i >= 0; i--) {
@@ -43,9 +46,7 @@ export function publishedLeaf(record) {
43
46
  export function stampProvenance(record, provenance) {
44
47
  record.appendCustomEntry(FORK_PROVENANCE, { provenance });
45
48
  }
46
- /** What fork this record IS, or undefined for a record that was not forked. The LAST stamp wins — a
47
- * fork of a fork carries its own — and the whole journal is read rather than the active path, so a
48
- * later leaf move cannot make a fork stop being one. */
49
+ /** What fork this record IS, or undefined for a record that was not forked. */
49
50
  export function forkProvenance(record) {
50
51
  let found;
51
52
  for (const raw of record.getEntries()) {