prompt_objects 0.6.0 → 0.7.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 (291) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/README.md +93 -22
  4. data/exe/poop +5 -0
  5. data/exe/prompt_objects +1 -1108
  6. data/exe/prompt_objects_mcp +5 -3
  7. data/lib/prompt_objects/capability.rb +33 -0
  8. data/lib/prompt_objects/cli/diagnostics_command.rb +69 -0
  9. data/lib/prompt_objects/cli/env_command.rb +435 -0
  10. data/lib/prompt_objects/cli/events_command.rb +145 -0
  11. data/lib/prompt_objects/cli/explore_command.rb +155 -0
  12. data/lib/prompt_objects/cli/helpers.rb +91 -0
  13. data/lib/prompt_objects/cli/message_command.rb +216 -0
  14. data/lib/prompt_objects/cli/repl_command.rb +366 -0
  15. data/lib/prompt_objects/cli/serve_command.rb +162 -0
  16. data/lib/prompt_objects/cli.rb +64 -422
  17. data/lib/prompt_objects/connectors/base.rb +16 -29
  18. data/lib/prompt_objects/connectors/mcp.rb +82 -98
  19. data/lib/prompt_objects/domain/records.rb +45 -0
  20. data/lib/prompt_objects/environment/exporter.rb +5 -4
  21. data/lib/prompt_objects/environment/git.rb +21 -1
  22. data/lib/prompt_objects/environment/importer.rb +5 -4
  23. data/lib/prompt_objects/environment/manager.rb +44 -6
  24. data/lib/prompt_objects/environment/manifest.rb +26 -4
  25. data/lib/prompt_objects/environment.rb +1337 -20
  26. data/lib/prompt_objects/execution/blocking_executor.rb +88 -0
  27. data/lib/prompt_objects/execution/context.rb +137 -0
  28. data/lib/prompt_objects/execution/contracts.rb +59 -0
  29. data/lib/prompt_objects/execution/permit_pool.rb +49 -0
  30. data/lib/prompt_objects/execution/policy.rb +47 -0
  31. data/lib/prompt_objects/execution/resource_lock_manager.rb +63 -0
  32. data/lib/prompt_objects/execution/runner.rb +498 -0
  33. data/lib/prompt_objects/execution/scheduler.rb +325 -0
  34. data/lib/prompt_objects/llm/factory.rb +4 -4
  35. data/lib/prompt_objects/llm/gemini_adapter.rb +25 -8
  36. data/lib/prompt_objects/llm/openai_adapter.rb +9 -1
  37. data/lib/prompt_objects/llm/pricing.rb +9 -2
  38. data/lib/prompt_objects/llm/response.rb +9 -4
  39. data/lib/prompt_objects/loader.rb +28 -1
  40. data/lib/prompt_objects/mcp/server.rb +6 -3
  41. data/lib/prompt_objects/mcp/tools/get_conversation.rb +15 -11
  42. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +2 -2
  43. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +3 -3
  44. data/lib/prompt_objects/mcp/tools/send_message.rb +42 -32
  45. data/lib/prompt_objects/operations/diagnostics.rb +115 -0
  46. data/lib/prompt_objects/operations/retention.rb +75 -0
  47. data/lib/prompt_objects/operations/run_trace_builder.rb +152 -0
  48. data/lib/prompt_objects/primitives/http_get.rb +2 -0
  49. data/lib/prompt_objects/primitives/list_files.rb +2 -0
  50. data/lib/prompt_objects/primitives/read_file.rb +2 -0
  51. data/lib/prompt_objects/primitives/think.rb +36 -0
  52. data/lib/prompt_objects/primitives/write_file.rb +9 -0
  53. data/lib/prompt_objects/prompt_object.rb +171 -519
  54. data/lib/prompt_objects/repositories/artifact_repository.rb +109 -0
  55. data/lib/prompt_objects/repositories/base_repository.rb +37 -0
  56. data/lib/prompt_objects/repositories/definition_repository.rb +36 -0
  57. data/lib/prompt_objects/repositories/env_data_repository.rb +103 -0
  58. data/lib/prompt_objects/repositories/event_repository.rb +105 -0
  59. data/lib/prompt_objects/repositories/message_repository.rb +73 -0
  60. data/lib/prompt_objects/repositories/run_repository.rb +254 -0
  61. data/lib/prompt_objects/repositories/thread_repository.rb +87 -0
  62. data/lib/prompt_objects/repositories/tool_execution_repository.rb +88 -0
  63. data/lib/prompt_objects/repositories/workspace_session_repository.rb +61 -0
  64. data/lib/prompt_objects/server/api/routes.rb +549 -53
  65. data/lib/prompt_objects/server/app.rb +133 -14
  66. data/lib/prompt_objects/server/file_watcher.rb +9 -0
  67. data/lib/prompt_objects/server/public/assets/index-BjKlODDD.js +90 -0
  68. data/lib/prompt_objects/server/public/assets/index-w3fpIb5c.css +1 -0
  69. data/lib/prompt_objects/server/public/index.html +2 -2
  70. data/lib/prompt_objects/server/websocket_handler.rb +202 -227
  71. data/lib/prompt_objects/server.rb +170 -1
  72. data/lib/prompt_objects/service.rb +129 -0
  73. data/lib/prompt_objects/session/store.rb +600 -13
  74. data/lib/prompt_objects/universal/add_capability.rb +1 -1
  75. data/lib/prompt_objects/universal/create_capability.rb +4 -2
  76. data/lib/prompt_objects/universal/delete_env_data.rb +23 -15
  77. data/lib/prompt_objects/universal/delete_primitive.rb +1 -1
  78. data/lib/prompt_objects/universal/dispatch_po.rb +54 -0
  79. data/lib/prompt_objects/universal/get_env_data.rb +8 -14
  80. data/lib/prompt_objects/universal/get_run_status.rb +30 -0
  81. data/lib/prompt_objects/universal/join_runs.rb +35 -0
  82. data/lib/prompt_objects/universal/list_env_data.rb +10 -14
  83. data/lib/prompt_objects/universal/modify_primitive.rb +1 -1
  84. data/lib/prompt_objects/universal/render_artifact.rb +91 -0
  85. data/lib/prompt_objects/universal/request_primitive.rb +1 -1
  86. data/lib/prompt_objects/universal/{store_env_data.rb → set_env_data.rb} +34 -20
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +33 -8
  89. data/objects/coordinator.md +8 -4
  90. data/templates/arc-agi-1/objects/data_manager.md +2 -2
  91. data/templates/arc-agi-1/objects/observer.md +2 -2
  92. data/templates/arc-agi-1/objects/solver.md +6 -5
  93. data/templates/arc-agi-1/objects/verifier.md +2 -2
  94. data/templates/arc-agi-3/manifest.yml +19 -0
  95. data/templates/arc-agi-3/objects/analyst.md +79 -0
  96. data/templates/arc-agi-3/objects/player.md +76 -0
  97. data/templates/arc-agi-3/objects/strategist.md +97 -0
  98. data/templates/arc-agi-3/primitives/action_log.rb +133 -0
  99. data/templates/arc-agi-3/primitives/arc3_api.rb +223 -0
  100. data/templates/arc-agi-3/primitives/arc3_scorecard.rb +131 -0
  101. data/templates/arc-agi-3/primitives/frame_diff.rb +143 -0
  102. data/templates/arc-agi-3/primitives/frame_info.rb +96 -0
  103. data/templates/arc-agi-3/primitives/render_frame.rb +140 -0
  104. data/templates/basic/objects/basic.md +11 -3
  105. data/templates/chiptune/extras/sonic_pi_boot.rb +220 -0
  106. data/templates/chiptune/manifest.yml +20 -0
  107. data/templates/chiptune/objects/conductor.md +184 -0
  108. data/templates/chiptune/objects/sonic_pi.md +65 -0
  109. data/templates/chiptune/objects/voice_bass.md +44 -0
  110. data/templates/chiptune/objects/voice_lead.md +44 -0
  111. data/templates/chiptune/objects/voice_pad.md +43 -0
  112. data/templates/chiptune/objects/voice_perc.md +45 -0
  113. data/templates/chiptune/primitives/play_note.rb +59 -0
  114. data/templates/chiptune/primitives/play_sequence.rb +55 -0
  115. data/templates/chiptune/primitives/query_sonic_pi.rb +141 -0
  116. data/templates/chiptune/primitives/set_synth.rb +57 -0
  117. data/templates/chiptune/primitives/set_tempo.rb +45 -0
  118. data/templates/chiptune/primitives/stop_voice.rb +46 -0
  119. data/templates/chiptune/primitives/support/osc_bridge.rb +33 -0
  120. data/templates/chiptune/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  121. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  122. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  123. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  124. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  125. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  126. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  127. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  128. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  129. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  130. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  131. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  132. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  133. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  134. data/templates/developer/objects/code_reviewer.md +1 -1
  135. data/templates/developer/objects/coordinator.md +16 -3
  136. data/templates/developer/objects/debugger.md +3 -3
  137. data/templates/empty/objects/assistant.md +10 -5
  138. data/templates/minimal/objects/assistant.md +10 -5
  139. data/templates/music/extras/sonic_pi_boot.rb +220 -0
  140. data/templates/music/manifest.yml +16 -0
  141. data/templates/music/objects/conductor.md +154 -0
  142. data/templates/music/objects/sonic_pi.md +65 -0
  143. data/templates/music/primitives/play_note.rb +55 -0
  144. data/templates/music/primitives/play_sequence.rb +51 -0
  145. data/templates/music/primitives/query_sonic_pi.rb +139 -0
  146. data/templates/music/primitives/set_synth.rb +53 -0
  147. data/templates/music/primitives/set_tempo.rb +43 -0
  148. data/templates/music/primitives/stop_voice.rb +44 -0
  149. data/templates/music/primitives/support/osc_bridge.rb +33 -0
  150. data/templates/music/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  151. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  152. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  153. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  154. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  155. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  156. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  157. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  158. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  159. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  160. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  161. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  162. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  163. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  164. data/templates/music-beginner/README.md +100 -0
  165. data/templates/music-beginner/extras/README.md +27 -0
  166. data/templates/music-beginner/extras/sonic_pi_boot.rb +297 -0
  167. data/templates/music-beginner/manifest.yml +25 -0
  168. data/templates/music-beginner/objects/conductor.md +222 -0
  169. data/templates/music-beginner/objects/engine_setup.md +54 -0
  170. data/templates/music-beginner/objects/voice_bass.md +43 -0
  171. data/templates/music-beginner/objects/voice_keys.md +48 -0
  172. data/templates/music-beginner/objects/voice_pad.md +46 -0
  173. data/templates/music-beginner/objects/voice_percussion.md +45 -0
  174. data/templates/music-beginner/objects/voice_texture.md +37 -0
  175. data/templates/music-beginner/primitives/play_pattern.rb +118 -0
  176. data/templates/music-beginner/primitives/query_music_engine.rb +94 -0
  177. data/templates/music-beginner/primitives/set_instrument.rb +47 -0
  178. data/templates/music-beginner/primitives/set_tempo.rb +43 -0
  179. data/templates/music-beginner/primitives/set_voice_level.rb +46 -0
  180. data/templates/music-beginner/primitives/stop_voice.rb +37 -0
  181. data/templates/music-beginner/primitives/support/osc_bridge.rb +25 -0
  182. data/templates/music-beginner/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  183. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  184. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  185. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  186. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  187. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  188. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  189. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  190. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  191. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  192. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  193. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  194. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  195. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  196. data/templates/music-pro/extras/AUDITION.md +100 -0
  197. data/templates/music-pro/extras/sonic_pi_boot.rb +546 -0
  198. data/templates/music-pro/manifest.yml +19 -0
  199. data/templates/music-pro/objects/conductor.md +126 -0
  200. data/templates/music-pro/objects/sonic_pi.md +35 -0
  201. data/templates/music-pro/objects/voice_harmony.md +48 -0
  202. data/templates/music-pro/objects/voice_rhythm.md +43 -0
  203. data/templates/music-pro/objects/voice_texture.md +42 -0
  204. data/templates/music-pro/primitives/define_patch.rb +46 -0
  205. data/templates/music-pro/primitives/query_music_engine.rb +69 -0
  206. data/templates/music-pro/primitives/set_live_pattern.rb +70 -0
  207. data/templates/music-pro/primitives/set_tempo.rb +31 -0
  208. data/templates/music-pro/primitives/stop_voice.rb +31 -0
  209. data/templates/music-pro/primitives/support/music_protocol.rb +136 -0
  210. data/templates/music-pro/primitives/support/osc_bridge.rb +33 -0
  211. data/templates/music-pro/primitives/trigger_event.rb +44 -0
  212. data/templates/music-pro/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  213. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  214. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  215. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  216. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  217. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  218. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  219. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  220. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  221. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  222. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  223. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  224. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  225. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  226. data/templates/writer/objects/coordinator.md +14 -3
  227. data/templates/writer/objects/editor.md +1 -1
  228. data/templates/writer/objects/researcher.md +2 -2
  229. data/tools/thread-explorer.html +1 -1
  230. metadata +172 -65
  231. data/CLAUDE.md +0 -177
  232. data/Gemfile +0 -10
  233. data/Rakefile +0 -27
  234. data/frontend/.gitignore +0 -3
  235. data/frontend/index.html +0 -17
  236. data/frontend/package-lock.json +0 -4540
  237. data/frontend/package.json +0 -36
  238. data/frontend/postcss.config.js +0 -6
  239. data/frontend/src/App.tsx +0 -104
  240. data/frontend/src/canvas/CanvasView.tsx +0 -113
  241. data/frontend/src/canvas/ForceLayout.ts +0 -115
  242. data/frontend/src/canvas/SceneManager.ts +0 -587
  243. data/frontend/src/canvas/canvasStore.ts +0 -47
  244. data/frontend/src/canvas/constants.ts +0 -95
  245. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  246. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  247. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  248. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  249. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  250. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  251. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  252. data/frontend/src/canvas/types.ts +0 -24
  253. data/frontend/src/components/ContextMenu.tsx +0 -68
  254. data/frontend/src/components/EnvDataPane.tsx +0 -69
  255. data/frontend/src/components/Inspector.tsx +0 -263
  256. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  257. data/frontend/src/components/MethodList.tsx +0 -90
  258. data/frontend/src/components/ModelSelector.tsx +0 -115
  259. data/frontend/src/components/NotificationPanel.tsx +0 -130
  260. data/frontend/src/components/ObjectList.tsx +0 -78
  261. data/frontend/src/components/PaneSlot.tsx +0 -30
  262. data/frontend/src/components/SourcePane.tsx +0 -202
  263. data/frontend/src/components/SystemBar.tsx +0 -74
  264. data/frontend/src/components/Transcript.tsx +0 -76
  265. data/frontend/src/components/UsagePanel.tsx +0 -105
  266. data/frontend/src/components/Workspace.tsx +0 -260
  267. data/frontend/src/components/index.ts +0 -12
  268. data/frontend/src/hooks/useResize.ts +0 -55
  269. data/frontend/src/hooks/useWebSocket.ts +0 -559
  270. data/frontend/src/index.css +0 -102
  271. data/frontend/src/main.tsx +0 -10
  272. data/frontend/src/store/index.ts +0 -311
  273. data/frontend/src/types/index.ts +0 -176
  274. data/frontend/tailwind.config.js +0 -44
  275. data/frontend/tsconfig.json +0 -30
  276. data/frontend/vite.config.ts +0 -29
  277. data/lib/prompt_objects/server/public/assets/index-DEPawnfZ.js +0 -4345
  278. data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +0 -1
  279. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  280. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  281. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  282. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  283. data/lib/prompt_objects/universal/think.rb +0 -41
  284. data/lib/prompt_objects/universal/update_env_data.rb +0 -88
  285. data/prompt_objects.gemspec +0 -50
  286. data/templates/basic/.gitignore +0 -2
  287. data/templates/developer/.gitignore +0 -5
  288. data/templates/empty/.gitignore +0 -5
  289. data/templates/empty/objects/.gitkeep +0 -0
  290. data/templates/minimal/.gitignore +0 -5
  291. data/templates/writer/.gitignore +0 -5
