@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,31 @@
1
+ package pisession
2
+
3
+ import (
4
+ "bufio"
5
+ "encoding/json"
6
+ "strings"
7
+ )
8
+
9
+ // ParseLine decodes one session-file line; blank and malformed lines report false.
10
+ func ParseLine(line string) (Entry, bool) {
11
+ line = strings.TrimSpace(line)
12
+ if line == "" {
13
+ return nil, false
14
+ }
15
+ var e Entry
16
+ if err := json.Unmarshal([]byte(line), &e); err != nil || e == nil {
17
+ return nil, false
18
+ }
19
+ return e, true
20
+ }
21
+
22
+ func writeLine(w *bufio.Writer, e Entry) error {
23
+ raw, err := json.Marshal(e)
24
+ if err != nil {
25
+ return err
26
+ }
27
+ if _, err := w.Write(raw); err != nil {
28
+ return err
29
+ }
30
+ return w.WriteByte('\n')
31
+ }
@@ -0,0 +1,580 @@
1
+ // Package pisession reads and writes Pi's session files (JSONL, version 3): a header line, then
2
+ // entries forming a tree by id/parentId. It ports the parts of Pi's SessionManager
3
+ // (dist/core/session-manager.js, 0.87.1) that pi-ahp uses; the format is Pi's, so files it writes
4
+ // open in Pi and files Pi writes load here.
5
+ //
6
+ // Entries are kept as decoded JSON: Pi's session format is open-ended (extensions add custom
7
+ // entry types) and this package must round-trip what it does not understand.
8
+ package pisession
9
+
10
+ import (
11
+ "bufio"
12
+ "crypto/rand"
13
+ "encoding/hex"
14
+ "errors"
15
+ "fmt"
16
+ "os"
17
+ "path/filepath"
18
+ "regexp"
19
+ "strings"
20
+ "sync"
21
+ "time"
22
+ )
23
+
24
+ // CurrentVersion is the session format version this package writes.
25
+ const CurrentVersion = 3
26
+
27
+ // Entry is one session entry (or the header) as decoded JSON.
28
+ type Entry map[string]any
29
+
30
+ // Type is the entry's `type`.
31
+ func (e Entry) Type() string { s, _ := e["type"].(string); return s }
32
+
33
+ // ID is the entry id.
34
+ func (e Entry) ID() string { s, _ := e["id"].(string); return s }
35
+
36
+ // ParentID is the parent entry id ("" for a root).
37
+ func (e Entry) ParentID() string { s, _ := e["parentId"].(string); return s }
38
+
39
+ // Timestamp is the entry's ISO timestamp.
40
+ func (e Entry) Timestamp() string { s, _ := e["timestamp"].(string); return s }
41
+
42
+ // Message is the message of a "message" entry.
43
+ func (e Entry) Message() map[string]any {
44
+ if e.Type() != "message" {
45
+ return nil
46
+ }
47
+ m, _ := e["message"].(map[string]any)
48
+ return m
49
+ }
50
+
51
+ // Role is the role of a "message" entry's message.
52
+ func (e Entry) Role() string {
53
+ s, _ := e.Message()["role"].(string)
54
+ return s
55
+ }
56
+
57
+ var validID = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$`)
58
+
59
+ // ValidateID applies Pi's session id rules.
60
+ func ValidateID(id string) error {
61
+ if !validID.MatchString(id) {
62
+ return errors.New("Session id must be non-empty, contain only alphanumeric characters, '-', '_', and '.', and start and end with an alphanumeric character")
63
+ }
64
+ return nil
65
+ }
66
+
67
+ // Options for a new session.
68
+ type Options struct {
69
+ // ID of the new session; generated when empty.
70
+ ID string
71
+ ParentSession string
72
+ }
73
+
74
+ // Manager is a session: its entry tree, a leaf pointer and (optionally) its file.
75
+ //
76
+ // Like Pi's, a persistent session withholds its file until the first assistant message exists,
77
+ // then writes every entry so far in one go and appends from there on.
78
+ type Manager struct {
79
+ mu sync.Mutex
80
+ cwd string
81
+ dir string
82
+ file string
83
+ persist bool
84
+ flushed bool
85
+ sessionID string
86
+ entries []Entry // header first
87
+ byID map[string]Entry
88
+ leafID string
89
+ now func() time.Time
90
+ }
91
+
92
+ func newID() string {
93
+ var b [16]byte
94
+ _, _ = rand.Read(b[:])
95
+ return hex.EncodeToString(b[:])
96
+ }
97
+
98
+ func uuidLike() string {
99
+ b := make([]byte, 16)
100
+ _, _ = rand.Read(b)
101
+ b[6] = (b[6] & 0x0f) | 0x70
102
+ b[8] = (b[8] & 0x3f) | 0x80
103
+ h := hex.EncodeToString(b)
104
+ return h[0:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:32]
105
+ }
106
+
107
+ // InMemory creates a session that never touches disk.
108
+ func InMemory(cwd string, opts Options) (*Manager, error) {
109
+ return create(cwd, "", false, opts)
110
+ }
111
+
112
+ // Create makes a persistent session whose file will live under dir.
113
+ func Create(cwd, dir string, opts Options) (*Manager, error) {
114
+ return create(cwd, dir, true, opts)
115
+ }
116
+
117
+ func create(cwd, dir string, persist bool, opts Options) (*Manager, error) {
118
+ if opts.ID != "" {
119
+ if err := ValidateID(opts.ID); err != nil {
120
+ return nil, err
121
+ }
122
+ }
123
+ abs, err := filepath.Abs(cwd)
124
+ if err != nil {
125
+ abs = cwd
126
+ }
127
+ m := &Manager{cwd: abs, dir: dir, persist: persist, now: time.Now, byID: map[string]Entry{}}
128
+ if persist && dir != "" {
129
+ if err := os.MkdirAll(dir, 0o755); err != nil {
130
+ return nil, err
131
+ }
132
+ }
133
+ m.newSession(opts)
134
+ return m, nil
135
+ }
136
+
137
+ func (m *Manager) timestamp() string { return m.now().UTC().Format("2006-01-02T15:04:05.000Z") }
138
+
139
+ func (m *Manager) newSession(opts Options) {
140
+ m.sessionID = opts.ID
141
+ if m.sessionID == "" {
142
+ m.sessionID = uuidLike()
143
+ }
144
+ ts := m.timestamp()
145
+ header := Entry{"type": "session", "version": float64(CurrentVersion), "id": m.sessionID, "timestamp": ts, "cwd": m.cwd}
146
+ if opts.ParentSession != "" {
147
+ header["parentSession"] = opts.ParentSession
148
+ }
149
+ m.entries = []Entry{header}
150
+ m.byID = map[string]Entry{}
151
+ m.leafID = ""
152
+ m.flushed = false
153
+ if m.persist {
154
+ stamp := strings.NewReplacer(":", "-", ".", "-").Replace(ts)
155
+ m.file = filepath.Join(m.dir, stamp+"_"+m.sessionID+".jsonl")
156
+ }
157
+ }
158
+
159
+ // Open loads an existing session file, appending to it from here on.
160
+ func Open(path string) (*Manager, error) {
161
+ entries, err := LoadEntries(path)
162
+ if err != nil {
163
+ return nil, err
164
+ }
165
+ if len(entries) == 0 {
166
+ return nil, fmt.Errorf("Session file is not a valid pi session: %s", path)
167
+ }
168
+ abs, _ := filepath.Abs(path)
169
+ m := &Manager{file: abs, dir: filepath.Dir(abs), persist: true, flushed: true, now: time.Now}
170
+ m.entries = entries
171
+ m.sessionID, _ = entries[0]["id"].(string)
172
+ m.cwd, _ = entries[0]["cwd"].(string)
173
+ m.buildIndex()
174
+ return m, nil
175
+ }
176
+
177
+ // LoadEntries reads a session file: header first. Malformed lines are skipped; a file whose first
178
+ // line is not a session header yields nothing.
179
+ func LoadEntries(path string) ([]Entry, error) {
180
+ f, err := os.Open(path)
181
+ if err != nil {
182
+ if os.IsNotExist(err) {
183
+ return nil, nil
184
+ }
185
+ return nil, err
186
+ }
187
+ defer f.Close()
188
+ var entries []Entry
189
+ r := bufio.NewReaderSize(f, 1<<20)
190
+ for {
191
+ line, err := r.ReadString('\n')
192
+ if e, ok := ParseLine(line); ok {
193
+ entries = append(entries, e)
194
+ }
195
+ if err != nil {
196
+ break
197
+ }
198
+ }
199
+ if len(entries) == 0 {
200
+ return nil, nil
201
+ }
202
+ if entries[0].Type() != "session" {
203
+ return nil, nil
204
+ }
205
+ if _, ok := entries[0]["id"].(string); !ok {
206
+ return nil, nil
207
+ }
208
+ migrate(entries)
209
+ return entries, nil
210
+ }
211
+
212
+ // migrate brings entries of an older format up to the current version (in place).
213
+ func migrate(entries []Entry) {
214
+ version := 1.0
215
+ if v, ok := entries[0]["version"].(float64); ok {
216
+ version = v
217
+ }
218
+ if version >= CurrentVersion {
219
+ return
220
+ }
221
+ if version < 2 {
222
+ prev := ""
223
+ ids := map[string]bool{}
224
+ for i, e := range entries {
225
+ if e.Type() == "session" {
226
+ e["version"] = float64(2)
227
+ continue
228
+ }
229
+ id := newID()[:8]
230
+ for ids[id] {
231
+ id = newID()[:8]
232
+ }
233
+ ids[id] = true
234
+ e["id"] = id
235
+ if prev == "" {
236
+ e["parentId"] = nil
237
+ } else {
238
+ e["parentId"] = prev
239
+ }
240
+ prev = id
241
+ if e.Type() == "compaction" {
242
+ if idx, ok := e["firstKeptEntryIndex"].(float64); ok {
243
+ if int(idx) >= 0 && int(idx) < len(entries) && entries[int(idx)].Type() != "session" {
244
+ e["firstKeptEntryId"] = entries[int(idx)]["id"]
245
+ }
246
+ delete(e, "firstKeptEntryIndex")
247
+ }
248
+ }
249
+ _ = i
250
+ }
251
+ }
252
+ for _, e := range entries {
253
+ if e.Type() == "session" {
254
+ e["version"] = float64(CurrentVersion)
255
+ continue
256
+ }
257
+ if msg := e.Message(); msg != nil && msg["role"] == "hookMessage" {
258
+ msg["role"] = "custom"
259
+ }
260
+ }
261
+ }
262
+
263
+ func (m *Manager) buildIndex() {
264
+ m.byID = map[string]Entry{}
265
+ m.leafID = ""
266
+ for _, e := range m.entries {
267
+ if e.Type() == "session" {
268
+ continue
269
+ }
270
+ m.byID[e.ID()] = e
271
+ m.leafID = e.ID()
272
+ }
273
+ }
274
+
275
+ // SessionID is the session's id.
276
+ func (m *Manager) SessionID() string { return m.sessionID }
277
+
278
+ // Cwd is the working directory the session was made for.
279
+ func (m *Manager) Cwd() string { return m.cwd }
280
+
281
+ // File is the session file path; "" for an in-memory session. It may not exist yet.
282
+ func (m *Manager) File() string { return m.file }
283
+
284
+ // Header is the session header.
285
+ func (m *Manager) Header() Entry {
286
+ m.mu.Lock()
287
+ defer m.mu.Unlock()
288
+ return m.entries[0]
289
+ }
290
+
291
+ func (m *Manager) hasAssistant() bool {
292
+ for _, e := range m.entries {
293
+ if e.Role() == "assistant" {
294
+ return true
295
+ }
296
+ }
297
+ return false
298
+ }
299
+
300
+ func (m *Manager) writeAll() error {
301
+ f, err := os.OpenFile(m.file, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
302
+ if err != nil {
303
+ return err
304
+ }
305
+ defer f.Close()
306
+ w := bufio.NewWriter(f)
307
+ for _, e := range m.entries {
308
+ if err := writeLine(w, e); err != nil {
309
+ return err
310
+ }
311
+ }
312
+ return w.Flush()
313
+ }
314
+
315
+ func (m *Manager) appendLine(e Entry) error {
316
+ f, err := os.OpenFile(m.file, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
317
+ if err != nil {
318
+ return err
319
+ }
320
+ defer f.Close()
321
+ w := bufio.NewWriter(f)
322
+ if err := writeLine(w, e); err != nil {
323
+ return err
324
+ }
325
+ return w.Flush()
326
+ }
327
+
328
+ func (m *Manager) persistEntry(e Entry) error {
329
+ if !m.persist || m.file == "" {
330
+ return nil
331
+ }
332
+ if !m.hasAssistant() {
333
+ if m.flushed {
334
+ return m.appendLine(e)
335
+ }
336
+ return nil
337
+ }
338
+ if !m.flushed {
339
+ if err := m.writeAll(); err != nil {
340
+ return err
341
+ }
342
+ m.flushed = true
343
+ return nil
344
+ }
345
+ return m.appendLine(e)
346
+ }
347
+
348
+ func (m *Manager) generateID() string {
349
+ for i := 0; i < 100; i++ {
350
+ id := newID()[:8]
351
+ if _, taken := m.byID[id]; !taken {
352
+ return id
353
+ }
354
+ }
355
+ return uuidLike()
356
+ }
357
+
358
+ func (m *Manager) appendEntry(e Entry) (string, error) {
359
+ m.entries = append(m.entries, e)
360
+ m.byID[e.ID()] = e
361
+ m.leafID = e.ID()
362
+ return e.ID(), m.persistEntry(e)
363
+ }
364
+
365
+ func (m *Manager) child(typ string) Entry {
366
+ e := Entry{"type": typ, "id": m.generateID(), "timestamp": m.timestamp()}
367
+ if m.leafID == "" {
368
+ e["parentId"] = nil
369
+ } else {
370
+ e["parentId"] = m.leafID
371
+ }
372
+ return e
373
+ }
374
+
375
+ // AppendMessage appends a message as a child of the leaf and returns its entry id.
376
+ func (m *Manager) AppendMessage(message map[string]any) (string, error) {
377
+ m.mu.Lock()
378
+ defer m.mu.Unlock()
379
+ e := m.child("message")
380
+ e["message"] = message
381
+ return m.appendEntry(e)
382
+ }
383
+
384
+ // AppendSessionInfo records the session's display name.
385
+ func (m *Manager) AppendSessionInfo(name string) (string, error) {
386
+ m.mu.Lock()
387
+ defer m.mu.Unlock()
388
+ e := m.child("session_info")
389
+ e["name"] = strings.TrimSpace(strings.NewReplacer("\r\n", " ", "\n", " ", "\r", " ").Replace(name))
390
+ return m.appendEntry(e)
391
+ }
392
+
393
+ // AppendCompaction records a compaction.
394
+ func (m *Manager) AppendCompaction(summary, firstKeptEntryID string, tokensBefore int) (string, error) {
395
+ m.mu.Lock()
396
+ defer m.mu.Unlock()
397
+ e := m.child("compaction")
398
+ e["summary"], e["firstKeptEntryId"], e["tokensBefore"] = summary, firstKeptEntryID, float64(tokensBefore)
399
+ return m.appendEntry(e)
400
+ }
401
+
402
+ // Append appends an arbitrary entry of the given type as a child of the leaf; fields are merged in.
403
+ func (m *Manager) Append(typ string, fields map[string]any) (string, error) {
404
+ m.mu.Lock()
405
+ defer m.mu.Unlock()
406
+ e := m.child(typ)
407
+ for k, v := range fields {
408
+ e[k] = v
409
+ }
410
+ return m.appendEntry(e)
411
+ }
412
+
413
+ // SessionName is the latest session_info name, "" if none (an empty name clears it).
414
+ func (m *Manager) SessionName() string {
415
+ m.mu.Lock()
416
+ defer m.mu.Unlock()
417
+ for i := len(m.entries) - 1; i >= 0; i-- {
418
+ if m.entries[i].Type() == "session_info" {
419
+ s, _ := m.entries[i]["name"].(string)
420
+ return strings.TrimSpace(s)
421
+ }
422
+ }
423
+ return ""
424
+ }
425
+
426
+ // LeafID is the current leaf ("" before any entry).
427
+ func (m *Manager) LeafID() string { m.mu.Lock(); defer m.mu.Unlock(); return m.leafID }
428
+
429
+ // LeafEntry is the current leaf entry, or nil.
430
+ func (m *Manager) LeafEntry() Entry {
431
+ m.mu.Lock()
432
+ defer m.mu.Unlock()
433
+ return m.byID[m.leafID]
434
+ }
435
+
436
+ // Entry looks an entry up by id.
437
+ func (m *Manager) Entry(id string) (Entry, bool) {
438
+ m.mu.Lock()
439
+ defer m.mu.Unlock()
440
+ e, ok := m.byID[id]
441
+ return e, ok
442
+ }
443
+
444
+ // Entries are all entries except the header, in file order.
445
+ func (m *Manager) Entries() []Entry {
446
+ m.mu.Lock()
447
+ defer m.mu.Unlock()
448
+ return append([]Entry(nil), m.entries[1:]...)
449
+ }
450
+
451
+ // Branch is the path from the root to fromID (the leaf when empty).
452
+ func (m *Manager) Branch(fromID string) []Entry {
453
+ m.mu.Lock()
454
+ defer m.mu.Unlock()
455
+ if fromID == "" {
456
+ fromID = m.leafID
457
+ }
458
+ return pathTo(m.byID, fromID)
459
+ }
460
+
461
+ func pathTo(byID map[string]Entry, id string) []Entry {
462
+ var path []Entry
463
+ seen := map[string]bool{}
464
+ for cur, ok := byID[id]; ok && !seen[cur.ID()]; cur, ok = byID[cur.ParentID()] {
465
+ seen[cur.ID()] = true // a corrupt file must not loop forever
466
+ path = append(path, cur)
467
+ if cur.ParentID() == "" {
468
+ break
469
+ }
470
+ }
471
+ for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
472
+ path[i], path[j] = path[j], path[i]
473
+ }
474
+ return path
475
+ }
476
+
477
+ // SetLeaf moves the leaf to an existing entry ("" moves it before the first entry).
478
+ func (m *Manager) SetLeaf(id string) error {
479
+ m.mu.Lock()
480
+ defer m.mu.Unlock()
481
+ if id != "" {
482
+ if _, ok := m.byID[id]; !ok {
483
+ return fmt.Errorf("Entry %s not found", id)
484
+ }
485
+ }
486
+ m.leafID = id
487
+ return nil
488
+ }
489
+
490
+ // ContextEntries is the compaction-aware entry list along the current branch.
491
+ func (m *Manager) ContextEntries() []Entry {
492
+ m.mu.Lock()
493
+ defer m.mu.Unlock()
494
+ return BuildContextEntries(m.entries[1:], m.leafID, m.byID)
495
+ }
496
+
497
+ // BuildContextEntries follows the branch to leafID (the last entry when leafID is empty and
498
+ // byID has no such entry). If the path holds compaction entries, the latest is represented by the
499
+ // compaction entry itself, followed by the kept entries starting at firstKeptEntryId and every
500
+ // entry after it; older summarised entries are omitted.
501
+ func BuildContextEntries(entries []Entry, leafID string, byID map[string]Entry) []Entry {
502
+ if byID == nil {
503
+ byID = map[string]Entry{}
504
+ for _, e := range entries {
505
+ byID[e.ID()] = e
506
+ }
507
+ }
508
+ leaf, ok := byID[leafID]
509
+ if !ok || leafID == "" {
510
+ if leafID == "" && len(entries) > 0 {
511
+ return nil // an explicit "before any entry" leaf
512
+ }
513
+ if len(entries) == 0 {
514
+ return nil
515
+ }
516
+ leaf = entries[len(entries)-1]
517
+ }
518
+ path := pathTo(byID, leaf.ID())
519
+ compactionIdx := -1
520
+ for i, e := range path {
521
+ if e.Type() == "compaction" {
522
+ compactionIdx = i
523
+ }
524
+ }
525
+ if compactionIdx < 0 {
526
+ return path
527
+ }
528
+ compaction := path[compactionIdx]
529
+ out := []Entry{compaction}
530
+ found := false
531
+ kept, _ := compaction["firstKeptEntryId"].(string)
532
+ for _, e := range path[:compactionIdx] {
533
+ if e.ID() == kept {
534
+ found = true
535
+ }
536
+ if found && !(e.Type() == "message" && e.Role() == "system") {
537
+ out = append(out, e)
538
+ }
539
+ }
540
+ return append(out, path[compactionIdx+1:]...)
541
+ }
542
+
543
+ // ContextSettings is the thinking level and model in effect at the leaf: the last
544
+ // thinking_level_change, and the last model_change or assistant message, along the branch.
545
+ func (m *Manager) ContextSettings() (thinkingLevel, provider, modelID string) {
546
+ thinkingLevel = "off"
547
+ for _, e := range m.Branch("") {
548
+ switch {
549
+ case e.Type() == "thinking_level_change":
550
+ thinkingLevel, _ = e["thinkingLevel"].(string)
551
+ case e.Type() == "model_change":
552
+ provider, _ = e["provider"].(string)
553
+ modelID, _ = e["modelId"].(string)
554
+ case e.Role() == "assistant":
555
+ provider, _ = e.Message()["provider"].(string)
556
+ modelID, _ = e.Message()["model"].(string)
557
+ }
558
+ }
559
+ return thinkingLevel, provider, modelID
560
+ }
561
+
562
+ // FromEntries builds a read-only manager over entries obtained elsewhere (the host's session
563
+ // mirror), which carry no header: one is synthesised from id and cwd. The leaf is the last entry
564
+ // unless leafID names another. file is only reported by File.
565
+ func FromEntries(cwd, id, file string, entries []Entry, leafID string) (*Manager, error) {
566
+ timestamp := ""
567
+ if len(entries) > 0 {
568
+ timestamp = entries[0].Timestamp()
569
+ }
570
+ header := Entry{"type": "session", "version": CurrentVersion, "id": id, "timestamp": timestamp, "cwd": cwd}
571
+ m := &Manager{file: file, sessionID: id, cwd: cwd, now: time.Now, flushed: true}
572
+ m.entries = append([]Entry{header}, entries...)
573
+ m.buildIndex()
574
+ if leafID != "" {
575
+ if err := m.SetLeaf(leafID); err != nil {
576
+ return nil, err
577
+ }
578
+ }
579
+ return m, nil
580
+ }
@@ -0,0 +1,63 @@
1
+ package settings
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ )
8
+
9
+ // Access is what the settings file lets remote clients do beyond talking to the session. Every
10
+ // capability is off unless the file turns it on; a file without these keys grants nothing.
11
+ type Access struct {
12
+ // AllowedOrigins are browser origins that may connect (a request without an Origin header,
13
+ // from a native client, is always allowed).
14
+ AllowedOrigins []string
15
+ // Filesystem: browse, read, write and watch files. Roots default to the session's working
16
+ // directory; Unrestricted lifts the confinement and must be stated.
17
+ Filesystem struct {
18
+ Enabled bool
19
+ Roots []string
20
+ Unrestricted bool
21
+ }
22
+ // Terminals: shells on this machine, claimed by the client that starts them.
23
+ Terminals struct{ Enabled bool }
24
+ // AllowSessionDeletion lets a client delete other sessions' files. The running session can
25
+ // never be deleted by a client.
26
+ AllowSessionDeletion bool
27
+ }
28
+
29
+ // LoadAccess reads the optional capability keys of the settings file at path. The file must exist
30
+ // (LoadDirect creates it); keys of the wrong type are errors naming the file.
31
+ func LoadAccess(path string) (Access, error) {
32
+ raw, err := os.ReadFile(path)
33
+ if err != nil {
34
+ return Access{}, err
35
+ }
36
+ var stored struct {
37
+ AllowedOrigins []string `json:"allowedOrigins"`
38
+ Filesystem *struct {
39
+ Enabled bool `json:"enabled"`
40
+ Roots []string `json:"roots"`
41
+ Unrestricted bool `json:"unrestricted"`
42
+ } `json:"filesystem"`
43
+ Terminals *struct {
44
+ Enabled bool `json:"enabled"`
45
+ } `json:"terminals"`
46
+ AllowSessionDeletion bool `json:"allowSessionDeletion"`
47
+ }
48
+ if err := json.Unmarshal(raw, &stored); err != nil {
49
+ return Access{}, &Error{path, fmt.Sprintf("a capability setting has the wrong type (%v)", err)}
50
+ }
51
+ var out Access
52
+ out.AllowedOrigins = stored.AllowedOrigins
53
+ if stored.Filesystem != nil {
54
+ out.Filesystem.Enabled = stored.Filesystem.Enabled
55
+ out.Filesystem.Roots = stored.Filesystem.Roots
56
+ out.Filesystem.Unrestricted = stored.Filesystem.Unrestricted
57
+ }
58
+ if stored.Terminals != nil {
59
+ out.Terminals.Enabled = stored.Terminals.Enabled
60
+ }
61
+ out.AllowSessionDeletion = stored.AllowSessionDeletion
62
+ return out, nil
63
+ }