@pasko70/pibo 3.2.7 → 3.4.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 (254) hide show
  1. package/dist/agent-runtime/capabilities.js +5 -0
  2. package/dist/agent-runtime/contract.js +2 -0
  3. package/dist/agent-runtime/routed-session.js +68 -11
  4. package/dist/agent-runtimes/codex-native/adapter.js +4 -1
  5. package/dist/agent-runtimes/codex-native/process.js +1 -1
  6. package/dist/agent-runtimes/codex-native/protocol-version.js +6 -6
  7. package/dist/agent-runtimes/codex-native/thread.js +26 -4
  8. package/dist/agent-runtimes/omp/adapter.js +1 -0
  9. package/dist/agent-runtimes/omp/process.js +23 -3
  10. package/dist/agent-runtimes/omp/protocol-version.js +2 -0
  11. package/dist/agent-runtimes/pi/adapter.js +5 -1
  12. package/dist/agent-runtimes/pi/model-catalog.js +2 -2
  13. package/dist/agent-runtimes/pi/routed-session.js +53 -5
  14. package/dist/agent-runtimes/pi/runtime.js +6 -3
  15. package/dist/apps/chat/chat-api-routes.js +12 -38
  16. package/dist/apps/chat/chat-request-normalizers.js +15 -50
  17. package/dist/apps/chat/data/legacy-project-migration.js +492 -0
  18. package/dist/apps/chat/data/room-service.js +2 -2
  19. package/dist/apps/chat/data/workflow-session-model.js +1 -0
  20. package/dist/apps/chat/data/workflow-session-service.js +352 -0
  21. package/dist/apps/chat/rooms.js +1 -1
  22. package/dist/apps/chat/session-metadata.js +30 -0
  23. package/dist/apps/chat/trace.js +15 -2
  24. package/dist/apps/chat/types/rooms.js +30 -0
  25. package/dist/apps/chat/web-app.js +355 -651
  26. package/dist/apps/chat/workflow-catalog.js +18 -18
  27. package/dist/apps/chat/{project-workflow-human-actions.js → workflow-human-actions.js} +14 -14
  28. package/dist/apps/chat/workflow-manual-trigger-runtime.js +5 -1
  29. package/dist/apps/chat/workflow-persistence.js +20 -28
  30. package/dist/apps/chat/{project-workflow-sessions.js → workflow-sessions.js} +18 -17
  31. package/dist/apps/chat-ui/assets/{dist-ENXYFYda.js → dist-AhtdAETR.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-CGgUuVB6.js → dist-Bk_L1qbJ.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DG6T1maU.js → dist-Dyp6sLQn.js} +1 -1
  34. package/dist/apps/chat-ui/assets/{dist-CFtZTGHa.js → dist-jVaiGXFD.js} +1 -1
  35. package/dist/apps/chat-ui/assets/{dist-BikaGikn.js → dist-t25moyJS.js} +1 -1
  36. package/dist/apps/chat-ui/assets/index-CZjNw-aK.js +228 -0
  37. package/dist/apps/chat-ui/assets/index-CcEjFITM.css +1 -0
  38. package/dist/apps/chat-ui/index.html +2 -2
  39. package/dist/apps/chat-vscode-web/assets/index-Spj6M0tn.js +43 -0
  40. package/dist/apps/chat-vscode-web/index.html +1 -1
  41. package/dist/compute/pool/cli.js +1 -1
  42. package/dist/compute/pool/seeds.js +2 -3
  43. package/dist/config/config.js +1 -1
  44. package/dist/core/session-router.js +19 -10
  45. package/dist/debug/agents.js +9 -3
  46. package/dist/plugins/builtin.js +2 -0
  47. package/dist/plugins/registry.js +1 -0
  48. package/dist/previews/cli.js +154 -79
  49. package/dist/previews/compute-worker.js +37 -0
  50. package/dist/previews/config.js +2 -1
  51. package/dist/previews/migrations/session-ownership.js +7 -0
  52. package/dist/previews/proxy.js +4 -2
  53. package/dist/previews/public-setup.js +108 -0
  54. package/dist/previews/store.js +20 -13
  55. package/dist/previews/web-app.js +12 -2
  56. package/dist/providers/openai-gpt56.js +41 -3
  57. package/dist/sessions/workflow-session-kind.js +0 -3
  58. package/dist/subagents/context.js +2 -2
  59. package/dist/subagents/observation-query.js +49 -6
  60. package/dist/subagents/observation-text-regex.js +124 -0
  61. package/dist/subagents/tool.js +3 -2
  62. package/dist/tools/hashline.js +64 -0
  63. package/npm-shrinkwrap.json +786 -284
  64. package/package.json +11 -6
  65. package/packages/workflows/README.md +21 -0
  66. package/packages/workflows/dist/api/index.d.ts +48 -0
  67. package/packages/workflows/dist/api/index.d.ts.map +1 -0
  68. package/packages/workflows/dist/api/index.js +96 -0
  69. package/packages/workflows/dist/api/index.js.map +1 -0
  70. package/packages/workflows/dist/definition-hash.d.ts +4 -0
  71. package/packages/workflows/dist/definition-hash.d.ts.map +1 -0
  72. package/packages/workflows/dist/definition-hash.js +27 -0
  73. package/packages/workflows/dist/definition-hash.js.map +1 -0
  74. package/packages/workflows/dist/fixtures/index.d.ts +39 -0
  75. package/packages/workflows/dist/fixtures/index.d.ts.map +1 -0
  76. package/packages/workflows/dist/fixtures/index.js +608 -0
  77. package/packages/workflows/dist/fixtures/index.js.map +1 -0
  78. package/packages/workflows/dist/index.d.ts +16 -0
  79. package/packages/workflows/dist/index.d.ts.map +1 -0
  80. package/packages/workflows/dist/index.js +10 -0
  81. package/packages/workflows/dist/index.js.map +1 -0
  82. package/packages/workflows/dist/inspection/index.d.ts +47 -0
  83. package/packages/workflows/dist/inspection/index.d.ts.map +1 -0
  84. package/packages/workflows/dist/inspection/index.js +131 -0
  85. package/packages/workflows/dist/inspection/index.js.map +1 -0
  86. package/packages/workflows/dist/registry/index.d.ts +30 -0
  87. package/packages/workflows/dist/registry/index.d.ts.map +1 -0
  88. package/packages/workflows/dist/registry/index.js +161 -0
  89. package/packages/workflows/dist/registry/index.js.map +1 -0
  90. package/packages/workflows/dist/runtime/adapter-node.d.ts +28 -0
  91. package/packages/workflows/dist/runtime/adapter-node.d.ts.map +1 -0
  92. package/packages/workflows/dist/runtime/adapter-node.js +172 -0
  93. package/packages/workflows/dist/runtime/adapter-node.js.map +1 -0
  94. package/packages/workflows/dist/runtime/agent-node.d.ts +32 -0
  95. package/packages/workflows/dist/runtime/agent-node.d.ts.map +1 -0
  96. package/packages/workflows/dist/runtime/agent-node.js +231 -0
  97. package/packages/workflows/dist/runtime/agent-node.js.map +1 -0
  98. package/packages/workflows/dist/runtime/agent-runtime.d.ts +25 -0
  99. package/packages/workflows/dist/runtime/agent-runtime.d.ts.map +1 -0
  100. package/packages/workflows/dist/runtime/agent-runtime.js +111 -0
  101. package/packages/workflows/dist/runtime/agent-runtime.js.map +1 -0
  102. package/packages/workflows/dist/runtime/code-node.d.ts +31 -0
  103. package/packages/workflows/dist/runtime/code-node.d.ts.map +1 -0
  104. package/packages/workflows/dist/runtime/code-node.js +223 -0
  105. package/packages/workflows/dist/runtime/code-node.js.map +1 -0
  106. package/packages/workflows/dist/runtime/dispatch-failures.d.ts +36 -0
  107. package/packages/workflows/dist/runtime/dispatch-failures.d.ts.map +1 -0
  108. package/packages/workflows/dist/runtime/dispatch-failures.js +80 -0
  109. package/packages/workflows/dist/runtime/dispatch-failures.js.map +1 -0
  110. package/packages/workflows/dist/runtime/edge-payloads.d.ts +3 -0
  111. package/packages/workflows/dist/runtime/edge-payloads.d.ts.map +1 -0
  112. package/packages/workflows/dist/runtime/edge-payloads.js +11 -0
  113. package/packages/workflows/dist/runtime/edge-payloads.js.map +1 -0
  114. package/packages/workflows/dist/runtime/edge-transfer.d.ts +39 -0
  115. package/packages/workflows/dist/runtime/edge-transfer.d.ts.map +1 -0
  116. package/packages/workflows/dist/runtime/edge-transfer.js +356 -0
  117. package/packages/workflows/dist/runtime/edge-transfer.js.map +1 -0
  118. package/packages/workflows/dist/runtime/human-action.d.ts +43 -0
  119. package/packages/workflows/dist/runtime/human-action.d.ts.map +1 -0
  120. package/packages/workflows/dist/runtime/human-action.js +231 -0
  121. package/packages/workflows/dist/runtime/human-action.js.map +1 -0
  122. package/packages/workflows/dist/runtime/human-node.d.ts +27 -0
  123. package/packages/workflows/dist/runtime/human-node.d.ts.map +1 -0
  124. package/packages/workflows/dist/runtime/human-node.js +126 -0
  125. package/packages/workflows/dist/runtime/human-node.js.map +1 -0
  126. package/packages/workflows/dist/runtime/ids.d.ts +2 -0
  127. package/packages/workflows/dist/runtime/ids.d.ts.map +1 -0
  128. package/packages/workflows/dist/runtime/ids.js +4 -0
  129. package/packages/workflows/dist/runtime/ids.js.map +1 -0
  130. package/packages/workflows/dist/runtime/index.d.ts +23 -0
  131. package/packages/workflows/dist/runtime/index.d.ts.map +1 -0
  132. package/packages/workflows/dist/runtime/index.js +12 -0
  133. package/packages/workflows/dist/runtime/index.js.map +1 -0
  134. package/packages/workflows/dist/runtime/manual-trigger.d.ts +51 -0
  135. package/packages/workflows/dist/runtime/manual-trigger.d.ts.map +1 -0
  136. package/packages/workflows/dist/runtime/manual-trigger.js +403 -0
  137. package/packages/workflows/dist/runtime/manual-trigger.js.map +1 -0
  138. package/packages/workflows/dist/runtime/nested-workflow-node.d.ts +48 -0
  139. package/packages/workflows/dist/runtime/nested-workflow-node.d.ts.map +1 -0
  140. package/packages/workflows/dist/runtime/nested-workflow-node.js +246 -0
  141. package/packages/workflows/dist/runtime/nested-workflow-node.js.map +1 -0
  142. package/packages/workflows/dist/runtime/one-node-agent.d.ts +34 -0
  143. package/packages/workflows/dist/runtime/one-node-agent.d.ts.map +1 -0
  144. package/packages/workflows/dist/runtime/one-node-agent.js +334 -0
  145. package/packages/workflows/dist/runtime/one-node-agent.js.map +1 -0
  146. package/packages/workflows/dist/runtime/persistence.d.ts +9 -0
  147. package/packages/workflows/dist/runtime/persistence.d.ts.map +1 -0
  148. package/packages/workflows/dist/runtime/persistence.js +67 -0
  149. package/packages/workflows/dist/runtime/persistence.js.map +1 -0
  150. package/packages/workflows/dist/runtime/pibo-routing.d.ts +126 -0
  151. package/packages/workflows/dist/runtime/pibo-routing.d.ts.map +1 -0
  152. package/packages/workflows/dist/runtime/pibo-routing.js +96 -0
  153. package/packages/workflows/dist/runtime/pibo-routing.js.map +1 -0
  154. package/packages/workflows/dist/runtime/prompts.d.ts +20 -0
  155. package/packages/workflows/dist/runtime/prompts.d.ts.map +1 -0
  156. package/packages/workflows/dist/runtime/prompts.js +235 -0
  157. package/packages/workflows/dist/runtime/prompts.js.map +1 -0
  158. package/packages/workflows/dist/runtime/retry.d.ts +35 -0
  159. package/packages/workflows/dist/runtime/retry.d.ts.map +1 -0
  160. package/packages/workflows/dist/runtime/retry.js +80 -0
  161. package/packages/workflows/dist/runtime/retry.js.map +1 -0
  162. package/packages/workflows/dist/runtime/state.d.ts +14 -0
  163. package/packages/workflows/dist/runtime/state.d.ts.map +1 -0
  164. package/packages/workflows/dist/runtime/state.js +139 -0
  165. package/packages/workflows/dist/runtime/state.js.map +1 -0
  166. package/packages/workflows/dist/runtime/time.d.ts +6 -0
  167. package/packages/workflows/dist/runtime/time.d.ts.map +1 -0
  168. package/packages/workflows/dist/runtime/time.js +42 -0
  169. package/packages/workflows/dist/runtime/time.js.map +1 -0
  170. package/packages/workflows/dist/store/catalog-records.d.ts +18 -0
  171. package/packages/workflows/dist/store/catalog-records.d.ts.map +1 -0
  172. package/packages/workflows/dist/store/catalog-records.js +59 -0
  173. package/packages/workflows/dist/store/catalog-records.js.map +1 -0
  174. package/packages/workflows/dist/store/contracts.d.ts +149 -0
  175. package/packages/workflows/dist/store/contracts.d.ts.map +1 -0
  176. package/packages/workflows/dist/store/contracts.js +2 -0
  177. package/packages/workflows/dist/store/contracts.js.map +1 -0
  178. package/packages/workflows/dist/store/draft-writes.d.ts +7 -0
  179. package/packages/workflows/dist/store/draft-writes.d.ts.map +1 -0
  180. package/packages/workflows/dist/store/draft-writes.js +18 -0
  181. package/packages/workflows/dist/store/draft-writes.js.map +1 -0
  182. package/packages/workflows/dist/store/index.d.ts +55 -0
  183. package/packages/workflows/dist/store/index.d.ts.map +1 -0
  184. package/packages/workflows/dist/store/index.js +650 -0
  185. package/packages/workflows/dist/store/index.js.map +1 -0
  186. package/packages/workflows/dist/store/list-query.d.ts +13 -0
  187. package/packages/workflows/dist/store/list-query.d.ts.map +1 -0
  188. package/packages/workflows/dist/store/list-query.js +20 -0
  189. package/packages/workflows/dist/store/list-query.js.map +1 -0
  190. package/packages/workflows/dist/store/row-mappers.d.ts +206 -0
  191. package/packages/workflows/dist/store/row-mappers.d.ts.map +1 -0
  192. package/packages/workflows/dist/store/row-mappers.js +262 -0
  193. package/packages/workflows/dist/store/row-mappers.js.map +1 -0
  194. package/packages/workflows/dist/store/schema.d.ts +13 -0
  195. package/packages/workflows/dist/store/schema.d.ts.map +1 -0
  196. package/packages/workflows/dist/store/schema.js +458 -0
  197. package/packages/workflows/dist/store/schema.js.map +1 -0
  198. package/packages/workflows/dist/store/write-values.d.ts +18 -0
  199. package/packages/workflows/dist/store/write-values.d.ts.map +1 -0
  200. package/packages/workflows/dist/store/write-values.js +227 -0
  201. package/packages/workflows/dist/store/write-values.js.map +1 -0
  202. package/packages/workflows/dist/types/index.d.ts +1202 -0
  203. package/packages/workflows/dist/types/index.d.ts.map +1 -0
  204. package/packages/workflows/dist/types/index.js +8 -0
  205. package/packages/workflows/dist/types/index.js.map +1 -0
  206. package/packages/workflows/dist/validation/diagnostics.d.ts +27 -0
  207. package/packages/workflows/dist/validation/diagnostics.d.ts.map +1 -0
  208. package/packages/workflows/dist/validation/diagnostics.js +104 -0
  209. package/packages/workflows/dist/validation/diagnostics.js.map +1 -0
  210. package/packages/workflows/dist/validation/graph-cycles.d.ts +4 -0
  211. package/packages/workflows/dist/validation/graph-cycles.d.ts.map +1 -0
  212. package/packages/workflows/dist/validation/graph-cycles.js +130 -0
  213. package/packages/workflows/dist/validation/graph-cycles.js.map +1 -0
  214. package/packages/workflows/dist/validation/graph-edges.d.ts +5 -0
  215. package/packages/workflows/dist/validation/graph-edges.d.ts.map +1 -0
  216. package/packages/workflows/dist/validation/graph-edges.js +48 -0
  217. package/packages/workflows/dist/validation/graph-edges.js.map +1 -0
  218. package/packages/workflows/dist/validation/graph-ports.d.ts +5 -0
  219. package/packages/workflows/dist/validation/graph-ports.d.ts.map +1 -0
  220. package/packages/workflows/dist/validation/graph-ports.js +55 -0
  221. package/packages/workflows/dist/validation/graph-ports.js.map +1 -0
  222. package/packages/workflows/dist/validation/index.d.ts +18 -0
  223. package/packages/workflows/dist/validation/index.d.ts.map +1 -0
  224. package/packages/workflows/dist/validation/index.js +175 -0
  225. package/packages/workflows/dist/validation/index.js.map +1 -0
  226. package/packages/workflows/dist/validation/json-schema.d.ts +12 -0
  227. package/packages/workflows/dist/validation/json-schema.d.ts.map +1 -0
  228. package/packages/workflows/dist/validation/json-schema.js +556 -0
  229. package/packages/workflows/dist/validation/json-schema.js.map +1 -0
  230. package/packages/workflows/dist/validation/registry-refs.d.ts +21 -0
  231. package/packages/workflows/dist/validation/registry-refs.d.ts.map +1 -0
  232. package/packages/workflows/dist/validation/registry-refs.js +283 -0
  233. package/packages/workflows/dist/validation/registry-refs.js.map +1 -0
  234. package/packages/workflows/dist/validation/retry-policy.d.ts +3 -0
  235. package/packages/workflows/dist/validation/retry-policy.d.ts.map +1 -0
  236. package/packages/workflows/dist/validation/retry-policy.js +114 -0
  237. package/packages/workflows/dist/validation/retry-policy.js.map +1 -0
  238. package/packages/workflows/dist/validation/schema-declarations.d.ts +5 -0
  239. package/packages/workflows/dist/validation/schema-declarations.d.ts.map +1 -0
  240. package/packages/workflows/dist/validation/schema-declarations.js +36 -0
  241. package/packages/workflows/dist/validation/schema-declarations.js.map +1 -0
  242. package/packages/workflows/dist/validation/state-access.d.ts +4 -0
  243. package/packages/workflows/dist/validation/state-access.d.ts.map +1 -0
  244. package/packages/workflows/dist/validation/state-access.js +121 -0
  245. package/packages/workflows/dist/validation/state-access.js.map +1 -0
  246. package/packages/workflows/dist/xstate/index.d.ts +73 -0
  247. package/packages/workflows/dist/xstate/index.d.ts.map +1 -0
  248. package/packages/workflows/dist/xstate/index.js +728 -0
  249. package/packages/workflows/dist/xstate/index.js.map +1 -0
  250. package/packages/workflows/package.json +23 -0
  251. package/dist/apps/chat/data/project-service.js +0 -1029
  252. package/dist/apps/chat-ui/assets/index-BwT5grLv.css +0 -1
  253. package/dist/apps/chat-ui/assets/index-PTJanD3E.js +0 -228
  254. package/dist/apps/chat-vscode-web/assets/index-D1bpAUkR.js +0 -43