@@ -3,13 +3,16 @@
3
3
  require "sqlite3"
4
4
  require "json"
5
5
  require "securerandom"
6
+ require "monitor"
6
7
 
7
8
  module PromptObjects
8
9
  module Session
9
10
  # SQLite-based session storage for conversation history.
10
11
  # Each environment has its own sessions.db file (gitignored for privacy).
11
12
  class Store
12
- SCHEMA_VERSION = 7
13
+ SCHEMA_VERSION = 11
14
+
15
+ attr_reader :migration_backup_path
13
16
 
14
17
  # Thread types for conversation branching
15
18
  THREAD_TYPES = %w[root continuation delegation fork].freeze
@@ -20,6 +23,7 @@ module PromptObjects
20
23
  # @param db_path [String] Path to the SQLite database file
21
24
  def initialize(db_path)
22
25
  @db_path = db_path
26
+ @db_monitor = Monitor.new
23
27
  @db = SQLite3::Database.new(db_path)
24
28
  @db.results_as_hash = true
25
29
 
@@ -29,12 +33,27 @@ module PromptObjects
29
33
  # Set busy timeout to 5 seconds - wait for locks instead of failing immediately
30
34
  @db.busy_timeout = 5000
31
35
 
36
+ backup_before_destructive_migration
32
37
  setup_schema
