@astrosheep/keiyaku 4.6.7 → 4.6.9

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 (336) hide show
  1. package/README.md +46 -16
  2. package/build/plugins/claude/.claude-plugin/plugin.json +1 -1
  3. package/build/plugins/claude/skills/keiyaku/SKILL.md +5 -6
  4. package/build/plugins/claude/skills/keiyaku-akuma/SKILL.md +1 -1
  5. package/build/plugins/claude/skills/keiyaku-akuma/references/automation.md +8 -8
  6. package/build/plugins/claude/skills/keiyaku-babysit/SKILL.md +1 -1
  7. package/build/plugins/claude/skills/keiyaku-bind/SKILL.md +9 -9
  8. package/build/plugins/claude/skills/keiyaku-debt/SKILL.md +91 -0
  9. package/build/plugins/claude/skills/keiyaku-workflow/SKILL.md +2 -2
  10. package/build/plugins/codex/.codex-plugin/plugin.json +1 -1
  11. package/build/plugins/codex/skills/keiyaku/SKILL.md +5 -6
  12. package/build/plugins/codex/skills/keiyaku-akuma/SKILL.md +1 -1
  13. package/build/plugins/codex/skills/keiyaku-akuma/references/automation.md +8 -8
  14. package/build/plugins/codex/skills/keiyaku-babysit/SKILL.md +1 -1
  15. package/build/plugins/codex/skills/keiyaku-bind/SKILL.md +9 -9
  16. package/build/plugins/codex/skills/keiyaku-debt/SKILL.md +91 -0
  17. package/build/plugins/codex/skills/keiyaku-workflow/SKILL.md +2 -2
  18. package/build/plugins/opencode/index.js +1 -1
  19. package/build/plugins/opencode/skills/keiyaku/SKILL.md +5 -6
  20. package/build/plugins/opencode/skills/keiyaku-akuma/SKILL.md +1 -1
  21. package/build/plugins/opencode/skills/keiyaku-akuma/references/automation.md +8 -8
  22. package/build/plugins/opencode/skills/keiyaku-babysit/SKILL.md +1 -1
  23. package/build/plugins/opencode/skills/keiyaku-bind/SKILL.md +9 -9
  24. package/build/plugins/opencode/skills/keiyaku-debt/SKILL.md +91 -0
  25. package/build/plugins/opencode/skills/keiyaku-workflow/SKILL.md +2 -2
  26. package/build/plugins/pi/keiyaku.ts +0 -5
  27. package/build/skills/keiyaku/SKILL.md +5 -6
  28. package/build/skills/keiyaku-akuma/SKILL.md +1 -1
  29. package/build/skills/keiyaku-akuma/references/automation.md +8 -8
  30. package/build/skills/keiyaku-babysit/SKILL.md +1 -1
  31. package/build/skills/keiyaku-bind/SKILL.md +9 -9
  32. package/build/skills/keiyaku-debt/SKILL.md +91 -0
  33. package/build/skills/keiyaku-workflow/SKILL.md +2 -2
  34. package/build/src/akuma/akuma-call.d.ts +2 -0
  35. package/build/src/akuma/akuma-call.js +8 -2
  36. package/build/src/akuma/akuma-instance.d.ts +14 -1
  37. package/build/src/akuma/akuma-instance.js +37 -7
  38. package/build/src/akuma/akuma-observe.d.ts +33 -8
  39. package/build/src/akuma/akuma-observe.js +89 -63
  40. package/build/src/akuma/akuma-owner.d.ts +14 -2
  41. package/build/src/akuma/akuma-owner.js +49 -35
  42. package/build/src/akuma/akuma-selection-execution.js +9 -1
  43. package/build/src/akuma/akuma-stream.d.ts +22 -0
  44. package/build/src/akuma/akuma-stream.js +116 -0
  45. package/build/src/akuma/akuma-wait.d.ts +1 -40
  46. package/build/src/akuma/akuma-wait.js +23 -84
  47. package/build/src/akuma/akuma.d.ts +52 -16
  48. package/build/src/akuma/akuma.js +10 -10
  49. package/build/src/akuma/allowed.d.ts +7 -12
  50. package/build/src/akuma/allowed.js +13 -4
  51. package/build/src/akuma/archetype.d.ts +7 -15
  52. package/build/src/akuma/archetype.js +36 -35
  53. package/build/src/akuma/body-supervisor.d.ts +5 -5
  54. package/build/src/akuma/body-supervisor.js +22 -20
  55. package/build/src/akuma/body.d.ts +5 -3
  56. package/build/src/akuma/body.js +57 -8
  57. package/build/src/akuma/call-request.d.ts +30 -32
  58. package/build/src/akuma/call-request.js +10 -3
  59. package/build/src/akuma/caller.d.ts +39 -0
  60. package/build/src/akuma/caller.js +36 -0
  61. package/build/src/akuma/dispatch-association.d.ts +0 -1
  62. package/build/src/akuma/dispatch-association.js +0 -3
  63. package/build/src/akuma/heart/activity-schema.d.ts +12 -0
  64. package/build/src/akuma/heart/activity-schema.js +6 -0
  65. package/build/src/akuma/heart/facts.d.ts +16 -12
  66. package/build/src/akuma/heart/facts.js +8 -0
  67. package/build/src/akuma/heart/index.d.ts +61 -9
  68. package/build/src/akuma/heart/index.js +89 -24
  69. package/build/src/akuma/heart/rows.d.ts +3 -3
  70. package/build/src/akuma/heart/rows.js +5 -3
  71. package/build/src/akuma/heart/soul.d.ts +10 -1
  72. package/build/src/akuma/heart/soul.js +34 -24
  73. package/build/src/akuma/heart/storage.d.ts +20 -0
  74. package/build/src/akuma/heart/storage.js +62 -3
  75. package/build/src/akuma/heart/timeline.d.ts +7 -0
  76. package/build/src/akuma/heart/timeline.js +10 -0
  77. package/build/src/akuma/identity.d.ts +0 -1
  78. package/build/src/akuma/identity.js +2 -5
  79. package/build/src/akuma/index.d.ts +6 -2
  80. package/build/src/akuma/index.js +1 -0
  81. package/build/src/akuma/nuke.js +6 -28
  82. package/build/src/akuma/projection.d.ts +25 -11
  83. package/build/src/akuma/projection.js +24 -7
  84. package/build/src/akuma/provider-recipe.d.ts +18 -6
  85. package/build/src/akuma/provider-recipe.js +24 -1
  86. package/build/src/akuma/provider.d.ts +31 -10
  87. package/build/src/akuma/provider.js +105 -14
  88. package/build/src/akuma/providers/acp/events.js +29 -19
  89. package/build/src/akuma/providers/acp/index.js +2 -3
  90. package/build/src/akuma/providers/claude/events.js +110 -69
  91. package/build/src/akuma/providers/claude/index.d.ts +2 -2
  92. package/build/src/akuma/providers/claude/index.js +4 -8
  93. package/build/src/akuma/providers/codex-app-server/events.js +17 -2
  94. package/build/src/akuma/providers/codex-app-server/index.js +17 -11
  95. package/build/src/akuma/providers/decode.d.ts +33 -0
  96. package/build/src/akuma/providers/decode.js +65 -0
  97. package/build/src/akuma/providers/execution-environment.d.ts +2 -1
  98. package/build/src/akuma/providers/execution-environment.js +3 -0
  99. package/build/src/akuma/providers/grok-build/index.js +16 -48
  100. package/build/src/akuma/providers/index.js +2 -0
  101. package/build/src/akuma/providers/lifecycle.d.ts +9 -0
  102. package/build/src/akuma/providers/lifecycle.js +10 -0
  103. package/build/src/akuma/providers/opencode-sdk/event-stream.d.ts +3 -3
  104. package/build/src/akuma/providers/opencode-sdk/event-stream.js +2 -25
  105. package/build/src/akuma/providers/opencode-sdk/events.d.ts +7 -43
  106. package/build/src/akuma/providers/opencode-sdk/events.js +146 -296
  107. package/build/src/akuma/providers/opencode-sdk/index.js +270 -431
  108. package/build/src/akuma/providers/opencode-sdk/native-http.d.ts +9 -0
  109. package/build/src/akuma/providers/opencode-sdk/native-http.js +69 -0
  110. package/build/src/akuma/providers/opencode-sdk/session.d.ts +42 -36
  111. package/build/src/akuma/providers/opencode-sdk/session.js +75 -84
  112. package/build/src/akuma/providers/paseo/events.d.ts +33 -0
  113. package/build/src/akuma/providers/paseo/events.js +204 -0
  114. package/build/src/akuma/providers/paseo/index.d.ts +9 -0
  115. package/build/src/akuma/providers/paseo/index.js +357 -0
  116. package/build/src/akuma/providers/pi/events.d.ts +2 -0
  117. package/build/src/akuma/providers/pi/events.js +36 -38
  118. package/build/src/akuma/providers/pi/index.js +21 -35
  119. package/build/src/akuma/publication.d.ts +2 -0
  120. package/build/src/akuma/publication.js +5 -0
  121. package/build/src/akuma/request-lifecycle.d.ts +4 -2
  122. package/build/src/akuma/request-lifecycle.js +27 -12
  123. package/build/src/akuma/request-rendezvous.js +38 -55
  124. package/build/src/akuma/request-serve.d.ts +1 -1
  125. package/build/src/akuma/request-serve.js +77 -113
  126. package/build/src/akuma/requests.d.ts +3 -1
  127. package/build/src/akuma/requests.js +32 -0
  128. package/build/src/akuma/selection-observation.d.ts +129 -53
  129. package/build/src/akuma/selection-observation.js +3 -9
  130. package/build/src/akuma/turn-drive.d.ts +3 -0
  131. package/build/src/akuma/turn-drive.js +28 -10
  132. package/build/src/akuma-body.js +12 -22
  133. package/build/src/cli/akuma-caller.d.ts +3 -0
  134. package/build/src/cli/akuma-caller.js +31 -0
  135. package/build/src/cli/commands/akuma-grammar.js +10 -16
  136. package/build/src/cli/commands/akuma.js +25 -55
  137. package/build/src/cli/commands/contract-grammar.d.ts +9 -7
  138. package/build/src/cli/commands/contract-grammar.js +40 -47
  139. package/build/src/cli/commands/contract-help.d.ts +17 -7
  140. package/build/src/cli/commands/contract-help.js +50 -11
  141. package/build/src/cli/commands/contract.js +34 -63
  142. package/build/src/cli/commands/debt-grammar.d.ts +19 -0
  143. package/build/src/cli/commands/debt-grammar.js +323 -0
  144. package/build/src/cli/commands/debt-query.d.ts +5 -0
  145. package/build/src/cli/commands/{task-query.js → debt-query.js} +9 -9
  146. package/build/src/cli/commands/{task.d.ts → debt.d.ts} +2 -2
  147. package/build/src/cli/commands/{task.js → debt.js} +87 -164
  148. package/build/src/cli/coordinates.d.ts +1 -1
  149. package/build/src/cli/coordinates.js +1 -1
  150. package/build/src/cli/parse.d.ts +5 -5
  151. package/build/src/cli/parse.js +17 -16
  152. package/build/src/cli/render/akuma-activity.d.ts +11 -83
  153. package/build/src/cli/render/akuma-activity.js +20 -382
  154. package/build/src/cli/render/catalog.d.ts +4 -7
  155. package/build/src/cli/render/catalog.js +5 -5
  156. package/build/src/cli/render/contract-history.js +3 -6
  157. package/build/src/cli/render/contract.js +17 -52
  158. package/build/src/cli/render/debt.d.ts +28 -0
  159. package/build/src/cli/render/{task.js → debt.js} +55 -133
  160. package/build/src/cli/render/kanshi.js +28 -28
  161. package/build/src/cli/render/marks.d.ts +5 -5
  162. package/build/src/cli/render/marks.js +4 -10
  163. package/build/src/cli/render/nuke.js +1 -1
  164. package/build/src/cli/render/receipt.js +3 -10
  165. package/build/src/cli/runtime.d.ts +1 -1
  166. package/build/src/cli/runtime.js +14 -20
  167. package/build/src/cli/scan.d.ts +1 -1
  168. package/build/src/cli/selectors.d.ts +3 -6
  169. package/build/src/cli/selectors.js +22 -53
  170. package/build/src/cli/usage.d.ts +1 -1
  171. package/build/src/cli/usage.js +4 -4
  172. package/build/src/contract-worktree.js +3 -3
  173. package/build/src/debt/board.d.ts +160 -0
  174. package/build/src/{task → debt}/board.js +89 -89
  175. package/build/src/debt/catalog.d.ts +11 -0
  176. package/build/src/debt/catalog.js +59 -0
  177. package/build/src/{task → debt}/context.d.ts +1 -1
  178. package/build/src/{task → debt}/context.js +1 -1
  179. package/build/src/{task → debt}/created-observation.d.ts +6 -10
  180. package/build/src/{task → debt}/created-observation.js +7 -7
  181. package/build/src/debt/document.d.ts +37 -0
  182. package/build/src/{task → debt}/document.js +37 -37
  183. package/build/src/debt/identity.d.ts +21 -0
  184. package/build/src/{task → debt}/identity.js +35 -35
  185. package/build/src/debt/index.d.ts +111 -0
  186. package/build/src/debt/index.js +203 -0
  187. package/build/src/{task → debt}/input.d.ts +8 -8
  188. package/build/src/{task → debt}/input.js +21 -21
  189. package/build/src/debt/lifecycle-operations.d.ts +6 -0
  190. package/build/src/{task → debt}/lifecycle-operations.js +27 -31
  191. package/build/src/debt/mutation-result.d.ts +981 -0
  192. package/build/src/debt/mutation-result.js +109 -0
  193. package/build/src/debt/mutation.d.ts +424 -0
  194. package/build/src/debt/mutation.js +223 -0
  195. package/build/src/debt/operation-types.d.ts +71 -0
  196. package/build/src/debt/operations.d.ts +28 -0
  197. package/build/src/{task → debt}/operations.js +59 -60
  198. package/build/src/debt/query.d.ts +76 -0
  199. package/build/src/{task → debt}/query.js +55 -55
  200. package/build/src/{task → debt}/store.d.ts +13 -13
  201. package/build/src/{task → debt}/store.js +58 -49
  202. package/build/src/debt/view.d.ts +4 -0
  203. package/build/src/debt/view.js +7 -0
  204. package/build/src/dispatch/index.d.ts +3 -3
  205. package/build/src/dispatch/index.js +1 -2
  206. package/build/src/git/observe.d.ts +6 -2
  207. package/build/src/git/observe.js +6 -2
  208. package/build/src/identity/coordinates.d.ts +1 -1
  209. package/build/src/index.d.ts +17 -17
  210. package/build/src/index.js +11 -11
  211. package/build/src/kanshi/index.d.ts +2 -2
  212. package/build/src/kanshi/read.js +54 -39
  213. package/build/src/kanshi/report.d.ts +7 -7
  214. package/build/src/kanshi/select.js +4 -4
  215. package/build/src/library/address.js +2 -4
  216. package/build/src/library/akuma-creation.d.ts +5 -1
  217. package/build/src/library/akuma-creation.js +27 -47
  218. package/build/src/library/akumas.d.ts +7 -7
  219. package/build/src/library/akumas.js +13 -9
  220. package/build/src/library/bind.d.ts +17 -17
  221. package/build/src/library/bind.js +16 -12
  222. package/build/src/library/contract-address.d.ts +9 -0
  223. package/build/src/library/contract-address.js +71 -0
  224. package/build/src/library/contract-composition.d.ts +4 -7
  225. package/build/src/library/contract-composition.js +6 -18
  226. package/build/src/library/contract-creation.d.ts +3 -5
  227. package/build/src/library/contract-creation.js +24 -22
  228. package/build/src/library/contract-execution.js +21 -26
  229. package/build/src/library/contract-outcomes.d.ts +2 -14
  230. package/build/src/library/contract-outcomes.js +2 -13
  231. package/build/src/library/contract-types.d.ts +38 -4
  232. package/build/src/library/delivery.d.ts +0 -2
  233. package/build/src/library/delivery.js +2 -5
  234. package/build/src/library/input.d.ts +2 -2
  235. package/build/src/library/input.js +5 -5
  236. package/build/src/library/invocation.js +2 -2
  237. package/build/src/library/keiyaku.d.ts +0 -5
  238. package/build/src/library/keiyaku.js +6 -12
  239. package/build/src/library/nuke.js +3 -3
  240. package/build/src/library/outcome.d.ts +626 -625
  241. package/build/src/library/outcome.js +13 -3
  242. package/build/src/library/reconcile.js +8 -16
  243. package/build/src/library/refusal.d.ts +2 -2
  244. package/build/src/library/repo.d.ts +3 -2
  245. package/build/src/library/repo.js +10 -5
  246. package/build/src/library/selection.d.ts +5 -8
  247. package/build/src/library/selection.js +22 -73
  248. package/build/src/plugin/akuma-signals.d.ts +2 -0
  249. package/build/src/plugin/akuma-signals.js +4 -0
  250. package/build/src/plugin/public.d.ts +32 -2
  251. package/build/src/plugin/runtime.js +5 -3
  252. package/build/src/protocol/amend.js +3 -2
  253. package/build/src/protocol/attempt.js +3 -8
  254. package/build/src/protocol/bind.d.ts +2 -2
  255. package/build/src/protocol/bind.js +1 -1
  256. package/build/src/protocol/deliver.js +3 -3
  257. package/build/src/protocol/operations.d.ts +1 -3
  258. package/build/src/protocol/operations.js +0 -8
  259. package/build/src/protocol/outcome.d.ts +1 -2
  260. package/build/src/protocol/outcome.js +1 -13
  261. package/build/src/protocol/placement.js +3 -10
  262. package/build/src/protocol/read/status.js +31 -70
  263. package/build/src/protocol/reintegrate.js +3 -10
  264. package/build/src/protocol/review.js +3 -3
  265. package/build/src/protocol/run.d.ts +5 -1
  266. package/build/src/protocol/run.js +3 -3
  267. package/build/src/runtime/proc/windows-launch.exe +0 -0
  268. package/build/src/settlement/fence.d.ts +3 -3
  269. package/build/src/settlement/fence.js +4 -4
  270. package/build/src/settlement/holder.d.ts +16 -16
  271. package/build/src/settlement/holder.js +69 -63
  272. package/build/src/settlement/settle.d.ts +5 -5
  273. package/build/src/settlement/settle.js +53 -53
  274. package/package.json +19 -9
  275. package/plugins/paseo/plugin/index.server.ts +17 -0
  276. package/plugins/paseo/plugin/paseo-plugin.json +4 -0
  277. package/plugins/paseo/plugin/server/attach.ts +52 -0
  278. package/plugins/paseo/plugin/server/keiyaku.ts +44 -0
  279. package/plugins/paseo/plugin/server/message.ts +60 -0
  280. package/plugins/paseo/plugin/server/mirror.ts +193 -0
  281. package/plugins/paseo/plugin/server/model.ts +14 -0
  282. package/plugins/paseo/plugin/server/provider.ts +276 -0
  283. package/plugins/paseo/plugin/server/sdk-worker.ts +237 -0
  284. package/plugins/paseo/plugin/server/sdk.ts +289 -0
  285. package/plugins/paseo/plugin/server/stop.ts +26 -0
  286. package/plugins/paseo/plugin/server/transport.ts +35 -0
  287. package/plugins/paseo/plugin/shared/attach-rpc.ts +10 -0
  288. package/plugins/paseo/plugin/shared/refresh-rpc.ts +6 -0
  289. package/plugins/paseo/plugin/tsconfig.json +6 -0
  290. package/plugins/pi/keiyaku.ts +57 -0
  291. package/skills/keiyaku/SKILL.md +43 -0
  292. package/skills/keiyaku-akuma/SKILL.md +130 -0
  293. package/skills/keiyaku-akuma/references/automation.md +269 -0
  294. package/skills/keiyaku-babysit/SKILL.md +65 -0
  295. package/skills/keiyaku-bind/SKILL.md +216 -0
  296. package/skills/keiyaku-bind/agents/openai.yaml +4 -0
  297. package/skills/keiyaku-debt/SKILL.md +91 -0
  298. package/skills/keiyaku-workflow/SKILL.md +129 -0
  299. package/build/plugins/claude/skills/keiyaku-task/SKILL.md +0 -104
  300. package/build/plugins/codex/skills/keiyaku-task/SKILL.md +0 -104
  301. package/build/plugins/opencode/skills/keiyaku-task/SKILL.md +0 -104
  302. package/build/skills/keiyaku-task/SKILL.md +0 -104
  303. package/build/src/cli/commands/amend.d.ts +0 -12
  304. package/build/src/cli/commands/amend.js +0 -10
  305. package/build/src/cli/commands/bind.d.ts +0 -11
  306. package/build/src/cli/commands/bind.js +0 -23
  307. package/build/src/cli/commands/task-grammar.d.ts +0 -19
  308. package/build/src/cli/commands/task-grammar.js +0 -384
  309. package/build/src/cli/commands/task-query.d.ts +0 -5
  310. package/build/src/cli/render/task.d.ts +0 -41
  311. package/build/src/dispatch/association.d.ts +0 -1
  312. package/build/src/dispatch/association.js +0 -1
  313. package/build/src/task/board.d.ts +0 -168
  314. package/build/src/task/catalog.d.ts +0 -11
  315. package/build/src/task/catalog.js +0 -59
  316. package/build/src/task/compose-language.d.ts +0 -57
  317. package/build/src/task/compose-language.js +0 -364
  318. package/build/src/task/compose-parser.d.ts +0 -32
  319. package/build/src/task/compose-parser.js +0 -180
  320. package/build/src/task/compose.d.ts +0 -16
  321. package/build/src/task/compose.js +0 -189
  322. package/build/src/task/document.d.ts +0 -39
  323. package/build/src/task/identity.d.ts +0 -21
  324. package/build/src/task/index.d.ts +0 -138
  325. package/build/src/task/index.js +0 -269
  326. package/build/src/task/lifecycle-operations.d.ts +0 -6
  327. package/build/src/task/mutation-result.d.ts +0 -1726
  328. package/build/src/task/mutation-result.js +0 -198
  329. package/build/src/task/mutation.d.ts +0 -590
  330. package/build/src/task/mutation.js +0 -262
  331. package/build/src/task/operation-types.d.ts +0 -71
  332. package/build/src/task/operations.d.ts +0 -28
  333. package/build/src/task/query.d.ts +0 -76
  334. package/build/src/task/view.d.ts +0 -4
  335. package/build/src/task/view.js +0 -7
  336. /package/build/src/{task → debt}/operation-types.js +0 -0
