@pi-in-go/pigpen-ahp 0.1.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 (304) hide show
  1. package/CREDITS.md +27 -0
  2. package/LICENSE +23 -0
  3. package/README.md +148 -0
  4. package/extensions/ahp/access_options_test.go +116 -0
  5. package/extensions/ahp/extension.go +54 -0
  6. package/extensions/ahp/go.mod +12 -0
  7. package/extensions/ahp/go.sum +2 -0
  8. package/extensions/ahp/go.work +8 -0
  9. package/extensions/ahp/internal/channels/chat.go +110 -0
  10. package/extensions/ahp/internal/channels/root.go +32 -0
  11. package/extensions/ahp/internal/channels/session.go +128 -0
  12. package/extensions/ahp/internal/channels/session_test.go +108 -0
  13. package/extensions/ahp/internal/compose/compose.go +153 -0
  14. package/extensions/ahp/internal/compose/compose_test.go +120 -0
  15. package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
  16. package/extensions/ahp/internal/host/handshake_test.go +206 -0
  17. package/extensions/ahp/internal/host/host.go +1305 -0
  18. package/extensions/ahp/internal/host/reconnect_test.go +197 -0
  19. package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
  20. package/extensions/ahp/internal/host/schema_test.go +79 -0
  21. package/extensions/ahp/internal/host/store.go +291 -0
  22. package/extensions/ahp/internal/host/store_update_test.go +85 -0
  23. package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
  24. package/extensions/ahp/internal/host/surface_test.go +95 -0
  25. package/extensions/ahp/internal/host/workarounds.go +458 -0
  26. package/extensions/ahp/internal/host/workarounds_test.go +96 -0
  27. package/extensions/ahp/internal/live/live.go +328 -0
  28. package/extensions/ahp/internal/live/live_test.go +91 -0
  29. package/extensions/ahp/internal/live/store.go +113 -0
  30. package/extensions/ahp/internal/mapper/activity.go +199 -0
  31. package/extensions/ahp/internal/mapper/activity_test.go +188 -0
  32. package/extensions/ahp/internal/mapper/bench_test.go +38 -0
  33. package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
  34. package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
  35. package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
  36. package/extensions/ahp/internal/mapper/js.go +131 -0
  37. package/extensions/ahp/internal/mapper/mapper.go +600 -0
  38. package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
  39. package/extensions/ahp/internal/mapper/messageinput.go +272 -0
  40. package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
  41. package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
  42. package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
  43. package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
  44. package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
  45. package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
  46. package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
  47. package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
  48. package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
  49. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
  50. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
  51. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
  52. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
  53. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
  54. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
  55. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
  56. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
  57. package/extensions/ahp/internal/mapper/title.go +34 -0
  58. package/extensions/ahp/internal/mapper/usermsg.go +87 -0
  59. package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
  60. package/extensions/ahp/internal/pi/backend.go +64 -0
  61. package/extensions/ahp/internal/pi/catalogue.go +370 -0
  62. package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
  63. package/extensions/ahp/internal/pi/chatdriver.go +533 -0
  64. package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
  65. package/extensions/ahp/internal/pi/clientactions.go +212 -0
  66. package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
  67. package/extensions/ahp/internal/pi/completions.go +230 -0
  68. package/extensions/ahp/internal/pi/completions_test.go +272 -0
  69. package/extensions/ahp/internal/pi/config_test.go +269 -0
  70. package/extensions/ahp/internal/pi/deletesession.go +69 -0
  71. package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
  72. package/extensions/ahp/internal/pi/disposal_test.go +362 -0
  73. package/extensions/ahp/internal/pi/fixture_test.go +308 -0
  74. package/extensions/ahp/internal/pi/foreign_test.go +122 -0
  75. package/extensions/ahp/internal/pi/harness_test.go +83 -0
  76. package/extensions/ahp/internal/pi/history.go +269 -0
  77. package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
  78. package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
  79. package/extensions/ahp/internal/pi/hydration_test.go +209 -0
  80. package/extensions/ahp/internal/pi/hydrator.go +212 -0
  81. package/extensions/ahp/internal/pi/imageinput.go +224 -0
  82. package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
  83. package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
  84. package/extensions/ahp/internal/pi/models.go +212 -0
  85. package/extensions/ahp/internal/pi/models_test.go +128 -0
  86. package/extensions/ahp/internal/pi/paging.go +95 -0
  87. package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
  88. package/extensions/ahp/internal/pi/projecttrust.go +222 -0
  89. package/extensions/ahp/internal/pi/registry.go +924 -0
  90. package/extensions/ahp/internal/pi/restart_test.go +185 -0
  91. package/extensions/ahp/internal/pi/services.go +70 -0
  92. package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
  93. package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
  94. package/extensions/ahp/internal/pi/sessionstore.go +83 -0
  95. package/extensions/ahp/internal/pi/summary_test.go +426 -0
  96. package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
  97. package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
  98. package/extensions/ahp/internal/pisession/json.go +31 -0
  99. package/extensions/ahp/internal/pisession/pisession.go +580 -0
  100. package/extensions/ahp/internal/settings/access.go +63 -0
  101. package/extensions/ahp/internal/settings/access_test.go +52 -0
  102. package/extensions/ahp/internal/settings/settings.go +117 -0
  103. package/extensions/ahp/internal/settings/settings_test.go +104 -0
  104. package/extensions/ahp/internal/svc/etag_other.go +15 -0
  105. package/extensions/ahp/internal/svc/etag_unix.go +34 -0
  106. package/extensions/ahp/internal/svc/glob.go +197 -0
  107. package/extensions/ahp/internal/svc/mime.go +37 -0
  108. package/extensions/ahp/internal/svc/paths.go +108 -0
  109. package/extensions/ahp/internal/svc/pty.go +64 -0
  110. package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
  111. package/extensions/ahp/internal/svc/pty_linux.go +36 -0
  112. package/extensions/ahp/internal/svc/pty_other.go +9 -0
  113. package/extensions/ahp/internal/svc/pty_test.go +114 -0
  114. package/extensions/ahp/internal/svc/pty_unix.go +147 -0
  115. package/extensions/ahp/internal/svc/resource.go +495 -0
  116. package/extensions/ahp/internal/svc/resource_test.go +501 -0
  117. package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
  118. package/extensions/ahp/internal/svc/stat_linux.go +9 -0
  119. package/extensions/ahp/internal/svc/terminal.go +454 -0
  120. package/extensions/ahp/internal/svc/terminal_test.go +512 -0
  121. package/extensions/ahp/internal/svc/watch.go +590 -0
  122. package/extensions/ahp/internal/svc/watch_test.go +627 -0
  123. package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
  124. package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
  125. package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
  126. package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
  127. package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
  128. package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
  129. package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
  130. package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
  131. package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
  132. package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
  133. package/extensions/ahp/internal/testkit/schema.go +360 -0
  134. package/extensions/ahp/internal/testkit/testkit.go +367 -0
  135. package/extensions/ahp/internal/twin/twin.go +27 -0
  136. package/extensions/ahp/internal/wire/dispatchable.go +106 -0
  137. package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
  138. package/extensions/ahp/internal/wire/helpers_test.go +5 -0
  139. package/extensions/ahp/internal/wire/wire.go +218 -0
  140. package/extensions/ahp/internal/wire/wire_test.go +123 -0
  141. package/extensions/ahp/internal/ws/helpers_test.go +7 -0
  142. package/extensions/ahp/internal/ws/vscode_test.go +198 -0
  143. package/extensions/ahp/internal/ws/ws.go +674 -0
  144. package/extensions/ahp/internal/ws/ws_test.go +324 -0
  145. package/extensions/ahp/realpig_test.go +589 -0
  146. package/extensions/ahp/runtime.go +311 -0
  147. package/extensions/ahp/runtime_test.go +110 -0
  148. package/extensions/ahp/testdata/upstream-tests.json +3833 -0
  149. package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
  150. package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
  151. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
  152. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
  153. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
  154. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
  155. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
  156. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
  157. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
  158. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
  159. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
  160. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
  161. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
  162. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
  163. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
  164. package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
  165. package/extensions/ahp/twins_test.go +91 -0
  166. package/package.json +40 -0
  167. package/proof/PORT.md +121 -0
  168. package/proof/mutations.json +152 -0
  169. package/proof/oracle/LICENSE +21 -0
  170. package/proof/oracle/README.md +120 -0
  171. package/proof/oracle/UPSTREAM.md +7 -0
  172. package/proof/oracle/package.json +68 -0
  173. package/proof/oracle/src/bin/cli.ts +66 -0
  174. package/proof/oracle/src/bin/tunnel.ts +128 -0
  175. package/proof/oracle/src/channels/chat.ts +134 -0
  176. package/proof/oracle/src/channels/root.ts +39 -0
  177. package/proof/oracle/src/channels/session.ts +118 -0
  178. package/proof/oracle/src/channels/terminal.ts +11 -0
  179. package/proof/oracle/src/core/channels.ts +108 -0
  180. package/proof/oracle/src/core/client-workarounds.ts +358 -0
  181. package/proof/oracle/src/core/connection.ts +41 -0
  182. package/proof/oracle/src/core/host.ts +882 -0
  183. package/proof/oracle/src/core/sequencer.ts +75 -0
  184. package/proof/oracle/src/core/state-store.ts +162 -0
  185. package/proof/oracle/src/core/uri.ts +25 -0
  186. package/proof/oracle/src/host/direct-settings.ts +110 -0
  187. package/proof/oracle/src/host/pi-host.ts +220 -0
  188. package/proof/oracle/src/host/serve.ts +71 -0
  189. package/proof/oracle/src/host/terminal-service.ts +346 -0
  190. package/proof/oracle/src/pi/activity.ts +171 -0
  191. package/proof/oracle/src/pi/changeset-service.ts +646 -0
  192. package/proof/oracle/src/pi/changeset-uri.ts +64 -0
  193. package/proof/oracle/src/pi/chat-driver.ts +529 -0
  194. package/proof/oracle/src/pi/completions.ts +160 -0
  195. package/proof/oracle/src/pi/delete-session.ts +53 -0
  196. package/proof/oracle/src/pi/event-mapper.ts +648 -0
  197. package/proof/oracle/src/pi/git-changes.ts +605 -0
  198. package/proof/oracle/src/pi/history.ts +305 -0
  199. package/proof/oracle/src/pi/image-input.ts +56 -0
  200. package/proof/oracle/src/pi/image-mime.ts +8 -0
  201. package/proof/oracle/src/pi/in-process-backend.ts +155 -0
  202. package/proof/oracle/src/pi/message-input.ts +212 -0
  203. package/proof/oracle/src/pi/models.ts +124 -0
  204. package/proof/oracle/src/pi/project-trust.ts +66 -0
  205. package/proof/oracle/src/pi/provider.ts +2 -0
  206. package/proof/oracle/src/pi/resource-paths.ts +80 -0
  207. package/proof/oracle/src/pi/resource-service.ts +356 -0
  208. package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
  209. package/proof/oracle/src/pi/resource-watch.ts +357 -0
  210. package/proof/oracle/src/pi/session-catalogue.ts +318 -0
  211. package/proof/oracle/src/pi/session-config.ts +84 -0
  212. package/proof/oracle/src/pi/session-history.ts +75 -0
  213. package/proof/oracle/src/pi/session-hydrator.ts +219 -0
  214. package/proof/oracle/src/pi/session-registry.ts +815 -0
  215. package/proof/oracle/src/pi/session-storage.ts +29 -0
  216. package/proof/oracle/src/pi/session-title.ts +18 -0
  217. package/proof/oracle/src/pi/turn-paging.ts +94 -0
  218. package/proof/oracle/src/pi/user-message.ts +66 -0
  219. package/proof/oracle/src/protocol/errors.ts +44 -0
  220. package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
  221. package/proof/oracle/src/protocol/version.ts +39 -0
  222. package/proof/oracle/src/transport/websocket.ts +136 -0
  223. package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
  224. package/proof/oracle/src/tunnel/discovery.ts +37 -0
  225. package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
  226. package/proof/oracle/test/activity.test.ts +171 -0
  227. package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
  228. package/proof/oracle/test/changeset-uri.test.ts +37 -0
  229. package/proof/oracle/test/changeset.test.serial.ts +487 -0
  230. package/proof/oracle/test/chat-driver.test.ts +691 -0
  231. package/proof/oracle/test/client-actions.test.ts +409 -0
  232. package/proof/oracle/test/client-workarounds.test.ts +320 -0
  233. package/proof/oracle/test/completions.test.ts +323 -0
  234. package/proof/oracle/test/delete-session.test.ts +142 -0
  235. package/proof/oracle/test/direct-settings.test.ts +81 -0
  236. package/proof/oracle/test/event-mapper.test.ts +619 -0
  237. package/proof/oracle/test/fetch-turns.test.ts +207 -0
  238. package/proof/oracle/test/fixtures/abort.json +711 -0
  239. package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
  240. package/proof/oracle/test/fixtures/compaction.json +6285 -0
  241. package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
  242. package/proof/oracle/test/fixtures/plain-text.json +1856 -0
  243. package/proof/oracle/test/fixtures/single-tool.json +1128 -0
  244. package/proof/oracle/test/fixtures/steering.json +3926 -0
  245. package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
  246. package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
  247. package/proof/oracle/test/fixtures/tool-error.json +2850 -0
  248. package/proof/oracle/test/fixtures/tool-find.json +1162 -0
  249. package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
  250. package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
  251. package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
  252. package/proof/oracle/test/fixtures/tool-write.json +1271 -0
  253. package/proof/oracle/test/handshake.test.ts +275 -0
  254. package/proof/oracle/test/harness.ts +153 -0
  255. package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
  256. package/proof/oracle/test/image-input.test.ts +28 -0
  257. package/proof/oracle/test/image-session.test.ts +69 -0
  258. package/proof/oracle/test/live-turn.test.ts +304 -0
  259. package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
  260. package/proof/oracle/test/message-input.test.ts +181 -0
  261. package/proof/oracle/test/model-discovery.test.ts +170 -0
  262. package/proof/oracle/test/models.test.ts +109 -0
  263. package/proof/oracle/test/pi-host.test.ts +91 -0
  264. package/proof/oracle/test/pi-replay.test.ts +141 -0
  265. package/proof/oracle/test/project-trust.test.ts +125 -0
  266. package/proof/oracle/test/protocol-surface.test.ts +121 -0
  267. package/proof/oracle/test/pty.test.ts +100 -0
  268. package/proof/oracle/test/reconnect.test.ts +389 -0
  269. package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
  270. package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
  271. package/proof/oracle/test/resource.test.ts +499 -0
  272. package/proof/oracle/test/schema.test.ts +88 -0
  273. package/proof/oracle/test/session-catalogue.test.ts +314 -0
  274. package/proof/oracle/test/session-config.test.ts +224 -0
  275. package/proof/oracle/test/session-disposal.test.ts +364 -0
  276. package/proof/oracle/test/session-hydration.test.ts +194 -0
  277. package/proof/oracle/test/session-lifecycle.test.ts +416 -0
  278. package/proof/oracle/test/session-storage.test.serial.ts +32 -0
  279. package/proof/oracle/test/session-summary.test.ts +418 -0
  280. package/proof/oracle/test/subscriptions.test.ts +198 -0
  281. package/proof/oracle/test/support/assertions.ts +32 -0
  282. package/proof/oracle/test/support/async.ts +26 -0
  283. package/proof/oracle/test/support/hydrated-session.ts +205 -0
  284. package/proof/oracle/test/support/images.ts +8 -0
  285. package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
  286. package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
  287. package/proof/oracle/test/support/replay.ts +194 -0
  288. package/proof/oracle/test/support/schema.ts +221 -0
  289. package/proof/oracle/test/support/session-files.ts +12 -0
  290. package/proof/oracle/test/support/session-storage.ts +22 -0
  291. package/proof/oracle/test/support/upstream.ts +25 -0
  292. package/proof/oracle/test/support/watch-events.ts +108 -0
  293. package/proof/oracle/test/terminal-service.test.ts +495 -0
  294. package/proof/oracle/test/truncate.test.ts +230 -0
  295. package/proof/oracle/test/tunnel.test.ts +258 -0
  296. package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
  297. package/proof/oracle/test/uri.test.ts +24 -0
  298. package/proof/oracle/test/watch-events.test.ts +118 -0
  299. package/proof/oracle/tsconfig.json +32 -0
  300. package/proof/proof-piglet/piglet.yaml +18 -0
  301. package/proof/tools/fakellm/go.mod +3 -0
  302. package/proof/tools/fakellm/main.go +109 -0
  303. package/proof/tools/gen-dispatchable.py +30 -0
  304. package/provenance.json +28 -0
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Server sequence allocation and the action replay buffer.
3
+ *
4
+ * The host is the sole authority for ordering: every accepted action gets a
5
+ * monotonically increasing `serverSeq` and is retained in a bounded ring
6
+ * buffer so a reconnecting client can replay the gap instead of refetching
7
+ * every snapshot.
8
+ *
9
+ * @see https://microsoft.github.io/agent-host-protocol/specification/lifecycle
10
+ */
11
+
12
+ import type { ActionEnvelope } from "@microsoft/agent-host-protocol";
13
+
14
+ /**
15
+ * Local memory bound for replay. A client whose gap predates the buffer gets
16
+ * fresh snapshots instead, preserving correctness at the cost of more data.
17
+ */
18
+ const DEFAULT_REPLAY_BUFFER_CAPACITY = 1000;
19
+
20
+ export class Sequencer {
21
+ #serverSeq = 0;
22
+ readonly #buffer: ActionEnvelope[] = [];
23
+ readonly #capacity: number;
24
+
25
+ constructor(capacity: number = DEFAULT_REPLAY_BUFFER_CAPACITY) {
26
+ this.#capacity = Math.max(1, capacity);
27
+ }
28
+
29
+ /** The seq of the most recently emitted envelope (0 before anything is emitted). */
30
+ get current(): number {
31
+ return this.#serverSeq;
32
+ }
33
+
34
+ /** Allocates the next seq. Callers must emit the envelope they build with it. */
35
+ next(): number {
36
+ return ++this.#serverSeq;
37
+ }
38
+
39
+ /** Retains an envelope for replay, evicting the oldest when at capacity. */
40
+ retain(envelope: ActionEnvelope): void {
41
+ this.#buffer.push(envelope);
42
+ if (this.#buffer.length > this.#capacity) {
43
+ this.#buffer.shift();
44
+ }
45
+ }
46
+
47
+ /** The oldest seq still replayable, or `undefined` when the buffer is empty. */
48
+ get oldestRetainedSeq(): number | undefined {
49
+ return this.#buffer[0]?.serverSeq;
50
+ }
51
+
52
+ /**
53
+ * Whether the gap after `lastSeenServerSeq` is fully covered by the buffer.
54
+ *
55
+ * A client that is already current (`lastSeenServerSeq >= current`) trivially
56
+ * qualifies. Otherwise the buffer must still hold `lastSeenServerSeq + 1`.
57
+ */
58
+ canReplayFrom(lastSeenServerSeq: number): boolean {
59
+ if (lastSeenServerSeq >= this.#serverSeq) {
60
+ return true;
61
+ }
62
+ const oldest = this.oldestRetainedSeq;
63
+ return oldest !== undefined && oldest <= lastSeenServerSeq + 1;
64
+ }
65
+
66
+ /**
67
+ * Envelopes after `lastSeenServerSeq` whose channel the client subscribes to.
68
+ *
69
+ * Returned in seq order. Only call after {@link canReplayFrom} returns true;
70
+ * otherwise the result silently omits evicted envelopes.
71
+ */
72
+ replayFrom(lastSeenServerSeq: number, channels: ReadonlySet<string>): ActionEnvelope[] {
73
+ return this.#buffer.filter((envelope) => envelope.serverSeq > lastSeenServerSeq && channels.has(envelope.channel));
74
+ }
75
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The authoritative state store.
3
+ *
4
+ * Every state-bearing channel holds an immutable state tree mutated only by
5
+ * actions run through the protocol's pure reducers. Terminal state additionally
6
+ * caps retained output so reconnect snapshots cannot grow without bound.
7
+ *
8
+ * @see https://microsoft.github.io/agent-host-protocol/guide/state-model
9
+ */
10
+
11
+ import {
12
+ ActionType,
13
+ type ChangesetState,
14
+ type ChatState,
15
+ changesetReducer,
16
+ chatReducer,
17
+ type ResourceWatchState,
18
+ type RootState,
19
+ resourceWatchReducer,
20
+ rootReducer,
21
+ type SessionState,
22
+ type Snapshot,
23
+ type StateAction,
24
+ sessionReducer,
25
+ type TerminalAction,
26
+ type TerminalContentPart,
27
+ type TerminalState,
28
+ terminalReducer,
29
+ type URI,
30
+ } from "@microsoft/agent-host-protocol";
31
+ import { type ChannelKind, channelKind } from "./channels.ts";
32
+
33
+ /** Any state tree this host serves. */
34
+ export type ChannelState = RootState | SessionState | ChatState | TerminalState | ChangesetState | ResourceWatchState;
35
+
36
+ /** Maximum output characters retained for snapshots; live clients still receive every action. */
37
+ const MAX_RETAINED_TERMINAL_CHARS = 1_000_000;
38
+
39
+ function terminalOutput(part: TerminalContentPart): string {
40
+ return part.type === "unclassified" ? part.value : part.output;
41
+ }
42
+
43
+ function withTerminalOutput(part: TerminalContentPart, output: string): TerminalContentPart {
44
+ return part.type === "unclassified" ? { ...part, value: output } : { ...part, output };
45
+ }
46
+
47
+ /** Applies the official reducer, then trims only the oldest retained output. */
48
+ function reduceTerminal(state: TerminalState, action: TerminalAction): TerminalState {
49
+ const reduced = terminalReducer(state, action);
50
+ if (action.type !== ActionType.TerminalData) return reduced;
51
+
52
+ let remaining = MAX_RETAINED_TERMINAL_CHARS;
53
+ let trimmed = false;
54
+ const newestFirst: TerminalContentPart[] = [];
55
+
56
+ for (let index = reduced.content.length - 1; index >= 0; index -= 1) {
57
+ const part = reduced.content[index];
58
+ if (!part) continue;
59
+ const output = terminalOutput(part);
60
+ if (output.length <= remaining) {
61
+ newestFirst.push(part);
62
+ remaining -= output.length;
63
+ continue;
64
+ }
65
+ if (remaining > 0) {
66
+ newestFirst.push(withTerminalOutput(part, output.slice(-remaining)));
67
+ }
68
+ trimmed = true;
69
+ break;
70
+ }
71
+
72
+ return trimmed ? { ...reduced, content: newestFirst.reverse() } : reduced;
73
+ }
74
+
75
+ interface ChannelEntry {
76
+ readonly kind: ChannelKind;
77
+ state: ChannelState;
78
+ }
79
+
80
+ /**
81
+ * Applies an action to a channel's state using the reducer for that channel
82
+ * kind. Reducers are total: an action they do not recognise is a no-op, which
83
+ * is how forward compatibility works.
84
+ */
85
+ function reduce(kind: ChannelKind, state: ChannelState, action: StateAction): ChannelState {
86
+ switch (kind) {
87
+ case "root":
88
+ // Channel routing chooses the reducer; generated unions still require
89
+ // narrowing at this boundary.
90
+ return rootReducer(state as RootState, action as never);
91
+ case "session":
92
+ return sessionReducer(state as SessionState, action as never);
93
+ case "chat":
94
+ return chatReducer(state as ChatState, action as never);
95
+ case "terminal":
96
+ return reduceTerminal(state as TerminalState, action as never);
97
+ case "changeset":
98
+ return changesetReducer(state as ChangesetState, action as never);
99
+ case "resourceWatch":
100
+ // Pass-through by design: a watch's state describes what is being
101
+ // watched, and `resourceWatch/changed` carries pure event traffic.
102
+ return resourceWatchReducer(state as ResourceWatchState, action as never);
103
+ default:
104
+ return state;
105
+ }
106
+ }
107
+
108
+ export class StateStore {
109
+ readonly #channels = new Map<URI, ChannelEntry>();
110
+
111
+ /**
112
+ * Registers a channel with its initial state. Replaces any existing entry.
113
+ *
114
+ * `kind` is normally inferred from the scheme. Callers pass it explicitly
115
+ * when the command establishes the kind of a client-chosen URI, such as a
116
+ * provider-aliased session or VS Code terminal.
117
+ */
118
+ create(uri: URI, state: ChannelState, kind: ChannelKind | undefined = channelKind(uri)): void {
119
+ if (kind === undefined) {
120
+ throw new Error(`Cannot create channel with unknown scheme: ${uri}`);
121
+ }
122
+ this.#channels.set(uri, { kind, state });
123
+ }
124
+
125
+ delete(uri: URI): boolean {
126
+ return this.#channels.delete(uri);
127
+ }
128
+
129
+ has(uri: URI): boolean {
130
+ return this.#channels.has(uri);
131
+ }
132
+
133
+ get(uri: URI): ChannelState | undefined {
134
+ return this.#channels.get(uri)?.state;
135
+ }
136
+
137
+ kindOf(uri: URI): ChannelKind | undefined {
138
+ return this.#channels.get(uri)?.kind;
139
+ }
140
+
141
+ /**
142
+ * Reduces an action into a channel.
143
+ *
144
+ * Returns `false` when the channel does not exist — the spec requires the
145
+ * host to silently ignore actions targeting an unknown channel rather than
146
+ * echoing a rejection.
147
+ */
148
+ apply(uri: URI, action: StateAction): boolean {
149
+ const entry = this.#channels.get(uri);
150
+ if (!entry) {
151
+ return false;
152
+ }
153
+ entry.state = reduce(entry.kind, entry.state, action);
154
+ return true;
155
+ }
156
+
157
+ /** Builds a subscribe/initialize snapshot for a channel at the given seq. */
158
+ snapshot(uri: URI, fromSeq: number): Snapshot | undefined {
159
+ const entry = this.#channels.get(uri);
160
+ return entry === undefined ? undefined : { resource: uri, state: entry.state, fromSeq };
161
+ }
162
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * URI helpers shared across channels and backends. Filesystem conversion uses
3
+ * `node:url` consistently so platform and remote-authority semantics cannot
4
+ * diverge between commands.
5
+ */
6
+
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ /**
10
+ * Converts a `file:` URI to a filesystem path.
11
+ *
12
+ * A value that is not a `file:` URI is returned unchanged: clients occasionally
13
+ * send a bare path where the protocol asks for a URI, and treating that as a
14
+ * path is friendlier than failing.
15
+ *
16
+ * @throws {TypeError} when the URI is malformed or names a remote host.
17
+ */
18
+ export function fileUriToPath(uri: string): string {
19
+ return uri.startsWith("file://") ? fileURLToPath(uri) : uri;
20
+ }
21
+
22
+ /** Converts a filesystem path to a `file:` URI. */
23
+ export function pathToFileUri(path: string): string {
24
+ return pathToFileURL(path).toString();
25
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Direct `pi-ahp` listener settings, persisted at `~/.pi/ahp/settings.json`.
3
+ *
4
+ * Two file states, deliberately kept apart:
5
+ *
6
+ * - **No file.** Nothing can be defaulted usefully — a port has to be free and
7
+ * a token has to be secret — so one of each is produced and written out. This
8
+ * is the only time the host invents anything.
9
+ * - **A file exists.** It is taken literally. A missing `token` means no token,
10
+ * a missing `port` is an error, and a value of the wrong shape is an error
11
+ * rather than a silent substitution: a setting quietly replaced by something
12
+ * the user did not write is worse than a refusal to start.
13
+ */
14
+
15
+ import { randomBytes } from "node:crypto";
16
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { createServer } from "node:net";
18
+ import { homedir } from "node:os";
19
+ import { dirname, join } from "node:path";
20
+
21
+ export interface DirectListenerSettings {
22
+ port: number;
23
+ /** `null` disables the check; clients then connect without `?token=`. */
24
+ token: string | null;
25
+ /** Bind address. Keep on loopback unless clients connect from another machine. */
26
+ host: string;
27
+ }
28
+
29
+ const CONFIG_DIR_NAME = ".pi";
30
+ const DEFAULT_HOST = "127.0.0.1";
31
+
32
+ function getAhpDir(): string {
33
+ const configDir = process.env.PI_CONFIG_DIR || join(homedir(), CONFIG_DIR_NAME);
34
+ return join(configDir, "ahp");
35
+ }
36
+
37
+ function getSettingsPath(): string {
38
+ return process.env.PI_AHP_SETTINGS || join(getAhpDir(), "settings.json");
39
+ }
40
+
41
+ /** Asks the OS for a free port and immediately gives it back. */
42
+ async function freePort(): Promise<number> {
43
+ const probe = createServer();
44
+ return new Promise<number>((resolve, reject) => {
45
+ probe.once("error", reject);
46
+ probe.listen(0, DEFAULT_HOST, () => {
47
+ const address = probe.address();
48
+ const port = typeof address === "object" && address ? address.port : 0;
49
+ probe.close(() => resolve(port));
50
+ });
51
+ });
52
+ }
53
+
54
+ export class SettingsError extends Error {
55
+ constructor(path: string, detail: string) {
56
+ super(`${path}: ${detail}`);
57
+ this.name = "SettingsError";
58
+ }
59
+ }
60
+
61
+ export async function loadDirectListenerSettings(path: string = getSettingsPath()): Promise<DirectListenerSettings> {
62
+ let raw: string;
63
+ try {
64
+ raw = readFileSync(path, "utf8");
65
+ } catch (error) {
66
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
67
+ throw error;
68
+ }
69
+ const settings: DirectListenerSettings = {
70
+ port: await freePort(),
71
+ token: randomBytes(32).toString("base64url"),
72
+ host: DEFAULT_HOST,
73
+ };
74
+ writeSettings(settings, path);
75
+ return settings;
76
+ }
77
+
78
+ let stored: Record<string, unknown>;
79
+ try {
80
+ stored = JSON.parse(raw) as Record<string, unknown>;
81
+ } catch (error) {
82
+ throw new SettingsError(path, `not valid JSON (${(error as Error).message})`);
83
+ }
84
+
85
+ if (stored.port === undefined) {
86
+ throw new SettingsError(path, "no `port`. Set one, or delete the file to have it chosen.");
87
+ }
88
+ if (!Number.isInteger(stored.port) || (stored.port as number) < 1 || (stored.port as number) > 65535) {
89
+ throw new SettingsError(path, `\`port\` must be an integer 1-65535, got ${JSON.stringify(stored.port)}`);
90
+ }
91
+ if (stored.token !== undefined && stored.token !== null && typeof stored.token !== "string") {
92
+ throw new SettingsError(path, `\`token\` must be a string or null, got ${JSON.stringify(stored.token)}`);
93
+ }
94
+ if (stored.host !== undefined && typeof stored.host !== "string") {
95
+ throw new SettingsError(path, `\`host\` must be a string, got ${JSON.stringify(stored.host)}`);
96
+ }
97
+
98
+ return {
99
+ port: stored.port as number,
100
+ // Absent means absent. Only a missing *file* mints a token.
101
+ token: (stored.token as string | null | undefined) ?? null,
102
+ host: (stored.host as string | undefined) ?? DEFAULT_HOST,
103
+ };
104
+ }
105
+
106
+ function writeSettings(settings: DirectListenerSettings, path: string = getSettingsPath()): void {
107
+ mkdirSync(dirname(path), { recursive: true });
108
+ // Owner-only: the token is a bearer credential for the endpoint.
109
+ writeFileSync(path, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
110
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Assembles a fully wired host: root channel, session catalogue, and session
3
+ * lifecycle, all backed by pi.
4
+ */
5
+
6
+ import { clampThinkingLevel } from "@earendil-works/pi-ai";
7
+ import { createAgentSessionServices, type ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
8
+ import type { CreateSessionParams, ModelSelection, URI } from "@microsoft/agent-host-protocol";
9
+ import { installRootChannel } from "../channels/root.ts";
10
+ import { sessionUri } from "../core/channels.ts";
11
+ import { AhpHost, type HostOptions } from "../core/host.ts";
12
+ import { ChangesetService } from "../pi/changeset-service.ts";
13
+ import { CompletionService, MENTION_TRIGGER } from "../pi/completions.ts";
14
+ import { deleteSessionFile } from "../pi/delete-session.ts";
15
+ import { InProcessPiBackend } from "../pi/in-process-backend.ts";
16
+ import { buildAgentInfo, modelSelectionId, THINKING_CONFIG_KEY } from "../pi/models.ts";
17
+ import { type ProjectTrustPolicy, resolveProjectTrust } from "../pi/project-trust.ts";
18
+ import { ResourcePathPolicy } from "../pi/resource-paths.ts";
19
+ import { ResourceService } from "../pi/resource-service.ts";
20
+ import { ResourceWatchService } from "../pi/resource-watch.ts";
21
+ import type { PiSessionCatalogue } from "../pi/session-catalogue.ts";
22
+ import { SessionConfigService } from "../pi/session-config.ts";
23
+ import { SessionHydrator } from "../pi/session-hydrator.ts";
24
+ import { type BackendFactory, type SessionFileDeletionResult, SessionRegistry } from "../pi/session-registry.ts";
25
+ import type { PiSessionStorage } from "../pi/session-storage.ts";
26
+ import { TerminalService } from "./terminal-service.ts";
27
+
28
+ export interface PiHostOptions extends HostOptions {
29
+ /** Working directory for sessions created without one. Defaults to `process.cwd()`. */
30
+ readonly workingDirectory?: string;
31
+ /** Injectable for tests; defaults to pi's real model runtime. */
32
+ readonly modelRuntime?: Pick<ModelRuntime, "getAvailable">;
33
+ /** Injectable for tests; defaults to checked trash-then-unlink deletion. */
34
+ readonly deleteFile?: (path: string) => SessionFileDeletionResult | Promise<SessionFileDeletionResult>;
35
+ /** Injectable for tests; defaults to an in-process `AgentSession`. */
36
+ readonly createBackend?: BackendFactory;
37
+ /** Explicit storage boundary; tests cannot accidentally fall back to user data. */
38
+ readonly sessionStorage: PiSessionStorage;
39
+ /**
40
+ * How to treat trust-gated pi resources in a session's working directory.
41
+ *
42
+ * Defaults to `trust`, matching the pi SDK default. Switch to `inherit` when
43
+ * the host is reachable from outside its own trust domain: the working
44
+ * directory then arrives from a client, and `inherit` will only load project
45
+ * resources the user already approved through pi's CLI.
46
+ */
47
+ readonly projectTrustPolicy?: ProjectTrustPolicy;
48
+ /**
49
+ * Directories the resource commands and watches may reach. Empty means
50
+ * unrestricted. See `src/pi/resource-paths.ts` for why that is the default.
51
+ */
52
+ readonly resourceRoots?: readonly string[];
53
+ }
54
+
55
+ export interface PiHost {
56
+ readonly host: AhpHost;
57
+ readonly sessions: SessionRegistry;
58
+ readonly catalogue: PiSessionCatalogue;
59
+ readonly changesets: ChangesetService;
60
+ readonly watches: ResourceWatchService;
61
+ readonly terminals: { shutdown(): void };
62
+ }
63
+
64
+ /**
65
+ * Builds the host and registers every supported channel. The model list is a
66
+ * startup snapshot; dynamic refresh would require publishing
67
+ * `root/agentsChanged` and is not implemented.
68
+ */
69
+ export async function createPiHost(options: PiHostOptions): Promise<PiHost> {
70
+ const workingDirectory = options.workingDirectory ?? process.cwd();
71
+ const host = new AhpHost({
72
+ ...options,
73
+ // `@` is the only trigger this host can answer: every completion item
74
+ // must carry an attachment, and pi's `/` commands attach nothing.
75
+ completionTriggerCharacters: options.completionTriggerCharacters ?? [MENTION_TRIGGER],
76
+ });
77
+
78
+ let modelRuntime: Pick<ModelRuntime, "getAvailable">;
79
+ let settingsManager: SettingsManager | undefined;
80
+ if (options.modelRuntime) {
81
+ modelRuntime = options.modelRuntime;
82
+ } else {
83
+ // Package extensions can register providers; the bare ModelRuntime cannot
84
+ // see them until the standard service loader flushes those registrations.
85
+ const trust = resolveProjectTrust(workingDirectory, options.projectTrustPolicy);
86
+ const services = await createAgentSessionServices({
87
+ cwd: workingDirectory,
88
+ settingsManager: SettingsManager.create(workingDirectory, undefined, { projectTrusted: trust.trusted }),
89
+ });
90
+ modelRuntime = services.modelRuntime;
91
+ settingsManager = services.settingsManager;
92
+ for (const diagnostic of services.diagnostics) {
93
+ options.log?.(`[pi models] ${diagnostic.type}: ${diagnostic.message}`);
94
+ }
95
+ }
96
+ // A user with no configured provider still gets a usable host — they just
97
+ // see an agent with no models, which is the honest representation.
98
+ const models = await modelRuntime.getAvailable().catch(() => []);
99
+ installRootChannel(host, [buildAgentInfo(models as never)]);
100
+
101
+ const configuredProvider = settingsManager?.getDefaultProvider();
102
+ const configuredId = settingsManager?.getDefaultModel();
103
+ const configured = models.find((model) => model.provider === configuredProvider && model.id === configuredId);
104
+ const fallback = configured ?? models[0];
105
+ const fallbackThinking = fallback
106
+ ? clampThinkingLevel(fallback, configured ? (settingsManager?.getDefaultThinkingLevel() ?? "medium") : "medium")
107
+ : undefined;
108
+ const fallbackSelection = (): ModelSelection | undefined =>
109
+ fallback && fallbackThinking
110
+ ? { id: modelSelectionId(fallback), config: { [THINKING_CONFIG_KEY]: fallbackThinking } }
111
+ : undefined;
112
+
113
+ const catalogue = options.sessionStorage.catalogue;
114
+ const resourcePaths = new ResourcePathPolicy(options.resourceRoots);
115
+ const watches = new ResourceWatchService(host, {
116
+ pathPolicy: resourcePaths,
117
+ ...(options.log ? { log: options.log } : {}),
118
+ });
119
+ const terminals = new TerminalService(host, {
120
+ defaultWorkingDirectory: workingDirectory,
121
+ ...(options.log ? { log: options.log } : {}),
122
+ });
123
+
124
+ const createBackend: BackendFactory =
125
+ options.createBackend ??
126
+ ((session) =>
127
+ InProcessPiBackend.create({
128
+ cwd: session.workingDirectory,
129
+ sessionManager: session.sessionManager,
130
+ ...(options.projectTrustPolicy ? { projectTrustPolicy: options.projectTrustPolicy } : {}),
131
+ }));
132
+
133
+ const sessions = new SessionRegistry({
134
+ host,
135
+ defaultWorkingDirectory: workingDirectory,
136
+ createBackend,
137
+ createSessionManager: options.sessionStorage.createSessionManager,
138
+ defaultSelection: fallbackSelection,
139
+ deleteFile: options.deleteFile ?? deleteSessionFile,
140
+ findSessionFile: (id) => catalogue.findSessionFile(id),
141
+ ...(options.log ? { log: options.log } : {}),
142
+ });
143
+ const sessionHydrator = new SessionHydrator({
144
+ host,
145
+ catalogue,
146
+ isLive: (session) => sessions.has(session),
147
+ isDisposing: (session) => sessions.isDisposing(session),
148
+ // Adopted without a backend; one starts on the first turn.
149
+ adopt: (session) => void sessions.adopt(session),
150
+ fallbackSelection,
151
+ ...(options.log ? { log: options.log } : {}),
152
+ });
153
+ const changesets = new ChangesetService(host, {
154
+ getSession: (sessionId) => sessions.get(sessionUri(sessionId)),
155
+ getSessionByChat: (chat) => sessions.getByChat(chat),
156
+ hydrateSession: async (sessionId) => {
157
+ await sessionHydrator.hydrate(sessionUri(sessionId));
158
+ return sessions.get(sessionUri(sessionId));
159
+ },
160
+ onSessionAvailable: (listener) => sessions.onSessionAvailable(listener),
161
+ onSessionDeletionCommitted: (listener) => sessions.onSessionDeletionCommitted(listener),
162
+ authorizeResource: async (uri) => {
163
+ await resourcePaths.pathFor(uri);
164
+ },
165
+ ...(options.log ? { log: options.log } : {}),
166
+ });
167
+ const resources = new ResourceService({
168
+ pathPolicy: resourcePaths,
169
+ readVirtual: (params) => changesets.readResource(params),
170
+ });
171
+
172
+ host.serve({
173
+ catalogue: {
174
+ list: (limit, cursor) => catalogue.list(limit, cursor, () => sessions.catalogueOverrides()),
175
+ },
176
+ resources,
177
+ resourceWatches: watches,
178
+ terminals,
179
+ sessionConfig: new SessionConfigService({
180
+ defaultWorkingDirectory: workingDirectory,
181
+ ...(options.projectTrustPolicy ? { projectTrustPolicy: options.projectTrustPolicy } : {}),
182
+ }),
183
+ completions: new CompletionService({
184
+ // Mentions resolve against the chat's own directory, which is the
185
+ // session's working directory for the single chat this host serves.
186
+ workingDirectoryFor: (chat) => sessions.getByChat(chat)?.workingDirectory,
187
+ }),
188
+ turnPaging: {
189
+ async fetchTurns(params) {
190
+ await sessions.fetchTurns(params.channel, params.cursor);
191
+ return {};
192
+ },
193
+ },
194
+ sessions: {
195
+ create(params: CreateSessionParams): void {
196
+ // The registry's narrower structural type makes the unsupported optional
197
+ // fields explicit: config, activeClient, and progressToken are ignored.
198
+ sessions.create(params);
199
+ },
200
+ async dispose(channel: URI): Promise<void> {
201
+ const sessionId = sessions.get(channel)?.sessionId;
202
+ if (sessionId) await changesets.suspendSession(sessionId);
203
+ try {
204
+ await sessions.dispose(channel);
205
+ } catch (error) {
206
+ if (sessionId) changesets.resumeSession(sessionId);
207
+ throw error;
208
+ }
209
+ },
210
+ },
211
+ // Both session/chat and their advertised changesets survive host restarts.
212
+ hydrator: {
213
+ async hydrate(channel) {
214
+ return (await changesets.hydrate(channel)) || sessionHydrator.hydrate(channel);
215
+ },
216
+ },
217
+ });
218
+
219
+ return { host, sessions, catalogue, changesets, watches, terminals };
220
+ }
@@ -0,0 +1,71 @@
1
+ /** Shared host construction; each entry point owns its endpoint and authentication policy. */
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { pathToFileUri } from "../core/uri.ts";
7
+ import { createDefaultPiSessionStorage } from "../pi/session-storage.ts";
8
+ import { type RunningServer, serveWebSocket } from "../transport/websocket.ts";
9
+ import { createPiHost } from "./pi-host.ts";
10
+
11
+ /**
12
+ * Read from the manifest rather than restated here.
13
+ *
14
+ * The name and version go out in `serverInfo` during the handshake, so a copy
15
+ * that drifts from the package tells clients something untrue about what they
16
+ * are connected to. Resolved relative to this file, which keeps working when
17
+ * the package is installed somewhere else.
18
+ */
19
+ const manifest = JSON.parse(
20
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"), "utf8"),
21
+ ) as { name: string; version: string };
22
+
23
+ export const NAME = manifest.name;
24
+ export const VERSION = manifest.version;
25
+
26
+ export interface ServeOptions {
27
+ readonly host: string;
28
+ readonly port: number;
29
+ /** Absent or empty disables the WebSocket upgrade credential. */
30
+ readonly connectionToken?: string | undefined;
31
+ readonly workingDirectory: string;
32
+ readonly log?: ((message: string) => void) | undefined;
33
+ }
34
+
35
+ export async function startHost(options: ServeOptions): Promise<RunningServer> {
36
+ const {
37
+ host: ahpHost,
38
+ changesets,
39
+ terminals,
40
+ watches,
41
+ } = await createPiHost({
42
+ serverInfo: { name: NAME, version: VERSION },
43
+ defaultDirectory: pathToFileUri(options.workingDirectory),
44
+ workingDirectory: options.workingDirectory,
45
+ sessionStorage: createDefaultPiSessionStorage(),
46
+ ...(options.log ? { log: options.log } : {}),
47
+ });
48
+
49
+ const server = await serveWebSocket(ahpHost, {
50
+ host: options.host,
51
+ port: options.port,
52
+ ...(options.connectionToken ? { connectionToken: options.connectionToken } : {}),
53
+ ...(options.log ? { log: options.log } : {}),
54
+ });
55
+ return {
56
+ ...server,
57
+ async close() {
58
+ terminals.shutdown();
59
+ await Promise.all([server.close(), changesets.dispose(), watches.dispose()]);
60
+ },
61
+ };
62
+ }
63
+
64
+ /** Closes `server` on SIGINT/SIGTERM. */
65
+ export function closeOnSignal(server: RunningServer): void {
66
+ const shutdown = (): void => {
67
+ void server.close().then(() => process.exit(0));
68
+ };
69
+ process.on("SIGINT", shutdown);
70
+ process.on("SIGTERM", shutdown);
71
+ }