38
+ @db.execute("PRAGMA foreign_keys=ON")
33
39
  end
34
40
 
35
41
  # Close the database connection.
36
42
  def close
37
- @db.close if @db
43
+ @db_monitor.synchronize { @db.close if @db }
44
+ end
45
+
46
+ # Repositories use this serialized connection boundary during the
47
+ # migration away from direct Store SQL. Monitor is reentrant so a
48
+ # repository transaction may safely call a repository read helper.
49
+ def with_connection
50
+ @db_monitor.synchronize { yield @db }
51
+ end
52
+
53
+ def transaction
54
+ @db_monitor.synchronize do
55
+ @db.transaction { yield @db }
56
+ end
38
57
  end
39
58
 
40
59
  # --- Session/Thread CRUD ---
@@ -51,13 +70,13 @@ module PromptObjects
51
70
  # @param thread_type [String] Type of thread: root, continuation, delegation, fork
52
71
  # @return [String] Session ID
53
72
  def create_session(po_name:, name: nil, source: "tui", source_client: nil, metadata: {},
54
- parent_session_id: nil, parent_message_id: nil, parent_po: nil, thread_type: "root")
55
- id = SecureRandom.uuid
73
+ parent_session_id: nil, parent_message_id: nil, parent_po: nil,
74
+ thread_type: "root", workspace_session_id: nil, id: SecureRandom.uuid)
56
75
  now = Time.now.utc.iso8601
57
76
 
58
- @db.execute(<<~SQL, [id, po_name, name, source, source_client, source, now, now, metadata.to_json, parent_session_id, parent_message_id, parent_po, thread_type])
59
- INSERT INTO sessions (id, po_name, name, source, source_client, last_message_source, created_at, updated_at, metadata, parent_session_id, parent_message_id, parent_po, thread_type)
60
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
77
+ @db.execute(<<~SQL, [id, po_name, name, source, source_client, source, now, now, metadata.to_json, parent_session_id, parent_message_id, parent_po, thread_type, workspace_session_id])
78
+ INSERT INTO sessions (id, po_name, name, source, source_client, last_message_source, created_at, updated_at, metadata, parent_session_id, parent_message_id, parent_po, thread_type, workspace_session_id)
79
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
61
80
  SQL
62
81
 
63
82
  id
@@ -165,6 +184,29 @@ module PromptObjects
165
184
  rows.map { |row| parse_session_row(row, include_count: true) }
166
185
  end
167
186
 
