prompt_objects 0.5.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 (292) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -0
  3. data/README.md +211 -19
  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 +1350 -18
  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 +190 -432
  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 +561 -52
  65. data/lib/prompt_objects/server/app.rb +134 -1
  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 -207
  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 +774 -15
  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 +78 -0
  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 +58 -0
  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 +57 -0
  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/set_env_data.rb +101 -0
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +36 -6
  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 +6 -2
  92. data/templates/arc-agi-1/objects/solver.md +14 -4
  93. data/templates/arc-agi-1/objects/verifier.md +6 -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 +28 -1
  230. metadata +175 -65
  231. data/CLAUDE.md +0 -176
  232. data/Gemfile +0 -10
  233. data/Gemfile.lock +0 -233
  234. data/IMPLEMENTATION_PLAN.md +0 -1073
  235. data/Rakefile +0 -27
  236. data/design-doc-v2.md +0 -1232
  237. data/frontend/.gitignore +0 -3
  238. data/frontend/index.html +0 -17
  239. data/frontend/package-lock.json +0 -4540
  240. data/frontend/package.json +0 -36
  241. data/frontend/postcss.config.js +0 -6
  242. data/frontend/src/App.tsx +0 -103
  243. data/frontend/src/canvas/CanvasView.tsx +0 -113
  244. data/frontend/src/canvas/ForceLayout.ts +0 -115
  245. data/frontend/src/canvas/SceneManager.ts +0 -587
  246. data/frontend/src/canvas/canvasStore.ts +0 -47
  247. data/frontend/src/canvas/constants.ts +0 -95
  248. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  249. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  250. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  251. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  252. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  253. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  254. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  255. data/frontend/src/canvas/types.ts +0 -24
  256. data/frontend/src/components/ContextMenu.tsx +0 -68
  257. data/frontend/src/components/Inspector.tsx +0 -232
  258. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  259. data/frontend/src/components/MethodList.tsx +0 -90
  260. data/frontend/src/components/ModelSelector.tsx +0 -115
  261. data/frontend/src/components/NotificationPanel.tsx +0 -130
  262. data/frontend/src/components/ObjectList.tsx +0 -78
  263. data/frontend/src/components/PaneSlot.tsx +0 -30
  264. data/frontend/src/components/SourcePane.tsx +0 -202
  265. data/frontend/src/components/SystemBar.tsx +0 -74
  266. data/frontend/src/components/Transcript.tsx +0 -76
  267. data/frontend/src/components/UsagePanel.tsx +0 -105
  268. data/frontend/src/components/Workspace.tsx +0 -260
  269. data/frontend/src/components/index.ts +0 -12
  270. data/frontend/src/hooks/useResize.ts +0 -55
  271. data/frontend/src/hooks/useWebSocket.ts +0 -517
  272. data/frontend/src/index.css +0 -103
  273. data/frontend/src/main.tsx +0 -10
  274. data/frontend/src/store/index.ts +0 -279
  275. data/frontend/src/types/index.ts +0 -165
  276. data/frontend/tailwind.config.js +0 -44
  277. data/frontend/tsconfig.json +0 -30
  278. data/frontend/vite.config.ts +0 -29
  279. data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +0 -4345
  280. data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +0 -1
  281. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  282. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  283. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  284. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  285. data/lib/prompt_objects/universal/think.rb +0 -41
  286. data/prompt_objects.gemspec +0 -50
  287. data/templates/basic/.gitignore +0 -2
  288. data/templates/developer/.gitignore +0 -5
  289. data/templates/empty/.gitignore +0 -5
  290. data/templates/empty/objects/.gitkeep +0 -0
  291. data/templates/minimal/.gitignore +0 -5
  292. 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 = 6
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
 
@@ -595,6 +640,137 @@ module PromptObjects
595
640
  row["count"]
596
641
  end
597
642
 