@@ -1,12 +1,12 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
- import { isAbsolute, resolve } from "node:path";
2
+ import { isAbsolute } from "node:path";
3
3
  import { savePiboModelDefaults } from "../../core/model-defaults.js";
4
4
  import { DEFAULT_AGENT_RUNTIME_INSTANCE_ID } from "../../core/profiles.js";
5
5
  import { isPiboThinkingLevel } from "../../core/thinking.js";
6
6
  import { PiboWebHttpError } from "../../web/http.js";
7
7
  import { listPiPackages } from "../../pi-packages/store.js";
8
- import { withChatWebArchived } from "./session-metadata.js";
9
- import { isDefaultPiboRoom, withPiboRoomArchived, withPiboRoomWorkspace } from "./types/rooms.js";
8
+ import { withChatWebArchived, withChatWebSessionPinned } from "./session-metadata.js";
9
+ import { isDefaultPiboRoom, withPiboRoomArchived, withPiboRoomPinned, withPiboRoomWorkspace } from "./types/rooms.js";
10
10
  import { isValidCustomAgentName } from "./agent-store.js";
11
11
  export function normalizeRoomName(value, fallback = "New Chat") {
12
12
  if (value === undefined)
@@ -595,52 +595,6 @@ export function updateChatModelDefaults(body, cwd = process.cwd()) {
595
595
  subagentFast: normalizeOptionalBoolean(body.subagentFast, "subagentFast"),
596
596
  }, cwd);