187
+ # List environment-level sessions (po_name IS NULL) — the shared-context
188
+ # "jam" sessions managed from the UI, most recent first.
189
+ # @param limit [Integer, nil] Maximum number of sessions
190
+ # @return [Array<Hash>] Session data with message counts
191
+ def list_environment_sessions(limit: nil)
192
+ sql = <<~SQL
193
+ SELECT s.*, COUNT(m.id) as message_count
194
+ FROM sessions s
195
+ LEFT JOIN messages m ON m.session_id = s.id
196
+ WHERE s.po_name IS NULL
197
+ GROUP BY s.id
198
+ ORDER BY s.updated_at DESC
199
+ SQL
200
+
201
+ params = []
202
+ if limit
203
+ sql += " LIMIT ?"
204
+ params << limit
205
+ end
206
+
207
+ @db.execute(sql, params).map { |row| parse_session_row(row, include_count: true) }
208
+ end
209
+
168
210
  # --- Thread Navigation ---
169
211
 
170
212
  # Get all child threads of a session.
@@ -409,10 +451,13 @@ module PromptObjects
409
451
  @db.execute("UPDATE sessions SET #{updates.join(', ')} WHERE id = ?", params)
410
452
  end
411
453
 
412
- # Delete a session and all its messages.
454
+ # Delete a session, its messages, and any env_data scoped to it.
455
+ # (env_data is keyed by root_thread_id; for a non-root sub-thread this
456
+ # DELETE is a harmless no-op since its data lives under the root.)
413
457
  # @param id [String] Session ID
414
458
  def delete_session(id)
415
459
  @db.execute("DELETE FROM messages WHERE session_id = ?", [id])
460
+ @db.execute("DELETE FROM env_data WHERE root_thread_id = ?", [id])
416
461
  @db.execute("DELETE FROM sessions WHERE id = ?", [id])
417
462
  end
418
463
 
@@ -1097,9 +1142,18 @@ module PromptObjects
1097
1142
 
1098
1143
  def create_schema
1099
1144
  @db.execute_batch(<<~SQL)
1145
+ CREATE TABLE IF NOT EXISTS workspace_sessions (
1146
+ id TEXT PRIMARY KEY,
1147
+ name TEXT,
1148
+ status TEXT NOT NULL DEFAULT 'active',
1149
+ created_at TEXT NOT NULL,
1150
+ updated_at TEXT NOT NULL,
1151
+ metadata TEXT NOT NULL DEFAULT '{}'
1152
+ );
1153
+
1100
1154
  CREATE TABLE IF NOT EXISTS sessions (
1101
1155
  id TEXT PRIMARY KEY,
1102
- po_name TEXT NOT NULL,
1156
+ po_name TEXT,
1103
1157
  name TEXT,
1104
1158
  source TEXT DEFAULT 'tui',
1105
1159
  source_client TEXT,
@@ -1111,13 +1165,24 @@ module PromptObjects
1111
1165
  parent_session_id TEXT,
1112
1166
  parent_message_id INTEGER,
1113
1167
  parent_po TEXT,
1114
- thread_type TEXT DEFAULT 'root'
1168
+ thread_type TEXT DEFAULT 'root',
1169
+ -- Environment-level sessions (v8)
1170
+ session_type TEXT DEFAULT 'environment',
1171
+ initiated_by TEXT,
1172
+ -- Run-based runtime foundation (v10)
1173
+ workspace_session_id TEXT REFERENCES workspace_sessions(id),
1174
+ parent_run_id TEXT,
1175
+ retention_policy TEXT,
1176
+ archived INTEGER NOT NULL DEFAULT 0,
1177
+ last_run_status TEXT,
1178
+ last_activity_at TEXT
1115
1179
  );
1116
1180
 
1117
1181
  CREATE INDEX IF NOT EXISTS idx_sessions_po_name ON sessions(po_name);
1118
1182
  CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at);
1119
1183
  CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
1120
1184
  CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
1185
+ CREATE INDEX IF NOT EXISTS idx_sessions_workspace ON sessions(workspace_session_id);
1121
1186
 
1122
1187
  CREATE TABLE IF NOT EXISTS messages (
1123
1188
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1128,10 +1193,21 @@ module PromptObjects
1128
1193
  tool_calls TEXT,
1129
1194
  tool_results TEXT,
1130
1195
  usage TEXT,
1131
- created_at TEXT NOT NULL
1196
+ created_at TEXT NOT NULL,
1197
+ -- Environment-level sessions (v8)
1198
+ target_po TEXT,
1199
+ from_source TEXT,
1200
+ -- Run provenance and stable logical ordering (v10)
1201
+ message_uid TEXT,
1202
+ run_id TEXT,
1203
+ turn_ordinal INTEGER,
1204
+ position INTEGER
1132
1205
  );
1133
1206
 
1134
1207
  CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id);
1208
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uid ON messages(message_uid) WHERE message_uid IS NOT NULL;
1209
+ CREATE INDEX IF NOT EXISTS idx_messages_run ON messages(run_id);
1210
+ CREATE INDEX IF NOT EXISTS idx_messages_turn ON messages(session_id, turn_ordinal, position);
1135
1211
 
1136
1212
  -- Full-text search index for message content
1137
1213
  CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
@@ -1169,20 +1245,143 @@ module PromptObjects
1169
1245
  CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
1170
1246
  CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
1171
1247
 
1172
- -- Shared environment data for delegation chains (v7)
1248
+ -- Shared environment data, scoped to session (v7 original, v8 adds session_id alias)
1173
1249
  CREATE TABLE IF NOT EXISTS env_data (
1174
1250
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1175
1251
  root_thread_id TEXT NOT NULL,
1252
+ session_id TEXT,
1176
1253
  key TEXT NOT NULL,
1177
1254
  short_description TEXT NOT NULL,
1178
1255
  value TEXT NOT NULL,
1179
1256
  stored_by TEXT NOT NULL,
1180
1257
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1181
1258
  updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
1259
+ workspace_session_id TEXT REFERENCES workspace_sessions(id),
1182
1260
  UNIQUE(root_thread_id, key)
1183
1261
  );
1184
1262
 
1185
1263
  CREATE INDEX IF NOT EXISTS idx_env_data_root ON env_data(root_thread_id);