643
+ # --- Environment Data (Shared Key-Value Store) ---
644
+
645
+ # Resolve the root thread ID for a session by walking up the delegation chain.
646
+ # @param session_id [String] Any session ID in a delegation chain
647
+ # @return [String] The root thread's session ID
648
+ def resolve_root_thread(session_id)
649
+ lineage = get_thread_lineage(session_id)
650
+ return session_id if lineage.empty?
651
+
652
+ lineage.first[:id]
653
+ end
654
+
655
+ # Store a key-value pair scoped to a root thread.
656
+ # Uses INSERT OR REPLACE to create or overwrite.
657
+ # @param root_thread_id [String] Root thread scope
658
+ # @param key [String] Data key
659
+ # @param short_description [String] Brief description for discoverability
660
+ # @param value [Object] Data value (will be JSON-serialized)
661
+ # @param stored_by [String] PO name that stored this
662
+ def store_env_data(root_thread_id:, key:, short_description:, value:, stored_by:)
663
+ now = Time.now.utc.iso8601
664
+ json_value = JSON.generate(value)
665
+
666
+ @db.execute(<<~SQL, [root_thread_id, key, short_description, json_value, stored_by, now, now])
667
+ INSERT INTO env_data (root_thread_id, key, short_description, value, stored_by, created_at, updated_at)
668
+ VALUES (?, ?, ?, ?, ?, ?, ?)
669
+ ON CONFLICT(root_thread_id, key) DO UPDATE SET
670
+ short_description = excluded.short_description,
671
+ value = excluded.value,
672
+ stored_by = excluded.stored_by,
673
+ updated_at = excluded.updated_at
674
+ SQL
675
+ end
676
+
677
+ # Get a single env data entry by key.
678
+ # @param root_thread_id [String] Root thread scope
679
+ # @param key [String] Data key
680
+ # @return [Hash, nil] Entry with parsed value, or nil if not found
681
+ def get_env_data(root_thread_id:, key:)
682
+ row = @db.get_first_row(<<~SQL, [root_thread_id, key])
683
+ SELECT key, short_description, value, stored_by, created_at, updated_at
684
+ FROM env_data
685
+ WHERE root_thread_id = ? AND key = ?
686
+ SQL
687
+
688
+ return nil unless row
689
+
690
+ {
691
+ key: row["key"],
692
+ short_description: row["short_description"],
693
+ value: JSON.parse(row["value"], symbolize_names: true),
694
+ stored_by: row["stored_by"],
695
+ created_at: row["created_at"],
696
+ updated_at: row["updated_at"]
697
+ }
698
+ end
699
+
700
+ # List all env data keys and descriptions for a root thread (no values).
701
+ # @param root_thread_id [String] Root thread scope
702
+ # @return [Array<Hash>] Entries with key and short_description only
703
+ def list_env_data(root_thread_id:)
704
+ rows = @db.execute(<<~SQL, [root_thread_id])
705
+ SELECT key, short_description FROM env_data
706
+ WHERE root_thread_id = ?
707
+ ORDER BY key ASC
708
+ SQL
709
+
710
+ rows.map { |row| { key: row["key"], short_description: row["short_description"] } }
711
+ end
712
+
713
+ # List all env data entries with full values for a root thread.
714
+ # @param root_thread_id [String] Root thread scope
715
+ # @return [Array<Hash>] Full entries with parsed values
716
+ def list_env_data_full(root_thread_id:)
717
+ rows = @db.execute(<<~SQL, [root_thread_id])
718
+ SELECT key, short_description, value, stored_by, created_at, updated_at
719
+ FROM env_data WHERE root_thread_id = ? ORDER BY key ASC
720
+ SQL
721
+
722
+ rows.map do |row|
723
+ {
724
+ key: row["key"],
725
+ short_description: row["short_description"],
726
+ value: JSON.parse(row["value"]),
727
+ stored_by: row["stored_by"],
728
+ created_at: row["created_at"],
729
+ updated_at: row["updated_at"]
730
+ }
731
+ end
732
+ end
733
+
734
+ # Update an existing env data entry.
735
+ # @param root_thread_id [String] Root thread scope
736
+ # @param key [String] Data key
737
+ # @param short_description [String, nil] New description (keeps existing if nil)
738
+ # @param value [Object, nil] New value (keeps existing if nil)
739
+ # @param stored_by [String] PO name performing the update
740
+ # @return [Boolean] True if updated, false if key not found
741
+ def update_env_data(root_thread_id:, key:, short_description: nil, value: nil, stored_by:)
742
+ existing = get_env_data(root_thread_id: root_thread_id, key: key)
743
+ return false unless existing
744
+
745
+ updates = ["updated_at = ?", "stored_by = ?"]
746
+ params = [Time.now.utc.iso8601, stored_by]
747
+
748
+ if short_description
749
+ updates << "short_description = ?"
750
+ params << short_description
751
+ end
752
+
753
+ if value
754
+ updates << "value = ?"
755
+ params << JSON.generate(value)
756
+ end
757
+
758
+ params << root_thread_id
759
+ params << key
760
+
761
+ @db.execute("UPDATE env_data SET #{updates.join(', ')} WHERE root_thread_id = ? AND key = ?", params)
762
+ true
763
+ end
764
+
765
+ # Delete an env data entry.
766
+ # @param root_thread_id [String] Root thread scope
767
+ # @param key [String] Data key
768
+ # @return [Boolean] True if deleted, false if key not found
769
+ def delete_env_data(root_thread_id:, key:)
770
+ @db.execute("DELETE FROM env_data WHERE root_thread_id = ? AND key = ?", [root_thread_id, key])
771
+ @db.changes > 0
772
+ end
773
+
598
774
  # --- Usage Aggregation ---
599
775
 
600
776
  # Get total token usage for a session.
@@ -763,7 +939,7 @@ module PromptObjects
763
939
  tree = get_thread_tree(session_id)
764
940
  return nil unless tree
765
941
 
766
- serialize_tree_for_export(tree)
942
+ serialize_tree_for_export(tree, is_root: true)
767
943
  end
768
944
 
769
945
  # --- Import ---
@@ -908,11 +1084,11 @@ module PromptObjects
908
1084
  end
