@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,389 @@
1
+ /**
2
+ * Reconnection: replay from the buffer, snapshot fallback, and unresumable
3
+ * subscriptions.
4
+ *
5
+ * @see https://microsoft.github.io/agent-host-protocol/specification/lifecycle
6
+ */
7
+
8
+ import assert from "node:assert/strict";
9
+ import { randomUUID } from "node:crypto";
10
+ import { mkdtempSync, rmSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { after, before, describe, it } from "node:test";
14
+ import { type AgentSessionEvent, SessionManager } from "@earendil-works/pi-coding-agent";
15
+ import {
16
+ ActionType,
17
+ type ChatState,
18
+ JsonRpcErrorCodes,
19
+ MessageKind,
20
+ type ReconnectReplayResult,
21
+ ReconnectResultType,
22
+ type ReconnectSnapshotResult,
23
+ ResponsePartKind,
24
+ type SessionState,
25
+ SUPPORTED_PROTOCOL_VERSIONS,
26
+ TurnState,
27
+ } from "@microsoft/agent-host-protocol";
28
+ import { initialSessionState } from "../src/channels/session.ts";
29
+ import { chatUri, ROOT_CHANNEL, sessionUri } from "../src/core/channels.ts";
30
+ import type { PiBackend } from "../src/pi/chat-driver.ts";
31
+ import { type Harness, startHarness } from "./harness.ts";
32
+ import { expectRpcError, must } from "./support/assertions.ts";
33
+ import { eventually } from "./support/async.ts";
34
+
35
+ const CLIENT_ID = "reconnecting-client";
36
+
37
+ function bumpActiveSessions(harness: Harness, count: number): void {
38
+ harness.host.dispatchServerAction(ROOT_CHANNEL, {
39
+ type: ActionType.RootActiveSessionsChanged,
40
+ activeSessions: count,
41
+ });
42
+ }
43
+
44
+ function writeDurableSession(root: string, id: string, cwd: string): void {
45
+ const manager = SessionManager.create(cwd, join(root, "fixture"), { id });
46
+ manager.appendMessage({ role: "user", content: "before restart", timestamp: 0 });
47
+ manager.appendMessage({
48
+ role: "assistant",
49
+ content: [{ type: "text", text: "persisted reply" }],
50
+ timestamp: 0,
51
+ } as never);
52
+ }
53
+
54
+ class RestartBackend implements PiBackend {
55
+ readonly prompts: string[] = [];
56
+ readonly #listeners = new Set<(event: AgentSessionEvent) => void>();
57
+
58
+ subscribe(listener: (event: AgentSessionEvent) => void): () => void {
59
+ this.#listeners.add(listener);
60
+ return () => this.#listeners.delete(listener);
61
+ }
62
+
63
+ async prompt(text: string): Promise<void> {
64
+ this.prompts.push(text);
65
+ for (const listener of this.#listeners) {
66
+ listener({ type: "agent_start" } as AgentSessionEvent);
67
+ listener({ type: "agent_settled" } as AgentSessionEvent);
68
+ }
69
+ }
70
+
71
+ async steer(): Promise<void> {}
72
+ async abort(): Promise<void> {}
73
+ }
74
+
75
+ describe("reconnect", () => {
76
+ let harness: Harness;
77
+
78
+ before(async () => {
79
+ // A tiny buffer makes the eviction path cheap to exercise.
80
+ harness = await startHarness({ replayBufferCapacity: 4 });
81
+ });
82
+
83
+ after(async () => {
84
+ await harness.dispose();
85
+ });
86
+
87
+ it("keeps non-VS Code and explicitly misrouted reconnects strict", async () => {
88
+ const client = await harness.connect();
89
+ for (const [context, params] of [
90
+ [
91
+ "non-VS Code reconnect without a channel",
92
+ {
93
+ clientId: `${CLIENT_ID}-missing-channel`,
94
+ lastSeenServerSeq: 0,
95
+ subscriptions: [ROOT_CHANNEL],
96
+ },
97
+ ],
98
+ [
99
+ "VS Code reconnect with the wrong channel",
100
+ {
101
+ channel: sessionUri("wrong-channel"),
102
+ clientId: `${CLIENT_ID}-wrong-channel`,
103
+ lastSeenServerSeq: 0,
104
+ subscriptions: [ROOT_CHANNEL],
105
+ _meta: { "vscode.telemetryLevel": "off" },
106
+ },
107
+ ],
108
+ ] as const) {
109
+ const error = await expectRpcError(
110
+ client.request("reconnect", params as never),
111
+ JsonRpcErrorCodes.InvalidParams,
112
+ context,
113
+ );
114
+ assert.match(error.message, /reconnect requires channel ahp-root:\/\/$/u, context);
115
+ }
116
+ });
117
+
118
+ it("replays only the actions the client missed", async () => {
119
+ const first = await harness.connect();
120
+ const init = await first.initialize({
121
+ clientId: CLIENT_ID,
122
+ protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
123
+ initialSubscriptions: [ROOT_CHANNEL],
124
+ });
125
+
126
+ bumpActiveSessions(harness, 1);
127
+ bumpActiveSessions(harness, 2);
128
+ await first.shutdown();
129
+
130
+ const resumed = await harness.connect();
131
+ const result = (await resumed.reconnect({
132
+ clientId: CLIENT_ID,
133
+ lastSeenServerSeq: init.serverSeq,
134
+ subscriptions: [ROOT_CHANNEL],
135
+ })) as ReconnectReplayResult;
136
+
137
+ assert.equal(result.type, ReconnectResultType.Replay);
138
+ assert.equal(result.actions.length, 2);
139
+ assert.deepEqual(
140
+ result.actions.map((envelope) => envelope.serverSeq),
141
+ [init.serverSeq + 1, init.serverSeq + 2],
142
+ );
143
+ assert.deepEqual(result.missing, []);
144
+ });
145
+
146
+ it("falls back to snapshots when the gap predates the replay buffer", async () => {
147
+ const client = await harness.connect();
148
+ await client.initialize({
149
+ clientId: `${CLIENT_ID}-gap`,
150
+ protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
151
+ initialSubscriptions: [ROOT_CHANNEL],
152
+ });
153
+
154
+ const staleSeq = harness.host.serverSeq;
155
+ // Overflow the 4-entry buffer so `staleSeq + 1` is evicted.
156
+ for (let i = 0; i < 6; i++) {
157
+ bumpActiveSessions(harness, i);
158
+ }
159
+
160
+ await client.shutdown();
161
+ const resumed = await harness.connect();
162
+ const result = (await resumed.reconnect({
163
+ clientId: `${CLIENT_ID}-gap`,
164
+ lastSeenServerSeq: staleSeq,
165
+ subscriptions: [ROOT_CHANNEL],
166
+ })) as ReconnectSnapshotResult;
167
+
168
+ assert.equal(result.type, ReconnectResultType.Snapshot);
169
+ assert.equal(result.snapshots.length, 1);
170
+ assert.equal(must(result.snapshots[0]).resource, ROOT_CHANNEL);
171
+ assert.equal(must(result.snapshots[0]).fromSeq, harness.host.serverSeq);
172
+ });
173
+
174
+ it("reports subscriptions it cannot resume as missing", async () => {
175
+ const client = await harness.connect();
176
+ const init = await client.initialize({
177
+ clientId: `${CLIENT_ID}-missing`,
178
+ protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
179
+ initialSubscriptions: [ROOT_CHANNEL],
180
+ });
181
+
182
+ const disposed = sessionUri("already-gone");
183
+ await client.shutdown();
184
+ const resumed = await harness.connect();
185
+ const result = (await resumed.reconnect({
186
+ clientId: `${CLIENT_ID}-missing`,
187
+ lastSeenServerSeq: init.serverSeq,
188
+ subscriptions: [ROOT_CHANNEL, disposed],
189
+ })) as ReconnectReplayResult;
190
+
191
+ assert.equal(result.type, ReconnectResultType.Replay);
192
+ assert.deepEqual(result.missing, [disposed]);
193
+ });
194
+
195
+ it("returns an empty replay for a client that is already current", async () => {
196
+ const client = await harness.connect();
197
+ await client.initialize({
198
+ clientId: `${CLIENT_ID}-current`,
199
+ protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
200
+ initialSubscriptions: [ROOT_CHANNEL],
201
+ });
202
+
203
+ await client.shutdown();
204
+ const resumed = await harness.connect();
205
+ const result = (await resumed.reconnect({
206
+ clientId: `${CLIENT_ID}-current`,
207
+ lastSeenServerSeq: harness.host.serverSeq,
208
+ subscriptions: [ROOT_CHANNEL],
209
+ })) as ReconnectReplayResult;
210
+
211
+ assert.equal(result.type, ReconnectResultType.Replay);
212
+ assert.deepEqual(result.actions, []);
213
+ });
214
+
215
+ it("replaces a half-open connection that reuses a clientId", async () => {
216
+ const clientId = `${CLIENT_ID}-duplicate`;
217
+ const subscribersBefore = harness.host.subscriberCount(ROOT_CHANNEL);
218
+ const first = await harness.connect();
219
+ await first.initialize({ clientId, protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
220
+ await first.subscribe(ROOT_CHANNEL);
221
+ assert.equal(harness.host.subscriberCount(ROOT_CHANNEL), subscribersBefore + 1);
222
+
223
+ const replacement = await harness.connect();
224
+ await replacement.initialize({ clientId, protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
225
+ await replacement.subscribe(ROOT_CHANNEL);
226
+
227
+ assert.equal(harness.host.subscriberCount(ROOT_CHANNEL), subscribersBefore + 1);
228
+ });
229
+
230
+ it("retains clientInfo when a replacement initialize omits it", async () => {
231
+ const clientId = `${CLIENT_ID}-reinitialize`;
232
+ const id = "vscode-reinitialize";
233
+ const uri = sessionUri(id);
234
+ const clientUri = `pi:/${id}`;
235
+ harness.host.store.create(uri, initialSessionState("pi", "Reinitialize", "/tmp"), "session");
236
+
237
+ const first = await harness.connect();
238
+ await first.request("initialize", {
239
+ channel: ROOT_CHANNEL,
240
+ clientId,
241
+ protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
242
+ clientInfo: { name: "vscode-editor-window" },
243
+ });
244
+ await first.shutdown();
245
+
246
+ const replacement = await harness.connect();
247
+ await replacement.initialize({ clientId, protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
248
+ assert.equal((await replacement.subscribe(clientUri)).result.snapshot?.resource, clientUri);
249
+ });
250
+
251
+ it("infers the provider URI dialect without clientInfo", async () => {
252
+ const clientId = `${CLIENT_ID}-provider-alias`;
253
+ const id = "provider-alias-reconnect";
254
+ const canonical = sessionUri(id);
255
+ const alias = `pi:/${id}`;
256
+ harness.host.store.create(canonical, initialSessionState("pi", "Reconnect", "/tmp"), "session");
257
+
258
+ const first = await harness.connect();
259
+ await first.initialize({ clientId, protocolVersions: SUPPORTED_PROTOCOL_VERSIONS });
260
+ assert.equal((await first.subscribe(alias)).result.snapshot?.resource, alias);
261
+ const staleSeq = harness.host.serverSeq;
262
+ await first.shutdown();
263
+ for (let i = 0; i < 6; i++) bumpActiveSessions(harness, i);
264
+
265
+ const resumed = await harness.connect();
266
+ const result = (await resumed.reconnect({
267
+ clientId,
268
+ lastSeenServerSeq: staleSeq,
269
+ subscriptions: [alias],
270
+ })) as ReconnectSnapshotResult;
271
+
272
+ assert.equal(result.type, ReconnectResultType.Snapshot);
273
+ assert.deepEqual(
274
+ result.snapshots.map((snapshot) => snapshot.resource),
275
+ [alias],
276
+ );
277
+ assert.equal(harness.host.store.has(canonical), true);
278
+ assert.equal(harness.host.store.has(alias), false);
279
+ });
280
+
281
+ it("retains VS Code's URI dialect across connections", async () => {
282
+ const clientId = `${CLIENT_ID}-vscode`;
283
+ const id = "vscode-reconnect";
284
+ const uri = sessionUri(id);
285
+ const clientUri = `pi:/${id}`;
286
+ harness.host.store.create(uri, initialSessionState("pi", "Reconnect", "/tmp"), "session");
287
+
288
+ const first = await harness.connect();
289
+ await first.request("initialize", {
290
+ channel: ROOT_CHANNEL,
291
+ clientId,
292
+ protocolVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
293
+ clientInfo: { name: "vscode-editor-window" },
294
+ });
295
+ assert.equal((await first.subscribe(clientUri)).result.snapshot?.resource, clientUri);
296
+ const staleSeq = harness.host.serverSeq;
297
+ await first.shutdown();
298
+
299
+ for (let i = 0; i < 6; i++) bumpActiveSessions(harness, i);
300
+
301
+ const resumed = await harness.connect();
302
+ const result = (await resumed.reconnect({
303
+ clientId,
304
+ lastSeenServerSeq: staleSeq,
305
+ subscriptions: [clientUri],
306
+ })) as ReconnectSnapshotResult;
307
+
308
+ assert.equal(result.type, ReconnectResultType.Snapshot);
309
+ assert.deepEqual(
310
+ result.snapshots.map((snapshot) => snapshot.resource),
311
+ [clientUri],
312
+ );
313
+ });
314
+ });
315
+
316
+ describe("reconnect after host restart", () => {
317
+ it("repairs VS Code's channel-less reconnect and restores a durable session", async () => {
318
+ const root = mkdtempSync(join(tmpdir(), "pi-ahp-restart-sessions-"));
319
+ const workspace = mkdtempSync(join(tmpdir(), "pi-ahp-restart-cwd-"));
320
+ const id = randomUUID();
321
+ writeDurableSession(root, id, workspace);
322
+ const backend = new RestartBackend();
323
+ const harness = await startHarness({
324
+ sessions: true,
325
+ sessionRoot: root,
326
+ workingDirectory: workspace,
327
+ createBackend: () => backend,
328
+ });
329
+
330
+ try {
331
+ const clientSession = `pi:/${id}`;
332
+ const clientChat = `ahp-chat://default/${Buffer.from(clientSession).toString("base64url")}`;
333
+ const client = await harness.connect();
334
+ const result = (await client.request("reconnect", {
335
+ clientId: "vscode-from-previous-host",
336
+ lastSeenServerSeq: 42,
337
+ subscriptions: [ROOT_CHANNEL, clientSession],
338
+ _meta: { "vscode.telemetryLevel": "off" },
339
+ } as never)) as ReconnectSnapshotResult;
340
+
341
+ assert.equal(result.type, ReconnectResultType.Snapshot);
342
+ assert.deepEqual(
343
+ result.snapshots.map((snapshot) => snapshot.resource).sort(),
344
+ [ROOT_CHANNEL, clientSession].sort(),
345
+ );
346
+ const sessionSnapshot = must(
347
+ result.snapshots.find((snapshot) => snapshot.resource === clientSession),
348
+ "VS Code session snapshot",
349
+ );
350
+ assert.equal((sessionSnapshot.state as SessionState).defaultChat, clientChat);
351
+
352
+ const disposalError = await expectRpcError(
353
+ client.request("disposeSession", { channel: clientSession }),
354
+ JsonRpcErrorCodes.InvalidRequest,
355
+ );
356
+ assert.match(disposalError.message, /VS Code provisional-session lifecycle bug/u);
357
+ assert.equal(harness.host.store.has(sessionUri(id)), true);
358
+
359
+ const subscribed = await client.subscribe(clientChat);
360
+ const chatSnapshot = must(subscribed.result.snapshot);
361
+ const restoredTurn = (chatSnapshot.state as ChatState).turns[0];
362
+ assert.equal(restoredTurn?.message.text, "before restart");
363
+ assert.equal(
364
+ restoredTurn?.responseParts.find((part) => part.kind === ResponsePartKind.Markdown)?.content,
365
+ "persisted reply",
366
+ );
367
+ assert.equal(restoredTurn?.state, TurnState.Complete);
368
+ assert.equal(harness.host.store.has(sessionUri(id)), true);
369
+ assert.equal(harness.host.store.has(chatUri(id)), true);
370
+
371
+ client.dispatch(clientChat, {
372
+ type: ActionType.ChatTurnStarted,
373
+ turnId: "after-restart",
374
+ startedAt: new Date().toISOString(),
375
+ message: { text: "continue after restart", origin: { kind: MessageKind.User } },
376
+ });
377
+ await eventually("the post-restart prompt to reach the backend", () => backend.prompts.length === 1);
378
+ assert.deepEqual(backend.prompts, ["continue after restart"]);
379
+ const state = harness.host.store.get(chatUri(id)) as ChatState;
380
+ assert.equal(state.activeTurn, undefined);
381
+ assert.equal(state.turns.at(-1)?.message.text, "continue after restart");
382
+ assert.equal(state.turns.at(-1)?.state, TurnState.Complete);
383
+ } finally {
384
+ await harness.dispose();
385
+ rmSync(root, { recursive: true, force: true });
386
+ rmSync(workspace, { recursive: true, force: true });
387
+ }
388
+ });
389
+ });
@@ -0,0 +1,78 @@
1
+ /** Deterministic watch policy tests: no OS events, WebSockets, or clocks. */
2
+ import assert from "node:assert/strict";
3
+ import { resolve } from "node:path";
4
+ import { describe, it } from "node:test";
5
+ import { ResourceChangeType as Change } from "@microsoft/agent-host-protocol";
6
+ import { isExcluded, matchesPatterns, mergeChange, relativeWatchPath } from "../src/pi/resource-watch-policy.ts";
7
+
8
+ describe("watch path scope and filters", () => {
9
+ it("normalizes descendants and rejects siblings and prefix lookalikes", () => {
10
+ const root = resolve("workspace");
11
+ assert.equal(relativeWatchPath(root, root), "");
12
+ assert.equal(relativeWatchPath(root, resolve(root, "nested/../file.txt")), "file.txt");
13
+ assert.equal(relativeWatchPath(root, resolve(root, "nested/file.txt")), "nested/file.txt");
14
+ for (const path of [resolve(root, ".."), resolve(root, "../sibling/file.txt"), `${root}-other/file.txt`]) {
15
+ assert.equal(relativeWatchPath(root, path), undefined, path);
16
+ }
17
+ });
18
+
19
+ it("keeps the watched root visible regardless of filters", () => {
20
+ assert.equal(isExcluded("", ["**"]), false);
21
+ assert.equal(matchesPatterns("", ["*.md"], ["**"]), true);
22
+ });
23
+
24
+ it("applies include alternatives and exclude precedence to every path", () => {
25
+ const cases: Array<[string, readonly string[], readonly string[], boolean]> = [
26
+ ["file.txt", [], [], true],
27
+ ["nested/file.md", ["**/*.md"], [], true],
28
+ ["nested/file.txt", ["**/*.md"], [], false],
29
+ ["file.ts", ["**/*.md", "**/*.ts"], [], true],
30
+ ["nested/file.md", ["**/*.md"], ["nested/**"], false],
31
+ ["node_modules/pkg/index.js", [], ["**/node_modules/**"], false],
32
+ ["nested/node_modules/pkg/index.js", [], ["**/node_modules/**"], false],
33
+ ["node_modules", [], ["**/node_modules/**"], true],
34
+ [".git/config", [], ["**/.git/**"], false],
35
+ ["src/.hidden.ts", ["**/*.ts"], [], false],
36
+ ["src/.hidden.ts", ["**/.*.ts"], [], true],
37
+ ];
38
+ for (const [path, includes, excludes, expected] of cases) {
39
+ assert.equal(matchesPatterns(path, includes, excludes), expected, JSON.stringify({ path, includes, excludes }));
40
+ }
41
+ });
42
+
43
+ it("does not prune a directory just because its children alone match the include filter", () => {
44
+ assert.equal(matchesPatterns("src", ["**/*.ts"], []), false);
45
+ assert.equal(isExcluded("src", []), false);
46
+ assert.equal(matchesPatterns("src/file.ts", ["**/*.ts"], []), true);
47
+ assert.equal(isExcluded("src/generated", ["**/generated"]), true);
48
+ });
49
+ });
50
+
51
+ describe("watch batch transitions", () => {
52
+ const next = [Change.Added, Change.Updated, Change.Deleted];
53
+ const rows: Array<[Change | undefined, Array<Change | undefined>]> = [
54
+ [undefined, [Change.Added, Change.Updated, Change.Deleted]],
55
+ [Change.Added, [Change.Added, Change.Added, undefined]],
56
+ [Change.Updated, [Change.Updated, Change.Updated, Change.Deleted]],
57
+ [Change.Deleted, [Change.Updated, Change.Updated, Change.Deleted]],
58
+ ];
59
+ for (const [previous, expected] of rows) {
60
+ it(`coalesces ${previous ?? "empty"} followed by each native event type`, () => {
61
+ for (const [index, change] of next.entries())
62
+ assert.equal(mergeChange(previous, change), expected[index], change);
63
+ });
64
+ }
65
+
66
+ it("handles replacement and transient paths across longer sequences", () => {
67
+ const cases: Array<[Change[], Change | undefined]> = [
68
+ [[Change.Added, Change.Updated, Change.Deleted], undefined],
69
+ [[Change.Added, Change.Deleted, Change.Added], Change.Added],
70
+ [[Change.Deleted, Change.Added, Change.Deleted], Change.Deleted],
71
+ [[Change.Deleted, Change.Added, Change.Deleted, Change.Added], Change.Updated],
72
+ [[Change.Updated, Change.Deleted, Change.Added], Change.Updated],
73
+ ];
74
+ for (const [sequence, expected] of cases) {
75
+ assert.equal(sequence.reduce<Change | undefined>(mergeChange, undefined), expected, sequence.join(" → "));
76
+ }
77
+ });
78
+ });