1264
+ CREATE INDEX IF NOT EXISTS idx_env_data_session ON env_data(session_id);
1265
+ CREATE INDEX IF NOT EXISTS idx_env_data_workspace ON env_data(workspace_session_id);
1266
+
1267
+ CREATE TABLE IF NOT EXISTS runs (
1268
+ id TEXT PRIMARY KEY,
1269
+ client_request_id TEXT,
1270
+ workspace_session_id TEXT NOT NULL REFERENCES workspace_sessions(id),
1271
+ thread_id TEXT NOT NULL REFERENCES sessions(id),
1272
+ po_name TEXT NOT NULL,
1273
+ turn_ordinal INTEGER NOT NULL,
1274
+ input_content TEXT,
1275
+ input_message_id INTEGER REFERENCES messages(id),
1276
+ history_cutoff_message_id INTEGER REFERENCES messages(id),
1277
+ definition_version TEXT NOT NULL,
1278
+ provider TEXT,
1279
+ model TEXT,
1280
+ capability_policy_version TEXT,
1281
+ source_type TEXT NOT NULL,
1282
+ source_name TEXT,
1283
+ parent_run_id TEXT REFERENCES runs(id),
1284
+ root_run_id TEXT,
1285
+ status TEXT NOT NULL,
1286
+ priority INTEGER NOT NULL DEFAULT 0,
1287
+ enqueued_at TEXT NOT NULL,
1288
+ started_at TEXT,
1289
+ finished_at TEXT,
1290
+ cancellation_reason TEXT,
1291
+ final_response TEXT,
1292
+ usage TEXT,
1293
+ error_class TEXT,
1294
+ error_message TEXT,
1295
+ metadata TEXT NOT NULL DEFAULT '{}',
1296
+ UNIQUE(thread_id, turn_ordinal)
1297
+ );
1298
+
1299
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_client_request
1300
+ ON runs(workspace_session_id, source_type, client_request_id)
1301
+ WHERE client_request_id IS NOT NULL;
1302
+ CREATE INDEX IF NOT EXISTS idx_runs_workspace_status ON runs(workspace_session_id, status);
1303
+ CREATE INDEX IF NOT EXISTS idx_runs_thread_status ON runs(thread_id, status);
1304
+ CREATE INDEX IF NOT EXISTS idx_runs_parent ON runs(parent_run_id);
1305
+
1306
+ CREATE TABLE IF NOT EXISTS run_definitions (
1307
+ run_id TEXT PRIMARY KEY REFERENCES runs(id),
1308
+ po_name TEXT NOT NULL,
1309
+ version TEXT NOT NULL,
1310
+ prompt TEXT NOT NULL,
1311
+ config TEXT NOT NULL,
1312
+ provider TEXT,
1313
+ model TEXT,
1314
+ capability_policy_version TEXT NOT NULL,
1315
+ created_at TEXT NOT NULL
1316
+ );
1317
+
1318
+ CREATE TABLE IF NOT EXISTS workspace_events (
1319
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1320
+ workspace_session_id TEXT NOT NULL REFERENCES workspace_sessions(id),
1321
+ event_type TEXT NOT NULL,
1322
+ entity_type TEXT,
1323
+ entity_id TEXT,
1324
+ payload TEXT NOT NULL DEFAULT '{}',
1325
+ created_at TEXT NOT NULL
1326
+ );
1327
+
1328
+ CREATE INDEX IF NOT EXISTS idx_workspace_events_scope ON workspace_events(workspace_session_id, id);
1329
+
1330
+ CREATE TABLE IF NOT EXISTS run_events (
1331
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1332
+ run_id TEXT NOT NULL REFERENCES runs(id),
1333
+ sequence INTEGER NOT NULL,
1334
+ workspace_cursor INTEGER NOT NULL UNIQUE REFERENCES workspace_events(id),
1335
+ workspace_session_id TEXT NOT NULL REFERENCES workspace_sessions(id),
1336
+ thread_id TEXT NOT NULL REFERENCES sessions(id),
1337
+ kind TEXT NOT NULL,
1338
+ timestamp TEXT NOT NULL,
1339
+ data TEXT NOT NULL DEFAULT '{}',
1340
+ UNIQUE(run_id, sequence)
1341
+ );
1342
+
1343
+ CREATE INDEX IF NOT EXISTS idx_run_events_run ON run_events(run_id, sequence);
1344
+ CREATE INDEX IF NOT EXISTS idx_run_events_workspace ON run_events(workspace_session_id, workspace_cursor);
1345
+
1346
+ CREATE TABLE IF NOT EXISTS tool_executions (
1347
+ id TEXT PRIMARY KEY,
1348
+ provider_tool_call_id TEXT NOT NULL,
1349
+ run_id TEXT NOT NULL REFERENCES runs(id),
1350
+ batch_id TEXT NOT NULL,
1351
+ batch_position INTEGER NOT NULL,
1352
+ capability_name TEXT NOT NULL,
1353
+ capability_policy_version TEXT,
1354
+ status TEXT NOT NULL,
1355
+ resource_keys TEXT NOT NULL DEFAULT '[]',
1356
+ arguments TEXT NOT NULL DEFAULT '{}',
1357
+ result TEXT,
1358
+ error_class TEXT,
1359
+ error_message TEXT,
1360
+ cancellation_reason TEXT,
1361
+ started_at TEXT,
1362
+ finished_at TEXT,
1363
+ UNIQUE(run_id, provider_tool_call_id)
1364
+ );
1365
+
1366
+ CREATE INDEX IF NOT EXISTS idx_tool_executions_run ON tool_executions(run_id, batch_position);
1367
+
1368
+ CREATE TABLE IF NOT EXISTS artifacts (
1369
+ id TEXT PRIMARY KEY,
1370
+ workspace_session_id TEXT NOT NULL REFERENCES workspace_sessions(id),
1371
+ owner_po TEXT NOT NULL,
1372
+ owner_thread_id TEXT REFERENCES sessions(id),
1373
+ producing_run_id TEXT REFERENCES runs(id),
1374
+ title TEXT NOT NULL,
1375
+ html TEXT NOT NULL,
1376
+ revision INTEGER NOT NULL DEFAULT 1,
1377
+ mode TEXT NOT NULL DEFAULT 'replace',
1378
+ created_at TEXT NOT NULL,
1379
+ updated_at TEXT NOT NULL,
1380
+ metadata TEXT NOT NULL DEFAULT '{}'
1381
+ );
1382
+
1383
+ CREATE INDEX IF NOT EXISTS idx_artifacts_workspace ON artifacts(workspace_session_id, updated_at);
1384
+ CREATE INDEX IF NOT EXISTS idx_artifacts_owner_thread ON artifacts(owner_thread_id);
1186
1385
  SQL
1187
1386
  end
1188
1387
 
@@ -1279,6 +1478,380 @@ module PromptObjects
1279
1478
  CREATE INDEX IF NOT EXISTS idx_env_data_root ON env_data(root_thread_id);
1280
1479
  SQL
1281
1480
  end