909
1085
  end
910
1086
 
911
- def serialize_tree_for_export(node)
1087
+ def serialize_tree_for_export(node, is_root: false)
912
1088
  session = node[:session]
913
1089
  messages = get_messages(session[:id])
914
1090
 
915
- {
1091
+ result = {
916
1092
  session: {
917
1093
  id: session[:id],
918
1094
  po_name: session[:po_name],
@@ -934,6 +1110,13 @@ module PromptObjects
934
1110
  },
935
1111
  children: (node[:children] || []).map { |c| serialize_tree_for_export(c) }
936
1112
  }
1113
+
1114
+ if is_root
1115
+ env_data = list_env_data_full(root_thread_id: session[:id])
1116
+ result[:env_data] = env_data unless env_data.empty?
1117
+ end
1118
+
1119
+ result
937
1120
  end
938
1121
 
939
1122
  def setup_schema
@@ -959,9 +1142,18 @@ module PromptObjects
959
1142
 
960
1143
  def create_schema
961
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
+
962
1154
  CREATE TABLE IF NOT EXISTS sessions (
963
1155
  id TEXT PRIMARY KEY,
964
- po_name TEXT NOT NULL,
1156
+ po_name TEXT,
965
1157
  name TEXT,
966
1158
  source TEXT DEFAULT 'tui',
967
1159
  source_client TEXT,
@@ -973,13 +1165,24 @@ module PromptObjects
973
1165
  parent_session_id TEXT,
974
1166
  parent_message_id INTEGER,
975
1167
  parent_po TEXT,
976
- 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
977
1179
  );
978
1180
 
979
1181
  CREATE INDEX IF NOT EXISTS idx_sessions_po_name ON sessions(po_name);
980
1182
  CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at);
981
1183
  CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
982
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);
983
1186
 
984
1187
  CREATE TABLE IF NOT EXISTS messages (
985
1188
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -990,10 +1193,21 @@ module PromptObjects
990
1193
  tool_calls TEXT,
991
1194
  tool_results TEXT,
992
1195
  usage TEXT,
993
- 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
994
1205
  );
995
1206
 
996
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);
997
1211
 
998
1212
  -- Full-text search index for message content
999
1213
  CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
@@ -1030,6 +1244,144 @@ module PromptObjects
1030
1244
 
1031
1245
  CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
1032
1246
  CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
1247
+
1248
+ -- Shared environment data, scoped to session (v7 original, v8 adds session_id alias)
1249
+ CREATE TABLE IF NOT EXISTS env_data (
1250
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1251
+ root_thread_id TEXT NOT NULL,
1252
+ session_id TEXT,
1253
+ key TEXT NOT NULL,
1254
+ short_description TEXT NOT NULL,
1255
+ value TEXT NOT NULL,
1256
+ stored_by TEXT NOT NULL,
1257
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1258
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
1259
+ workspace_session_id TEXT REFERENCES workspace_sessions(id),
1260
+ UNIQUE(root_thread_id, key)
1261
+ );
1262
+
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);
1033
1385
  SQL
1034
1386
  end
1035
1387
 
@@ -1107,6 +1459,399 @@ module PromptObjects
1107
1459
  # Add usage column for token tracking
1108
1460
  @db.execute("ALTER TABLE messages ADD COLUMN usage TEXT")
1109
1461
  end
1462
+
1463
+ if from_version < 7
1464
+ # Add shared environment data table for delegation chains
1465
+ @db.execute_batch(<<~SQL)
1466
+ CREATE TABLE IF NOT EXISTS env_data (
1467
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1468
+ root_thread_id TEXT NOT NULL,
1469
+ key TEXT NOT NULL,
1470
+ short_description TEXT NOT NULL,
1471
+ value TEXT NOT NULL,
1472
+ stored_by TEXT NOT NULL,
1473
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1474
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
1475
+ UNIQUE(root_thread_id, key)
1476
+ );
1477
+
1478
+ CREATE INDEX IF NOT EXISTS idx_env_data_root ON env_data(root_thread_id);
1479
+ SQL
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
1110
1855
  end
1111
1856
 
1112
1857
  def empty_usage
@@ -1184,7 +1929,17 @@ module PromptObjects
1184
1929
  parent_session_id: row["parent_session_id"],
1185
1930
  parent_message_id: row["parent_message_id"],
1186
1931
  parent_po: row["parent_po"],
1187
- 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
1188
1943
  }
1189
1944
  result[:message_count] = row["message_count"] if include_count && row["message_count"]
1190
1945
  result
@@ -1213,6 +1968,10 @@ module PromptObjects
1213
1968
  tool_calls: row["tool_calls"] ? JSON.parse(row["tool_calls"], symbolize_names: true) : nil,
1214
1969
  tool_results: row["tool_results"] ? JSON.parse(row["tool_results"], symbolize_names: true) : nil,
1215
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"],
1216
1975
  created_at: row["created_at"] ? Time.parse(row["created_at"]) : nil
1217
1976
  }
1218
1977
  end