@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,458 @@
1
+ package host
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "encoding/json"
6
+ "regexp"
7
+ "strings"
8
+
9
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
10
+ )
11
+
12
+ // Repairs to the traffic of clients whose reading of the protocol differs from this host's. Each
13
+ // entry says what the client does and what would let it go. Port of src/core/client-workarounds.ts.
14
+ // Messages are handled as generic decoded JSON (map[string]any, numbers as json.Number or float64).
15
+
16
+ // rewritableFields carry session or chat URIs in state, actions and catalogue notifications.
17
+ var rewritableFields = map[string]bool{"channel": true, "resource": true, "defaultChat": true, "session": true, "chat": true}
18
+
19
+ // derivedChatURI is VS Code's derived chat URI: ahp-chat://<anything>/<base64url(sessionUri)>.
20
+ var derivedChatURI = regexp.MustCompile(`^ahp-chat://[^/]+/([^/?#]+)$`)
21
+
22
+ var vscodeClientNames = map[string]bool{"vscode-editor-window": true, "vscode-agents-window": true}
23
+
24
+ const vscodeMaterializedSessionDisposalRefusal = "Materialized session disposal is temporarily disabled for VS Code because of a VS Code provisional-session lifecycle bug; this session was kept."
25
+
26
+ // materializingActions are those after which a session is no longer an abandoned empty draft.
27
+ var materializingActions = map[string]bool{"chat/turnStarted": true, "chat/pendingMessageSet": true, "session/titleChanged": true}
28
+
29
+ // providerSessionMethods are the methods where a direct pi:/... target can only mean a session.
30
+ var providerSessionMethods = map[string]bool{
31
+ "createSession": true, "disposeSession": true, "subscribe": true, "unsubscribe": true, "dispatchAction": true, "completions": true,
32
+ }
33
+
34
+ type uriDialect int
35
+
36
+ const (
37
+ dialectCanonical uriDialect = iota
38
+ dialectProvider
39
+ dialectVSCode
40
+ )
41
+
42
+ // ClientWorkarounds translates one connection's traffic. VS Code computes provider-scoped session
43
+ // URIs and derived default-chat URIs instead of using the canonical resources published by the host.
44
+ // It also targets completions at the session URI rather than the chat URI. Publishing those shapes
45
+ // globally would impose one client's dialect on every client, so translation stays per connection
46
+ // and core services see canonical URIs.
47
+ type ClientWorkarounds struct {
48
+ provider string
49
+ isVSCode bool
50
+ usesProv bool
51
+ guard *vscodeDisposalGuard
52
+ }
53
+
54
+ // NewClientWorkarounds returns a translator for the given provider scheme ("pi").
55
+ func NewClientWorkarounds(providerScheme string) *ClientWorkarounds {
56
+ return &ClientWorkarounds{provider: providerScheme, guard: newVSCodeDisposalGuard(providerScheme)}
57
+ }
58
+
59
+ // Identify reads the implementation name given at initialize without erasing observations from wire traffic.
60
+ func (w *ClientWorkarounds) Identify(clientName string) {
61
+ if vscodeClientNames[clientName] {
62
+ w.isVSCode = true
63
+ }
64
+ }
65
+
66
+ func (w *ClientWorkarounds) providerSessionID(uri string) (string, bool) {
67
+ return wire.SessionIDFromURI(uri, w.provider)
68
+ }
69
+
70
+ func (w *ClientWorkarounds) isProviderSession(uri string) bool {
71
+ return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(w.provider)+":/") && func() bool { _, ok := w.providerSessionID(uri); return ok }()
72
+ }
73
+
74
+ func (w *ClientWorkarounds) canonicalSessionURI(uri string) (string, bool) {
75
+ id, ok := w.providerSessionID(uri)
76
+ if !ok {
77
+ return "", false
78
+ }
79
+ return wire.SessionURI(id), true
80
+ }
81
+
82
+ func (w *ClientWorkarounds) owningSessionURI(uri string) (string, bool) {
83
+ if s, ok := w.canonicalSessionURI(uri); ok {
84
+ return s, true
85
+ }
86
+ if id, ok := wire.ChatIDFromURI(uri); ok {
87
+ return wire.SessionURI(id), true
88
+ }
89
+ return "", false
90
+ }
91
+
92
+ func params(msg map[string]any) map[string]any {
93
+ p, _ := msg["params"].(map[string]any)
94
+ return p
95
+ }
96
+
97
+ func str(v any) (string, bool) { s, ok := v.(string); return s, ok }
98
+
99
+ func actionTypeOf(v any) string {
100
+ m, ok := v.(map[string]any)
101
+ if !ok {
102
+ return ""
103
+ }
104
+ s, _ := m["type"].(string)
105
+ return s
106
+ }
107
+
108
+ // ApplyToIncoming rewrites a parsed request or notification in place. A *wire.Error result means
109
+ // the request must be refused with that error.
110
+ func (w *ClientWorkarounds) ApplyToIncoming(msg map[string]any) error {
111
+ p := params(msg)
112
+ if p == nil {
113
+ return nil
114
+ }
115
+ method, _ := msg["method"].(string)
116
+ _, hasID := msg["id"]
117
+ w.observeTraffic(method, p)
118
+ // Remove when VS Code includes the AHP 0.9 root-channel discriminant in reconnect.
119
+ if hasID && w.isVSCode && method == "reconnect" {
120
+ if _, present := p["channel"]; !present {
121
+ p["channel"] = wire.RootChannel
122
+ }
123
+ }
124
+
125
+ dialect := w.dialect()
126
+ rewrite := func(uri string) string { return w.inbound(uri, dialect) }
127
+ if channel, ok := str(p["channel"]); ok {
128
+ channel = rewrite(channel)
129
+ if method == "completions" && dialect != dialectCanonical {
130
+ // Both VS Code and the iOS client currently target completions at the provider-style session URI.
131
+ if id, ok := w.providerSessionID(channel); ok {
132
+ channel = wire.ChatURI(id)
133
+ }
134
+ }
135
+ // VS Code addresses its session rename to the selected chat. AHP defines session/titleChanged
136
+ // only on the owning session.
137
+ if method == "dispatchAction" && w.isVSCode && actionTypeOf(p["action"]) == "session/titleChanged" {
138
+ if id, ok := wire.ChatIDFromURI(channel); ok {
139
+ channel = wire.SessionURI(id)
140
+ }
141
+ }
142
+ p["channel"] = channel
143
+ }
144
+ // Handshake requests name their subscribed channels in arrays rather than the routing channel.
145
+ for _, field := range []string{"initialSubscriptions", "subscriptions"} {
146
+ if list, ok := p[field].([]any); ok {
147
+ out := make([]any, len(list))
148
+ for i, item := range list {
149
+ if s, ok := str(item); ok {
150
+ out[i] = rewrite(s)
151
+ } else {
152
+ out[i] = item
153
+ }
154
+ }
155
+ p[field] = out
156
+ }
157
+ }
158
+ if w.isVSCode {
159
+ return w.guard.applyToIncoming(msg, p)
160
+ }
161
+ return nil
162
+ }
163
+
164
+ // ApplyToOutgoing returns the outgoing message, rewritten when this client needs it.
165
+ func (w *ClientWorkarounds) ApplyToOutgoing(msg map[string]any) map[string]any {
166
+ if w.isVSCode {
167
+ w.guard.applyToOutgoing(msg)
168
+ }
169
+ dialect := w.dialect()
170
+ if dialect == dialectCanonical {
171
+ return msg
172
+ }
173
+ return rewriteFields(msg, func(uri string) string { return w.outbound(uri, dialect) }).(map[string]any)
174
+ }
175
+
176
+ // Active reports whether this connection needs any translation, so the host can skip decoding
177
+ // messages for the common canonical client.
178
+ func (w *ClientWorkarounds) Active() bool { return w.isVSCode || w.usesProv }
179
+
180
+ func (w *ClientWorkarounds) observeTraffic(method string, p map[string]any) {
181
+ var listed []string
182
+ for _, field := range []string{"initialSubscriptions", "subscriptions"} {
183
+ if list, ok := p[field].([]any); ok {
184
+ for _, item := range list {
185
+ if s, ok := str(item); ok {
186
+ listed = append(listed, s)
187
+ }
188
+ }
189
+ }
190
+ }
191
+ observed := listed
192
+ if direct, ok := str(p["channel"]); ok {
193
+ observed = append([]string{direct}, listed...)
194
+ }
195
+ w.observeVSCode(p, observed)
196
+ if w.usesProv {
197
+ return
198
+ }
199
+ check := listed
200
+ if providerSessionMethods[method] {
201
+ check = observed
202
+ }
203
+ for _, uri := range check {
204
+ if w.isProviderSession(uri) {
205
+ w.usesProv = true
206
+ return
207
+ }
208
+ }
209
+ }
210
+
211
+ func (w *ClientWorkarounds) observeVSCode(p map[string]any, observed []string) {
212
+ if meta, ok := p["_meta"].(map[string]any); ok {
213
+ _, a := meta["vscode.telemetryLevel"]
214
+ _, b := meta["vscode.clientConnectionKind"]
215
+ if a || b {
216
+ w.isVSCode = true
217
+ return
218
+ }
219
+ }
220
+ for _, uri := range observed {
221
+ if _, ok := w.sessionFromDerivedChat(uri); ok {
222
+ w.isVSCode = true
223
+ return
224
+ }
225
+ }
226
+ }
227
+
228
+ func (w *ClientWorkarounds) dialect() uriDialect {
229
+ if w.isVSCode {
230
+ return dialectVSCode
231
+ }
232
+ if w.usesProv {
233
+ return dialectProvider
234
+ }
235
+ return dialectCanonical
236
+ }
237
+
238
+ // sessionFromDerivedChat translates a URI this client computed into the one this host minted: a
239
+ // derived chat URI is unwrapped regardless of who sent it, since it names no channel this host could
240
+ // otherwise serve.
241
+ func (w *ClientWorkarounds) sessionFromDerivedChat(uri string) (string, bool) {
242
+ m := derivedChatURI.FindStringSubmatch(uri)
243
+ if m == nil || m[1] == "" {
244
+ return "", false
245
+ }
246
+ raw, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(m[1], "="))
247
+ if err != nil {
248
+ return "", false
249
+ }
250
+ session := string(raw)
251
+ if !w.isProviderSession(session) {
252
+ return "", false
253
+ }
254
+ return session, true
255
+ }
256
+
257
+ func (w *ClientWorkarounds) inbound(uri string, dialect uriDialect) string {
258
+ if session, ok := w.sessionFromDerivedChat(uri); ok {
259
+ if id, ok := w.providerSessionID(session); ok {
260
+ return wire.ChatURI(id)
261
+ }
262
+ }
263
+ if dialect == dialectCanonical || !w.isProviderSession(uri) {
264
+ return uri
265
+ }
266
+ if id, ok := w.providerSessionID(uri); ok {
267
+ return wire.SessionURI(id)
268
+ }
269
+ return uri
270
+ }
271
+
272
+ // outbound translates a URI this host minted into the dialect this client expects.
273
+ func (w *ClientWorkarounds) outbound(uri string, dialect uriDialect) string {
274
+ if id, ok := wire.ChatIDFromURI(uri); ok {
275
+ if dialect == dialectVSCode {
276
+ return "ahp-chat://default/" + base64.RawURLEncoding.EncodeToString([]byte(w.provider+":/"+id))
277
+ }
278
+ return uri
279
+ }
280
+ if id, ok := w.providerSessionID(uri); ok {
281
+ return w.provider + ":/" + id
282
+ }
283
+ return uri
284
+ }
285
+
286
+ // rewriteFields applies rewrite to every rewritable field, however deeply nested.
287
+ func rewriteFields(v any, rewrite func(string) string) any {
288
+ switch x := v.(type) {
289
+ case []any:
290
+ out := make([]any, len(x))
291
+ for i, item := range x {
292
+ out[i] = rewriteFields(item, rewrite)
293
+ }
294
+ return out
295
+ case map[string]any:
296
+ out := make(map[string]any, len(x))
297
+ for k, item := range x {
298
+ if s, ok := item.(string); ok && rewritableFields[k] {
299
+ out[k] = rewrite(s)
300
+ } else {
301
+ out[k] = rewriteFields(item, rewrite)
302
+ }
303
+ }
304
+ return out
305
+ }
306
+ return v
307
+ }
308
+
309
+ // ── VS Code provisional-session disposal guard ──────────────────────────
310
+
311
+ type trackedState string
312
+
313
+ const (
314
+ stateCreating trackedState = "creating"
315
+ stateEmpty trackedState = "empty"
316
+ stateMaterialized trackedState = "materialized"
317
+ stateDisposing trackedState = "disposing"
318
+ )
319
+
320
+ type pendingLifecycle struct {
321
+ kind string // "create" | "dispose"
322
+ session string
323
+ }
324
+
325
+ // vscodeDisposalGuard allows VS Code to clean up only sessions known to be unused drafts on this
326
+ // connection. Unknown sessions are protected: reconnect does not carry enough history to prove that
327
+ // they are empty. Remove it when VS Code graduates materialized remote sessions before tearing down
328
+ // its provisional-session service.
329
+ type vscodeDisposalGuard struct {
330
+ provider string
331
+ sessions map[string]trackedState
332
+ pending map[string]pendingLifecycle
333
+ }
334
+
335
+ func newVSCodeDisposalGuard(provider string) *vscodeDisposalGuard {
336
+ return &vscodeDisposalGuard{provider: provider, sessions: map[string]trackedState{}, pending: map[string]pendingLifecycle{}}
337
+ }
338
+
339
+ func idKey(v any) string {
340
+ switch n := v.(type) {
341
+ case json.Number:
342
+ return n.String()
343
+ case float64:
344
+ b, _ := json.Marshal(n)
345
+ return string(b)
346
+ case string:
347
+ return "s:" + n
348
+ }
349
+ return ""
350
+ }
351
+
352
+ func (g *vscodeDisposalGuard) canonical(uri string) (string, bool) {
353
+ id, ok := wire.SessionIDFromURI(uri, g.provider)
354
+ if !ok {
355
+ return "", false
356
+ }
357
+ return wire.SessionURI(id), true
358
+ }
359
+
360
+ func (g *vscodeDisposalGuard) owning(uri string) (string, bool) {
361
+ if s, ok := g.canonical(uri); ok {
362
+ return s, true
363
+ }
364
+ if id, ok := wire.ChatIDFromURI(uri); ok {
365
+ return wire.SessionURI(id), true
366
+ }
367
+ return "", false
368
+ }
369
+
370
+ func (g *vscodeDisposalGuard) markMaterialized(channel string) {
371
+ if session, ok := g.owning(channel); ok {
372
+ if _, tracked := g.sessions[session]; tracked {
373
+ g.sessions[session] = stateMaterialized
374
+ }
375
+ }
376
+ }
377
+
378
+ func (g *vscodeDisposalGuard) applyToIncoming(msg map[string]any, p map[string]any) error {
379
+ channel, _ := str(p["channel"])
380
+ if t := actionTypeOf(p["action"]); channel != "" && t != "" && materializingActions[t] {
381
+ g.markMaterialized(channel)
382
+ }
383
+ id, isRequest := msg["id"]
384
+ if !isRequest || channel == "" {
385
+ return nil
386
+ }
387
+ session, ok := g.canonical(channel)
388
+ if !ok {
389
+ return nil
390
+ }
391
+ method, _ := msg["method"].(string)
392
+ switch method {
393
+ case "createSession":
394
+ if _, imported := p["importConversation"]; imported {
395
+ g.sessions[session] = stateMaterialized
396
+ } else {
397
+ g.sessions[session] = stateCreating
398
+ }
399
+ g.pending[idKey(id)] = pendingLifecycle{"create", session}
400
+ case "disposeSession":
401
+ if g.sessions[session] != stateEmpty {
402
+ return &wire.Error{Code: wire.CodeInvalidRequest, Message: vscodeMaterializedSessionDisposalRefusal}
403
+ }
404
+ g.sessions[session] = stateDisposing
405
+ g.pending[idKey(id)] = pendingLifecycle{"dispose", session}
406
+ }
407
+ return nil
408
+ }
409
+
410
+ func (g *vscodeDisposalGuard) applyToOutgoing(msg map[string]any) {
411
+ method, isNotification := msg["method"].(string)
412
+ if !isNotification {
413
+ g.applyResponse(msg)
414
+ return
415
+ }
416
+ p := params(msg)
417
+ if p == nil {
418
+ return
419
+ }
420
+ if t := actionTypeOf(p["action"]); method == "action" && p["rejectionReason"] == nil && t != "" && materializingActions[t] {
421
+ if channel, ok := str(p["channel"]); ok {
422
+ g.markMaterialized(channel)
423
+ }
424
+ }
425
+ if method == "root/sessionRemoved" {
426
+ if s, ok := str(p["session"]); ok {
427
+ if session, ok := g.canonical(s); ok {
428
+ delete(g.sessions, session)
429
+ }
430
+ }
431
+ }
432
+ }
433
+
434
+ func (g *vscodeDisposalGuard) applyResponse(msg map[string]any) {
435
+ key := idKey(msg["id"])
436
+ pending, ok := g.pending[key]
437
+ if !ok {
438
+ return
439
+ }
440
+ delete(g.pending, key)
441
+ state := g.sessions[pending.session]
442
+ _, success := msg["result"]
443
+ if pending.kind == "create" {
444
+ if success {
445
+ if state == stateCreating {
446
+ g.sessions[pending.session] = stateEmpty
447
+ }
448
+ } else {
449
+ delete(g.sessions, pending.session)
450
+ }
451
+ return
452
+ }
453
+ if success {
454
+ delete(g.sessions, pending.session)
455
+ } else if state == stateDisposing {
456
+ g.sessions[pending.session] = stateEmpty
457
+ }
458
+ }
@@ -0,0 +1,96 @@
1
+ package host_test
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "reflect"
6
+ "testing"
7
+
8
+ "github.com/MichaelKinsy/pigpen/ahp/internal/host"
9
+ "github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
10
+ "github.com/MichaelKinsy/pigpen/ahp/internal/twin"
11
+ "github.com/MichaelKinsy/pigpen/ahp/internal/wire"
12
+ )
13
+
14
+ func derivedChat(providerSession string) string {
15
+ return "ahp-chat://default/" + base64.RawURLEncoding.EncodeToString([]byte(providerSession))
16
+ }
17
+
18
+ // Twins of upstream test/client-workarounds.test.ts, describe "session URI classification"
19
+ // (the cases that need no session registry; the wire-level ones live with the registry tests).
20
+ func TestSessionURIClassification(t *testing.T) {
21
+ twin.Run(t, "client-workarounds", "recognises only canonical or explicitly allowed session schemes", func(t *testing.T) {
22
+ id := "9991C40A-74CC-4991-85CC-F37CD2BFF065"
23
+ check := func(uri string, aliases []string, want string, ok bool) {
24
+ t.Helper()
25
+ got, gotOK := wire.SessionIDFromURI(uri, aliases...)
26
+ if got != want || gotOK != ok {
27
+ t.Errorf("SessionIDFromURI(%q, %v) = %q, %v; want %q, %v", uri, aliases, got, gotOK, want, ok)
28
+ }
29
+ }
30
+ check("pi:/"+id, []string{"pi"}, id, true)
31
+ check("ahp-session:/abc", nil, "abc", true)
32
+ check("copilot:/test-session", nil, "", false)
33
+ check("copilot:/test-session", []string{"copilot"}, "test-session", true)
34
+ check("pi://test-session", []string{"pi"}, "", false)
35
+ })
36
+
37
+ twin.Run(t, "client-workarounds", "does not infer sessions from other channel schemes", func(t *testing.T) {
38
+ for _, uri := range []string{"ahp-chat:/c1", "ahp-terminal:/t1", "agenthost-terminal:/t1", "file:///etc/passwd"} {
39
+ if id, ok := wire.SessionIDFromURI(uri, "pi"); ok {
40
+ t.Errorf("%s classified as session %q", uri, id)
41
+ }
42
+ }
43
+ })
44
+
45
+ twin.Run(t, "client-workarounds", "retargets VS Code's chat-addressed rename to the owning session", func(t *testing.T) {
46
+ id := "rename-me"
47
+ w := host.NewClientWorkarounds("pi")
48
+ w.Identify("vscode-editor-window")
49
+ msg := map[string]any{"jsonrpc": "2.0", "method": "dispatchAction", "params": map[string]any{
50
+ "channel": derivedChat("pi:/" + id), "clientSeq": float64(1),
51
+ "action": map[string]any{"type": "session/titleChanged", "title": "Renamed"},
52
+ }}
53
+ if err := w.ApplyToIncoming(msg); err != nil {
54
+ t.Fatal(err)
55
+ }
56
+ if got := msg["params"].(map[string]any)["channel"]; got != wire.SessionURI(id) {
57
+ t.Fatalf("channel = %v, want %s", got, wire.SessionURI(id))
58
+ }
59
+ })
60
+
61
+ twin.Run(t, "client-workarounds", "rewrites URI-bearing action and catalogue fields for VS Code", func(t *testing.T) {
62
+ id := "nested-fields"
63
+ session, chat := wire.SessionURI(id), wire.ChatURI(id)
64
+ providerSession := "pi:/" + id
65
+ derived := derivedChat(providerSession)
66
+ w := host.NewClientWorkarounds("pi")
67
+ w.Identify("vscode-editor-window")
68
+
69
+ removed := w.ApplyToOutgoing(map[string]any{"jsonrpc": "2.0", "method": "root/sessionRemoved", "params": map[string]any{"channel": wire.RootChannel, "session": session}})
70
+ wantRemoved := map[string]any{"jsonrpc": "2.0", "method": "root/sessionRemoved", "params": map[string]any{"channel": wire.RootChannel, "session": providerSession}}
71
+ if !reflect.DeepEqual(removed, wantRemoved) {
72
+ t.Fatalf("removed = %#v", removed)
73
+ }
74
+
75
+ updated := w.ApplyToOutgoing(map[string]any{"jsonrpc": "2.0", "method": "action", "params": map[string]any{
76
+ "channel": session, "serverSeq": float64(1),
77
+ "action": map[string]any{"type": "session/chatUpdated", "chat": chat, "changes": map[string]any{"resource": chat}},
78
+ }})
79
+ wantUpdated := map[string]any{"jsonrpc": "2.0", "method": "action", "params": map[string]any{
80
+ "channel": providerSession, "serverSeq": float64(1),
81
+ "action": map[string]any{"type": "session/chatUpdated", "chat": derived, "changes": map[string]any{"resource": derived}},
82
+ }}
83
+ if !reflect.DeepEqual(updated, wantUpdated) {
84
+ t.Fatalf("updated = %#v", testkit.Normalize(t, updated))
85
+ }
86
+ })
87
+
88
+ twin.Run(t, "client-workarounds", "leaves VS Code's client-chosen terminal URI outside session translation", func(t *testing.T) {
89
+ w := host.NewClientWorkarounds("pi")
90
+ w.Identify("vscode-editor-window")
91
+ msg := map[string]any{"jsonrpc": "2.0", "method": "action", "params": map[string]any{"channel": "agenthost-terminal:/terminal-1"}}
92
+ if got := w.ApplyToOutgoing(msg); !reflect.DeepEqual(got, msg) {
93
+ t.Fatalf("got %#v", got)
94
+ }
95
+ })
96
+ }