1481
+
1482
+ if from_version < 8
1483
+ # Environment-level sessions: add session_type, initiated_by to sessions;
1484
+ # target_po, from_source to messages; session_id to env_data
1485
+ @db.execute_batch(<<~SQL)
1486
+ ALTER TABLE sessions ADD COLUMN session_type TEXT DEFAULT 'environment';
1487
+ ALTER TABLE sessions ADD COLUMN initiated_by TEXT;
1488
+
1489
+ ALTER TABLE messages ADD COLUMN target_po TEXT;
1490
+ ALTER TABLE messages ADD COLUMN from_source TEXT;
1491
+
1492
+ ALTER TABLE env_data ADD COLUMN session_id TEXT;
1493
+ CREATE INDEX IF NOT EXISTS idx_env_data_session ON env_data(session_id);
1494
+ SQL
1495
+
1496
+ # Migrate existing data: set session_type based on thread_type,
1497
+ # populate target_po from session's po_name, copy root_thread_id to session_id
1498
+ @db.execute("UPDATE sessions SET session_type = 'delegation' WHERE thread_type = 'delegation'")
1499
+ @db.execute("UPDATE sessions SET initiated_by = 'human' WHERE session_type = 'environment'")
1500
+ @db.execute("UPDATE messages SET target_po = (SELECT po_name FROM sessions WHERE sessions.id = messages.session_id) WHERE target_po IS NULL")
1501
+ @db.execute("UPDATE messages SET from_source = 'human' WHERE role = 'user' AND from_source IS NULL")
1502
+ @db.execute("UPDATE env_data SET session_id = root_thread_id WHERE session_id IS NULL")
1503
+ end
1504
+
1505
+ if from_version < 9
1506
+ # v8 introduced environment sessions (po_name IS NULL), but databases
1507
+ # created before that have `po_name TEXT NOT NULL`, and ALTER TABLE
1508
+ # can't drop a NOT NULL constraint. Rebuild the sessions table with a
1509
+ # nullable po_name so create_environment_session works on upgrade.
1510
+ # (Fresh DBs go through create_schema, which is already nullable.)
1511
+ @db.execute_batch(<<~SQL)
1512
+ CREATE TABLE sessions_new (
1513
+ id TEXT PRIMARY KEY,
1514
+ po_name TEXT,
1515
+ name TEXT,
1516
+ source TEXT DEFAULT 'tui',
1517
+ source_client TEXT,
1518
+ last_message_source TEXT,
1519
+ created_at TEXT NOT NULL,
1520
+ updated_at TEXT NOT NULL,
1521
+ metadata TEXT DEFAULT '{}',
1522
+ parent_session_id TEXT,
1523
+ parent_message_id INTEGER,
1524
+ parent_po TEXT,
1525
+ thread_type TEXT DEFAULT 'root',
1526
+ session_type TEXT DEFAULT 'environment',
1527
+ initiated_by TEXT
1528
+ );
1529
+
1530
+ INSERT INTO sessions_new (
1531
+ id, po_name, name, source, source_client, last_message_source,
1532
+ created_at, updated_at, metadata, parent_session_id,
1533
+ parent_message_id, parent_po, thread_type, session_type, initiated_by
1534
+ )
1535
+ SELECT
1536
+ id, po_name, name, source, source_client, last_message_source,
1537
+ created_at, updated_at, metadata, parent_session_id,
1538
+ parent_message_id, parent_po, thread_type, session_type, initiated_by
1539
+ FROM sessions;
1540
+
1541
+ DROP TABLE sessions;
1542
+ ALTER TABLE sessions_new RENAME TO sessions;
1543
+
1544
+ CREATE INDEX IF NOT EXISTS idx_sessions_po_name ON sessions(po_name);
1545
+ CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at);
1546
+ CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
1547
+ CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
1548
+ SQL
1549
+ end
1550
+
1551
+ migrate_schema_v10 if from_version < 10
1552
+ migrate_schema_v11 if from_version < 11
1553
+ end
1554
+
1555
+ def migrate_schema_v11
1556
+ @db.transaction do
1557
+ @db.execute_batch(<<~SQL)
1558
+ CREATE TABLE IF NOT EXISTS run_definitions (
1559
+ run_id TEXT PRIMARY KEY REFERENCES runs(id),
1560
+ po_name TEXT NOT NULL,
1561
+ version TEXT NOT NULL,
1562
+ prompt TEXT NOT NULL,
1563
+ config TEXT NOT NULL,
1564
+ provider TEXT,
1565
+ model TEXT,
1566
+ capability_policy_version TEXT NOT NULL,
1567
+ created_at TEXT NOT NULL
1568
+ );
1569
+ SQL
1570
+ end
1571
+ end
1572
+
1573
+ def migrate_schema_v10
1574
+ @db.transaction do
1575
+ @db.execute_batch(<<~SQL)
1576
+ CREATE TABLE IF NOT EXISTS workspace_sessions (
1577
+ id TEXT PRIMARY KEY,
1578
+ name TEXT,
1579
+ status TEXT NOT NULL DEFAULT 'active',
1580
+ created_at TEXT NOT NULL,
1581
+ updated_at TEXT NOT NULL,
1582
+ metadata TEXT NOT NULL DEFAULT '{}'
1583
+ );
1584
+ SQL
1585
+
1586
+ add_column_unless_exists("sessions", "workspace_session_id", "TEXT REFERENCES workspace_sessions(id)")
1587
+ add_column_unless_exists("sessions", "parent_run_id", "TEXT")
1588
+ add_column_unless_exists("sessions", "retention_policy", "TEXT")
1589
+ add_column_unless_exists("sessions", "archived", "INTEGER NOT NULL DEFAULT 0")
1590
+ add_column_unless_exists("sessions", "last_run_status", "TEXT")
1591
+ add_column_unless_exists("sessions", "last_activity_at", "TEXT")
1592
+
1593
+ if table_exists?("messages")
1594
+ add_column_unless_exists("messages", "message_uid", "TEXT")
1595
+ add_column_unless_exists("messages", "run_id", "TEXT")
1596
+ add_column_unless_exists("messages", "turn_ordinal", "INTEGER")
1597
+ add_column_unless_exists("messages", "position", "INTEGER")
1598
+ end
1599
+
1600
+ if table_exists?("env_data")
1601
+ add_column_unless_exists("env_data", "workspace_session_id", "TEXT REFERENCES workspace_sessions(id)")
1602
+ end
1603
+
1604
+ create_v10_runtime_tables
1605
+ backfill_workspace_sessions
1606
+ backfill_artifacts_from_env_data
1607
+ create_v10_indexes
1608
+ end
1609
+ end
1610
+
1611
+ def create_v10_runtime_tables
1612
+ @db.execute_batch(<<~SQL)
1613
+ CREATE TABLE IF NOT EXISTS runs (
1614
+ id TEXT PRIMARY KEY,
1615
+ client_request_id TEXT,
1616
+ workspace_session_id TEXT NOT NULL REFERENCES workspace_sessions(id),
1617
+ thread_id TEXT NOT NULL REFERENCES sessions(id),
1618
+ po_name TEXT NOT NULL,
1619
+ turn_ordinal INTEGER NOT NULL,
1620
+ input_content TEXT,
1621
+ input_message_id INTEGER,
1622
+ history_cutoff_message_id INTEGER,
1623
+ definition_version TEXT NOT NULL,
1624
+ provider TEXT,
1625
+ model TEXT,
1626
+ capability_policy_version TEXT,
1627
+ source_type TEXT NOT NULL,
1628
+ source_name TEXT,
1629
+ parent_run_id TEXT REFERENCES runs(id),
1630
+ root_run_id TEXT,
1631
+ status TEXT NOT NULL,
1632
+ priority INTEGER NOT NULL DEFAULT 0,
1633
+ enqueued_at TEXT NOT NULL,
1634
+ started_at TEXT,
1635
+ finished_at TEXT,
1636
+ cancellation_reason TEXT,
1637
+ final_response TEXT,
1638
+ usage TEXT,
1639
+ error_class TEXT,
1640
+ error_message TEXT,
1641
+ metadata TEXT NOT NULL DEFAULT '{}',
1642
+ UNIQUE(thread_id, turn_ordinal)
1643
+ );
1644
+
1645
+ CREATE TABLE IF NOT EXISTS workspace_events (
1646
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1647
+ workspace_session_id TEXT NOT NULL REFERENCES workspace_sessions(id),
1648
+ event_type TEXT NOT NULL,
1649
+ entity_type TEXT,
1650
+ entity_id TEXT,
1651
+ payload TEXT NOT NULL DEFAULT '{}',
1652
+ created_at TEXT NOT NULL
1653
+ );
1654
+
1655
+ CREATE TABLE IF NOT EXISTS run_events (
1656
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1657
+ run_id TEXT NOT NULL REFERENCES runs(id),
1658
+ sequence INTEGER NOT NULL,
1659
+ workspace_cursor INTEGER NOT NULL UNIQUE REFERENCES workspace_events(id),
1660
+ workspace_session_id TEXT NOT NULL REFERENCES workspace_sessions(id),
1661
+ thread_id TEXT NOT NULL REFERENCES sessions(id),
1662
+ kind TEXT NOT NULL,
1663
+ timestamp TEXT NOT NULL,
1664
+ data TEXT NOT NULL DEFAULT '{}',
1665
+ UNIQUE(run_id, sequence)
1666
+ );
1667
+
1668
+ CREATE TABLE IF NOT EXISTS tool_executions (
1669
+ id TEXT PRIMARY KEY,
1670
+ provider_tool_call_id TEXT NOT NULL,
1671
+ run_id TEXT NOT NULL REFERENCES runs(id),
1672
+ batch_id TEXT NOT NULL,
1673
+ batch_position INTEGER NOT NULL,
1674
+ capability_name TEXT NOT NULL,
1675
+ capability_policy_version TEXT,
1676
+ status TEXT NOT NULL,
1677
+ resource_keys TEXT NOT NULL DEFAULT '[]',
1678
+ arguments TEXT NOT NULL DEFAULT '{}',
1679
+ result TEXT,
1680
+ error_class TEXT,
1681
+ error_message TEXT,
1682
+ cancellation_reason TEXT,
1683
+ started_at TEXT,
1684
+ finished_at TEXT,
1685
+ UNIQUE(run_id, provider_tool_call_id)
1686
+ );
1687
+
1688
+ CREATE TABLE IF NOT EXISTS artifacts (
1689
+ id TEXT PRIMARY KEY,
1690
+ workspace_session_id TEXT NOT NULL REFERENCES workspace_sessions(id),
1691
+ owner_po TEXT NOT NULL,
1692
+ owner_thread_id TEXT REFERENCES sessions(id),
1693
+ producing_run_id TEXT REFERENCES runs(id),
1694
+ title TEXT NOT NULL,
1695
+ html TEXT NOT NULL,
1696
+ revision INTEGER NOT NULL DEFAULT 1,
1697
+ mode TEXT NOT NULL DEFAULT 'replace',
1698
+ created_at TEXT NOT NULL,
1699
+ updated_at TEXT NOT NULL,
1700
+ metadata TEXT NOT NULL DEFAULT '{}'
1701
+ );
1702
+ SQL
1703
+ end
1704
+
1705
+ def create_v10_indexes
1706
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_sessions_workspace ON sessions(workspace_session_id)")
1707
+ if table_exists?("messages")
1708
+ @db.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_uid ON messages(message_uid) WHERE message_uid IS NOT NULL")
1709
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_messages_run ON messages(run_id)")
1710
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_messages_turn ON messages(session_id, turn_ordinal, position)")
1711
+ end
1712
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_env_data_workspace ON env_data(workspace_session_id)") if table_exists?("env_data")
1713
+ @db.execute(<<~SQL)
1714
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_client_request
1715
+ ON runs(workspace_session_id, source_type, client_request_id)
1716
+ WHERE client_request_id IS NOT NULL
1717
+ SQL
1718
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_runs_workspace_status ON runs(workspace_session_id, status)")
1719
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_runs_thread_status ON runs(thread_id, status)")
1720
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_runs_parent ON runs(parent_run_id)")
1721
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_workspace_events_scope ON workspace_events(workspace_session_id, id)")
1722
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_run_events_run ON run_events(run_id, sequence)")
1723
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_run_events_workspace ON run_events(workspace_session_id, workspace_cursor)")
1724
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_tool_executions_run ON tool_executions(run_id, batch_position)")
1725
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_artifacts_workspace ON artifacts(workspace_session_id, updated_at)")
1726
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_artifacts_owner_thread ON artifacts(owner_thread_id)")
1727
+ end
1728
+
1729
+ def backup_before_destructive_migration
1730
+ version = get_schema_version
1731
+ return if @db_path == ":memory:" || version.zero? || version >= SCHEMA_VERSION
1732
+ return unless File.file?(@db_path)
1733
+
1734
+ timestamp = Time.now.utc.strftime("%Y%m%dT%H%M%S%6NZ")
1735
+ @migration_backup_path = "#{@db_path}.backup-v#{version}-#{timestamp}"
1736
+ destination = SQLite3::Database.new(@migration_backup_path)
1737
+ backup = SQLite3::Backup.new(destination, "main", @db, "main")
1738
+ backup.step(-1)
1739
+ backup.finish
1740
+ ensure
1741
+ destination&.close
1742
+ end
1743
+
1744
+ def table_exists?(table)
1745
+ !@db.get_first_value(
1746
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
1747
+ [table]
1748
+ ).nil?
1749
+ end
1750
+
1751
+ def column_exists?(table, column)
1752
+ return false unless table_exists?(table)
1753
+
1754
+ @db.table_info(table).any? { |definition| definition["name"] == column }
1755
+ end
1756
+
1757
+ def add_column_unless_exists(table, column, definition)
1758
+ return if column_exists?(table, column)
1759
+
1760
+ @db.execute("ALTER TABLE #{table} ADD COLUMN #{column} #{definition}")
1761
+ end
1762
+
1763
+ def backfill_workspace_sessions
1764
+ environment_rows = @db.execute(<<~SQL)
1765
+ SELECT id, name, created_at, updated_at, metadata
1766
+ FROM sessions
1767
+ WHERE po_name IS NULL
1768
+ ORDER BY created_at ASC
1769
+ SQL
1770
+
1771
+ environment_rows.each do |row|
1772
+ @db.execute(<<~SQL, [row["id"], row["name"], row["created_at"], row["updated_at"], row["metadata"] || "{}"])
1773
+ INSERT OR IGNORE INTO workspace_sessions (id, name, status, created_at, updated_at, metadata)
1774
+ VALUES (?, ?, 'active', ?, ?, ?)
1775
+ SQL
1776
+ @db.execute("UPDATE sessions SET workspace_session_id = ? WHERE id = ?", [row["id"], row["id"]])
1777
+ end
1778
+
1779
+ workspace_ids = @db.execute("SELECT id FROM workspace_sessions ORDER BY created_at ASC").map { |row| row["id"] }
1780
+ fallback_id = if workspace_ids.one?
1781
+ workspace_ids.first
1782
+ else
1783
+ create_imported_workspace
1784
+ end
1785
+
1786
+ @db.execute(
1787
+ "UPDATE sessions SET workspace_session_id = ? WHERE workspace_session_id IS NULL",
1788
+ [fallback_id]
1789
+ )
1790
+
1791
+ return unless table_exists?("env_data")
1792
+
1793
+ @db.execute(<<~SQL)
1794
+ UPDATE env_data
1795
+ SET workspace_session_id = session_id
1796
+ WHERE workspace_session_id IS NULL
1797
+ AND session_id IN (SELECT id FROM workspace_sessions)
1798
+ SQL
1799
+ @db.execute(<<~SQL)
1800
+ UPDATE env_data
1801
+ SET workspace_session_id = (
1802
+ SELECT workspace_session_id FROM sessions WHERE sessions.id = env_data.root_thread_id
1803
+ )
1804
+ WHERE workspace_session_id IS NULL
1805
+ SQL
1806
+ @db.execute(
1807
+ "UPDATE env_data SET workspace_session_id = ? WHERE workspace_session_id IS NULL",
1808
+ [fallback_id]
1809
+ )
1810
+ end
1811
+
1812
+ def create_imported_workspace
1813
+ existing = @db.get_first_value("SELECT id FROM workspace_sessions WHERE name = 'Imported legacy threads' LIMIT 1")
1814
+ return existing if existing
1815
+
1816
+ id = SecureRandom.uuid
1817
+ now = Time.now.utc.iso8601
1818
+ @db.execute(<<~SQL, [id, now, now])
1819
+ INSERT INTO workspace_sessions (id, name, status, created_at, updated_at, metadata)
1820
+ VALUES (?, 'Imported legacy threads', 'active', ?, ?, '{}')
1821
+ SQL
1822
+ id
1823
+ end
1824
+
1825
+ def backfill_artifacts_from_env_data
1826
+ return unless table_exists?("env_data")
1827
+
1828
+ rows = @db.execute(<<~SQL)
1829
+ SELECT key, workspace_session_id, value, stored_by, created_at, updated_at
1830
+ FROM env_data
1831
+ WHERE key LIKE 'artifact_%'
1832
+ SQL
1833
+ rows.each do |row|
1834
+ value = JSON.parse(row["value"])
1835
+ title = value["title"]
1836
+ html = value["html"]
1837
+ next unless row["workspace_session_id"] && title && html
1838
+
1839
+ owner_po = value["rendered_by"] || row["stored_by"] || "unknown"
1840
+ owner_thread_id = @db.get_first_value(<<~SQL, [row["workspace_session_id"], owner_po])
1841
+ SELECT id FROM sessions
1842
+ WHERE workspace_session_id = ? AND po_name = ?
1843
+ ORDER BY updated_at DESC
1844
+ LIMIT 1
1845
+ SQL
1846
+ @db.execute(<<~SQL, [row["key"], row["workspace_session_id"], owner_po, owner_thread_id, title, html, row["created_at"], row["updated_at"]])
1847
+ INSERT OR IGNORE INTO artifacts (
1848
+ id, workspace_session_id, owner_po, owner_thread_id, producing_run_id,
1849
+ title, html, revision, mode, created_at, updated_at, metadata
1850
+ ) VALUES (?, ?, ?, ?, NULL, ?, ?, 1, 'replace', ?, ?, '{}')
1851
+ SQL
1852
+ rescue JSON::ParserError
1853
+ next
1854
+ end
1282
1855
  end