@@ -0,0 +1,4 @@
1
+ {
2
+ "id": "keiyaku-paseo",
3
+ "requirements": { "paseo": ">=0.11.2" }
4
+ }
@@ -0,0 +1,52 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import type { PluginHandlerContext } from "@getpaseo/plugin/server";
3
+ import type { z } from "zod";
4
+ import type { attachRpc } from "../shared/attach-rpc.js";
5
+
6
+ function skip(reason: string) {
7
+ console.warn(`Keiyaku mirror not attached: ${reason}`);
8
+ return { attached: false };
9
+ }
10
+ export async function attach(input: z.output<typeof attachRpc.input>, { paseo }: PluginHandlerContext,
11
+ sync: (input: { world: string; akuId: string }) => Promise<void>) {
12
+ const caller = input.caller;
13
+ if (caller?.kind !== "external") return skip("caller is not an external host");
14
+ const native = ["codex", "pi", "claude"].includes(caller.integration);
15
+ let parentId = caller.id;
16
+ const matches = (agent: { persistence?: { provider: string; sessionId: string } | null }) =>
17
+ agent.persistence?.provider === caller.integration && agent.persistence.sessionId === caller.id;
18
+ if (native) {
19
+ const candidates: string[] = [];
20
+ let cursor: string | undefined;
21
+ do {
22
+ const filter = { includeArchived: true };
23
+ const page = await paseo.agents.list({ filter, page: { limit: 200, ...(cursor ? { cursor } : {}) } });
24
+ for (const { agent } of page.entries) if (matches(agent)) candidates.push(agent.id);
25
+ if (candidates.length > 1) return skip(`ambiguous ${caller.integration} session ${caller.id}: multiple parents`);
26
+ if (!page.pageInfo.hasMore) break;
27
+ const next = page.pageInfo.nextCursor;
28
+ if (!next || next === cursor) return skip("incomplete parent directory pagination");
29
+ cursor = next;
30
+ } while (cursor);
31
+ if (!candidates[0]) return skip(`no parent for ${caller.integration} session ${caller.id}`);
32
+ parentId = candidates[0];
33
+ } else if (caller.integration !== "paseo") {
34
+ return skip(`unsupported caller integration ${caller.integration}`);
35
+ }
36
+ const parent = await paseo.agents.ref(parentId).refresh();
37
+ if (!parent) return skip(`parent ${parentId} does not exist`);
38
+ if (parent.agent.archivedAt) return skip(`parent ${parentId} is archived`);
39
+ if (native && !matches(parent.agent)) return skip(`parent ${parentId} changed native identity`);
40
+ const world = await realpath(input.world);
41
+ // Keep the creation fingerprint immutable: aliases belong to the forwarder's
42
+ // rename path, not this idempotent request (including retries after rename).
43
+ await paseo.agents.create({ parent: parentId, cwd: parent.agent.cwd, title: input.akuId,
44
+ config: { provider: "keiyaku/mirror", options: { world, akuId: input.akuId } },
45
+ idempotencyKey: `keiyaku:${world}:${input.akuId}`,
46
+ });
47
+ // Creation returns only after the host has registered and subscribed to the session.
48
+ // Opening's ready event is too early to restore running presentation there.
49
+ try { await sync({ world, akuId: input.akuId }); }
50
+ catch (error) { console.warn(`Keiyaku mirror attached, but synchronization failed: ${error instanceof Error ? error.message : String(error)}`); }
51
+ return { attached: true };
52
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+
3
+ export const identityKeys = [
4
+ "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION", "CLAUDECODE", "CODEX_THREAD_ID",
5
+ "OPENCODE_SESSION_ID", "PI_SESSION_ID", "PI_SESSION_FILE", "PASEO_AGENT_ID",
6
+ "SQUARE_PARTICIPANT_NAME", "AKUMA_REQUESTS", "KEIYAKU_ACTOR_ID", "KEIYAKU_EXTERNAL_CALLER",
7
+ ] as const;
8
+ const count = z.number().int().nonnegative();
9
+ const base = z.object({ sequence: count, at: z.string() });
10
+ const turnFields = { turnSequence: count };
11
+ const input = z.object({ json: z.string(), truncated: z.boolean() });
12
+ const call = z.discriminatedUnion("kind", [
13
+ z.object({ kind: z.literal("run"), command: z.string() }),
14
+ z.object({ kind: z.literal("read"), path: z.string(), offset: count.optional(), limit: count.optional() }),
15
+ z.object({ kind: z.literal("search"), query: z.string(), scope: z.enum(["content", "files", "web"]).optional(),
16
+ path: z.string().optional(), glob: z.string().optional() }),
17
+ z.object({ kind: z.literal("fileChange"), changes: z.array(z.object({
18
+ op: z.enum(["add", "update", "delete", "unspecified"]), path: z.string(),
19
+ diffstat: z.object({ added: count, removed: count }).optional(), patch: z.string().optional(),
20
+ })) }),
21
+ z.object({ kind: z.literal("other"), display: z.string(), input: input.optional() }),
22
+ ]);
23
+ const row = z.union([
24
+ base.extend({ kind: z.literal("turn"), ...turnFields, bodySequence: count.optional() }),
25
+ base.extend({ kind: z.literal("tell"), tellId: z.string(), text: z.string(), state: z.enum(["pending", "told"]),
26
+ deliveries: z.array(z.object({ deliveredAt: z.string(), route: z.enum(["launch", "live"]),
27
+ turnSequence: count, receipt: z.enum(["unavailable", "required"]).optional() })).optional() }),
28
+ base.extend({ kind: z.literal("outcome"), ...turnFields, outcome: z.discriminatedUnion("kind", [
29
+ z.object({ kind: z.literal("answered"), historyId: z.string().optional(), answer: z.string(),
30
+ foldedSaidSequence: count.optional() }),
31
+ z.object({ kind: z.literal("failed"), historyId: z.string().optional(), diagnostic: z.string() }),
32
+ ]) }),
33
+ base.extend({ kind: z.literal("tool"), ...turnFields, name: z.string(), toolId: z.string().optional(), call,
34
+ state: z.union([z.enum(["active", "unsettled"]), z.object({ status: z.enum(["ok", "error"]),
35
+ message: z.string().optional(), exitCode: z.number().int().optional(), output: z.string().optional() })]),
36
+ durationMs: z.number().nonnegative().optional(), truncated: z.literal(true).optional() }),
37
+ base.extend({ kind: z.enum(["said", "thought", "note", "call"]), ...turnFields, text: z.string(),
38
+ truncated: z.literal(true).optional() }),
39
+ ]);
40
+ export const historySchema = z.object({ rows: z.array(row), omitted: count,
41
+ hasEarlier: z.boolean(), hasLater: z.boolean(), historyLost: z.boolean(),
42
+ lowestRetained: count.nullable(), highest: count.nullable() });
43
+ export type Row = z.infer<typeof row>;
44
+ export type History = z.infer<typeof historySchema>;
@@ -0,0 +1,60 @@
1
+ import type { ProviderEvent, ProviderPrompt } from "@getpaseo/plugin/server/provider";
2
+ import { z } from "zod";
3
+
4
+ const sequence = z.number().int().nonnegative();
5
+ export const tellReceiptSchema = z.object({
6
+ akuma: z.string(),
7
+ tell: z.object({
8
+ admission: z.object({ fact: z.literal("recorded"), tellId: z.string().min(1) }),
9
+ row: z.object({ kind: z.literal("tell"), sequence, at: z.string(), tellId: z.string().min(1), text: z.string(),
10
+ state: z.enum(["pending", "told"]), deliveries: z.array(z.object({
11
+ route: z.enum(["live", "launch"]), turnSequence: sequence, deliveredAt: z.string(),
12
+ receipt: z.enum(["unavailable", "required"]).optional(),
13
+ })).optional() }),
14
+ wake: z.discriminatedUnion("kind", [
15
+ z.object({ kind: z.literal("told") }), z.object({ kind: z.literal("held") }),
16
+ z.object({ kind: z.literal("pursuing"), bodySequence: sequence }),
17
+ z.object({ kind: z.literal("failed"), diagnostic: z.string() }),
18
+ ]),
19
+ }),
20
+ });
21
+ /** One durable Tell admission and the wake it observed, however that wake settled. */
22
+ export type TellAdmission = z.infer<typeof tellReceiptSchema>["tell"];
23
+ export type AdmittedMessage = TellAdmission["row"];
24
+ /** Submit one plain-text Tell and return its admission receipt. */
25
+ export type Tell = (world: string, akuId: string, signal: AbortSignal, text: string) => Promise<TellAdmission>;
26
+ type PromptResult = Extract<ProviderEvent, { type: "session.prompt_result" }>["result"];
27
+
28
+ export function messageText(prompt: ProviderPrompt): string {
29
+ // Paseo requests stale permission cleanup on ordinary steering. This mirror
30
+ // owns no permission requests, so there is nothing to clear.
31
+ if (prompt.outputSchema !== undefined) {
32
+ throw new Error("Keiyaku messages do not support output schemas");
33
+ }
34
+ if (prompt.input.type !== "message") {
35
+ throw new Error("Keiyaku messages support plain text only, not commands");
36
+ }
37
+ const parts: string[] = [];
38
+ for (const part of prompt.input.content) {
39
+ if (part.type !== "text" || "mimeType" in part) throw new Error("Keiyaku messages support plain text only, not attachments");
40
+ parts.push(part.text);
41
+ }
42
+ const text = parts.join("\n");
43
+ if (!text.trim()) throw new Error("Keiyaku messages require nonblank text");
44
+ return text;
45
+ }
46
+
47
+ /**
48
+ * Map one admitted Tell to its Paseo presentation. Paseo steering acknowledges
49
+ * durable input acceptance, never native delivery: the observed Turn is a host
50
+ * presentation correlation, never a Heart binding.
51
+ */
52
+ export function tellPresentation(admitted: TellAdmission, steerTurnId?: string): { row: AdmittedMessage; result: PromptResult } {
53
+ const { row, wake } = admitted;
54
+ if (wake.kind === "failed") console.warn(`Keiyaku Tell ${row.tellId} recorded; wake failed: ${wake.diagnostic}`);
55
+ if (steerTurnId !== undefined) return { row, result: { type: "steer", turnId: steerTurnId } };
56
+ const delivery = row.deliveries?.at(-1);
57
+ if (delivery) return { row, result: { type: "turn", turnId: `turn/${delivery.turnSequence}` } };
58
+ // The host may complete its send command; no Akuma outcome is manufactured.
59
+ return { row, result: { type: "completed" } };
60
+ }
@@ -0,0 +1,193 @@
1
+ import { z } from "zod";
2
+ import type { ProviderEvent, ProviderTimelineItem } from "@getpaseo/plugin/server/provider";
3
+ import type { History, Row } from "./keiyaku.js";
4
+ import type { AdmittedMessage } from "./message.js";
5
+
6
+ type Emit = (event: ProviderEvent) => void;
7
+ type Turn = { sequence: number; closed: boolean };
8
+ export function createMirror(sessionId: string, emit: Emit) {
9
+ let covered: { high: number; signatures: Map<number, string> } | undefined;
10
+ let turn: Turn | undefined;
11
+ const openTurns = new Map<number, Turn>();
12
+ let unhealthy = false;
13
+ let coverageNotice = "";
14
+ let submissions = 0;
15
+ let deferred: History | undefined;
16
+ let deferredReannounce = false;
17
+ const displayedTells = new Map<string, number>();
18
+ const item = (value: ProviderTimelineItem, timestamp?: string) => emit({
19
+ type: "timeline.item", sessionId, item: value, ...(timestamp === undefined ? {} : { timestamp }),
20
+ });
21
+ const finish = (target: Turn | undefined, state: "completed" | "failed" | "canceled", message?: string) => {
22
+ if (!target || target.closed) return;
23
+ target.closed = true;
24
+ openTurns.delete(target.sequence);
25
+ emit({ type: "session.turn", sessionId, turnId: `turn/${target.sequence}`, state,
26
+ ...(message === undefined ? {} : { error: { message } }) });
27
+ };
28
+ const userMessage = (row: Extract<Row, { kind: "tell" }>, clientMessageId?: string) => {
29
+ if (displayedTells.has(row.tellId)) return;
30
+ displayedTells.set(row.tellId, row.sequence);
31
+ item({ type: "user_message", id: `keiyaku:tell/${row.tellId}`, text: row.text,
32
+ ...(clientMessageId === undefined ? {} : { clientMessageId }) }, row.at);
33
+ };
34
+ const mirror = {
35
+ seed(history: History) {
36
+ const { rows } = history;
37
+ covered = { high: history.highest ?? 0,
38
+ signatures: new Map(rows.map((row) => [row.sequence, JSON.stringify(row)])) };
39
+ for (const row of rows) if (row.kind === "tell") displayedTells.set(row.tellId, row.sequence);
40
+ const latest = rows.filter((row) => row.kind === "turn").sort((a, b) => a.sequence - b.sequence).at(-1);
41
+ if (latest?.kind === "turn") {
42
+ turn = { sequence: latest.turnSequence,
43
+ closed: rows.some((row) => row.kind === "outcome" && row.turnSequence === latest.turnSequence) };
44
+ if (!turn.closed) {
45
+ openTurns.set(turn.sequence, turn);
46
+ emit({ type: "session.turn", sessionId, turnId: `turn/${turn.sequence}`, state: "started" });
47
+ }
48
+ }
49
+ },
50
+ hold() {
51
+ submissions++;
52
+ return (publish = true) => {
53
+ submissions--;
54
+ if (submissions === 0) {
55
+ const rows = deferred;
56
+ const reannounce = deferredReannounce;
57
+ deferred = undefined;
58
+ deferredReannounce = false;
59
+ if (rows && publish) mirror.publish(rows, reannounce);
60
+ }
61
+ };
62
+ },
63
+ currentTurnId: () => turn && !turn.closed ? `turn/${turn.sequence}` : undefined,
64
+ refreshedTurnId() {
65
+ const latest = deferred?.rows.filter((row) => row.kind === "turn").at(-1);
66
+ if (latest?.kind !== "turn") return mirror.currentTurnId();
67
+ if (deferred?.rows.some((row) => row.kind === "outcome" && row.turnSequence === latest.turnSequence)) return undefined;
68
+ if (turn?.sequence === latest.turnSequence && turn.closed) return undefined;
69
+ return `turn/${latest.turnSequence}`;
70
+ },
71
+ admit(row: AdmittedMessage, clientMessageId: string) {
72
+ userMessage(row, clientMessageId);
73
+ },
74
+ cancel(reason: string) {
75
+ for (const target of openTurns.values()) finish(target, "canceled", reason);
76
+ },
77
+ observedTurn(): Readonly<{ cancel(reason: string): void }> | undefined {
78
+ if (!turn || turn.closed) return undefined;
79
+ const observed = turn;
80
+ return { cancel(reason: string) {
81
+ if (deferred?.rows.some((row) => row.kind === "outcome" && row.turnSequence === observed.sequence)) return;
82
+ finish(observed, "canceled", reason);
83
+ } };
84
+ },
85
+ unreadable() {
86
+ deferredReannounce = false;
87
+ if (!unhealthy) item({ type: "notification", level: "warning",
88
+ id: `keiyaku:unreadable/${covered ? Math.min(...covered.signatures.keys()) : 0}`,
89
+ message: "Keiyaku read surface unavailable; retaining the previous mirror." });
90
+ unhealthy = true;
91
+ },
92
+ publish(input: History, reannounce = false) {
93
+ // Hold racing reads until native Tell identity can replace the optimistic item.
94
+ if (submissions > 0) { deferred = input; deferredReannounce ||= reannounce; return; }
95
+ const rows = [...input.rows].sort((a, b) => a.sequence - b.sequence);
96
+ const lowest = rows[0]?.sequence;
97
+ // Public history metadata, not projected sequence holes, owns coverage.
98
+ // Neither missing retention nor a bounded read is an execution outcome.
99
+ const uncoveredEarlier = input.hasEarlier && (!covered || lowest === undefined || lowest > covered.high);
100
+ const notice = input.historyLost ? "retention" : uncoveredEarlier ? "bounded" : "";
101
+ const noticeKey = notice ? `${notice}/${input.lowestRetained ?? "empty"}` : "";
102
+ if (noticeKey && noticeKey !== coverageNotice) {
103
+ item({ type: "notification", level: "warning", id: `keiyaku:window/${lowest ?? "empty"}`,
104
+ message: input.historyLost
105
+ ? "Keiyaku reports history outside retention; retained records remain available. Execution state is unchanged."
106
+ : "Earlier retained history is outside this bounded mirror read. Execution state is unchanged." });
107
+ }
108
+ coverageNotice = noticeKey;
109
+ for (const row of rows) {
110
+ if (covered?.signatures.get(row.sequence) === JSON.stringify(row)) continue;
111
+ if (row.kind === "tell") {
112
+ userMessage(row);
113
+ } else if (row.kind === "call") {
114
+ item({ type: "user_message", id: `keiyaku:call/${row.sequence}`, text: row.text }, row.at);
115
+ } else if (row.kind === "turn") {
116
+ // Publish retained starts even when their outcomes arrive in this read.
117
+ // Older starts must not reopen a sealed or superseded Turn.
118
+ if (!turn || row.turnSequence > turn.sequence) {
119
+ turn = { sequence: row.turnSequence, closed: false };
120
+ openTurns.set(turn.sequence, turn);
121
+ emit({ type: "session.turn", sessionId, turnId: `turn/${turn.sequence}`, state: "started" });
122
+ }
123
+ } else if (row.kind === "tool") {
124
+ // Tool identity comes from the producer, scoped to its Turn. An older
125
+ // CLI without it can show settled records, never guessed live pairing.
126
+ const state = row.state;
127
+ const toolIdentity = row.toolId === undefined ? `record/${row.sequence}` : `${row.turnSequence}/${row.toolId}`;
128
+ if (typeof state === "string" && row.toolId === undefined) {
129
+ item({ type: "notification", id: `keiyaku:tool/${row.sequence}`, level: "info",
130
+ message: `${row.name}: ${state === "active" ? "start observed" : "result not witnessed"}. ${JSON.stringify(row.call)}` }, row.at);
131
+ continue;
132
+ }
133
+ const result = typeof state === "object" ? state : undefined;
134
+ // The result's own output is the observed text; its diagnostic message is
135
+ // only the stand-in an older producer wrote instead, so an honestly empty
136
+ // output is never replaced by its diagnostic.
137
+ const output = result === undefined ? undefined : result.output === undefined ? result.message : result.output;
138
+ const single = row.call.kind === "fileChange" && row.call.changes.length === 1 ? row.call.changes[0] : undefined;
139
+ const detail: Extract<ProviderTimelineItem, { type: "tool_call" }>["detail"] = row.call.kind === "run"
140
+ ? { type: "shell", command: row.call.command,
141
+ ...(output === undefined ? {} : { output }),
142
+ ...(result?.exitCode === undefined ? {} : { exitCode: result.exitCode }) }
143
+ : row.call.kind === "read"
144
+ ? { type: "read", filePath: row.call.path,
145
+ ...(row.call.offset === undefined ? {} : { offset: row.call.offset }),
146
+ ...(row.call.limit === undefined ? {} : { limit: row.call.limit }),
147
+ ...(output === undefined ? {} : { content: output }) }
148
+ : row.call.kind === "search"
149
+ ? { type: "search", query: row.call.query,
150
+ ...(output === undefined ? {} : { content: output }),
151
+ ...(row.durationMs === undefined ? {} : { durationMs: row.durationMs }),
152
+ ...(row.truncated === undefined ? {} : { truncated: row.truncated }) }
153
+ // One observed file patch is the whole operation; anything else keeps
154
+ // the complete change list so no single file stands in for the rest.
155
+ : row.call.kind === "fileChange" && single?.patch !== undefined
156
+ ? { type: "edit", filePath: single.path, unifiedDiff: single.patch }
157
+ : { type: "unknown", input: z.json().parse(row.call), output: result === undefined ? null : z.json().parse(result) };
158
+ item({ type: "tool_call", id: `keiyaku:tool/${toolIdentity}`, callId: `tool/${toolIdentity}`, name: row.name,
159
+ detail, metadata: { turnSequence: row.turnSequence, sequence: row.sequence,
160
+ ...(row.durationMs === undefined ? {} : { durationMs: row.durationMs }),
161
+ ...(row.truncated === undefined ? {} : { truncated: row.truncated }),
162
+ call: z.json().parse(row.call), state: z.json().parse(state) },
163
+ ...(typeof state === "object" && state.status === "error"
164
+ ? { status: "failed", error: z.json().parse(state) }
165
+ : { status: state === "active" ? "running" : state === "unsettled" ? "canceled" : "completed", error: null }) }, row.at);
166
+ } else if (row.kind === "said") {
167
+ item({ type: "assistant_message", id: `keiyaku:said/${row.sequence}`, text: row.text }, row.at);
168
+ } else if (row.kind === "thought") {
169
+ item({ type: "reasoning", id: `keiyaku:thought/${row.sequence}`, text: row.text }, row.at);
170
+ } else if (row.kind === "note") {
171
+ item({ type: "notification", id: `keiyaku:note/${row.sequence}`, level: "info", message: row.text }, row.at);
172
+ } else if (row.kind === "outcome") {
173
+ item(row.outcome.kind === "answered"
174
+ ? { type: "assistant_message", id: row.outcome.foldedSaidSequence === undefined
175
+ ? `keiyaku:turn/${row.turnSequence}` : `keiyaku:said/${row.outcome.foldedSaidSequence}`, text: row.outcome.answer }
176
+ : { type: "error", id: `keiyaku:turn/${row.turnSequence}`, message: row.outcome.diagnostic }, row.at);
177
+ finish(openTurns.get(row.turnSequence), row.outcome.kind === "answered" ? "completed" : "failed",
178
+ row.outcome.kind === "failed" ? row.outcome.diagnostic : undefined);
179
+ }
180
+ }
181
+ covered = { high: Math.max(covered?.high ?? 0, input.highest ?? 0),
182
+ signatures: new Map(rows.map((row) => [row.sequence, JSON.stringify(row)])) };
183
+ if (lowest !== undefined) for (const [id, sequence] of displayedTells) {
184
+ if (sequence < lowest) displayedTells.delete(id);
185
+ }
186
+ unhealthy = false;
187
+ const current = turn;
188
+ if (reannounce && current && !current.closed && rows.some((row) => row.kind === "turn" && row.turnSequence === current.sequence))
189
+ emit({ type: "session.turn", sessionId, turnId: `turn/${current.sequence}`, state: "started" });
190
+ },
191
+ };
192
+ return mirror;
193
+ }
@@ -0,0 +1,14 @@
1
+ import type { ProviderConfigState } from "@getpaseo/plugin/server/provider";
2
+
3
+ /** A mirror reports the public Akuma configuration; it cannot change it. */
4
+ export function modelConfig(status: { provider?: string; model?: string }): ProviderConfigState {
5
+ return {
6
+ ...(status.model === undefined ? {} : { model: status.model }),
7
+ models: status.model === undefined ? [] : [{
8
+ id: status.model,
9
+ label: status.provider ? `${status.provider} / ${status.model}` : status.model,
10
+ isSelectable: false,
11
+ }],
12
+ modes: [], thinkingOptions: [], settings: [],
13
+ };
14
+ }
@@ -0,0 +1,276 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { z } from "zod";
3
+ import { negotiateProviderCapabilities, type ProviderConnection, type ProviderEvent, type ProviderPrompt, type ProviderRegistration } from "@getpaseo/plugin/server/provider";
4
+ import type { History } from "./keiyaku.js";
5
+ import type { AkumaTransport } from "./transport.js";
6
+ import { messageText, tellPresentation } from "./message.js";
7
+ import { createMirror } from "./mirror.js";
8
+ import { modelConfig } from "./model.js";
9
+ import type { StopAttempt } from "./stop.js";
10
+
11
+ const identity = z.object({ world: z.string().min(1), akuId: z.string().regex(/^aku\//u) });
12
+ export function createProvider(transport: AkumaTransport) {
13
+ const sessions = new Set<{ world: string; akuId: string; observe(reannounce?: boolean): Promise<void>; stop(): Promise<void> }>();
14
+ const provider: ProviderRegistration = {
15
+ id: "keiyaku", label: "Keiyaku", description: "Akuma runtime mirror",
16
+ async connect(request): Promise<ProviderConnection> {
17
+ if (!request.versions.includes(1)) throw new Error("Provider protocol version 1 is required");
18
+ const capabilities = negotiateProviderCapabilities(request.capabilities, ["session.persistence", "prompt.message", "prompt.steer"]);
19
+ const listeners = new Set<(event: ProviderEvent) => void>();
20
+ const local = new Map<string, { stop(): Promise<void>; stopExecution(requestId: string): Promise<void>; prompt(prompt: ProviderPrompt): Promise<void> }>();
21
+ let closed = false;
22
+ const emit = (event: ProviderEvent) => { if (!closed) for (const listener of listeners) listener(event); };
23
+ return {
24
+ version: 1, capabilities,
25
+ onEvent(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; },
26
+ async send(input) {
27
+ if (closed) throw new Error("Provider connection is closed");
28
+ if (input.type === "catalog") {
29
+ emit({ type: "catalog", requestId: input.requestId, catalog: {
30
+ models: [{ id: "mirror", label: "Akuma mirror" }], defaultModel: "mirror", modes: [],
31
+ } });
32
+ } else if (input.type === "session.open") {
33
+ if (local.has(input.sessionId)) throw new Error("Session already open");
34
+ if (input.persistence && input.persistence.version !== 1) throw new Error("Unsupported persistence version");
35
+ const target = identity.parse(input.persistence?.data ?? input.config.providerOptions);
36
+ const world = await realpath(target.world);
37
+ const mirror = createMirror(input.sessionId, emit);
38
+ const cancellation = new AbortController();
39
+ // The opening window seeds retained state instead of announcing it.
40
+ const skipRetained = input.history === "skip";
41
+ let opening = true;
42
+ let pending = false;
43
+ let pendingReannounce = false;
44
+ let running: Promise<void> | undefined;
45
+ let observed: Readonly<{ cancel(): void; pulling: Promise<void> }> | undefined;
46
+ const submissions = new Set<Promise<void>>();
47
+ let stopping: Promise<StopAttempt> | undefined;
48
+ // Release the live subscription. Its pending frame settles as
49
+ // released and is never published into the next observation.
50
+ const releaseObservation = (): void => {
51
+ const current = observed;
52
+ observed = undefined;
53
+ current?.cancel();
54
+ };
55
+ /**
56
+ * Observe the current window once, then keep pulling its changes.
57
+ * A slow pull only bounds the next frame: no producer queue exists
58
+ * here and no timer retries a failure. A failed or ended
59
+ * subscription keeps the previous display and waits for the next
60
+ * explicit refresh, which opens a fresh observation.
61
+ */
62
+ async function observeWindow(announce: boolean): Promise<void> {
63
+ releaseObservation();
64
+ if (cancellation.signal.aborted) return;
65
+ const controller = new AbortController();
66
+ const relay = () => controller.abort(cancellation.signal.reason);
67
+ cancellation.signal.addEventListener("abort", relay, { once: true });
68
+ const iterator = transport.stream(world, target.akuId, controller.signal)[Symbol.asyncIterator]();
69
+ const entry = { cancel: () => {
70
+ cancellation.signal.removeEventListener("abort", relay);
71
+ controller.abort();
72
+ }, pulling: Promise.resolve() };
73
+ observed = entry;
74
+ const publish = (history: History, reannounce: boolean): void => {
75
+ if (cancellation.signal.aborted || observed !== entry) return;
76
+ if (opening && skipRetained) mirror.seed(history);
77
+ else mirror.publish(history, reannounce);
78
+ };
79
+ try {
80
+ const first = await iterator.next();
81
+ if (first.done) {
82
+ if (observed === entry) mirror.unreadable();
83
+ return;
84
+ }
85
+ publish(first.value, announce);
86
+ } catch {
87
+ if (observed === entry && !cancellation.signal.aborted) mirror.unreadable();
88
+ return;
89
+ } finally {
90
+ opening = false;
91
+ }
92
+ entry.pulling = (async () => {
93
+ for (;;) {
94
+ let frame: IteratorResult<History>;
95
+ try {
96
+ frame = await iterator.next();
97
+ } catch {
98
+ // A released or failed observation never reports itself over
99
+ // the one that replaced it.
100
+ if (observed === entry && !cancellation.signal.aborted) mirror.unreadable();
101
+ return;
102
+ }
103
+ if (observed !== entry) return;
104
+ if (frame.done) {
105
+ mirror.unreadable();
106
+ return;
107
+ }
108
+ publish(frame.value, false);
109
+ }
110
+ })();
111
+ }
112
+ // Every explicit refresh, Tell and Stop synchronization reopens the
113
+ // one observation instead of reading beside it, so a late result can
114
+ // never overwrite a newer snapshot, and hints coalesce into one.
115
+ function observe(reannounce = false): Promise<void> {
116
+ if (cancellation.signal.aborted) return Promise.resolve();
117
+ pending = true;
118
+ pendingReannounce ||= reannounce;
119
+ running ??= (async () => {
120
+ while (pending && !cancellation.signal.aborted) {
121
+ pending = false;
122
+ const announce = pendingReannounce;
123
+ pendingReannounce = false;
124
+ await observeWindow(announce);
125
+ }
126
+ })().finally(() => { running = undefined; });
127
+ return running;
128
+ }
129
+ const session = {
130
+ world, akuId: target.akuId,
131
+ observe,
132
+ async prompt(prompt: ProviderPrompt) {
133
+ const respond = (result: Extract<ProviderEvent, { type: "session.prompt_result" }>["result"]) => emit({
134
+ type: "session.prompt_result", sessionId: input.sessionId, clientMessageId: prompt.clientMessageId, result,
135
+ });
136
+ let text: string;
137
+ try {
138
+ cancellation.signal.throwIfAborted();
139
+ text = messageText(prompt);
140
+ } catch (error) {
141
+ const failure = error instanceof Error ? error : new Error(String(error));
142
+ respond({ type: "failed", error: { message: failure.message } });
143
+ throw failure;
144
+ }
145
+ const steerTurnId = prompt.delivery === "steer" ? mirror.currentTurnId() : undefined;
146
+ if (prompt.delivery === "steer" && steerTurnId === undefined) {
147
+ const failure = new Error("No observed Paseo Turn for steering; Tell was not submitted");
148
+ respond({ type: "failed", error: { message: failure.message } });
149
+ throw failure;
150
+ }
151
+ const release = mirror.hold();
152
+ const submission = (async () => {
153
+ try {
154
+ const admitted = tellPresentation(await transport.tell(world, target.akuId, cancellation.signal, text), steerTurnId);
155
+ // Observe once while publication is held. An idle send may have
156
+ // started real work even when its receipt still says pending.
157
+ await session.observe();
158
+ mirror.admit(admitted.row, prompt.clientMessageId);
159
+ const observed = mirror.refreshedTurnId();
160
+ respond(admitted.result.type === "completed" && observed !== undefined
161
+ ? { type: "turn", turnId: observed } : admitted.result);
162
+ } catch (error) {
163
+ const failure = new Error(`${error instanceof Error ? error.message : String(error)}. Input may already be recorded; not retried.`);
164
+ respond({ type: "failed", error: { message: failure.message } });
165
+ await session.observe();
166
+ // Do not let the host treat an uncertain write as "unavailable"
167
+ // and replace execution before resubmitting the same text.
168
+ throw failure;
169
+ } finally {
170
+ release(!cancellation.signal.aborted);
171
+ }
172
+ })();
173
+ submissions.add(submission);
174
+ try { await submission; } finally { submissions.delete(submission); }
175
+ },
176
+ async stop() {
177
+ mirror.cancel("Observation closed; Akuma was not interrupted");
178
+ cancellation.abort();
179
+ const closing = observed;
180
+ releaseObservation();
181
+ sessions.delete(session);
182
+ local.delete(input.sessionId);
183
+ await Promise.allSettled([...submissions]);
184
+ await stopping;
185
+ await running;
186
+ await closing?.pulling;
187
+ emit({ type: "session.closed", sessionId: input.sessionId });
188
+ },
189
+ stopExecution,
190
+ };
191
+ async function attemptStop(): Promise<StopAttempt> {
192
+ if (cancellation.signal.aborted)
193
+ return { kind: "uncertain", message: "Keiyaku could not prove the stop: the mirror closed before it was attempted" };
194
+ const observed = mirror.observedTurn();
195
+ let attempt: StopAttempt;
196
+ try {
197
+ attempt = await transport.stop(world, target.akuId, cancellation.signal);
198
+ } catch (error) {
199
+ attempt = { kind: "uncertain", message: `Keiyaku could not prove the stop: ${error instanceof Error ? error.message : String(error)}` };
200
+ }
201
+ await session.observe();
202
+ if (attempt.kind === "stopped")
203
+ observed?.cancel("Akuma execution was stopped from Paseo; the Akuma is preserved for a later Tell");
204
+ return attempt;
205
+ }
206
+ async function stopExecution(requestId: string): Promise<void> {
207
+ stopping ??= attemptStop().finally(() => { stopping = undefined; });
208
+ const attempt = await stopping;
209
+ if (attempt.kind === "stopped") emit({ type: "request.completed", requestId });
210
+ else emit({ type: "request.failed", requestId, error: { message: attempt.message } });
211
+ }
212
+ sessions.add(session);
213
+ local.set(input.sessionId, session);
214
+ emit({ type: "session.opened", requestId: input.requestId, sessionId: input.sessionId,
215
+ capabilities, restoration: "core", cwd: input.config.cwd,
216
+ persistence: { version: 1, data: { world, akuId: target.akuId } } });
217
+ {
218
+ try {
219
+ const observed = await transport.status(world, target.akuId, cancellation.signal);
220
+ if (!cancellation.signal.aborted) emit({ type: "session.config", sessionId: input.sessionId, config: modelConfig(observed) });
221
+ } catch (error) {
222
+ if (!cancellation.signal.aborted) emit({ type: "session.notice", sessionId: input.sessionId, notice: {
223
+ id: "keiyaku:model", severity: "warning", title: "Akuma model unavailable",
224
+ description: error instanceof Error ? error.message : String(error),
225
+ } });
226
+ }
227
+ }
228
+ // Ready names the first observed window, or the explicit report that
229
+ // none could be read. It never waits for the observation to end.
230
+ await session.observe();
231
+ if (!cancellation.signal.aborted) emit({ type: "session.ready", requestId: input.requestId, sessionId: input.sessionId });
232
+ } else if (input.type === "session.interrupt" || input.type === "session.close") {
233
+ const session = local.get(input.sessionId);
234
+ if (input.type === "session.close") {
235
+ await session?.stop();
236
+ emit({ type: "request.completed", requestId: input.requestId });
237
+ } else if (session === undefined) {
238
+ emit({ type: "request.failed", requestId: input.requestId,
239
+ error: { message: "Keiyaku mirror session is not open" } });
240
+ } else await session.stopExecution(input.requestId);
241
+ } else if (input.type === "session.prompt") {
242
+ const session = local.get(input.sessionId);
243
+ if (session) await session.prompt(input.prompt);
244
+ else emit({ type: "session.prompt_result", sessionId: input.sessionId, clientMessageId: input.prompt.clientMessageId,
245
+ result: { type: "failed", error: { message: "Keiyaku mirror session is not open" } } });
246
+ } else if ("requestId" in input) {
247
+ emit({ type: "request.failed", requestId: input.requestId, error: { message: "Unsupported Keiyaku mirror operation" } });
248
+ }
249
+ },
250
+ async close() {
251
+ await Promise.all([...local.values()].map((session) => session.stop()));
252
+ closed = true;
253
+ listeners.clear();
254
+ },
255
+ };
256
+ },
257
+ };
258
+ return {
259
+ provider,
260
+ async refresh(input: { world: string; akuId: string }) {
261
+ const world = await realpath(input.world);
262
+ let woken = 0;
263
+ for (const session of sessions) if (session.world === world && session.akuId === input.akuId) {
264
+ void session.observe();
265
+ woken++;
266
+ }
267
+ return { woken };
268
+ },
269
+ async sync(input: { world: string; akuId: string }) {
270
+ const world = await realpath(input.world);
271
+ await Promise.all([...sessions].filter((session) => session.world === world && session.akuId === input.akuId)
272
+ .map((session) => session.observe(true)));
273
+ },
274
+ async close() { await Promise.all([...sessions].map((session) => session.stop())); },
275
+ };
276
+ }