597
597
  }
598
- const PROJECT_SESSION_PATCH_FIELDS = new Set([
599
- "title",
600
- "archived",
601
- ]);
602
- export function assertProjectSessionPatchFields(body) {
603
- if (!body || typeof body !== "object" || Array.isArray(body))
604
- throw new PiboWebHttpError("Invalid JSON body", 400);
605
- for (const key of Object.keys(body)) {
606
- if (!PROJECT_SESSION_PATCH_FIELDS.has(key)) {
607
- throw new PiboWebHttpError(`Unsupported project session update field: ${key}. Project workflow selection and configuration are immutable; create a new configured session to change workflow, input, prompt, model, thinking, or fast-mode values.`, 400);
608
- }
609
- }
610
- }
611
- export function normalizeProjectPath(value) {
612
- if (typeof value !== "string" || !value.trim())
613
- throw new PiboWebHttpError("Project folder is required", 400);
614
- let projectPath = value.trim();
615
- if (projectPath === "~")
616
- projectPath = process.env.HOME ?? projectPath;
617
- else if (projectPath.startsWith("~/"))
618
- projectPath = `${process.env.HOME ?? ""}${projectPath.slice(1)}`;
619
- if (!isAbsolute(projectPath))
620
- throw new PiboWebHttpError("Project folder must be an absolute path, e.g. ~/code/my-project or /home/me/code/my-project", 400);
621
- return resolve(projectPath);
622
- }
623
- export function normalizeProjectDescription(value) {
624
- if (value === undefined || value === null)
625
- return undefined;
626
- if (typeof value !== "string")
627
- throw new PiboWebHttpError("Project description must be a string", 400);
628
- return value.trim() || undefined;
629
- }
630
- export function normalizeProjectArchived(value) {
631
- if (value === undefined)
632
- return undefined;
633
- if (typeof value !== "boolean")
634
- throw new PiboWebHttpError("Project archived flag must be boolean", 400);
635
- return value;
636
- }
637
- export function normalizeProjectSessionArchived(value) {
638
- if (value === undefined)
639
- return undefined;
640
- if (typeof value !== "boolean")
641
- throw new PiboWebHttpError("Project session archived flag must be boolean", 400);
642
- return value;
643
- }
644
598
  export function normalizeSessionTitle(value) {
645
599
  if (value === undefined)
646
600
  return undefined;
@@ -670,7 +624,12 @@ export function createSessionUpdate(context, session, body) {
670
624
  const title = normalizeSessionTitle(body.title);
671
625
  if (title !== undefined)
672
626
  update.title = title;
673
- const metadata = metadataWithArchiveState(session, body.archived);
627
+ let metadata = metadataWithArchiveState(session, body.archived);
628
+ if (body.pinned !== undefined) {
629
+ if (typeof body.pinned !== "boolean")
630
+ throw new PiboWebHttpError("Session pinned flag must be boolean", 400);
631
+ metadata = withChatWebSessionPinned(metadata ?? session.metadata, body.pinned);
632
+ }
674
633
  if (metadata)
675
634
  update.metadata = metadata;
676
635
  if (body.profile !== undefined) {
@@ -707,6 +666,12 @@ export function createRoomUpdate(room, body) {
707
666
  metadata = withPiboRoomWorkspace(metadata, workspace ?? undefined);
708
667
  metadataChanged = true;
709
668
  }
669
+ if (body.pinned !== undefined) {
670
+ if (typeof body.pinned !== "boolean")
671
+ throw new PiboWebHttpError("Room pinned flag must be boolean", 400);
672
+ metadata = withPiboRoomPinned(metadata, body.pinned);
673
+ metadataChanged = true;
674
+ }
710
675
  if (metadataChanged)
711
676
  update.metadata = metadata;
712
677
  if (Object.keys(update).length === 0) {
@@ -0,0 +1,492 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, renameSync } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ const MIGRATION_ID = "remove-legacy-containers-v1";
6
+ const CATALOG_TABLES = ["workflow_ui_drafts", "workflow_published_versions", "workflow_archive_states", "workflow_delete_tombstones", "workflow_prompt_assets", "workflow_prompt_asset_revisions", "workflow_lifecycle_events"];
7
+ const LEGACY_TABLES = [
8
+ "projects", "project_sessions", "project_workflow_session_snapshots", "project_workflow_runs",
9
+ "project_workflow_wait_tokens", "project_workflow_human_actions",
10
+ ];
11
+ export function migrateLegacyProjects(input) {
12
+ if (input.dataStore.path === ":memory:" || input.workflowService.path === ":memory:") {
13
+ throw new Error("Legacy data migration requires file-backed Pibo and Workflow databases");
14
+ }
15
+ const sourcePath = resolve(input.legacyProjectPath ?? join(dirname(input.dataStore.path), "web-projects.sqlite"));
16
+ const archiveDirectory = resolve(input.archiveDirectory ?? join(dirname(input.dataStore.path), "backups"));
17
+ ensureReceiptTables(input.dataStore.db, input.workflowService.runtimeStore.db);
18
+ const dataReceipt = readReceipt(input.dataStore.db, "feature_migration_receipts");
19
+ const workflowReceipt = readReceipt(input.workflowService.runtimeStore.db, "workflow_migration_receipts");
20
+ if (dataReceipt && workflowReceipt) {
21
+ assertReceiptsEqual(dataReceipt, workflowReceipt);
22
+ if (dataReceipt.source_path)
23
+ completeSourceArchiveWithoutOpening(dataReceipt.source_path, dataReceipt.archive_path);
24
+ else if (existsSync(sourcePath))
25
+ throw new Error("A legacy source appeared after a catalog-only migration; preserve it for explicit recovery");
26
+ removeMigratedCatalogTables(input.dataStore);
27
+ return emptyResult("already-migrated", dataReceipt.archive_path || undefined);
28
+ }
29
+ const hasSource = existsSync(sourcePath);
30
+ const hasCatalog = CATALOG_TABLES.some((table) => input.dataStore.db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table));
31
+ if (!hasSource && (dataReceipt?.source_path || workflowReceipt?.source_path))
32
+ throw new Error("Legacy migration is partially committed but its recoverable source database is missing");
33
+ if (!hasSource && !hasCatalog) {
34
+ if (dataReceipt || workflowReceipt)
35
+ throw new Error("Catalog migration is partially committed but its recoverable source tables are missing");
36
+ return emptyResult("not-needed");
37
+ }
38
+ const db = input.dataStore.db;
39
+ const dataUserVersion = pragmaUserVersion(db);
40
+ const dataSynchronous = Number(db.prepare("PRAGMA main.synchronous").get().synchronous);
41
+ let sourceAttached = false;
42
+ let workflowAttached = false;
43
+ try {
44
+ if (hasSource) {
45
+ db.prepare("ATTACH DATABASE ? AS legacy_source").run(`${pathToFileURL(sourcePath).href}?mode=ro`);
46
+ sourceAttached = true;
47
+ }
48
+ db.prepare("ATTACH DATABASE ? AS workflow_target").run(input.workflowService.path);
49
+ workflowAttached = true;
50
+ if (hasSource)
51
+ validateLegacySchema(db);
52
+ // FULL makes each WAL commit durable before retiring recoverable sources.
53
+ // Matching receipts still detect a crash between the two database commits.
54
+ db.exec("PRAGMA main.synchronous = FULL; PRAGMA workflow_target.synchronous = FULL");
55
+ db.exec("BEGIN IMMEDIATE");
56
+ const digest = legacySourceDigest(db, hasSource);
57
+ if (dataReceipt && dataReceipt.source_digest !== digest)
58
+ throw new Error("Pibo migration receipt does not match the recoverable legacy source");
59
+ if (workflowReceipt && workflowReceipt.source_digest !== digest)
60
+ throw new Error("Workflow migration receipt does not match the recoverable legacy source");
61
+ const archivePath = hasSource ? join(archiveDirectory, `${basename(sourcePath)}.migrated-${digest.slice(0, 16)}`) : "";
62
+ const counts = hasSource ? legacyCounts(db) : { rooms: 0, sessions: 0, workflowSessions: 0, runs: 0 };
63
+ try {
64
+ if (hasSource) {
65
+ validateLegacyFacts(db);
66
+ migrateRoomsAndSessions(db);
67
+ migrateWorkflowFacts(db);
68
+ }
69
+ migrateCatalogFacts(db);
70
+ input.hooks?.beforeReceipts?.();
71
+ writeReceipt(db, "main", "feature_migration_receipts", digest, hasSource ? sourcePath : "", archivePath);
72
+ input.hooks?.afterDataReceipt?.();
73
+ writeReceipt(db, "workflow_target", "workflow_migration_receipts", digest, hasSource ? sourcePath : "", archivePath);
74
+ if (pragmaUserVersion(db) !== dataUserVersion)
75
+ throw new Error("Legacy migration attempted to change the Pibo data schema version");
76
+ db.exec("COMMIT");
77
+ }
78
+ catch (error) {
79
+ if (db.isTransaction)
80
+ db.exec("ROLLBACK");
81
+ throw error;
82
+ }
83
+ db.exec("DETACH DATABASE workflow_target");
84
+ workflowAttached = false;
85
+ if (sourceAttached)
86
+ db.exec("DETACH DATABASE legacy_source");
87
+ sourceAttached = false;
88
+ // Both WAL commits returned successfully before removing any recoverable
89
+ // catalog source. Partial commits retain those source tables for replay.
90
+ removeMigratedCatalogTables(input.dataStore);
91
+ if (hasSource)
92
+ completeSourceArchiveWithoutOpening(sourcePath, archivePath);
93
+ return {
94
+ status: "migrated",
95
+ roomsMigrated: counts.rooms,
96
+ sessionsMigrated: counts.sessions,
97
+ workflowSessionsMigrated: counts.workflowSessions,
98
+ workflowRunsMigrated: counts.runs,
99
+ ...(archivePath ? { archivePath } : {}),
100
+ };
101
+ }
102
+ finally {
103
+ if (db.isTransaction)
104
+ db.exec("ROLLBACK");
105
+ db.exec(`PRAGMA main.synchronous = ${dataSynchronous}`);
106
+ if (workflowAttached)
107
+ try {
108
+ db.exec("DETACH DATABASE workflow_target");
109
+ }
110
+ catch { /* preserve primary error */ }
111
+ if (sourceAttached)
112
+ try {
113
+ db.exec("DETACH DATABASE legacy_source");
114
+ }
115
+ catch { /* preserve primary error */ }
116
+ }
117
+ }
118
+ function ensureReceiptTables(dataDb, workflowDb) {
119
+ dataDb.exec(`CREATE TABLE IF NOT EXISTS feature_migration_receipts (
120
+ migration_id TEXT PRIMARY KEY, source_digest TEXT NOT NULL, source_path TEXT NOT NULL,
121
+ archive_path TEXT NOT NULL, completed_at TEXT NOT NULL
122
+ )`);
123
+ workflowDb.exec(`CREATE TABLE IF NOT EXISTS workflow_migration_receipts (
124
+ migration_id TEXT PRIMARY KEY, source_digest TEXT NOT NULL, source_path TEXT NOT NULL,
125
+ archive_path TEXT NOT NULL, completed_at TEXT NOT NULL
126
+ )`);
127
+ }
128
+ function readReceipt(db, table) {
129
+ return db.prepare(`SELECT * FROM ${table} WHERE migration_id = ?`).get(MIGRATION_ID);
130
+ }
131
+ function assertReceiptsEqual(left, right) {
132
+ for (const key of ["migration_id", "source_digest", "source_path", "archive_path"]) {
133
+ if (left[key] !== right[key])
134
+ throw new Error(`Legacy migration target receipts disagree on ${key}`);
135
+ }
136
+ }
137
+ function writeReceipt(db, schema, table, digest, sourcePath, archivePath) {
138
+ const existing = db.prepare(`SELECT * FROM ${schema}.${table} WHERE migration_id = ?`).get(MIGRATION_ID);
139
+ if (existing) {
140
+ if (existing.source_digest !== digest || existing.source_path !== sourcePath || existing.archive_path !== archivePath)
141
+ throw new Error("Legacy migration target has a conflicting receipt");
142
+ return;
143
+ }
144
+ db.prepare(`INSERT INTO ${schema}.${table} (migration_id, source_digest, source_path, archive_path, completed_at) VALUES (?, ?, ?, ?, ?)`)
145
+ .run(MIGRATION_ID, digest, sourcePath, archivePath, new Date().toISOString());
146
+ }
147
+ function validateLegacySchema(db) {
148
+ const names = new Set(db.prepare("SELECT name FROM legacy_source.sqlite_schema WHERE type = 'table'").all().map((row) => row.name));
149
+ const missing = LEGACY_TABLES.filter((table) => !names.has(table));
150
+ if (missing.length)
151
+ throw new Error(`Legacy database is malformed; missing table(s): ${missing.join(", ")}`);
152
+ }
153
+ function validateLegacyFacts(db) {
154
+ const missingSessions = db.prepare(`SELECT ps.pibo_session_id AS id FROM legacy_source.project_sessions ps
155
+ LEFT JOIN main.sessions s ON s.id = ps.pibo_session_id WHERE s.id IS NULL ORDER BY ps.pibo_session_id`).all();
156
+ if (missingSessions.length)
157
+ throw new Error(`Legacy links reference missing canonical Pibo Session(s): ${missingSessions.map((row) => row.id).join(", ")}`);
158
+ const missingParents = db.prepare(`SELECT ps.pibo_session_id AS id, ps.parent_main_session_id AS parent_id FROM legacy_source.project_sessions ps
159
+ LEFT JOIN main.sessions parent ON parent.id = ps.parent_main_session_id
160
+ WHERE ps.parent_main_session_id IS NOT NULL AND parent.id IS NULL`).all();
161
+ if (missingParents.length)
162
+ throw new Error(`Legacy hierarchy references missing parent Session '${missingParents[0].parent_id}' from '${missingParents[0].id}'`);
163
+ const hierarchyConflict = db.prepare(`SELECT ps.pibo_session_id AS id FROM legacy_source.project_sessions ps JOIN main.sessions s ON s.id = ps.pibo_session_id
164
+ WHERE ps.parent_main_session_id IS NOT NULL AND s.parent_id IS NOT NULL AND s.parent_id <> ps.parent_main_session_id LIMIT 1`).get();
165
+ if (hierarchyConflict)
166
+ throw new Error(`Legacy hierarchy conflicts with canonical Session '${hierarchyConflict.id}'`);
167
+ const missingRun = db.prepare(`SELECT ps.pibo_session_id AS id FROM legacy_source.project_sessions ps
168
+ LEFT JOIN legacy_source.project_workflow_runs r ON r.id = ps.workflow_run_id
169
+ LEFT JOIN workflow_target.workflow_runs current ON current.id = ps.workflow_run_id
170
+ WHERE ps.workflow_run_id IS NOT NULL AND r.id IS NULL AND current.id IS NULL LIMIT 1`).get();
171
+ if (missingRun)
172
+ throw new Error(`Legacy Workflow Session '${missingRun.id}' references a missing run`);
173
+ const orphanSnapshots = db.prepare(`SELECT x.id FROM legacy_source.project_workflow_session_snapshots x LEFT JOIN legacy_source.project_sessions ps ON ps.pibo_session_id = x.pibo_session_id
174
+ WHERE ps.pibo_session_id IS NULL OR x.project_id <> ps.project_id OR x.workflow_id <> ps.workflow_id OR x.workflow_version <> ps.workflow_version LIMIT 1`).get();
175
+ if (orphanSnapshots)
176
+ throw new Error(`Legacy Workflow snapshot '${orphanSnapshots.id}' references a missing Session link`);
177
+ const orphanRuns = db.prepare(`SELECT r.id FROM legacy_source.project_workflow_runs r
178
+ LEFT JOIN legacy_source.project_sessions ps ON ps.pibo_session_id = r.pibo_session_id
179
+ LEFT JOIN legacy_source.project_workflow_session_snapshots x ON x.id = r.snapshot_id
180
+ WHERE ps.pibo_session_id IS NULL OR x.id IS NULL OR r.project_id <> ps.project_id
181
+ OR x.pibo_session_id <> r.pibo_session_id OR x.workflow_id <> r.workflow_id OR x.workflow_version <> r.workflow_version OR x.effective_definition_hash <> r.effective_definition_hash LIMIT 1`).get();
182
+ if (orphanRuns)
183
+ throw new Error(`Legacy Workflow run '${orphanRuns.id}' has a missing Session or snapshot reference`);
184
+ const orphanWait = db.prepare(`SELECT w.id FROM legacy_source.project_workflow_wait_tokens w LEFT JOIN legacy_source.project_workflow_runs r ON r.id = w.workflow_run_id WHERE r.id IS NULL OR w.project_id <> r.project_id OR w.pibo_session_id <> r.pibo_session_id LIMIT 1`).get();
185
+ if (orphanWait)
186
+ throw new Error(`Legacy Workflow wait '${orphanWait.id}' references a missing run`);
187
+ const orphanAction = db.prepare(`SELECT a.id FROM legacy_source.project_workflow_human_actions a
188
+ LEFT JOIN legacy_source.project_workflow_runs r ON r.id = a.workflow_run_id
189
+ LEFT JOIN legacy_source.project_workflow_wait_tokens w ON w.id = a.wait_token_id
190
+ WHERE r.id IS NULL OR w.id IS NULL OR w.workflow_run_id <> a.workflow_run_id OR a.project_id <> r.project_id OR a.pibo_session_id <> r.pibo_session_id LIMIT 1`).get();
191
+ if (orphanAction)
192
+ throw new Error(`Legacy Workflow human action '${orphanAction.id}' has a missing run or wait reference`);
193
+ for (const [table, columns] of [
194
+ ["projects", ["metadata_json"]], ["project_sessions", ["configuration_json"]],
195
+ ["project_workflow_session_snapshots", ["snapshot_json"]],
196
+ ["project_workflow_runs", ["current_json", "input_json", "validation_json"]],
197
+ ["project_workflow_wait_tokens", ["actions_json", "schema_json", "resume_payload_json"]],
198
+ ["project_workflow_human_actions", ["actor_json", "payload_json"]],
199
+ ])
200
+ validateJsonColumns(db, `legacy_source.${table}`, columns);
201
+ for (const [table, columns] of [
202
+ ["projects", ["metadata_json"]], ["project_sessions", ["configuration_json"]],
203
+ ["project_workflow_session_snapshots", ["snapshot_json"]], ["project_workflow_runs", ["current_json", "input_json", "validation_json"]],
204
+ ["project_workflow_wait_tokens", ["schema_json"]], ["project_workflow_human_actions", ["actor_json"]],
205
+ ]) {
206
+ for (const column of columns)
207
+ if (db.prepare(`SELECT 1 FROM legacy_source.${table} WHERE ${column} IS NOT NULL AND json_type(${column}) <> 'object' LIMIT 1`).get())
208
+ throw new Error(`Legacy ${table}.${column} must contain JSON objects`);
209
+ }
210
+ if (db.prepare("SELECT 1 FROM legacy_source.project_workflow_wait_tokens WHERE json_type(actions_json) <> 'array' LIMIT 1").get())
211
+ throw new Error("Legacy wait actions must contain a JSON array");
212
+ }
213
+ function validateJsonColumns(db, table, columns) {
214
+ for (const column of columns) {
215
+ const bad = db.prepare(`SELECT rowid AS id FROM ${table} WHERE ${column} IS NOT NULL AND json_valid(${column}) = 0 LIMIT 1`).get();
216
+ if (bad)
217
+ throw new Error(`Legacy database contains malformed JSON in ${table}.${column} at row ${bad.id}`);
218
+ }
219
+ }
220
+ function migrateRoomsAndSessions(db) {
221
+ const projects = db.prepare("SELECT * FROM legacy_source.projects ORDER BY id").all();
222
+ for (const source of projects) {
223
+ const roomId = migratedRoomId(source.id);
224
+ const sourceMetadata = parseObject(source.metadata_json, `Legacy container '${source.id}' metadata`);
225
+ delete sourceMetadata.default;
226
+ const metadata = { ...sourceMetadata, workspace: source.project_folder, migration: { kind: "legacy-container", sourceId: source.id }, ...(source.archived_at ? { chatRoomArchivedAt: source.archived_at } : {}) };
227
+ insertOrAssert(db, "main.rooms", "id", {
228
+ id: roomId, name: source.name, topic: source.description, type: "chat", parent_room_id: null,
229
+ workspace: source.project_folder, retention_policy_id: null,
230
+ metadata_json: JSON.stringify(metadata), created_at: source.created_at, updated_at: source.updated_at,
231
+ });
232
+ }
233
+ const links = db.prepare("SELECT * FROM legacy_source.project_sessions ORDER BY created_at, pibo_session_id").all();
234
+ for (const link of links) {
235
+ const sourceProject = projects.find((project) => project.id === link.project_id);
236
+ if (!sourceProject)
237
+ throw new Error(`Legacy Session '${link.pibo_session_id}' references missing container '${link.project_id}'`);
238
+ const session = db.prepare("SELECT * FROM main.sessions WHERE id = ?").get(link.pibo_session_id);
239
+ const metadata = parseObject(session.metadata_json, `Session '${session.id}' metadata`);
240
+ delete metadata.projectId;
241
+ delete metadata.projectSessionKind;
242
+ metadata.chatRoomId = migratedRoomId(link.project_id);
243
+ const workflowBacked = isWorkflowBacked(link);
244
+ if (workflowBacked) {
245
+ if (!["main_workflow", "nested_workflow", "agent_node"].includes(String(metadata.workflowSessionKind)))
246
+ metadata.workflowSessionKind = link.kind === "main" ? "main_workflow" : "agent_node";
247
+ metadata.workflowId = link.workflow_id;
248
+ if (link.workflow_version)
249
+ metadata.workflowVersion = link.workflow_version;
250
+ if (link.workflow_run_id)
251
+ metadata.workflowRunId = link.workflow_run_id;
252
+ }
253
+ else {
254
+ delete metadata.workflowId;
255
+ delete metadata.workflowVersion;
256
+ delete metadata.workflowSessionKind;
257
+ }
258
+ const parentId = session.parent_id ?? link.parent_main_session_id;
259
+ const rootId = parentId ? canonicalRootId(db, parentId) : session.id;
260
+ if (parentId)
261
+ metadata.rootSessionId = rootId;
262
+ const archivedAt = session.archived_at ?? (typeof metadata.chatWebArchivedAt === "string" ? metadata.chatWebArchivedAt : link.archived ? link.updated_at : null);
263
+ if (archivedAt)
264
+ metadata.chatWebArchivedAt = archivedAt;
265
+ db.prepare(`UPDATE main.sessions SET kind = 'chat', room_id = ?, root_session_id = ?, parent_id = ?, workspace = COALESCE(NULLIF(workspace, ''), ?),
266
+ archived_at = ?, metadata_json = ? WHERE id = ?`).run(migratedRoomId(link.project_id), rootId, parentId, sourceProject.project_folder, archivedAt, JSON.stringify(metadata), session.id);
267
+ const updated = db.prepare("SELECT * FROM main.sessions WHERE id = ?").get(session.id);
268
+ const childCount = Number(db.prepare("SELECT COUNT(*) AS count FROM main.sessions WHERE parent_id = ? AND deleted_at IS NULL").get(session.id).count);
269
+ insertOrUpdateNavigation(db, updated, childCount);
270
+ }
271
+ }
272
+ function migrateWorkflowFacts(db) {
273
+ const links = db.prepare("SELECT * FROM legacy_source.project_sessions ORDER BY pibo_session_id").all();
274
+ for (const link of links.filter(isWorkflowBacked)) {
275
+ if (!link.workflow_version)
276
+ throw new Error(`Legacy Workflow Session '${link.pibo_session_id}' has no workflow version`);
277
+ const state = normalizeLinkState(link.state);
278
+ insertOrAssert(db, "workflow_target.workflow_session_links", "pibo_session_id", {
279
+ pibo_session_id: link.pibo_session_id, workflow_id: link.workflow_id, workflow_version: link.workflow_version,
280
+ workflow_run_id: link.workflow_run_id, state, configuration_json: link.configuration_json,
281
+ created_at: link.created_at, updated_at: link.updated_at,
282
+ });
283
+ }
284
+ const snapshots = db.prepare("SELECT * FROM legacy_source.project_workflow_session_snapshots ORDER BY id").all();
285
+ for (const row of snapshots) {
286
+ const snapshot = parseObject(row.snapshot_json, `Workflow snapshot '${row.id}'`);
287
+ delete snapshot.projectId;
288
+ snapshot.piboSessionId = row.pibo_session_id;
289
+ insertOrAssert(db, "workflow_target.workflow_session_snapshots", "id", {
290
+ id: row.id, pibo_session_id: row.pibo_session_id, workflow_id: row.workflow_id, workflow_version: row.workflow_version,
291
+ base_definition_hash: row.base_definition_hash, effective_definition_hash: row.effective_definition_hash,
292
+ snapshot_json: JSON.stringify(snapshot), created_at: row.created_at,
293
+ });
294
+ const effectiveDefinition = requireObject(snapshot.effectiveDefinition, `Workflow snapshot '${row.id}' effective definition`);
295
+ const existingDefinition = db.prepare("SELECT compiled_definition_json FROM workflow_target.workflow_definition_snapshots WHERE workflow_id = ? AND workflow_version = ? AND definition_hash = ?").get(row.workflow_id, row.workflow_version, row.effective_definition_hash);
296
+ if (existingDefinition) {
297
+ if (JSON.stringify(JSON.parse(existingDefinition.compiled_definition_json)) !== JSON.stringify(effectiveDefinition))
298
+ throw new Error(`Migration conflict in executable Workflow snapshot '${row.id}'`);
299
+ }
300
+ else
301
+ insertOrAssert(db, "workflow_target.workflow_definition_snapshots", "id", {
302
+ id: row.id, workflow_id: row.workflow_id, workflow_version: row.workflow_version,
303
+ definition_hash: row.effective_definition_hash, compiled_definition_json: JSON.stringify(effectiveDefinition), created_at: row.created_at,
304
+ });
305
+ }
306
+ const runs = db.prepare("SELECT * FROM legacy_source.project_workflow_runs ORDER BY id").all();
307
+ for (const run of runs) {
308
+ const executable = db.prepare("SELECT id FROM workflow_target.workflow_definition_snapshots WHERE workflow_id = ? AND workflow_version = ? AND definition_hash = ?").get(run.workflow_id, run.workflow_version, run.effective_definition_hash);
309
+ if (!executable)
310
+ throw new Error(`Legacy Workflow run '${run.id}' is missing its executable snapshot`);
311
+ insertOrAssert(db, "workflow_target.workflow_runs", "id", {
312
+ id: run.id, workflow_id: run.workflow_id, workflow_version: run.workflow_version,
313
+ workflow_definition_hash: run.effective_definition_hash, definition_snapshot_id: executable.id,
314
+ parent_run_id: null, parent_node_attempt_id: null, pibo_session_id: run.pibo_session_id, environment_json: null,
315
+ status: run.status, current_node_id: null, current_edge_id: null, current_status: run.status,
316
+ current_json: run.current_json, input_json: run.input_json, output_json: null, output_present: 0,
317
+ state_json: "{\"global\":{}}", checkpoint_json: null, validation_json: run.validation_json,
318
+ created_at: run.created_at, updated_at: run.updated_at, completed_at: run.completed_at, failed_at: run.failed_at, cancelled_at: run.cancelled_at,
319
+ });
320
+ }
321
+ const waits = db.prepare("SELECT * FROM legacy_source.project_workflow_wait_tokens ORDER BY id").all();
322
+ for (const wait of waits)
323
+ insertOrAssert(db, "workflow_target.workflow_wait_tokens", "id", {
324
+ id: wait.id, workflow_run_id: wait.workflow_run_id, node_attempt_id: wait.node_attempt_id, human_node_id: wait.human_node_id,
325
+ kind: null, available_actions_json: wait.actions_json, prompt: wait.prompt, schema_json: wait.schema_json, status: wait.status,
326
+ resume_payload_json: wait.resume_payload_json, resume_payload_present: wait.resume_payload_present, expires_at: wait.expires_at,
327
+ created_at: wait.created_at, resolved_at: wait.resolved_at,
328
+ });
329
+ const actions = db.prepare("SELECT * FROM legacy_source.project_workflow_human_actions ORDER BY id").all();
330
+ for (const action of actions)
331
+ insertOrAssert(db, "workflow_target.workflow_human_actions", "id", {
332
+ id: action.id, workflow_run_id: action.workflow_run_id, wait_token_id: action.wait_token_id, action_id: action.action_id,
333
+ kind: action.kind, actor_json: action.actor_json, payload_json: action.payload_present ? action.payload_json : null, created_at: action.created_at,
334
+ });
335
+ }
336
+ function migrateCatalogFacts(db) {
337
+ const tables = new Set(db.prepare("SELECT name FROM main.sqlite_schema WHERE type = 'table' AND name LIKE 'workflow_%'").all().map((row) => row.name));
338
+ if (tables.has("workflow_ui_drafts")) {
339
+ validateJsonColumns(db, "main.workflow_ui_drafts", ["definition_json", "diagnostics_json", "validation_json"]);
340
+ for (const row of db.prepare("SELECT * FROM main.workflow_ui_drafts ORDER BY draft_id").all()) {
341
+ insertOrAssert(db, "workflow_target.workflow_drafts", "draft_id", {
342
+ draft_id: row.draft_id, workflow_id: row.workflow_id, source: row.source, status: row.status,
343
+ base_workflow_id: row.base_workflow_id, base_workflow_version: row.base_workflow_version, base_definition_hash: row.base_definition_hash,
344
+ target_workflow_version: row.target_workflow_version, version_intent: row.version_intent, definition_json: row.definition_json,
345
+ diagnostics_json: row.diagnostics_json, validation_json: row.validation_json, validation_state: row.validation_state,
346
+ revision: row.revision, created_by: null, created_at: row.created_at, updated_by: null, updated_at: row.updated_at,
347
+ });
348
+ }
349
+ }
350
+ copySameShape(db, tables, "workflow_published_versions", ["workflow_id", "version"]);
351
+ copySameShape(db, tables, "workflow_archive_states", ["workflow_id"]);
352
+ copySameShape(db, tables, "workflow_prompt_assets", ["asset_id"]);
353
+ copySameShape(db, tables, "workflow_prompt_asset_revisions", ["revision_id"]);
354
+ if (tables.has("workflow_delete_tombstones")) {
355
+ for (const row of db.prepare("SELECT * FROM main.workflow_delete_tombstones ORDER BY workflow_id").all())
356
+ insertOrAssert(db, "workflow_target.workflow_delete_tombstones", "workflow_id", {
357
+ workflow_id: row.workflow_id, source: row.source, deleted: row.deleted, deleted_at: row.deleted_at, deleted_by: row.deleted_by,
358
+ last_known_title: row.last_known_title, last_known_version: row.last_known_version, last_definition_hash: row.last_definition_hash, created_at: row.updated_at,
359
+ });
360
+ }
361
+ if (tables.has("workflow_lifecycle_events")) {
362
+ for (const row of db.prepare("SELECT * FROM main.workflow_lifecycle_events ORDER BY id").all())
363
+ insertOrAssert(db, "workflow_target.workflow_lifecycle_events", "id", {
364
+ id: row.id, type: normalizeLifecycleType(String(row.type)), actor_id: row.actor_id, workflow_id: row.workflow_id, workflow_version: row.workflow_version,
365
+ draft_id: row.draft_id, pibo_session_id: row.pibo_session_id, workflow_run_id: row.workflow_run_id, status: row.status,
366
+ validation_json: row.validation_json, diagnostics_json: row.diagnostics_json, payload_json: row.payload_json, created_at: row.created_at,
367
+ });
368
+ }
369
+ migrateDerivedWorkflowIdentities(db);
370
+ }
371
+ function migrateDerivedWorkflowIdentities(db) {
372
+ const rows = db.prepare(`SELECT workflow_id, MIN(created_at) AS created_at, MAX(updated_at) AS updated_at FROM (
373
+ SELECT workflow_id, created_at, updated_at FROM workflow_target.workflow_drafts
374
+ UNION ALL SELECT workflow_id, created_at, published_at FROM workflow_target.workflow_published_versions
375
+ ) GROUP BY workflow_id ORDER BY workflow_id`).all();
376
+ for (const row of rows) {
377
+ const draft = db.prepare("SELECT draft_id, definition_json FROM workflow_target.workflow_drafts WHERE workflow_id = ? ORDER BY updated_at DESC LIMIT 1").get(row.workflow_id);
378
+ const published = db.prepare("SELECT version, definition_json FROM workflow_target.workflow_published_versions WHERE workflow_id = ? ORDER BY published_at DESC LIMIT 1").get(row.workflow_id);
379
+ const definition = parseObject(draft?.definition_json ?? published?.definition_json ?? "{}", `Workflow identity '${row.workflow_id}' definition`);
380
+ insertOrAssert(db, "workflow_target.workflow_identities", "workflow_id", {
381
+ workflow_id: row.workflow_id, source: "ui", title: typeof definition.title === "string" ? definition.title : row.workflow_id,
382
+ description: typeof definition.description === "string" ? definition.description : null,
383
+ tags_json: JSON.stringify(Array.isArray(definition.metadata) ? [] : requireObjectOrEmpty(definition.metadata).tags ?? []),
384
+ current_draft_id: draft?.draft_id ?? null, latest_version: published?.version ?? null, created_by: null,
385
+ created_at: row.created_at, updated_by: null, updated_at: row.updated_at,
386
+ });
387
+ }
388
+ }
389
+ function copySameShape(db, tables, table, keyColumns) {
390
+ if (!tables.has(table))
391
+ return;
392
+ const targetColumns = db.prepare(`PRAGMA workflow_target.table_info(${table})`).all().map((row) => row.name);
393
+ const sourceColumns = new Set(db.prepare(`PRAGMA main.table_info(${table})`).all().map((row) => row.name));
394
+ const columns = targetColumns.filter((column) => sourceColumns.has(column));
395
+ const rows = db.prepare(`SELECT ${columns.join(", ")} FROM main.${table} ORDER BY ${keyColumns.join(", ")}`).all();
396
+ for (const row of rows)
397
+ insertOrAssert(db, `workflow_target.${table}`, keyColumns, row);
398
+ }
399
+ function insertOrAssert(db, table, key, values) {
400
+ const keys = Array.isArray(key) ? key : [key];
401
+ const where = keys.map((column) => `${column} = ?`).join(" AND ");
402
+ const existing = db.prepare(`SELECT * FROM ${table} WHERE ${where}`).get(...keys.map((column) => values[column]));
403
+ if (existing) {
404
+ for (const [column, value] of Object.entries(values)) {
405
+ if (!sqliteValuesEqual(existing[column], value))
406
+ throw new Error(`Migration conflict in ${table} for ${keys.map((column) => `${column}=${String(values[column])}`).join(", ")} at ${column}`);
407
+ }
408
+ return;
409
+ }
410
+ const columns = Object.keys(values);
411
+ db.prepare(`INSERT INTO ${table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`).run(...columns.map((column) => (values[column] ?? null)));
412
+ }
413
+ function insertOrUpdateNavigation(db, session, childCount) {
414
+ db.prepare(`INSERT INTO main.session_navigation (
415
+ room_id, session_id, root_session_id, parent_id, origin_id, title, profile, status, archived_at,
416
+ last_activity_at, last_message_preview, child_count, sort_key, updated_at
417
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
418
+ ON CONFLICT(session_id) DO UPDATE SET room_id=excluded.room_id, root_session_id=excluded.root_session_id,
419
+ parent_id=excluded.parent_id, origin_id=excluded.origin_id, title=excluded.title, profile=excluded.profile,
420
+ status=excluded.status, archived_at=excluded.archived_at, last_activity_at=excluded.last_activity_at,
421
+ child_count=excluded.child_count, updated_at=excluded.updated_at`).run(session.room_id, session.id, session.root_session_id, session.parent_id, session.origin_id, session.title, session.profile, session.status, session.archived_at, session.last_activity_at, session.first_message_preview, childCount, session.last_activity_at, session.updated_at);
422
+ }
423
+ function removeMigratedCatalogTables(dataStore) {
424
+ dataStore.transaction(() => {
425
+ for (const table of CATALOG_TABLES)
426
+ dataStore.db.exec(`DROP TABLE IF EXISTS ${table}`);
427
+ });
428
+ }
429
+ function legacySourceDigest(db, hasSource) {
430
+ const facts = {};
431
+ if (hasSource)
432
+ for (const table of LEGACY_TABLES)
433
+ facts[table] = db.prepare(`SELECT * FROM legacy_source.${table} ORDER BY rowid`).all();
434
+ for (const table of CATALOG_TABLES) {
435
+ if (db.prepare("SELECT 1 FROM main.sqlite_schema WHERE type='table' AND name=?").get(table))
436
+ facts[`catalog:${table}`] = db.prepare(`SELECT * FROM main.${table} ORDER BY rowid`).all();
437
+ }
438
+ return createHash("sha256").update(JSON.stringify(facts)).digest("hex");
439
+ }
440
+ function legacyCounts(db) {
441
+ const count = (table, where = "") => Number(db.prepare(`SELECT COUNT(*) AS count FROM legacy_source.${table} ${where}`).get().count);
442
+ return { rooms: count("projects"), sessions: count("project_sessions"), workflowSessions: count("project_sessions", "WHERE workflow_id <> 'simple-chat' OR state NOT IN ('simple_chat', '')"), runs: count("project_workflow_runs") };
443
+ }
444
+ function migratedRoomId(sourceId) { return `room_migrated_${createHash("sha256").update(sourceId).digest("hex").slice(0, 24)}`; }
445
+ function canonicalRootId(db, parentId) {
446
+ const visited = new Set();
447
+ let id = parentId;
448
+ while (true) {
449
+ if (visited.has(id))
450
+ throw new Error(`Canonical Session hierarchy contains a cycle at '${id}'`);
451
+ visited.add(id);
452
+ const row = db.prepare("SELECT parent_id FROM main.sessions WHERE id = ?").get(id);
453
+ if (!row)
454
+ throw new Error(`Missing canonical parent Session '${id}'`);
455
+ if (!row.parent_id)
456
+ return id;
457
+ id = row.parent_id;
458
+ }
459
+ }
460
+ function isWorkflowBacked(row) { return row.workflow_id !== "simple-chat" || (row.state !== null && row.state !== "simple_chat"); }
461
+ function normalizeLinkState(value) { if (value === "running" || value === "waiting" || value === "completed" || value === "failed" || value === "cancelled" || value === "configured")
462
+ return value; if (value === "workflow")
463
+ return "configured"; throw new Error(`Unsupported legacy Workflow Session state '${String(value)}'`); }
464
+ function normalizeLifecycleType(value) { return value.replace(/^project\.workflow_session\.created$/, "workflow.session.created").replace(/^project\.workflow_start\./, "workflow.start."); }
465
+ function parseObject(value, label) { const parsed = typeof value === "string" ? JSON.parse(value) : value; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
466
+ throw new Error(`${label} is not a JSON object`); return parsed; }
467
+ function requireObject(value, label) { return parseObject(value, label); }
468
+ function requireObjectOrEmpty(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; }
469
+ function sqliteValuesEqual(left, right) { return (left ?? null) === (right ?? null); }
470
+ function pragmaUserVersion(db) { return Number(db.prepare("PRAGMA main.user_version").get().user_version); }
471
+ function completeSourceArchiveWithoutOpening(sourcePath, archivePath) {
472
+ const sourceExists = existsSync(sourcePath);
473
+ const archiveExists = existsSync(archivePath);
474
+ const sidecars = ["-wal", "-shm"].filter((suffix) => existsSync(`${sourcePath}${suffix}`));
475
+ if (!sourceExists && !archiveExists) {
476
+ if (sidecars.length)
477
+ throw new Error("Legacy archive has orphaned sidecars; preserve them for recovery");
478
+ return;
479
+ }
480
+ if (sourceExists && archiveExists)
481
+ throw new Error(`Cannot archive legacy source because '${archivePath}' already exists`);
482
+ for (const suffix of sidecars)
483
+ if (existsSync(`${archivePath}${suffix}`))
484
+ throw new Error(`Legacy archive sidecar conflicts at '${archivePath}${suffix}'`);
485
+ mkdirSync(dirname(archivePath), { recursive: true });
486
+ if (sourceExists)
487
+ renameSync(sourcePath, archivePath);
488
+ // A restart can arrive after the base rename but before its WAL/SHM rename.
489
+ for (const suffix of sidecars)
490
+ renameSync(`${sourcePath}${suffix}`, `${archivePath}${suffix}`);
491
+ }
492
+ function emptyResult(status, archivePath) { return { status, roomsMigrated: 0, sessionsMigrated: 0, workflowSessionsMigrated: 0, workflowRunsMigrated: 0, ...(archivePath ? { archivePath } : {}) }; }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { roomWorkspaceFromMetadata } from "../types/rooms.js";
2
+ import { comparePiboRoomsBySidebarOrder, roomWorkspaceFromMetadata } from "../types/rooms.js";
3
3
  import { roomFromRow } from "./chat-data-mappers.js";
4
4
  export class PiboRoomHierarchyCycleError extends Error {
5
5
  constructor() {
@@ -39,7 +39,7 @@ export class ChatRoomService {
39
39
  return Number(result.changes ?? 0);
40
40
  }
41
41
  getRoom(id) { const row = this.store.db.prepare("SELECT * FROM rooms WHERE id = ?").get(id); return row ? roomFromRow(row) : undefined; }
42
- listRooms() { return this.store.db.prepare("SELECT * FROM rooms ORDER BY updated_at DESC, id ASC").all().map(roomFromRow); }
42
+ listRooms() { return this.store.db.prepare("SELECT * FROM rooms").all().map(roomFromRow).sort(comparePiboRoomsBySidebarOrder); }
43
43
  listRoomTree() { const byId = new Map(this.listRooms().map((room) => [room.id, { ...room, children: [] }])); const roots = []; for (const node of byId.values()) {
44
44
  const parent = node.parentRoomId ? byId.get(node.parentRoomId) : undefined;
45
45
  if (parent)
@@ -0,0 +1 @@
1
+ export {};