1283
1856
 
1284
1857
  def empty_usage
@@ -1356,7 +1929,17 @@ module PromptObjects
1356
1929
  parent_session_id: row["parent_session_id"],
1357
1930
  parent_message_id: row["parent_message_id"],
1358
1931
  parent_po: row["parent_po"],
1359
- thread_type: row["thread_type"] || "root"
1932
+ thread_type: row["thread_type"] || "root",
1933
+ # Environment session fields (v8)
1934
+ session_type: row["session_type"] || "environment",
1935
+ initiated_by: row["initiated_by"],
1936
+ # Run-based runtime fields (v10)
1937
+ workspace_session_id: row["workspace_session_id"],
1938
+ parent_run_id: row["parent_run_id"],
1939
+ retention_policy: row["retention_policy"],
1940
+ archived: row["archived"].to_i == 1,
1941
+ last_run_status: row["last_run_status"],
1942
+ last_activity_at: row["last_activity_at"] ? Time.parse(row["last_activity_at"]) : nil
1360
1943
  }
1361
1944
  result[:message_count] = row["message_count"] if include_count && row["message_count"]
1362
1945
  result
@@ -1385,6 +1968,10 @@ module PromptObjects
1385
1968
  tool_calls: row["tool_calls"] ? JSON.parse(row["tool_calls"], symbolize_names: true) : nil,
1386
1969
  tool_results: row["tool_results"] ? JSON.parse(row["tool_results"], symbolize_names: true) : nil,
1387
1970
  usage: row["usage"] ? JSON.parse(row["usage"], symbolize_names: true) : nil,
1971
+ message_uid: row["message_uid"],
1972
+ run_id: row["run_id"],
1973
+ turn_ordinal: row["turn_ordinal"],
1974
+ position: row["position"],
1388
1975
  created_at: row["created_at"] ? Time.parse(row["created_at"]) : nil
1389
1976
  }
1390
1977
  end