forgeos 0.1.0-alpha.2 → 0.1.0-alpha.20

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 (406) hide show
  1. package/.npmignore +4 -0
  2. package/AGENTS.md +168 -81
  3. package/CHANGELOG.md +199 -0
  4. package/README.md +88 -14
  5. package/adapters/go/README.md +23 -0
  6. package/adapters/go/go.mod +3 -0
  7. package/adapters/go/http.go +149 -0
  8. package/adapters/go/registry.go +234 -0
  9. package/adapters/go/types.go +136 -0
  10. package/adapters/java/README.md +68 -0
  11. package/adapters/java/pom.xml +34 -0
  12. package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
  13. package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
  14. package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
  15. package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
  16. package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
  17. package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
  18. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
  19. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
  20. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
  21. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
  22. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
  23. package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
  24. package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
  25. package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
  26. package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
  27. package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
  28. package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
  29. package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
  30. package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
  31. package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
  32. package/adapters/java/target/classes/dev/forgeos/adapter/Auth.class +0 -0
  33. package/adapters/java/target/classes/dev/forgeos/adapter/Diagnostic.class +0 -0
  34. package/adapters/java/target/classes/dev/forgeos/adapter/Entry.class +0 -0
  35. package/adapters/java/target/classes/dev/forgeos/adapter/EntryKind.class +0 -0
  36. package/adapters/java/target/classes/dev/forgeos/adapter/ErrorInfo.class +0 -0
  37. package/adapters/java/target/classes/dev/forgeos/adapter/Forge.class +0 -0
  38. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeCall.class +0 -0
  39. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeContext.class +0 -0
  40. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHandler.class +0 -0
  41. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHttpHandler.class +0 -0
  42. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$EntryOption.class +0 -0
  43. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegisteredEntry.class +0 -0
  44. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegistryOption.class +0 -0
  45. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry.class +0 -0
  46. package/adapters/java/target/classes/dev/forgeos/adapter/Json.class +0 -0
  47. package/adapters/java/target/classes/dev/forgeos/adapter/Manifest.class +0 -0
  48. package/adapters/java/target/classes/dev/forgeos/adapter/RequestEnvelope.class +0 -0
  49. package/adapters/java/target/classes/dev/forgeos/adapter/ResponseEnvelope.class +0 -0
  50. package/adapters/java/target/classes/dev/forgeos/adapter/Risk.class +0 -0
  51. package/adapters/java/target/classes/dev/forgeos/adapter/Schemas.class +0 -0
  52. package/adapters/java/target/classes/dev/forgeos/adapter/Service.class +0 -0
  53. package/adapters/java/target/classes/dev/forgeos/adapter/TransactionMode.class +0 -0
  54. package/adapters/java/target/classes/dev/forgeos/adapter/TypedForgeHandler.class +0 -0
  55. package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
  56. package/adapters/java/target/maven-archiver/pom.properties +3 -0
  57. package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +23 -0
  58. package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +20 -0
  59. package/adapters/java-spring-boot-starter/README.md +32 -0
  60. package/adapters/java-spring-boot-starter/pom.xml +36 -0
  61. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
  62. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
  63. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
  64. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
  65. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
  66. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
  67. package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
  68. package/adapters/java-spring-boot-starter/target/classes/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
  69. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeCommand.class +0 -0
  70. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeExternalService.class +0 -0
  71. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeQuery.class +0 -0
  72. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.class +0 -0
  73. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.class +0 -0
  74. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringRuntime.class +0 -0
  75. package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
  76. package/adapters/java-spring-boot-starter/target/maven-archiver/pom.properties +3 -0
  77. package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +6 -0
  78. package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +6 -0
  79. package/bin/forge.mjs +18 -0
  80. package/docs/changelog.md +212 -0
  81. package/docs/forge-protocol.md +189 -0
  82. package/examples/go-billing/go.mod +7 -0
  83. package/examples/go-billing/main.go +120 -0
  84. package/examples/java-billing/pom.xml +52 -0
  85. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
  86. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
  87. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
  88. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/CreateInvoiceInput.class +0 -0
  89. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Invoice.class +0 -0
  90. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$EmptyInput.class +0 -0
  91. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$Options.class +0 -0
  92. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main.class +0 -0
  93. package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
  94. package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
  95. package/examples/java-billing/target/maven-archiver/pom.properties +3 -0
  96. package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +5 -0
  97. package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +3 -0
  98. package/package.json +29 -7
  99. package/schemas/forge-manifest.schema.json +57 -0
  100. package/src/forge/_generated/releaseManifest.json +1 -2
  101. package/src/forge/_generated/releaseManifest.ts +3 -3
  102. package/src/forge/agent-adapters/index.ts +1511 -123
  103. package/src/forge/agent-adapters/types.ts +216 -1
  104. package/src/forge/agent-memory/bridge.ts +1192 -0
  105. package/src/forge/agent-memory/context-pack.ts +151 -0
  106. package/src/forge/agent-memory/hook-runner.ts +312 -0
  107. package/src/forge/agent-memory/mcp.ts +224 -0
  108. package/src/forge/agent-memory/normalize.ts +498 -0
  109. package/src/forge/agent-memory/redaction.ts +103 -0
  110. package/src/forge/agent-memory/sources/claude-code.ts +51 -0
  111. package/src/forge/agent-memory/sources/codex-hook-runner.mjs +84 -0
  112. package/src/forge/agent-memory/sources/codex.ts +119 -0
  113. package/src/forge/agent-memory/sources/cursor.ts +35 -0
  114. package/src/forge/agent-memory/types.ts +191 -0
  115. package/src/forge/bench.ts +248 -0
  116. package/src/forge/brownfield-import/index.ts +736 -0
  117. package/src/forge/brownfield-import/types.ts +127 -0
  118. package/src/forge/cair/action-journal.ts +61 -0
  119. package/src/forge/cair/action-parser.ts +314 -0
  120. package/src/forge/cair/action-validator.ts +40 -0
  121. package/src/forge/cair/actions.ts +1818 -0
  122. package/src/forge/cair/format.ts +77 -0
  123. package/src/forge/cair/index.ts +106 -0
  124. package/src/forge/cair/query.ts +478 -0
  125. package/src/forge/cair/snapshot.ts +315 -0
  126. package/src/forge/cair/types.ts +248 -0
  127. package/src/forge/cli/ai.ts +671 -3
  128. package/src/forge/cli/auth.ts +36 -1
  129. package/src/forge/cli/build.ts +20 -4
  130. package/src/forge/cli/changed.ts +300 -0
  131. package/src/forge/cli/codex-app-server.ts +877 -0
  132. package/src/forge/cli/commands.ts +1285 -7
  133. package/src/forge/cli/db.ts +121 -2
  134. package/src/forge/cli/deps.ts +79 -12
  135. package/src/forge/cli/dev.ts +502 -38
  136. package/src/forge/cli/docs.ts +265 -0
  137. package/src/forge/cli/handoff.ts +250 -0
  138. package/src/forge/cli/index.ts +1 -0
  139. package/src/forge/cli/main.ts +49 -3
  140. package/src/forge/cli/new.ts +3 -1
  141. package/src/forge/cli/next-actions.ts +23 -0
  142. package/src/forge/cli/output.ts +290 -1
  143. package/src/forge/cli/parse.ts +770 -36
  144. package/src/forge/cli/query.ts +32 -0
  145. package/src/forge/cli/release.ts +35 -11
  146. package/src/forge/cli/rls.ts +568 -17
  147. package/src/forge/cli/run.ts +41 -0
  148. package/src/forge/cli/secrets.ts +46 -1
  149. package/src/forge/cli/security.ts +381 -0
  150. package/src/forge/cli/self-host.ts +56 -14
  151. package/src/forge/cli/studio.ts +2163 -0
  152. package/src/forge/cli/verify.ts +1422 -32
  153. package/src/forge/compiler/agent-contract/build.ts +725 -41
  154. package/src/forge/compiler/agent-contract/types.ts +85 -0
  155. package/src/forge/compiler/ai-registry/build.ts +62 -1
  156. package/src/forge/compiler/ai-registry/constants.ts +1 -1
  157. package/src/forge/compiler/ai-registry/parse.ts +168 -5
  158. package/src/forge/compiler/api-surface/build.ts +47 -0
  159. package/src/forge/compiler/app-graph/build.ts +68 -8
  160. package/src/forge/compiler/app-graph/extract.ts +107 -0
  161. package/src/forge/compiler/app-graph/forge-apis.ts +1 -0
  162. package/src/forge/compiler/app-graph/module-graph.ts +73 -78
  163. package/src/forge/compiler/app-graph/parser.ts +24 -24
  164. package/src/forge/compiler/app-graph/profile.ts +26 -0
  165. package/src/forge/compiler/app-graph/versions.ts +1 -1
  166. package/src/forge/compiler/classifier/capabilities.ts +3 -2
  167. package/src/forge/compiler/classifier/classify.ts +32 -8
  168. package/src/forge/compiler/classifier/secrets.ts +3 -2
  169. package/src/forge/compiler/classifier/signals.ts +91 -1
  170. package/src/forge/compiler/client-sdk/build-manifest.ts +59 -0
  171. package/src/forge/compiler/client-sdk/render-client.ts +188 -13
  172. package/src/forge/compiler/data-graph/parse.ts +3 -3
  173. package/src/forge/compiler/data-graph/sql/ddl.ts +60 -2
  174. package/src/forge/compiler/data-graph/sql/serialize.ts +4 -0
  175. package/src/forge/compiler/data-graph/sql/types.ts +1 -0
  176. package/src/forge/compiler/dev-manifest/build.ts +3 -0
  177. package/src/forge/compiler/diagnostics/codes.ts +35 -0
  178. package/src/forge/compiler/diagnostics/create.ts +8 -3
  179. package/src/forge/compiler/diagnostics/index.ts +2 -0
  180. package/src/forge/compiler/emitter/barrel.ts +3 -0
  181. package/src/forge/compiler/emitter/render.ts +5 -0
  182. package/src/forge/compiler/external-manifest/registry.ts +205 -0
  183. package/src/forge/compiler/external-manifest/types.ts +91 -0
  184. package/src/forge/compiler/external-manifest/validate.ts +373 -0
  185. package/src/forge/compiler/frontend-graph/build.ts +85 -13
  186. package/src/forge/compiler/integration/add.ts +498 -22
  187. package/src/forge/compiler/integration/snapshot.ts +2 -0
  188. package/src/forge/compiler/make-registry/build.ts +19 -7
  189. package/src/forge/compiler/orchestrator/plan-profile.ts +23 -0
  190. package/src/forge/compiler/orchestrator/plan.ts +78 -7
  191. package/src/forge/compiler/orchestrator/profile.ts +65 -0
  192. package/src/forge/compiler/orchestrator/run.ts +97 -31
  193. package/src/forge/compiler/orchestrator/serialize.ts +101 -8
  194. package/src/forge/compiler/package-graph/compiler.ts +13 -3
  195. package/src/forge/compiler/package-manager/adapter.ts +4 -1
  196. package/src/forge/compiler/package-manager/commands.ts +4 -0
  197. package/src/forge/compiler/package-manager/executor.ts +30 -1
  198. package/src/forge/compiler/policy-registry/build.ts +44 -1
  199. package/src/forge/compiler/test-graph/build.ts +11 -3
  200. package/src/forge/compiler/types/ai-registry.ts +25 -1
  201. package/src/forge/compiler/types/app-graph.ts +9 -2
  202. package/src/forge/compiler/types/cli.ts +76 -1
  203. package/src/forge/compiler/types/dev-manifest.ts +3 -0
  204. package/src/forge/compiler/types/frontend-graph.ts +2 -2
  205. package/src/forge/delta/classifier.ts +52 -0
  206. package/src/forge/delta/explain.ts +126 -0
  207. package/src/forge/delta/git-observer.ts +43 -0
  208. package/src/forge/delta/ids.ts +44 -0
  209. package/src/forge/delta/index.ts +13 -0
  210. package/src/forge/delta/recorder.ts +402 -0
  211. package/src/forge/delta/redaction.ts +50 -0
  212. package/src/forge/delta/schema.ts +240 -0
  213. package/src/forge/delta/session.ts +142 -0
  214. package/src/forge/delta/status.ts +489 -0
  215. package/src/forge/delta/store.ts +2975 -0
  216. package/src/forge/delta/timeline.ts +104 -0
  217. package/src/forge/dev/server.ts +768 -15
  218. package/src/forge/dev/types.ts +15 -1
  219. package/src/forge/dev/watch.ts +17 -7
  220. package/src/forge/dev-console/cycle.ts +233 -21
  221. package/src/forge/dev-console/types.ts +46 -1
  222. package/src/forge/impact/index.ts +46 -8
  223. package/src/forge/impact/types.ts +6 -0
  224. package/src/forge/intent/index.ts +35 -16
  225. package/src/forge/make/index.ts +149 -6
  226. package/src/forge/make/templates.ts +343 -2
  227. package/src/forge/make/types.ts +3 -1
  228. package/src/forge/refactor/index.ts +1 -0
  229. package/src/forge/repair/rules/index.ts +2 -2
  230. package/src/forge/review/index.ts +158 -12
  231. package/src/forge/review/types.ts +15 -0
  232. package/src/forge/runtime/ai/context.ts +210 -5
  233. package/src/forge/runtime/ai/types.ts +70 -0
  234. package/src/forge/runtime/auth/claims.ts +32 -0
  235. package/src/forge/runtime/auth/errors.ts +2 -0
  236. package/src/forge/runtime/context/create-context.ts +30 -6
  237. package/src/forge/runtime/db/generated-client.ts +13 -2
  238. package/src/forge/runtime/db/memory-adapter.ts +2 -2
  239. package/src/forge/runtime/db/pglite-adapter.ts +77 -2
  240. package/src/forge/runtime/db/postgres-adapter.ts +6 -3
  241. package/src/forge/runtime/executor.ts +112 -2
  242. package/src/forge/runtime/external/bridge.ts +649 -0
  243. package/src/forge/runtime/runner/run-entry.ts +16 -7
  244. package/src/forge/runtime/telemetry/scrubber.ts +91 -10
  245. package/src/forge/runtime/webhooks/security.ts +184 -0
  246. package/src/forge/server.ts +100 -2
  247. package/src/forge/version.ts +1 -1
  248. package/src/forge/vue/index.ts +407 -0
  249. package/src/forge/workspace/change-summary.ts +209 -0
  250. package/src/forge/workspace/forge-cli.ts +14 -0
  251. package/src/forge/workspace/git-summary.ts +279 -0
  252. package/templates/agent-workroom/AGENTS.md +29 -0
  253. package/templates/agent-workroom/README.md +34 -0
  254. package/templates/agent-workroom/forge.config.ts +3 -0
  255. package/templates/agent-workroom/package.json +33 -0
  256. package/templates/agent-workroom/src/actions/indexAgentSignal.ts +10 -0
  257. package/templates/agent-workroom/src/commands/openWorkroom.ts +61 -0
  258. package/templates/agent-workroom/src/commands/recordAgentSignal.ts +119 -0
  259. package/templates/agent-workroom/src/commands/recordCheckRun.ts +52 -0
  260. package/templates/agent-workroom/src/forge/schema.ts +54 -0
  261. package/templates/agent-workroom/src/policies.ts +6 -0
  262. package/templates/agent-workroom/src/queries/listWorkrooms.ts +11 -0
  263. package/templates/agent-workroom/src/queries/liveWorkroom.ts +63 -0
  264. package/templates/agent-workroom/tsconfig.json +16 -0
  265. package/templates/agent-workroom/web/index.html +12 -0
  266. package/templates/agent-workroom/web/package.json +21 -0
  267. package/templates/agent-workroom/web/src/App.tsx +345 -0
  268. package/templates/agent-workroom/web/src/lib/forge.ts +13 -0
  269. package/templates/agent-workroom/web/src/main.tsx +13 -0
  270. package/templates/agent-workroom/web/src/styles.css +545 -0
  271. package/templates/agent-workroom/web/tsconfig.json +27 -0
  272. package/templates/b2b-support-web/package.json +2 -0
  273. package/templates/b2b-support-web/tsconfig.json +4 -1
  274. package/templates/b2b-support-web/web/package.json +1 -1
  275. package/templates/minimal-web/package.json +2 -1
  276. package/templates/minimal-web/tsconfig.json +3 -1
  277. package/templates/minimal-web/web/package.json +2 -2
  278. package/src/forge/_generated/actionSubscriptions.json +0 -2
  279. package/src/forge/_generated/actionSubscriptions.ts +0 -10
  280. package/src/forge/_generated/agentAdapterManifest.json +0 -2
  281. package/src/forge/_generated/agentAdapterManifest.ts +0 -73
  282. package/src/forge/_generated/agentContract.json +0 -2
  283. package/src/forge/_generated/agentContract.ts +0 -7696
  284. package/src/forge/_generated/agentQuickstart.md +0 -32
  285. package/src/forge/_generated/aiContext.ts +0 -59
  286. package/src/forge/_generated/aiModels.json +0 -2
  287. package/src/forge/_generated/aiModels.ts +0 -35
  288. package/src/forge/_generated/aiProviders.json +0 -2
  289. package/src/forge/_generated/aiProviders.ts +0 -23
  290. package/src/forge/_generated/aiRegistry.json +0 -2
  291. package/src/forge/_generated/aiRegistry.ts +0 -29
  292. package/src/forge/_generated/api.json +0 -2
  293. package/src/forge/_generated/api.ts +0 -8
  294. package/src/forge/_generated/appGraph.json +0 -2
  295. package/src/forge/_generated/appGraph.ts +0 -14667
  296. package/src/forge/_generated/appMap.md +0 -35
  297. package/src/forge/_generated/artifactManifest.json +0 -2
  298. package/src/forge/_generated/artifactManifest.ts +0 -7
  299. package/src/forge/_generated/authClaims.json +0 -2
  300. package/src/forge/_generated/authClaims.ts +0 -13
  301. package/src/forge/_generated/authConfig.json +0 -2
  302. package/src/forge/_generated/authConfig.ts +0 -17
  303. package/src/forge/_generated/authContext.ts +0 -23
  304. package/src/forge/_generated/authRegistry.json +0 -2
  305. package/src/forge/_generated/authRegistry.ts +0 -25
  306. package/src/forge/_generated/buildInfo.json +0 -2
  307. package/src/forge/_generated/buildInfo.ts +0 -9
  308. package/src/forge/_generated/capabilityMap.json +0 -2
  309. package/src/forge/_generated/capabilityMap.md +0 -15
  310. package/src/forge/_generated/capabilityMap.ts +0 -17
  311. package/src/forge/_generated/client.ts +0 -282
  312. package/src/forge/_generated/clientApi.ts +0 -9
  313. package/src/forge/_generated/clientManifest.json +0 -2
  314. package/src/forge/_generated/clientManifest.ts +0 -39
  315. package/src/forge/_generated/clientTypes.ts +0 -78
  316. package/src/forge/_generated/configRegistry.json +0 -2
  317. package/src/forge/_generated/configRegistry.ts +0 -4
  318. package/src/forge/_generated/dataGraph.json +0 -2
  319. package/src/forge/_generated/dataGraph.ts +0 -8
  320. package/src/forge/_generated/db.json +0 -2
  321. package/src/forge/_generated/db.ts +0 -2
  322. package/src/forge/_generated/dbSecurityManifest.json +0 -2
  323. package/src/forge/_generated/dbSecurityManifest.ts +0 -15
  324. package/src/forge/_generated/dbSessionContext.json +0 -2
  325. package/src/forge/_generated/dbSessionContext.ts +0 -39
  326. package/src/forge/_generated/deployManifest.json +0 -2
  327. package/src/forge/_generated/deployManifest.ts +0 -14
  328. package/src/forge/_generated/devManifest.json +0 -2
  329. package/src/forge/_generated/devManifest.ts +0 -47
  330. package/src/forge/_generated/envSchema.json +0 -2
  331. package/src/forge/_generated/envSchema.ts +0 -59
  332. package/src/forge/_generated/frontendGraph.json +0 -2
  333. package/src/forge/_generated/frontendGraph.ts +0 -27
  334. package/src/forge/_generated/importGuards.json +0 -2
  335. package/src/forge/_generated/importGuards.ts +0 -686
  336. package/src/forge/_generated/index.ts +0 -67
  337. package/src/forge/_generated/liveProductionManifest.json +0 -2
  338. package/src/forge/_generated/liveProductionManifest.ts +0 -23
  339. package/src/forge/_generated/liveProtocol.json +0 -2
  340. package/src/forge/_generated/liveProtocol.ts +0 -21
  341. package/src/forge/_generated/liveQueryRegistry.json +0 -2
  342. package/src/forge/_generated/liveQueryRegistry.ts +0 -9
  343. package/src/forge/_generated/liveTransportConfig.json +0 -2
  344. package/src/forge/_generated/liveTransportConfig.ts +0 -19
  345. package/src/forge/_generated/makeRegistry.json +0 -2
  346. package/src/forge/_generated/makeRegistry.ts +0 -163
  347. package/src/forge/_generated/makeTemplates.json +0 -2
  348. package/src/forge/_generated/makeTemplates.ts +0 -61
  349. package/src/forge/_generated/mockMap.json +0 -2
  350. package/src/forge/_generated/mockMap.ts +0 -7
  351. package/src/forge/_generated/operationPlaybooks.md +0 -147
  352. package/src/forge/_generated/packageGraph.json +0 -2
  353. package/src/forge/_generated/packageGraph.ts +0 -245249
  354. package/src/forge/_generated/packageUpgradeRegistry.json +0 -2
  355. package/src/forge/_generated/packageUpgradeRegistry.ts +0 -15
  356. package/src/forge/_generated/permissionMatrix.json +0 -2
  357. package/src/forge/_generated/permissionMatrix.ts +0 -7
  358. package/src/forge/_generated/policyRegistry.json +0 -2
  359. package/src/forge/_generated/policyRegistry.ts +0 -11
  360. package/src/forge/_generated/queryRegistry.json +0 -2
  361. package/src/forge/_generated/queryRegistry.ts +0 -9
  362. package/src/forge/_generated/react.d.ts +0 -22
  363. package/src/forge/_generated/react.ts +0 -29
  364. package/src/forge/_generated/reactManifest.json +0 -2
  365. package/src/forge/_generated/reactManifest.ts +0 -19
  366. package/src/forge/_generated/rlsPolicies.json +0 -2
  367. package/src/forge/_generated/rlsPolicies.sql +0 -34
  368. package/src/forge/_generated/rlsPolicies.ts +0 -6
  369. package/src/forge/_generated/runtimeGraph.json +0 -2
  370. package/src/forge/_generated/runtimeGraph.ts +0 -8
  371. package/src/forge/_generated/runtimeMatrix.json +0 -2
  372. package/src/forge/_generated/runtimeMatrix.ts +0 -327385
  373. package/src/forge/_generated/runtimeRegistry.ts +0 -2
  374. package/src/forge/_generated/runtimeRules.md +0 -79
  375. package/src/forge/_generated/secretRegistry.json +0 -2
  376. package/src/forge/_generated/secretRegistry.ts +0 -50
  377. package/src/forge/_generated/secretsContext.ts +0 -11
  378. package/src/forge/_generated/serverApi.ts +0 -10
  379. package/src/forge/_generated/sourceMapManifest.json +0 -2
  380. package/src/forge/_generated/sourceMapManifest.ts +0 -7
  381. package/src/forge/_generated/sqlPlan.json +0 -2
  382. package/src/forge/_generated/sqlPlan.ts +0 -88
  383. package/src/forge/_generated/subscriptionManifest.json +0 -2
  384. package/src/forge/_generated/subscriptionManifest.ts +0 -7
  385. package/src/forge/_generated/symbolicationManifest.json +0 -2
  386. package/src/forge/_generated/symbolicationManifest.ts +0 -17
  387. package/src/forge/_generated/telemetryRegistry.json +0 -2
  388. package/src/forge/_generated/telemetryRegistry.ts +0 -9
  389. package/src/forge/_generated/telemetrySinks.json +0 -2
  390. package/src/forge/_generated/telemetrySinks.ts +0 -11
  391. package/src/forge/_generated/tenantScope.json +0 -2
  392. package/src/forge/_generated/tenantScope.ts +0 -8
  393. package/src/forge/_generated/testGraph.json +0 -2
  394. package/src/forge/_generated/testGraph.ts +0 -3108
  395. package/src/forge/_generated/testPlanRegistry.json +0 -2
  396. package/src/forge/_generated/testPlanRegistry.ts +0 -33
  397. package/src/forge/_generated/uiRoutes.json +0 -2
  398. package/src/forge/_generated/uiRoutes.ts +0 -16
  399. package/src/forge/_generated/uiScenarios.json +0 -2
  400. package/src/forge/_generated/uiScenarios.ts +0 -30
  401. package/src/forge/_generated/uiTestManifest.json +0 -2
  402. package/src/forge/_generated/uiTestManifest.ts +0 -27
  403. package/src/forge/_generated/workflowRegistry.json +0 -2
  404. package/src/forge/_generated/workflowRegistry.ts +0 -9
  405. package/src/forge/_generated/workflowSubscriptions.json +0 -2
  406. package/src/forge/_generated/workflowSubscriptions.ts +0 -10
@@ -0,0 +1,2975 @@
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative } from "node:path";
3
+ import { createPgliteAdapter } from "../runtime/db/pglite-adapter.ts";
4
+ import type { DbAdapter } from "../runtime/db/adapter.ts";
5
+ import { hashStable, hashUtf8Bytes } from "../compiler/primitives/hash.ts";
6
+ import { normalizePath } from "../compiler/primitives/paths.ts";
7
+ import { DELTA_SCHEMA_SQL, DELTA_SCHEMA_VERSION } from "./schema.ts";
8
+ import { createDeltaId } from "./ids.ts";
9
+ import { redactDeltaPayload } from "./redaction.ts";
10
+ import { classifyArtifactKind, classifyDeltaPath, type DeltaSemanticHint } from "./classifier.ts";
11
+ import { readDeltaGitSnapshot, type DeltaGitSnapshot } from "./git-observer.ts";
12
+ import type { AgentEventEnvelope, AgentMemoryEventRecord } from "../agent-memory/types.ts";
13
+
14
+ export type DeltaActorKind = "human" | "agent" | "forge" | "ci" | "git" | "unknown";
15
+ export type DeltaSessionSource = "forge-dev" | "forge-command" | "agent-adapter" | "git" | "auto";
16
+
17
+ export interface DeltaOperation {
18
+ id: string;
19
+ sessionId?: string;
20
+ txnId?: string;
21
+ kind: string;
22
+ timestamp: string;
23
+ actorId?: string;
24
+ summary?: string;
25
+ data: Record<string, unknown>;
26
+ redaction?: Record<string, unknown>;
27
+ hash?: string;
28
+ prevHash?: string;
29
+ }
30
+
31
+ export interface DeltaFileChangeInput {
32
+ path: string;
33
+ changeType: "created" | "modified" | "deleted" | "renamed" | "generated";
34
+ hashBefore?: string;
35
+ hashAfter?: string;
36
+ diffSummary?: string;
37
+ semanticHints?: DeltaSemanticHint[];
38
+ }
39
+
40
+ export interface DeltaCommandRunInput {
41
+ commandName: string;
42
+ argv?: string[];
43
+ exitCode?: number;
44
+ durationMs?: number;
45
+ diagnostics?: unknown[];
46
+ }
47
+
48
+ export interface DeltaRuntimeCallInput {
49
+ entryName: string;
50
+ entryKind?: string;
51
+ risk?: string;
52
+ policy?: string;
53
+ tenantScoped?: boolean;
54
+ needsApproval?: boolean;
55
+ result?: string;
56
+ diagnosticCode?: string;
57
+ traceId?: string;
58
+ service?: string;
59
+ language?: string;
60
+ }
61
+
62
+ export interface DeltaProofInput {
63
+ proofKind: string;
64
+ command?: string;
65
+ result: string;
66
+ assurance?: string;
67
+ diagnostics?: unknown[];
68
+ artifactPaths?: string[];
69
+ }
70
+
71
+ export interface DeltaArtifactInput {
72
+ path: string;
73
+ artifactKind?: string;
74
+ hash?: string;
75
+ generated?: boolean;
76
+ }
77
+
78
+ export interface DeltaAppendInput {
79
+ sessionId?: string;
80
+ txnId?: string;
81
+ kind: string;
82
+ actorId?: string;
83
+ summary?: string;
84
+ data?: Record<string, unknown>;
85
+ fileChanges?: DeltaFileChangeInput[];
86
+ commandRun?: DeltaCommandRunInput;
87
+ runtimeCall?: DeltaRuntimeCallInput;
88
+ proof?: DeltaProofInput;
89
+ artifacts?: DeltaArtifactInput[];
90
+ git?: { commitSha?: string; branch?: string; confidence?: number; metadata?: Record<string, unknown> };
91
+ }
92
+
93
+ export interface DeltaAgentMemoryEventInput {
94
+ envelope: AgentEventEnvelope;
95
+ summary?: string;
96
+ bindings?: {
97
+ toolName?: string;
98
+ command?: string;
99
+ exitCode?: number;
100
+ files?: string[];
101
+ entries?: string[];
102
+ proofs?: string[];
103
+ status?: string;
104
+ };
105
+ }
106
+
107
+ export interface DeltaTimelineFilter {
108
+ target?: string;
109
+ kind?: string;
110
+ workSessionId?: string;
111
+ limit?: number;
112
+ }
113
+
114
+ export interface DeltaTimelineEntry {
115
+ id: string;
116
+ kind: string;
117
+ timestamp: string;
118
+ summary?: string;
119
+ data: Record<string, unknown>;
120
+ }
121
+
122
+ export interface DeltaTimelineEntityRef {
123
+ kind: string;
124
+ name: string;
125
+ }
126
+
127
+ export interface DeltaSemanticTimelineEntity extends DeltaTimelineEntityRef {
128
+ id: string;
129
+ eventId: string;
130
+ role: string;
131
+ confidence: number;
132
+ }
133
+
134
+ export interface DeltaSemanticTimelineEvent {
135
+ id: string;
136
+ operationId?: string;
137
+ sessionId?: string;
138
+ changeId?: string;
139
+ timestamp: string;
140
+ kind: string;
141
+ title: string;
142
+ summary?: string;
143
+ severity?: string;
144
+ confidence: number;
145
+ data: Record<string, unknown>;
146
+ entities: DeltaSemanticTimelineEntity[];
147
+ }
148
+
149
+ export interface DeltaSemanticTimelineEdge {
150
+ id: string;
151
+ from: string;
152
+ to: string;
153
+ kind: string;
154
+ confidence: number;
155
+ reason?: Record<string, unknown>;
156
+ }
157
+
158
+ export interface DeltaSemanticTimelineFilter extends DeltaTimelineFilter {
159
+ since?: string;
160
+ until?: string;
161
+ }
162
+
163
+ export interface DeltaSemanticTimelineResult {
164
+ entity?: DeltaTimelineEntityRef;
165
+ currentState: Record<string, unknown>;
166
+ events: DeltaSemanticTimelineEvent[];
167
+ causalEdges: DeltaSemanticTimelineEdge[];
168
+ openQuestions: string[];
169
+ projection: {
170
+ version: string;
171
+ lastOperationId?: string;
172
+ lastRebuildAt?: string;
173
+ };
174
+ }
175
+
176
+ export interface DeltaStatus {
177
+ ok: true;
178
+ recording: boolean;
179
+ store: string;
180
+ external?: {
181
+ kind: "pglite-active";
182
+ reason: string;
183
+ };
184
+ session?: {
185
+ id: string;
186
+ startedAt: string;
187
+ operationCount: number;
188
+ };
189
+ workSession?: DeltaWorkSessionSummary;
190
+ recentOperations: Array<{ id: string; kind: string; summary?: string; timestamp: string }>;
191
+ details?: DeltaStatusDetails;
192
+ }
193
+
194
+ export interface DeltaStatusDetails {
195
+ schema: {
196
+ expectedVersion: string;
197
+ storedVersion?: string;
198
+ lastOperationId?: string;
199
+ lastRebuildAt?: string;
200
+ };
201
+ paths: {
202
+ store: string;
203
+ lock: string;
204
+ postmaster: string;
205
+ };
206
+ locks: {
207
+ forgeLockPresent: boolean;
208
+ postmasterPresent: boolean;
209
+ };
210
+ counts: {
211
+ sessions: number;
212
+ operations: number;
213
+ fileChanges: number;
214
+ commandRuns: number;
215
+ runtimeCalls: number;
216
+ proofs: number;
217
+ artifacts: number;
218
+ workSessions: number;
219
+ agentMemoryEvents: number;
220
+ semanticEvents: number;
221
+ };
222
+ }
223
+
224
+ export type DeltaWorkSessionKind = "auto" | "agent" | "human" | "ci" | "git" | "manual-corrected";
225
+ export type DeltaWorkSessionStatus = "open" | "idle" | "closed" | "merged" | "split" | "needs-review";
226
+ export type DeltaWorkSessionLinkType = "primary" | "related" | "causal" | "weak" | "manual";
227
+
228
+ export interface DeltaWorkSessionSignal {
229
+ signal: string;
230
+ weight: number;
231
+ value?: string;
232
+ metadata?: Record<string, unknown>;
233
+ }
234
+
235
+ export interface DeltaWorkSessionSummary {
236
+ id: string;
237
+ kind: DeltaWorkSessionKind;
238
+ status: DeltaWorkSessionStatus;
239
+ title: string;
240
+ inferredIntent?: string;
241
+ confidence: number;
242
+ startedAt: string;
243
+ endedAt?: string;
244
+ gitBranch?: string;
245
+ summary?: string;
246
+ operationCount: number;
247
+ reasons: DeltaWorkSessionSignal[];
248
+ metadata: DeltaWorkSessionMetadata;
249
+ }
250
+
251
+ export interface DeltaWorkSessionDetails extends DeltaWorkSessionSummary {
252
+ operations: DeltaTimelineEntry[];
253
+ signals: DeltaWorkSessionSignal[];
254
+ }
255
+
256
+ interface DeltaWorkSessionMetadata {
257
+ files: string[];
258
+ fileClusters: string[];
259
+ entries: string[];
260
+ diagnostics: string[];
261
+ proofs: string[];
262
+ services: string[];
263
+ traces: string[];
264
+ commands: string[];
265
+ operationKinds: string[];
266
+ actorIds: string[];
267
+ lastOperationAt?: string;
268
+ mergedFrom?: string[];
269
+ splitFrom?: string;
270
+ manualTitle?: boolean;
271
+ }
272
+
273
+ interface DeltaOperationContext {
274
+ id: string;
275
+ kind: string;
276
+ timestamp: string;
277
+ actorId?: string;
278
+ summary?: string;
279
+ data: Record<string, unknown>;
280
+ sessionId?: string;
281
+ branch?: string;
282
+ gitHead?: string;
283
+ files: string[];
284
+ fileClusters: string[];
285
+ entries: string[];
286
+ diagnostics: string[];
287
+ proofs: string[];
288
+ services: string[];
289
+ traces: string[];
290
+ commands: string[];
291
+ }
292
+
293
+ export type DeltaStoreAccess = "read" | "write";
294
+
295
+ export class DeltaStoreBusyError extends Error {
296
+ readonly code = "FORGE_DELTA_BUSY" as const;
297
+
298
+ constructor(
299
+ readonly lockPath: string,
300
+ readonly holder: Record<string, unknown> | null,
301
+ ) {
302
+ const holderText = holder?.pid ? ` by pid ${String(holder.pid)}` : "";
303
+ super(`Forge Delta local store is busy${holderText}`);
304
+ this.name = "DeltaStoreBusyError";
305
+ }
306
+ }
307
+
308
+ export interface DeltaStoreBusyInfo {
309
+ code: "FORGE_DELTA_BUSY";
310
+ lockPath: string;
311
+ relativeLockPath: string;
312
+ pid?: number;
313
+ processAlive: boolean;
314
+ createdAt?: string;
315
+ ageMs?: number;
316
+ cwd?: string;
317
+ command?: string;
318
+ holderKnown: boolean;
319
+ }
320
+
321
+ interface DeltaStoreLock {
322
+ path: string;
323
+ token: string;
324
+ }
325
+
326
+ function getDeltaLockPath(workspaceRoot: string): string {
327
+ return join(workspaceRoot, ".forge", "delta", "delta.lock");
328
+ }
329
+
330
+ function readLockHolder(lockPath: string): Record<string, unknown> | null {
331
+ try {
332
+ const parsed = JSON.parse(readFileSync(lockPath, "utf8")) as unknown;
333
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
334
+ ? parsed as Record<string, unknown>
335
+ : null;
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+
341
+ function processLooksAlive(pid: unknown): boolean {
342
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
343
+ return false;
344
+ }
345
+ try {
346
+ process.kill(pid, 0);
347
+ return true;
348
+ } catch (error) {
349
+ return Boolean(error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "EPERM");
350
+ }
351
+ }
352
+
353
+ function lockLooksStale(holder: Record<string, unknown> | null): boolean {
354
+ if (!holder) {
355
+ return true;
356
+ }
357
+ const pid = typeof holder.pid === "number" && Number.isInteger(holder.pid) && holder.pid > 0 ? holder.pid : undefined;
358
+ if (pid) {
359
+ return !processLooksAlive(pid);
360
+ }
361
+ const createdAt = typeof holder.createdAt === "string" ? Date.parse(holder.createdAt) : NaN;
362
+ return !Number.isFinite(createdAt) || Date.now() - createdAt > 30_000;
363
+ }
364
+
365
+ function redactDeltaBusyCommand(command: string): string {
366
+ return command
367
+ .replace(/\b(Bearer)\s+[A-Za-z0-9._~+/=-]+/gi, "$1 [REDACTED]")
368
+ .replace(/\b(token|secret|password|passwd|api[-_]?key|authorization)=\S+/gi, "$1=[REDACTED]")
369
+ .replace(/(--(?:token|secret|password|passwd|api-key|authorization))\s+\S+/gi, "$1 [REDACTED]");
370
+ }
371
+
372
+ function displayDeltaBusyCwd(workspaceRoot: string, cwd: string): string {
373
+ if (!isAbsolute(cwd)) {
374
+ return cwd;
375
+ }
376
+ const rel = normalizePath(relative(workspaceRoot, cwd));
377
+ if (rel === "") {
378
+ return ".";
379
+ }
380
+ if (rel === ".." || rel.startsWith("../")) {
381
+ return "[outside-workspace]";
382
+ }
383
+ return rel;
384
+ }
385
+
386
+ export function describeDeltaStoreBusy(
387
+ error: DeltaStoreBusyError,
388
+ workspaceRoot: string,
389
+ now = Date.now(),
390
+ ): DeltaStoreBusyInfo {
391
+ const holder = error.holder;
392
+ const pid = typeof holder?.pid === "number" && Number.isInteger(holder.pid) && holder.pid > 0
393
+ ? holder.pid
394
+ : undefined;
395
+ const createdAt = typeof holder?.createdAt === "string" ? holder.createdAt : undefined;
396
+ const createdMs = createdAt ? Date.parse(createdAt) : NaN;
397
+ const cwd = typeof holder?.cwd === "string" ? displayDeltaBusyCwd(workspaceRoot, holder.cwd) : undefined;
398
+ const command = typeof holder?.command === "string" ? redactDeltaBusyCommand(holder.command) : undefined;
399
+ return {
400
+ code: "FORGE_DELTA_BUSY",
401
+ lockPath: error.lockPath,
402
+ relativeLockPath: normalizePath(relative(workspaceRoot, error.lockPath)),
403
+ ...(pid ? { pid } : {}),
404
+ processAlive: pid ? processLooksAlive(pid) : false,
405
+ ...(createdAt ? { createdAt } : {}),
406
+ ...(Number.isFinite(createdMs) ? { ageMs: Math.max(0, now - createdMs) } : {}),
407
+ ...(cwd ? { cwd } : {}),
408
+ ...(command ? { command } : {}),
409
+ holderKnown: Boolean(holder),
410
+ };
411
+ }
412
+
413
+ export function summarizeDeltaStoreBusy(info: DeltaStoreBusyInfo): string {
414
+ return [
415
+ `lock=${info.relativeLockPath}`,
416
+ info.pid ? `pid=${info.pid}` : undefined,
417
+ info.processAlive ? "process=alive" : "process=unknown-or-exited",
418
+ typeof info.ageMs === "number" ? `age=${Math.round(info.ageMs / 1000)}s` : undefined,
419
+ info.cwd ? `cwd=${info.cwd}` : undefined,
420
+ info.command ? `command=${info.command}` : undefined,
421
+ ].filter(Boolean).join(", ");
422
+ }
423
+
424
+ function acquireDeltaStoreLock(workspaceRoot: string): DeltaStoreLock {
425
+ const lockPath = getDeltaLockPath(workspaceRoot);
426
+ mkdirSync(dirname(lockPath), { recursive: true });
427
+ const token = `${process.pid}:${Date.now()}:${createDeltaId("op")}`;
428
+ const content = `${JSON.stringify({
429
+ pid: process.pid,
430
+ token,
431
+ createdAt: new Date().toISOString(),
432
+ cwd: process.cwd(),
433
+ command: process.argv.slice(0, 6).join(" "),
434
+ }, null, 2)}\n`;
435
+
436
+ for (let attempt = 0; attempt < 2; attempt += 1) {
437
+ try {
438
+ const fd = openSync(lockPath, "wx");
439
+ try {
440
+ writeFileSync(fd, content, "utf8");
441
+ } finally {
442
+ closeSync(fd);
443
+ }
444
+ return { path: lockPath, token };
445
+ } catch (error) {
446
+ const holder = readLockHolder(lockPath);
447
+ if (attempt === 0 && lockLooksStale(holder)) {
448
+ try {
449
+ unlinkSync(lockPath);
450
+ continue;
451
+ } catch {
452
+ // Another process may have refreshed the lock first; report the live holder below.
453
+ }
454
+ }
455
+ const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
456
+ if (code === "EEXIST" || existsSync(lockPath)) {
457
+ throw new DeltaStoreBusyError(lockPath, holder);
458
+ }
459
+ throw error;
460
+ }
461
+ }
462
+
463
+ throw new DeltaStoreBusyError(lockPath, readLockHolder(lockPath));
464
+ }
465
+
466
+ export function probeDeltaStoreBusy(workspaceRoot: string): DeltaStoreBusyError | null {
467
+ const lockPath = getDeltaLockPath(workspaceRoot);
468
+ if (!existsSync(lockPath)) {
469
+ return null;
470
+ }
471
+ const holder = readLockHolder(lockPath);
472
+ if (lockLooksStale(holder)) {
473
+ try {
474
+ unlinkSync(lockPath);
475
+ return null;
476
+ } catch {
477
+ return new DeltaStoreBusyError(lockPath, readLockHolder(lockPath));
478
+ }
479
+ }
480
+ return new DeltaStoreBusyError(lockPath, holder);
481
+ }
482
+
483
+ function releaseDeltaStoreLock(lock: DeltaStoreLock): void {
484
+ const holder = readLockHolder(lock.path);
485
+ if (holder?.token !== lock.token) {
486
+ return;
487
+ }
488
+ try {
489
+ unlinkSync(lock.path);
490
+ } catch {
491
+ // Best effort; stale locks are cleaned by the next opener when their process is gone.
492
+ }
493
+ }
494
+
495
+ function deltaStoreInitialized(storePath: string): boolean {
496
+ return existsSync(join(storePath, "PG_VERSION"));
497
+ }
498
+
499
+ function readPglitePostmasterHolder(storePath: string): Record<string, unknown> | null {
500
+ const postmasterPath = join(storePath, "postmaster.pid");
501
+ if (!existsSync(postmasterPath)) {
502
+ return null;
503
+ }
504
+ try {
505
+ const lines = readFileSync(postmasterPath, "utf8").split(/\r?\n/);
506
+ const pid = Number(lines[0]);
507
+ return {
508
+ ...(Number.isInteger(pid) && pid > 0 ? { pid } : {}),
509
+ createdAt: statSync(postmasterPath).mtime.toISOString(),
510
+ command: "pglite postmaster.pid",
511
+ };
512
+ } catch {
513
+ return {
514
+ command: "pglite postmaster.pid",
515
+ };
516
+ }
517
+ }
518
+
519
+ export class DeltaStore {
520
+ private closed = false;
521
+
522
+ private constructor(
523
+ readonly workspaceRoot: string,
524
+ readonly storePath: string,
525
+ private readonly adapter: DbAdapter,
526
+ private readonly lock: DeltaStoreLock | null,
527
+ ) {}
528
+
529
+ static async open(workspaceRoot: string, options: { access?: DeltaStoreAccess } = {}): Promise<DeltaStore> {
530
+ const storePath = getDeltaStorePath(workspaceRoot);
531
+ mkdirSync(dirname(storePath), { recursive: true });
532
+ const initializedBeforeOpen = deltaStoreInitialized(storePath);
533
+ const lock = options.access === "read" ? null : acquireDeltaStoreLock(workspaceRoot);
534
+ let store: DeltaStore | null = null;
535
+ try {
536
+ const adapter = await createPgliteAdapter(storePath);
537
+ store = new DeltaStore(workspaceRoot, storePath, adapter, lock);
538
+ if (options.access !== "read" || !initializedBeforeOpen) {
539
+ await store.init();
540
+ } else if (await store.needsSchemaInit()) {
541
+ await store.close();
542
+ store = null;
543
+ const migrateLock = acquireDeltaStoreLock(workspaceRoot);
544
+ try {
545
+ const migrateAdapter = await createPgliteAdapter(storePath);
546
+ store = new DeltaStore(workspaceRoot, storePath, migrateAdapter, migrateLock);
547
+ await store.init();
548
+ } catch (error) {
549
+ releaseDeltaStoreLock(migrateLock);
550
+ throw error;
551
+ }
552
+ }
553
+ return store;
554
+ } catch (error) {
555
+ if (store) {
556
+ await store.close().catch(() => undefined);
557
+ } else if (lock) {
558
+ releaseDeltaStoreLock(lock);
559
+ }
560
+ if (!(error instanceof DeltaStoreBusyError)) {
561
+ const holder = readPglitePostmasterHolder(storePath);
562
+ if (holder) {
563
+ throw new DeltaStoreBusyError(join(storePath, "postmaster.pid"), holder);
564
+ }
565
+ }
566
+ throw error;
567
+ }
568
+ }
569
+
570
+ async close(): Promise<void> {
571
+ if (this.closed) {
572
+ return;
573
+ }
574
+ this.closed = true;
575
+ try {
576
+ await this.adapter.close();
577
+ } finally {
578
+ if (this.lock) {
579
+ releaseDeltaStoreLock(this.lock);
580
+ }
581
+ }
582
+ }
583
+
584
+ async init(): Promise<void> {
585
+ for (const sql of DELTA_SCHEMA_SQL) {
586
+ await this.adapter.query(sql);
587
+ }
588
+ await this.adapter.query(
589
+ `INSERT INTO delta_meta (key, value, updated_at)
590
+ VALUES ($1, $2, $3)
591
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at`,
592
+ ["schemaVersion", DELTA_SCHEMA_VERSION, new Date().toISOString()],
593
+ );
594
+ }
595
+
596
+ async ensureActor(kind: DeltaActorKind, name: string, metadata: Record<string, unknown> = {}): Promise<string> {
597
+ const existing = await this.adapter.query(`SELECT id FROM actors WHERE kind = $1 AND name = $2 LIMIT 1`, [kind, name]);
598
+ const id = typeof existing.rows[0]?.id === "string" ? existing.rows[0].id : createDeltaId("actor");
599
+ if (existing.rows.length === 0) {
600
+ await this.adapter.query(
601
+ `INSERT INTO actors (id, kind, name, metadata_json, created_at) VALUES ($1, $2, $3, $4, $5)`,
602
+ [id, kind, name, JSON.stringify(metadata), new Date().toISOString()],
603
+ );
604
+ }
605
+ return id;
606
+ }
607
+
608
+ async createSession(input: {
609
+ source: DeltaSessionSource;
610
+ summary?: string;
611
+ metadata?: Record<string, unknown>;
612
+ git?: DeltaGitSnapshot;
613
+ }): Promise<string> {
614
+ const id = createDeltaId("sess");
615
+ const git = input.git ?? readDeltaGitSnapshot(this.workspaceRoot);
616
+ await this.adapter.query(
617
+ `INSERT INTO sessions (id, workspace_root, source, branch, started_at, summary, metadata_json)
618
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
619
+ [
620
+ id,
621
+ this.workspaceRoot,
622
+ input.source,
623
+ git.branch ?? null,
624
+ new Date().toISOString(),
625
+ input.summary ?? null,
626
+ JSON.stringify({ ...(input.metadata ?? {}), git }),
627
+ ],
628
+ );
629
+ await this.appendOperation({
630
+ sessionId: id,
631
+ kind: "session.started",
632
+ summary: `Started ${input.source} session`,
633
+ data: { source: input.source, git },
634
+ });
635
+ return id;
636
+ }
637
+
638
+ async endSession(sessionId: string, summary?: string): Promise<void> {
639
+ await this.adapter.query(`UPDATE sessions SET ended_at = $1, summary = COALESCE($2, summary) WHERE id = $3`, [
640
+ new Date().toISOString(),
641
+ summary ?? null,
642
+ sessionId,
643
+ ]);
644
+ await this.appendOperation({
645
+ sessionId,
646
+ kind: "session.ended",
647
+ summary: summary ?? "Ended session",
648
+ data: {},
649
+ });
650
+ }
651
+
652
+ async appendOperation(input: DeltaAppendInput): Promise<string> {
653
+ const operationId = createDeltaId("op");
654
+ const timestamp = new Date().toISOString();
655
+ const data = input.data ?? {};
656
+ const redacted = redactDeltaPayload(data);
657
+ const previous = await this.adapter.query(`SELECT hash FROM operations ORDER BY timestamp DESC, id DESC LIMIT 1`);
658
+ const prevHash = typeof previous.rows[0]?.hash === "string" ? previous.rows[0].hash : null;
659
+ const hash = hashStable(JSON.stringify({
660
+ id: operationId,
661
+ kind: input.kind,
662
+ timestamp,
663
+ data: redacted.value,
664
+ prevHash,
665
+ }));
666
+
667
+ await this.adapter.query(
668
+ `INSERT INTO operations (id, session_id, txn_id, kind, timestamp, actor_id, summary, data_json, redaction_json, hash, prev_hash)
669
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
670
+ [
671
+ operationId,
672
+ input.sessionId ?? null,
673
+ input.txnId ?? null,
674
+ input.kind,
675
+ timestamp,
676
+ input.actorId ?? null,
677
+ input.summary ?? null,
678
+ JSON.stringify(redacted.value),
679
+ JSON.stringify(redacted.redaction),
680
+ hash,
681
+ prevHash,
682
+ ],
683
+ );
684
+
685
+ for (const fileChange of input.fileChanges ?? []) {
686
+ await this.insertFileChange(operationId, fileChange);
687
+ }
688
+ if (input.commandRun) {
689
+ await this.insertCommandRun(operationId, input.commandRun);
690
+ }
691
+ if (input.runtimeCall) {
692
+ await this.insertRuntimeCall(operationId, input.runtimeCall);
693
+ }
694
+ if (input.proof) {
695
+ await this.insertProof(operationId, input.proof);
696
+ }
697
+ for (const artifact of input.artifacts ?? []) {
698
+ await this.insertArtifact(operationId, artifact);
699
+ }
700
+ if (input.git) {
701
+ await this.adapter.query(
702
+ `INSERT INTO git_mappings (id, operation_id, commit_sha, branch, detected_at, confidence, metadata_json)
703
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
704
+ [
705
+ createDeltaId("gitmap"),
706
+ operationId,
707
+ input.git.commitSha ?? null,
708
+ input.git.branch ?? null,
709
+ timestamp,
710
+ input.git.confidence ?? 0.5,
711
+ JSON.stringify(input.git.metadata ?? {}),
712
+ ],
713
+ );
714
+ }
715
+ await this.inferWorkSessionForOperation(operationId).catch(() => undefined);
716
+ return operationId;
717
+ }
718
+
719
+ async recordAgentMemoryEvent(input: DeltaAgentMemoryEventInput): Promise<AgentMemoryEventRecord> {
720
+ const envelope = input.envelope;
721
+ const timestamp = envelope.event.timestamp || new Date().toISOString();
722
+ const sourceId = deterministicTimelineId("agsrc", [
723
+ String(envelope.source.agent),
724
+ String(envelope.source.integration),
725
+ String(envelope.capture.trustLevel),
726
+ ]);
727
+ await this.adapter.query(
728
+ `INSERT INTO agent_event_sources (id, source_name, source_kind, integration_kind, trust_level, config_json, created_at)
729
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
730
+ ON CONFLICT (id) DO UPDATE
731
+ SET source_name = EXCLUDED.source_name,
732
+ source_kind = EXCLUDED.source_kind,
733
+ integration_kind = EXCLUDED.integration_kind,
734
+ trust_level = EXCLUDED.trust_level,
735
+ config_json = EXCLUDED.config_json`,
736
+ [
737
+ sourceId,
738
+ String(envelope.source.agent),
739
+ "external-agent",
740
+ String(envelope.source.integration),
741
+ envelope.capture.trustLevel,
742
+ JSON.stringify({ version: envelope.source.version }),
743
+ timestamp,
744
+ ],
745
+ );
746
+
747
+ const actorId = await this.ensureActor("agent", envelope.actor.name, {
748
+ source: envelope.source.agent,
749
+ model: envelope.actor.model,
750
+ integration: envelope.source.integration,
751
+ });
752
+ const forgeSessionId = envelope.session.forgeSessionId ?? await this.createSession({
753
+ source: "agent-adapter",
754
+ summary: `${envelope.source.agent} ${envelope.event.kind}`,
755
+ metadata: {
756
+ externalSessionId: envelope.session.externalSessionId,
757
+ source: envelope.source,
758
+ },
759
+ git: { branch: envelope.workspace.gitBranch, head: envelope.workspace.gitHead },
760
+ });
761
+ const bindings = input.bindings ?? {};
762
+ const operationId = await this.appendOperation({
763
+ sessionId: forgeSessionId,
764
+ actorId,
765
+ kind: envelope.event.kind,
766
+ summary: input.summary,
767
+ data: {
768
+ source: envelope.source,
769
+ session: envelope.session,
770
+ capture: envelope.capture,
771
+ privacy: envelope.privacy,
772
+ payload: envelope.payload,
773
+ toolName: bindings.toolName,
774
+ status: bindings.status,
775
+ entries: bindings.entries,
776
+ files: bindings.files,
777
+ proofs: bindings.proofs,
778
+ },
779
+ commandRun: bindings.command
780
+ ? {
781
+ commandName: bindings.command,
782
+ argv: [bindings.command],
783
+ exitCode: bindings.exitCode,
784
+ }
785
+ : undefined,
786
+ fileChanges: bindings.files?.map((path) => ({
787
+ path,
788
+ changeType: envelope.event.kind === "agent.file.changed" ? "modified" : "modified",
789
+ semanticHints: classifyDeltaPath(path),
790
+ })),
791
+ proof: bindings.proofs?.[0]
792
+ ? {
793
+ proofKind: bindings.proofs[0],
794
+ command: bindings.command,
795
+ result: bindings.status === "failed" ? "failed" : "passed",
796
+ }
797
+ : undefined,
798
+ git: envelope.workspace.gitHead || envelope.workspace.gitBranch
799
+ ? {
800
+ commitSha: envelope.workspace.gitHead,
801
+ branch: envelope.workspace.gitBranch,
802
+ confidence: envelope.capture.confidence,
803
+ metadata: { source: envelope.source.agent },
804
+ }
805
+ : undefined,
806
+ });
807
+
808
+ const externalEventId = createDeltaId("aevt");
809
+ const payloadJson = JSON.stringify(envelope.payload);
810
+ await this.adapter.query(
811
+ `INSERT INTO external_agent_events
812
+ (id, source_id, external_session_id, external_turn_id, event_kind, captured_at, payload_redacted_json, payload_hash, raw_stored, normalization_status)
813
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 0, 'normalized')`,
814
+ [
815
+ externalEventId,
816
+ sourceId,
817
+ envelope.session.externalSessionId ?? null,
818
+ envelope.session.turnId ?? null,
819
+ envelope.event.kind,
820
+ timestamp,
821
+ payloadJson,
822
+ hashStable(payloadJson),
823
+ ],
824
+ );
825
+
826
+ const memoryId = createDeltaId("amem");
827
+ const data = {
828
+ envelope,
829
+ bindings,
830
+ };
831
+ await this.adapter.query(
832
+ `INSERT INTO agent_memory_events
833
+ (id, external_event_id, forge_session_id, forge_change_id, operation_id, normalized_kind, summary, confidence, data_json)
834
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
835
+ [
836
+ memoryId,
837
+ externalEventId,
838
+ forgeSessionId,
839
+ null,
840
+ operationId,
841
+ envelope.event.kind,
842
+ input.summary ?? null,
843
+ envelope.capture.confidence,
844
+ JSON.stringify(data),
845
+ ],
846
+ );
847
+
848
+ return {
849
+ id: memoryId,
850
+ externalEventId,
851
+ sourceName: String(envelope.source.agent),
852
+ integrationKind: String(envelope.source.integration),
853
+ trustLevel: envelope.capture.trustLevel,
854
+ externalSessionId: envelope.session.externalSessionId,
855
+ externalTurnId: envelope.session.turnId,
856
+ eventKind: envelope.event.kind,
857
+ normalizedKind: envelope.event.kind,
858
+ summary: input.summary,
859
+ confidence: envelope.capture.confidence,
860
+ capturedAt: timestamp,
861
+ operationId,
862
+ data,
863
+ };
864
+ }
865
+
866
+ async listAgentMemoryEvents(filter: { target?: string; limit?: number } = {}): Promise<AgentMemoryEventRecord[]> {
867
+ const limit = Math.max(1, Math.min(filter.limit ?? 50, 200));
868
+ const params: unknown[] = [];
869
+ const clauses: string[] = [];
870
+ if (filter.target) {
871
+ params.push(`%${filter.target}%`);
872
+ clauses.push(`(ame.summary ILIKE $${params.length} OR ame.data_json ILIKE $${params.length})`);
873
+ }
874
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
875
+ const rows = await this.adapter.query(
876
+ `SELECT ame.*, e.external_session_id, e.external_turn_id, e.event_kind, e.captured_at,
877
+ s.source_name, s.integration_kind, s.trust_level
878
+ FROM agent_memory_events ame
879
+ JOIN external_agent_events e ON e.id = ame.external_event_id
880
+ JOIN agent_event_sources s ON s.id = e.source_id
881
+ ${where}
882
+ ORDER BY e.captured_at DESC, ame.id DESC
883
+ LIMIT ${limit}`,
884
+ params,
885
+ );
886
+ return rows.rows.reverse().map((row) => ({
887
+ id: String(row.id),
888
+ externalEventId: String(row.external_event_id),
889
+ sourceName: String(row.source_name),
890
+ integrationKind: String(row.integration_kind),
891
+ trustLevel: String(row.trust_level),
892
+ externalSessionId: typeof row.external_session_id === "string" ? row.external_session_id : undefined,
893
+ externalTurnId: typeof row.external_turn_id === "string" ? row.external_turn_id : undefined,
894
+ eventKind: String(row.event_kind),
895
+ normalizedKind: String(row.normalized_kind),
896
+ summary: typeof row.summary === "string" ? row.summary : undefined,
897
+ confidence: Number(row.confidence ?? 0),
898
+ capturedAt: String(row.captured_at),
899
+ operationId: typeof row.operation_id === "string" ? row.operation_id : undefined,
900
+ data: parseJsonRecord(row.data_json),
901
+ }));
902
+ }
903
+
904
+ async status(): Promise<DeltaStatus> {
905
+ const sessionRows = await this.adapter.query(
906
+ `SELECT s.id, s.started_at, COUNT(o.id)::int AS operation_count
907
+ FROM sessions s
908
+ LEFT JOIN operations o ON o.session_id = s.id
909
+ GROUP BY s.id, s.started_at
910
+ ORDER BY s.started_at DESC
911
+ LIMIT 1`,
912
+ );
913
+ const recent = await this.adapter.query(
914
+ `SELECT id, kind, summary, timestamp FROM operations ORDER BY timestamp DESC, id DESC LIMIT 8`,
915
+ );
916
+ const session = sessionRows.rows[0];
917
+ return {
918
+ ok: true,
919
+ recording: true,
920
+ store: normalizePath(relative(this.workspaceRoot, this.storePath)),
921
+ session: session
922
+ ? {
923
+ id: String(session.id),
924
+ startedAt: String(session.started_at),
925
+ operationCount: Number(session.operation_count ?? 0),
926
+ }
927
+ : undefined,
928
+ workSession: await this.currentWorkSession(),
929
+ recentOperations: recent.rows.map((row) => ({
930
+ id: String(row.id),
931
+ kind: String(row.kind),
932
+ summary: typeof row.summary === "string" ? row.summary : undefined,
933
+ timestamp: String(row.timestamp),
934
+ })),
935
+ };
936
+ }
937
+
938
+ async statusDetails(): Promise<DeltaStatusDetails> {
939
+ const metaRows = await this.adapter.query(
940
+ `SELECT key, value FROM delta_meta WHERE key IN ('schemaVersion', 'semantic.lastOperationId', 'semantic.lastRebuildAt')`,
941
+ );
942
+ const meta = new Map(metaRows.rows.map((row) => [String(row.key), String(row.value)]));
943
+ const countQueries = await Promise.all([
944
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM sessions`),
945
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM operations`),
946
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM file_changes`),
947
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM command_runs`),
948
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM runtime_calls`),
949
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM proofs`),
950
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM artifacts`),
951
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM work_sessions`),
952
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM agent_memory_events`),
953
+ this.adapter.query(`SELECT COUNT(*)::int AS count FROM timeline_events`),
954
+ ]);
955
+ const countAt = (index: number) => Number(countQueries[index]?.rows[0]?.count ?? 0);
956
+ const lockPath = getDeltaLockPath(this.workspaceRoot);
957
+ const postmasterPath = join(this.storePath, "postmaster.pid");
958
+ const storedVersion = meta.get("schemaVersion");
959
+ const lastOperationId = meta.get("semantic.lastOperationId");
960
+ const lastRebuildAt = meta.get("semantic.lastRebuildAt");
961
+ return {
962
+ schema: {
963
+ expectedVersion: DELTA_SCHEMA_VERSION,
964
+ ...(storedVersion ? { storedVersion } : {}),
965
+ ...(lastOperationId ? { lastOperationId } : {}),
966
+ ...(lastRebuildAt ? { lastRebuildAt } : {}),
967
+ },
968
+ paths: {
969
+ store: normalizePath(relative(this.workspaceRoot, this.storePath)),
970
+ lock: normalizePath(relative(this.workspaceRoot, lockPath)),
971
+ postmaster: normalizePath(relative(this.workspaceRoot, postmasterPath)),
972
+ },
973
+ locks: {
974
+ forgeLockPresent: existsSync(lockPath),
975
+ postmasterPresent: existsSync(postmasterPath),
976
+ },
977
+ counts: {
978
+ sessions: countAt(0),
979
+ operations: countAt(1),
980
+ fileChanges: countAt(2),
981
+ commandRuns: countAt(3),
982
+ runtimeCalls: countAt(4),
983
+ proofs: countAt(5),
984
+ artifacts: countAt(6),
985
+ workSessions: countAt(7),
986
+ agentMemoryEvents: countAt(8),
987
+ semanticEvents: countAt(9),
988
+ },
989
+ };
990
+ }
991
+
992
+ async timeline(filter: DeltaTimelineFilter = {}): Promise<DeltaTimelineEntry[]> {
993
+ const limit = Math.max(1, Math.min(filter.limit ?? 50, 200));
994
+ const params: unknown[] = [];
995
+ const clauses: string[] = [];
996
+ if (filter.kind) {
997
+ params.push(filter.kind);
998
+ clauses.push(`o.kind = $${params.length}`);
999
+ }
1000
+ if (filter.workSessionId) {
1001
+ const workSessionId = await this.resolveWorkSessionId(filter.workSessionId);
1002
+ if (workSessionId) {
1003
+ params.push(workSessionId);
1004
+ clauses.push(`EXISTS (
1005
+ SELECT 1 FROM work_session_operations wso
1006
+ WHERE wso.operation_id = o.id AND wso.work_session_id = $${params.length}
1007
+ )`);
1008
+ } else {
1009
+ return [];
1010
+ }
1011
+ }
1012
+ if (filter.target) {
1013
+ params.push(filter.target);
1014
+ const exactIndex = params.length;
1015
+ params.push(`%${filter.target}%`);
1016
+ const likeIndex = params.length;
1017
+ clauses.push(`(
1018
+ o.summary ILIKE $${likeIndex}
1019
+ OR o.data_json ILIKE $${likeIndex}
1020
+ OR EXISTS (SELECT 1 FROM file_changes f WHERE f.operation_id = o.id AND f.path = $${exactIndex})
1021
+ OR EXISTS (SELECT 1 FROM runtime_calls r WHERE r.operation_id = o.id AND r.entry_name = $${exactIndex})
1022
+ OR EXISTS (SELECT 1 FROM artifacts a WHERE a.operation_id = o.id AND a.path = $${exactIndex})
1023
+ )`);
1024
+ }
1025
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
1026
+ const result = await this.adapter.query(
1027
+ `SELECT o.id, o.kind, o.timestamp, o.summary, o.data_json
1028
+ FROM operations o
1029
+ ${where}
1030
+ ORDER BY o.timestamp DESC, o.id DESC
1031
+ LIMIT ${limit}`,
1032
+ params,
1033
+ );
1034
+ return result.rows.reverse().map(rowToTimelineEntry);
1035
+ }
1036
+
1037
+ async semanticTimeline(filter: DeltaSemanticTimelineFilter = {}): Promise<DeltaSemanticTimelineResult> {
1038
+ await this.ensureSemanticTimelineFresh();
1039
+ const limit = Math.max(1, Math.min(filter.limit ?? 50, 200));
1040
+ const entity = parseTimelineEntityTarget(filter.target);
1041
+ const params: unknown[] = [];
1042
+ const clauses: string[] = [];
1043
+ if (filter.kind) {
1044
+ const kindFilters = normalizeSemanticKindFilter(filter.kind);
1045
+ if (kindFilters.length === 1) {
1046
+ params.push(kindFilters[0]);
1047
+ clauses.push(`te.event_kind = $${params.length}`);
1048
+ } else {
1049
+ const placeholders = kindFilters.map((kind) => {
1050
+ params.push(kind);
1051
+ return `$${params.length}`;
1052
+ });
1053
+ clauses.push(`te.event_kind IN (${placeholders.join(", ")})`);
1054
+ }
1055
+ }
1056
+ if (filter.since) {
1057
+ params.push(filter.since);
1058
+ clauses.push(`te.timestamp >= $${params.length}`);
1059
+ }
1060
+ if (filter.until) {
1061
+ params.push(filter.until);
1062
+ clauses.push(`te.timestamp <= $${params.length}`);
1063
+ }
1064
+ if (filter.workSessionId) {
1065
+ const workSessionId = await this.resolveWorkSessionId(filter.workSessionId);
1066
+ if (!workSessionId) {
1067
+ return await this.emptySemanticTimeline(entity);
1068
+ }
1069
+ params.push(workSessionId);
1070
+ clauses.push(`EXISTS (
1071
+ SELECT 1 FROM work_session_operations wso
1072
+ WHERE wso.operation_id = te.operation_id AND wso.work_session_id = $${params.length}
1073
+ )`);
1074
+ }
1075
+ if (entity) {
1076
+ params.push(entity.kind);
1077
+ const kindIndex = params.length;
1078
+ params.push(entity.name);
1079
+ const nameIndex = params.length;
1080
+ clauses.push(`EXISTS (
1081
+ SELECT 1 FROM timeline_entities ten
1082
+ WHERE ten.timeline_event_id = te.id
1083
+ AND ten.entity_kind = $${kindIndex}
1084
+ AND ten.entity_name = $${nameIndex}
1085
+ )`);
1086
+ }
1087
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
1088
+ const rows = await this.adapter.query(
1089
+ `SELECT te.*
1090
+ FROM timeline_events te
1091
+ ${where}
1092
+ ORDER BY te.timestamp DESC, te.id DESC
1093
+ LIMIT ${limit}`,
1094
+ params,
1095
+ );
1096
+ let events = rows.rows.reverse().map(rowToSemanticTimelineEvent);
1097
+ for (const event of events) {
1098
+ event.entities = await this.timelineEntitiesForEvent(event.id);
1099
+ }
1100
+ const baseEventIds = events.map((event) => event.id);
1101
+ let expandedIds = [...baseEventIds];
1102
+ for (let depth = 0; depth < 2; depth += 1) {
1103
+ const touchingEdges = expandedIds.length > 0 ? await this.timelineEdgesTouchingEvents(expandedIds) : [];
1104
+ const nextIds = uniqueStrings(touchingEdges.flatMap((edge) => [edge.from, edge.to]));
1105
+ if (nextIds.every((id) => expandedIds.includes(id))) {
1106
+ break;
1107
+ }
1108
+ expandedIds = uniqueStrings([...expandedIds, ...nextIds]);
1109
+ }
1110
+ const linkedIds = expandedIds.filter((id) => !baseEventIds.includes(id));
1111
+ if (linkedIds.length > 0) {
1112
+ events = [...events, ...(await this.semanticEventsByIds(linkedIds))]
1113
+ .sort((left, right) => left.timestamp.localeCompare(right.timestamp) || left.id.localeCompare(right.id));
1114
+ }
1115
+ const eventIds = events.map((event) => event.id);
1116
+ const causalEdges = eventIds.length > 0 ? await this.timelineEdgesForEvents(eventIds) : [];
1117
+ const projection = await this.timelineProjectionState();
1118
+ const currentState = await this.semanticCurrentState(entity, events, causalEdges);
1119
+ const openQuestions = semanticOpenQuestions(entity, currentState, events);
1120
+ return { entity, currentState, events, causalEdges, openQuestions, projection };
1121
+ }
1122
+
1123
+ async rebuildSemanticTimeline(): Promise<void> {
1124
+ await this.adapter.query(`DELETE FROM timeline_edges`);
1125
+ await this.adapter.query(`DELETE FROM timeline_entities`);
1126
+ await this.adapter.query(`DELETE FROM timeline_events`);
1127
+ const operations = await this.adapter.query(`SELECT id FROM operations ORDER BY timestamp, id`);
1128
+ const projected: ProjectedTimelineEvent[] = [];
1129
+ for (const row of operations.rows) {
1130
+ const context = await this.loadOperationContext(String(row.id));
1131
+ if (!context) {
1132
+ continue;
1133
+ }
1134
+ const event = await this.projectOperationToSemanticEvent(context);
1135
+ if (!event) {
1136
+ continue;
1137
+ }
1138
+ projected.push(event);
1139
+ await this.insertSemanticTimelineEvent(event);
1140
+ }
1141
+ await this.insertSemanticTimelineEdges(projected);
1142
+ const latest = await this.latestOperationId();
1143
+ const now = new Date().toISOString();
1144
+ const graphHash = hashStable(JSON.stringify(projected.map((event) => ({
1145
+ id: event.event.id,
1146
+ kind: event.event.kind,
1147
+ entities: event.entities.map((entity) => `${entity.kind}:${entity.name}:${entity.role}`),
1148
+ }))));
1149
+ await this.adapter.query(
1150
+ `INSERT INTO timeline_projection_state (id, last_operation_id, last_rebuild_at, projection_version, graph_hash)
1151
+ VALUES ('semantic', $1, $2, $3, $4)
1152
+ ON CONFLICT (id) DO UPDATE
1153
+ SET last_operation_id = EXCLUDED.last_operation_id,
1154
+ last_rebuild_at = EXCLUDED.last_rebuild_at,
1155
+ projection_version = EXCLUDED.projection_version,
1156
+ graph_hash = EXCLUDED.graph_hash`,
1157
+ [latest, now, DELTA_SCHEMA_VERSION, graphHash],
1158
+ );
1159
+ }
1160
+
1161
+ private async needsSchemaInit(): Promise<boolean> {
1162
+ try {
1163
+ const meta = await this.adapter.query(`SELECT value FROM delta_meta WHERE key = $1 LIMIT 1`, ["schemaVersion"]);
1164
+ const version = typeof meta.rows[0]?.value === "string" ? meta.rows[0].value : "";
1165
+ if (version !== DELTA_SCHEMA_VERSION) {
1166
+ return true;
1167
+ }
1168
+ await this.adapter.query(`SELECT 1 FROM agent_memory_events LIMIT 1`);
1169
+ return false;
1170
+ } catch {
1171
+ return true;
1172
+ }
1173
+ }
1174
+
1175
+ async explain(thing: string): Promise<Record<string, unknown>> {
1176
+ if (thing === "session" || thing.startsWith("session:")) {
1177
+ const sessionId = thing === "session" ? "current" : thing.slice("session:".length);
1178
+ const session = await this.getWorkSessionDetails(sessionId || "current");
1179
+ return {
1180
+ thing,
1181
+ type: "work-session",
1182
+ session,
1183
+ git: session?.gitBranch ? { branch: session.gitBranch } : await this.latestGitMapping(),
1184
+ };
1185
+ }
1186
+ const semanticTimeline = await this.semanticTimeline({ target: thing, limit: 100 });
1187
+ const timeline = await this.timeline({ target: thing, limit: 100 });
1188
+ const runtime = await this.adapter.query(`SELECT * FROM runtime_calls WHERE entry_name = $1 ORDER BY operation_id`, [thing]);
1189
+ const files = await this.adapter.query(`SELECT * FROM file_changes WHERE path = $1 ORDER BY operation_id`, [thing]);
1190
+ const artifacts = await this.adapter.query(`SELECT * FROM artifacts WHERE path = $1 ORDER BY operation_id`, [thing]);
1191
+ const manifestOps = await this.adapter.query(
1192
+ `SELECT id, timestamp, summary, data_json FROM operations WHERE kind IN ('manifest.imported', 'manifest.validated') AND data_json ILIKE $1 ORDER BY timestamp`,
1193
+ [`%${thing}%`],
1194
+ );
1195
+ const proofs = await this.adapter.query(
1196
+ `SELECT p.* FROM proofs p JOIN operations o ON o.id = p.operation_id WHERE o.data_json ILIKE $1 OR p.diagnostics_json ILIKE $1 ORDER BY o.timestamp`,
1197
+ [`%${thing}%`],
1198
+ );
1199
+ const latestRuntime = runtime.rows[runtime.rows.length - 1];
1200
+ const type = latestRuntime
1201
+ ? "runtime-entry"
1202
+ : files.rows.length > 0
1203
+ ? "file"
1204
+ : artifacts.rows.length > 0
1205
+ ? "artifact"
1206
+ : proofs.rows.length > 0
1207
+ ? "proof"
1208
+ : "unknown";
1209
+ return {
1210
+ thing,
1211
+ type: semanticTimeline.events.length > 0 ? semanticTimeline.entity?.kind ?? type : type,
1212
+ origin: manifestOps.rows.map((row) => parseJsonRecord(row.data_json)),
1213
+ runtime: latestRuntime ? normalizeRow(latestRuntime) : null,
1214
+ files: files.rows.map(normalizeRow),
1215
+ artifacts: artifacts.rows.map(normalizeRow),
1216
+ proofs: proofs.rows.map(normalizeRow),
1217
+ semanticTimeline,
1218
+ timeline,
1219
+ workSessions: await this.workSessionsForThing(thing),
1220
+ git: await this.latestGitMapping(),
1221
+ };
1222
+ }
1223
+
1224
+ async recordFilePath(
1225
+ sessionId: string | undefined,
1226
+ path: string,
1227
+ changeType: DeltaFileChangeInput["changeType"] = "modified",
1228
+ summary?: string,
1229
+ ): Promise<void> {
1230
+ const relativePath = normalizePath(path);
1231
+ const absolutePath = join(this.workspaceRoot, relativePath);
1232
+ const exists = existsSync(absolutePath);
1233
+ const hashAfter = exists && statSync(absolutePath).isFile() ? hashUtf8Bytes(readFileSync(absolutePath)) : undefined;
1234
+ await this.appendOperation({
1235
+ sessionId,
1236
+ kind: changeType === "generated" ? "artifact.generated" : `file.${changeType === "modified" ? "changed" : changeType}`,
1237
+ summary: summary ?? `${changeType} ${relativePath}`,
1238
+ data: { path: relativePath, changeType },
1239
+ fileChanges: [{
1240
+ path: relativePath,
1241
+ changeType,
1242
+ hashAfter,
1243
+ semanticHints: classifyDeltaPath(relativePath),
1244
+ }],
1245
+ artifacts: changeType === "generated"
1246
+ ? [{ path: relativePath, artifactKind: classifyArtifactKind(relativePath), hash: hashAfter, generated: true }]
1247
+ : undefined,
1248
+ });
1249
+ }
1250
+
1251
+ async currentWorkSession(): Promise<DeltaWorkSessionSummary | undefined> {
1252
+ const result = await this.adapter.query(
1253
+ `SELECT ws.*, COUNT(wso.operation_id)::int AS operation_count
1254
+ FROM work_sessions ws
1255
+ LEFT JOIN work_session_operations wso ON wso.work_session_id = ws.id
1256
+ WHERE ws.status IN ('open', 'idle', 'needs-review')
1257
+ GROUP BY ws.id
1258
+ ORDER BY ws.updated_at DESC, ws.started_at DESC
1259
+ LIMIT 1`,
1260
+ );
1261
+ return result.rows[0] ? this.rowToWorkSessionSummary(result.rows[0]) : undefined;
1262
+ }
1263
+
1264
+ async listWorkSessions(limit = 20): Promise<DeltaWorkSessionSummary[]> {
1265
+ const capped = Math.max(1, Math.min(limit, 100));
1266
+ const result = await this.adapter.query(
1267
+ `SELECT ws.*, COUNT(wso.operation_id)::int AS operation_count
1268
+ FROM work_sessions ws
1269
+ LEFT JOIN work_session_operations wso ON wso.work_session_id = ws.id
1270
+ GROUP BY ws.id
1271
+ ORDER BY ws.updated_at DESC, ws.started_at DESC
1272
+ LIMIT ${capped}`,
1273
+ );
1274
+ const sessions: DeltaWorkSessionSummary[] = [];
1275
+ for (const row of result.rows) {
1276
+ sessions.push(await this.rowToWorkSessionSummary(row));
1277
+ }
1278
+ return sessions;
1279
+ }
1280
+
1281
+ async getWorkSessionDetails(idOrCurrent: string): Promise<DeltaWorkSessionDetails | undefined> {
1282
+ const id = await this.resolveWorkSessionId(idOrCurrent);
1283
+ if (!id) {
1284
+ return undefined;
1285
+ }
1286
+ const result = await this.adapter.query(
1287
+ `SELECT ws.*, COUNT(wso.operation_id)::int AS operation_count
1288
+ FROM work_sessions ws
1289
+ LEFT JOIN work_session_operations wso ON wso.work_session_id = ws.id
1290
+ WHERE ws.id = $1
1291
+ GROUP BY ws.id`,
1292
+ [id],
1293
+ );
1294
+ if (!result.rows[0]) {
1295
+ return undefined;
1296
+ }
1297
+ const summary = await this.rowToWorkSessionSummary(result.rows[0]);
1298
+ const operations = await this.timeline({ workSessionId: id, limit: 200 });
1299
+ const signals = await this.signalsForWorkSession(id, 100);
1300
+ return { ...summary, operations, signals };
1301
+ }
1302
+
1303
+ async renameWorkSession(idOrCurrent: string, title: string): Promise<DeltaWorkSessionDetails | undefined> {
1304
+ const id = await this.resolveWorkSessionId(idOrCurrent);
1305
+ if (!id) {
1306
+ return undefined;
1307
+ }
1308
+ const now = new Date().toISOString();
1309
+ const existing = await this.getWorkSessionDetails(id);
1310
+ const metadata = existing ? { ...existing.metadata, manualTitle: true } : { ...emptyWorkSessionMetadata(), manualTitle: true };
1311
+ await this.adapter.query(
1312
+ `UPDATE work_sessions
1313
+ SET title = $1, kind = 'manual-corrected', confidence = GREATEST(confidence, 0.9), metadata_json = $2, updated_at = $3
1314
+ WHERE id = $4`,
1315
+ [title, JSON.stringify(metadata), now, id],
1316
+ );
1317
+ await this.insertWorkSessionSummary(id, `Renamed session to "${title}".`, "human-edited");
1318
+ return this.getWorkSessionDetails(id);
1319
+ }
1320
+
1321
+ async detachWorkSessionOperation(operationId: string): Promise<boolean> {
1322
+ const linked = await this.adapter.query(
1323
+ `SELECT DISTINCT work_session_id FROM work_session_operations WHERE operation_id = $1`,
1324
+ [operationId],
1325
+ );
1326
+ await this.adapter.query(`DELETE FROM work_session_operations WHERE operation_id = $1`, [operationId]);
1327
+ for (const row of linked.rows) {
1328
+ if (typeof row.work_session_id === "string") {
1329
+ await this.rebuildWorkSessionFromOperations(row.work_session_id);
1330
+ }
1331
+ }
1332
+ return linked.rows.length > 0;
1333
+ }
1334
+
1335
+ async mergeWorkSessions(targetIdOrCurrent: string, sourceId: string): Promise<DeltaWorkSessionDetails | undefined> {
1336
+ const targetId = await this.resolveWorkSessionId(targetIdOrCurrent);
1337
+ const source = await this.resolveWorkSessionId(sourceId);
1338
+ if (!targetId || !source || targetId === source) {
1339
+ return undefined;
1340
+ }
1341
+ const now = new Date().toISOString();
1342
+ await this.adapter.query(
1343
+ `UPDATE work_session_operations SET work_session_id = $1 WHERE work_session_id = $2`,
1344
+ [targetId, source],
1345
+ );
1346
+ await this.adapter.query(
1347
+ `UPDATE work_sessions SET status = 'merged', ended_at = $1, updated_at = $1 WHERE id = $2`,
1348
+ [now, source],
1349
+ );
1350
+ const target = await this.getWorkSessionDetails(targetId);
1351
+ const metadata = target
1352
+ ? mergeWorkSessionMetadata(target.metadata, { ...emptyWorkSessionMetadata(), mergedFrom: [source] })
1353
+ : emptyWorkSessionMetadata();
1354
+ await this.adapter.query(`UPDATE work_sessions SET metadata_json = $1, updated_at = $2 WHERE id = $3`, [
1355
+ JSON.stringify(metadata),
1356
+ now,
1357
+ targetId,
1358
+ ]);
1359
+ await this.rebuildWorkSessionFromOperations(targetId);
1360
+ await this.insertWorkSessionSummary(targetId, `Merged work session ${source} into ${targetId}.`, "human-edited");
1361
+ return this.getWorkSessionDetails(targetId);
1362
+ }
1363
+
1364
+ async splitWorkSession(idOrCurrent: string, fromOperationId: string): Promise<DeltaWorkSessionDetails | undefined> {
1365
+ const id = await this.resolveWorkSessionId(idOrCurrent);
1366
+ if (!id) {
1367
+ return undefined;
1368
+ }
1369
+ const operationRows = await this.adapter.query(
1370
+ `SELECT o.id, o.timestamp
1371
+ FROM operations o
1372
+ JOIN work_session_operations wso ON wso.operation_id = o.id
1373
+ WHERE wso.work_session_id = $1
1374
+ ORDER BY o.timestamp, o.id`,
1375
+ [id],
1376
+ );
1377
+ const index = operationRows.rows.findIndex((row) => row.id === fromOperationId);
1378
+ if (index < 0) {
1379
+ return undefined;
1380
+ }
1381
+ const moved = operationRows.rows.slice(index).map((row) => String(row.id));
1382
+ const firstContext = await this.loadOperationContext(moved[0]!);
1383
+ if (!firstContext) {
1384
+ return undefined;
1385
+ }
1386
+ const now = new Date().toISOString();
1387
+ const newId = createDeltaId("worksess");
1388
+ const metadata = { ...contextToWorkSessionMetadata(firstContext), splitFrom: id };
1389
+ await this.adapter.query(
1390
+ `INSERT INTO work_sessions (
1391
+ id, workspace_root, kind, status, title, inferred_intent, confidence, started_at, actor_ids_json,
1392
+ git_branch, git_head_start, summary, metadata_json, created_at, updated_at
1393
+ ) VALUES ($1, $2, 'manual-corrected', 'needs-review', $3, $4, 0.65, $5, $6, $7, $8, $9, $10, $11, $11)`,
1394
+ [
1395
+ newId,
1396
+ this.workspaceRoot,
1397
+ inferWorkSessionTitle(firstContext, metadata),
1398
+ inferIntent(firstContext, metadata),
1399
+ firstContext.timestamp,
1400
+ JSON.stringify(metadata.actorIds),
1401
+ firstContext.branch ?? null,
1402
+ firstContext.gitHead ?? null,
1403
+ summarizeWorkSession(metadata),
1404
+ JSON.stringify(metadata),
1405
+ now,
1406
+ ],
1407
+ );
1408
+ for (const operationId of moved) {
1409
+ await this.adapter.query(
1410
+ `UPDATE work_session_operations SET work_session_id = $1, link_type = 'manual', confidence = 0.8 WHERE work_session_id = $2 AND operation_id = $3`,
1411
+ [newId, id, operationId],
1412
+ );
1413
+ }
1414
+ await this.rebuildWorkSessionFromOperations(id);
1415
+ await this.rebuildWorkSessionFromOperations(newId);
1416
+ await this.insertWorkSessionSummary(newId, `Split from work session ${id}.`, "human-edited");
1417
+ return this.getWorkSessionDetails(newId);
1418
+ }
1419
+
1420
+ private async inferWorkSessionForOperation(operationId: string): Promise<void> {
1421
+ const context = await this.loadOperationContext(operationId);
1422
+ if (!context || !shouldInferWorkSession(context)) {
1423
+ return;
1424
+ }
1425
+ await this.closeIdleWorkSessions(context.timestamp);
1426
+ const candidates = await this.candidateWorkSessions(context);
1427
+ const scored = candidates
1428
+ .map((candidate) => ({ candidate, score: scoreWorkSessionCandidate(context, candidate) }))
1429
+ .sort((a, b) => b.score.score - a.score.score);
1430
+ const best = scored[0];
1431
+ if (!best || best.score.score < 0.4) {
1432
+ await this.createWorkSessionForOperation(context);
1433
+ return;
1434
+ }
1435
+ await this.attachOperationToWorkSession(
1436
+ best.candidate.id,
1437
+ context,
1438
+ best.score.score >= 0.65 ? "primary" : "weak",
1439
+ best.score.score,
1440
+ best.score.signals,
1441
+ );
1442
+ }
1443
+
1444
+ private async createWorkSessionForOperation(context: DeltaOperationContext): Promise<string> {
1445
+ const id = createDeltaId("worksess");
1446
+ const now = new Date().toISOString();
1447
+ const metadata = contextToWorkSessionMetadata(context);
1448
+ const title = inferWorkSessionTitle(context, metadata);
1449
+ const confidence = initialWorkSessionConfidence(context);
1450
+ const status: DeltaWorkSessionStatus = confidence >= 0.65 ? "open" : "needs-review";
1451
+ const summary = summarizeWorkSession(metadata);
1452
+ await this.adapter.query(
1453
+ `INSERT INTO work_sessions (
1454
+ id, workspace_root, kind, status, title, inferred_intent, confidence, started_at, actor_ids_json,
1455
+ git_branch, git_head_start, summary, metadata_json, created_at, updated_at
1456
+ ) VALUES ($1, $2, 'auto', $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $13)`,
1457
+ [
1458
+ id,
1459
+ this.workspaceRoot,
1460
+ status,
1461
+ title,
1462
+ inferIntent(context, metadata),
1463
+ confidence,
1464
+ context.timestamp,
1465
+ JSON.stringify(metadata.actorIds),
1466
+ context.branch ?? null,
1467
+ context.gitHead ?? null,
1468
+ summary,
1469
+ JSON.stringify(metadata),
1470
+ now,
1471
+ ],
1472
+ );
1473
+ const signals = seedSignalsForContext(context);
1474
+ await this.linkOperationToWorkSession(id, context.id, "primary", confidence, signals);
1475
+ await this.insertWorkSessionSignals(id, context.id, signals);
1476
+ await this.insertWorkSessionSummary(id, summary, "auto-short");
1477
+ return id;
1478
+ }
1479
+
1480
+ private async attachOperationToWorkSession(
1481
+ workSessionId: string,
1482
+ context: DeltaOperationContext,
1483
+ linkType: DeltaWorkSessionLinkType,
1484
+ confidence: number,
1485
+ signals: DeltaWorkSessionSignal[],
1486
+ ): Promise<void> {
1487
+ const current = await this.getWorkSessionDetails(workSessionId);
1488
+ const metadata = mergeWorkSessionMetadata(current?.metadata ?? emptyWorkSessionMetadata(), contextToWorkSessionMetadata(context));
1489
+ const title = current?.metadata.manualTitle ? current.title : inferWorkSessionTitle(context, metadata);
1490
+ const nextConfidence = roundConfidence(Math.max(confidence, ((current?.confidence ?? 0.5) * 0.7) + (confidence * 0.3)));
1491
+ const status: DeltaWorkSessionStatus = linkType === "weak" || nextConfidence < 0.65 ? "needs-review" : "open";
1492
+ const summary = summarizeWorkSession(metadata);
1493
+ await this.linkOperationToWorkSession(workSessionId, context.id, linkType, confidence, signals);
1494
+ await this.insertWorkSessionSignals(workSessionId, context.id, signals);
1495
+ await this.adapter.query(
1496
+ `UPDATE work_sessions
1497
+ SET status = $1, title = $2, inferred_intent = $3, confidence = $4, ended_at = NULL,
1498
+ actor_ids_json = $5, git_branch = COALESCE(git_branch, $6), git_head_end = COALESCE($7, git_head_end),
1499
+ summary = $8, metadata_json = $9, updated_at = $10
1500
+ WHERE id = $11`,
1501
+ [
1502
+ status,
1503
+ title,
1504
+ inferIntent(context, metadata),
1505
+ nextConfidence,
1506
+ JSON.stringify(metadata.actorIds),
1507
+ context.branch ?? null,
1508
+ context.gitHead ?? null,
1509
+ summary,
1510
+ JSON.stringify(metadata),
1511
+ context.timestamp,
1512
+ workSessionId,
1513
+ ],
1514
+ );
1515
+ await this.insertWorkSessionSummary(workSessionId, summary, "auto-short");
1516
+ }
1517
+
1518
+ private async linkOperationToWorkSession(
1519
+ workSessionId: string,
1520
+ operationId: string,
1521
+ linkType: DeltaWorkSessionLinkType,
1522
+ confidence: number,
1523
+ signals: DeltaWorkSessionSignal[],
1524
+ ): Promise<void> {
1525
+ await this.adapter.query(
1526
+ `INSERT INTO work_session_operations (work_session_id, operation_id, link_type, confidence, reason_json, created_at)
1527
+ VALUES ($1, $2, $3, $4, $5, $6)
1528
+ ON CONFLICT (work_session_id, operation_id)
1529
+ DO UPDATE SET link_type = EXCLUDED.link_type, confidence = EXCLUDED.confidence, reason_json = EXCLUDED.reason_json`,
1530
+ [
1531
+ workSessionId,
1532
+ operationId,
1533
+ linkType,
1534
+ roundConfidence(confidence),
1535
+ JSON.stringify(signals),
1536
+ new Date().toISOString(),
1537
+ ],
1538
+ );
1539
+ }
1540
+
1541
+ private async insertWorkSessionSignals(
1542
+ workSessionId: string,
1543
+ operationId: string,
1544
+ signals: DeltaWorkSessionSignal[],
1545
+ ): Promise<void> {
1546
+ for (const signal of signals) {
1547
+ await this.adapter.query(
1548
+ `INSERT INTO work_session_signals (id, work_session_id, operation_id, signal_type, weight, value, metadata_json, created_at)
1549
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
1550
+ [
1551
+ createDeltaId("wssig"),
1552
+ workSessionId,
1553
+ operationId,
1554
+ signal.signal,
1555
+ signal.weight,
1556
+ signal.value ?? null,
1557
+ JSON.stringify(signal.metadata ?? {}),
1558
+ new Date().toISOString(),
1559
+ ],
1560
+ );
1561
+ }
1562
+ }
1563
+
1564
+ private async insertWorkSessionSummary(workSessionId: string, content: string, summaryType: string): Promise<void> {
1565
+ await this.adapter.query(
1566
+ `INSERT INTO work_session_summaries (id, work_session_id, summary_type, content, generated_by, created_at, redaction_json)
1567
+ VALUES ($1, $2, $3, $4, 'forge-delta-h45', $5, $6)`,
1568
+ [createDeltaId("wssum"), workSessionId, summaryType, content, new Date().toISOString(), JSON.stringify({ redacted: false })],
1569
+ );
1570
+ }
1571
+
1572
+ private async candidateWorkSessions(context: DeltaOperationContext): Promise<DeltaWorkSessionSummary[]> {
1573
+ const result = await this.adapter.query(
1574
+ `SELECT ws.*, COUNT(wso.operation_id)::int AS operation_count
1575
+ FROM work_sessions ws
1576
+ LEFT JOIN work_session_operations wso ON wso.work_session_id = ws.id
1577
+ WHERE ws.status IN ('open', 'idle', 'needs-review')
1578
+ AND (
1579
+ ws.updated_at >= $1
1580
+ OR ws.git_branch = $2
1581
+ OR ws.metadata_json ILIKE $3
1582
+ OR ws.metadata_json ILIKE $4
1583
+ )
1584
+ GROUP BY ws.id
1585
+ ORDER BY ws.updated_at DESC, ws.started_at DESC
1586
+ LIMIT 20`,
1587
+ [
1588
+ new Date(Date.parse(context.timestamp) - 2 * 60 * 60 * 1000).toISOString(),
1589
+ context.branch ?? "",
1590
+ context.entries[0] ? `%${context.entries[0]}%` : "__forge_delta_no_entry__",
1591
+ context.services[0] ? `%${context.services[0]}%` : "__forge_delta_no_service__",
1592
+ ],
1593
+ );
1594
+ const candidates: DeltaWorkSessionSummary[] = [];
1595
+ for (const row of result.rows) {
1596
+ candidates.push(await this.rowToWorkSessionSummary(row));
1597
+ }
1598
+ return candidates;
1599
+ }
1600
+
1601
+ private async closeIdleWorkSessions(nowIso: string): Promise<void> {
1602
+ const idleBefore = new Date(Date.parse(nowIso) - 2 * 60 * 60 * 1000).toISOString();
1603
+ await this.adapter.query(
1604
+ `UPDATE work_sessions
1605
+ SET status = 'idle', ended_at = COALESCE(ended_at, updated_at)
1606
+ WHERE status = 'open' AND updated_at < $1`,
1607
+ [idleBefore],
1608
+ );
1609
+ }
1610
+
1611
+ private async loadOperationContext(operationId: string): Promise<DeltaOperationContext | undefined> {
1612
+ const operation = await this.adapter.query(
1613
+ `SELECT o.*, s.branch AS session_branch, s.metadata_json AS session_metadata_json
1614
+ FROM operations o
1615
+ LEFT JOIN sessions s ON s.id = o.session_id
1616
+ WHERE o.id = $1`,
1617
+ [operationId],
1618
+ );
1619
+ const row = operation.rows[0];
1620
+ if (!row) {
1621
+ return undefined;
1622
+ }
1623
+ const data = parseJsonRecord(row.data_json);
1624
+ const sessionMetadata = parseJsonRecord(row.session_metadata_json);
1625
+ const git = parseJsonRecord(data.git);
1626
+ const sessionGit = parseJsonRecord(sessionMetadata.git);
1627
+ const filesResult = await this.adapter.query(`SELECT * FROM file_changes WHERE operation_id = $1`, [operationId]);
1628
+ const runtimeResult = await this.adapter.query(`SELECT * FROM runtime_calls WHERE operation_id = $1`, [operationId]);
1629
+ const proofResult = await this.adapter.query(`SELECT * FROM proofs WHERE operation_id = $1`, [operationId]);
1630
+ const artifactResult = await this.adapter.query(`SELECT * FROM artifacts WHERE operation_id = $1`, [operationId]);
1631
+ const commandResult = await this.adapter.query(`SELECT * FROM command_runs WHERE operation_id = $1`, [operationId]);
1632
+ const files = uniqueStrings([
1633
+ ...filesResult.rows.map((item) => item.path),
1634
+ ...artifactResult.rows.map((item) => item.path),
1635
+ data.path,
1636
+ ]);
1637
+ const fileClusters = uniqueStrings([
1638
+ ...filesResult.rows.flatMap((item) => parseSemanticHints(item.semantic_hints_json).map((hint) => hint.kind)),
1639
+ ...files.map(clusterForPath),
1640
+ ]);
1641
+ const entries = uniqueStrings([
1642
+ ...runtimeResult.rows.map((item) => item.entry_name),
1643
+ data.entryName,
1644
+ ...arrayOfStrings(data.entries),
1645
+ ...arrayOfStrings(data.entryNames),
1646
+ ]);
1647
+ const diagnostics = uniqueStrings([
1648
+ ...runtimeResult.rows.map((item) => item.diagnostic_code),
1649
+ ...proofResult.rows.flatMap((item) => diagnosticCodesFromJson(item.diagnostics_json)),
1650
+ ...commandResult.rows.flatMap((item) => diagnosticCodesFromJson(item.diagnostics_json)),
1651
+ data.diagnosticCode,
1652
+ ]);
1653
+ const proofs = uniqueStrings([
1654
+ ...proofResult.rows.map((item) => item.proof_kind),
1655
+ row.kind === "proof.run" ? data.command : undefined,
1656
+ ]);
1657
+ const services = uniqueStrings([
1658
+ ...runtimeResult.rows.map((item) => item.service),
1659
+ data.service,
1660
+ ...arrayOfStrings(data.services),
1661
+ typeof data.path === "string" ? serviceFromManifestPath(data.path) : undefined,
1662
+ ...entries.map((entry) => entry.split(".")[0]),
1663
+ ]);
1664
+ const traces = uniqueStrings([
1665
+ ...runtimeResult.rows.map((item) => item.trace_id),
1666
+ data.traceId,
1667
+ ]);
1668
+ const commands = uniqueStrings([
1669
+ ...commandResult.rows.map((item) => item.command_name),
1670
+ data.command,
1671
+ data.toolName,
1672
+ ]);
1673
+ return {
1674
+ id: String(row.id),
1675
+ kind: String(row.kind),
1676
+ timestamp: String(row.timestamp),
1677
+ actorId: typeof row.actor_id === "string" ? row.actor_id : undefined,
1678
+ summary: typeof row.summary === "string" ? row.summary : undefined,
1679
+ data,
1680
+ sessionId: typeof row.session_id === "string" ? row.session_id : undefined,
1681
+ branch: stringOrUndefined(git.branch) ?? stringOrUndefined(sessionGit.branch) ?? stringOrUndefined(row.session_branch),
1682
+ gitHead: stringOrUndefined(git.head) ?? stringOrUndefined(git.commitSha) ?? stringOrUndefined(sessionGit.head),
1683
+ files,
1684
+ fileClusters,
1685
+ entries,
1686
+ diagnostics,
1687
+ proofs,
1688
+ services,
1689
+ traces,
1690
+ commands,
1691
+ };
1692
+ }
1693
+
1694
+ private async rowToWorkSessionSummary(row: Record<string, unknown>): Promise<DeltaWorkSessionSummary> {
1695
+ const metadata = normalizeWorkSessionMetadata(parseJsonRecord(row.metadata_json));
1696
+ const id = String(row.id);
1697
+ const latestSignals = await this.signalsForWorkSession(id, 8);
1698
+ return {
1699
+ id,
1700
+ kind: normalizeWorkSessionKind(row.kind),
1701
+ status: normalizeWorkSessionStatus(row.status),
1702
+ title: typeof row.title === "string" && row.title ? row.title : "Work session",
1703
+ inferredIntent: typeof row.inferred_intent === "string" ? row.inferred_intent : undefined,
1704
+ confidence: Number(row.confidence ?? 0),
1705
+ startedAt: String(row.started_at),
1706
+ endedAt: typeof row.ended_at === "string" ? row.ended_at : undefined,
1707
+ gitBranch: typeof row.git_branch === "string" ? row.git_branch : undefined,
1708
+ summary: typeof row.summary === "string" ? row.summary : undefined,
1709
+ operationCount: Number(row.operation_count ?? 0),
1710
+ reasons: latestSignals,
1711
+ metadata,
1712
+ };
1713
+ }
1714
+
1715
+ private async signalsForWorkSession(workSessionId: string, limit: number): Promise<DeltaWorkSessionSignal[]> {
1716
+ const result = await this.adapter.query(
1717
+ `SELECT signal_type, weight, value, metadata_json
1718
+ FROM work_session_signals
1719
+ WHERE work_session_id = $1
1720
+ ORDER BY created_at DESC, id DESC
1721
+ LIMIT ${Math.max(1, Math.min(limit, 200))}`,
1722
+ [workSessionId],
1723
+ );
1724
+ return result.rows.map((row) => ({
1725
+ signal: String(row.signal_type),
1726
+ weight: Number(row.weight ?? 0),
1727
+ value: typeof row.value === "string" ? row.value : undefined,
1728
+ metadata: parseJsonRecord(row.metadata_json),
1729
+ }));
1730
+ }
1731
+
1732
+ private async workSessionsForThing(thing: string): Promise<DeltaWorkSessionSummary[]> {
1733
+ const result = await this.adapter.query(
1734
+ `SELECT ws.*, COUNT(wso2.operation_id)::int AS operation_count
1735
+ FROM work_sessions ws
1736
+ JOIN work_session_operations wso ON wso.work_session_id = ws.id
1737
+ JOIN operations o ON o.id = wso.operation_id
1738
+ LEFT JOIN work_session_operations wso2 ON wso2.work_session_id = ws.id
1739
+ WHERE o.summary ILIKE $1
1740
+ OR o.data_json ILIKE $1
1741
+ OR EXISTS (SELECT 1 FROM runtime_calls r WHERE r.operation_id = o.id AND r.entry_name = $2)
1742
+ OR EXISTS (SELECT 1 FROM file_changes f WHERE f.operation_id = o.id AND f.path = $2)
1743
+ OR EXISTS (SELECT 1 FROM artifacts a WHERE a.operation_id = o.id AND a.path = $2)
1744
+ GROUP BY ws.id
1745
+ ORDER BY ws.started_at`,
1746
+ [`%${thing}%`, thing],
1747
+ );
1748
+ const sessions: DeltaWorkSessionSummary[] = [];
1749
+ for (const row of result.rows) {
1750
+ sessions.push(await this.rowToWorkSessionSummary(row));
1751
+ }
1752
+ return sessions;
1753
+ }
1754
+
1755
+ private async resolveWorkSessionId(idOrCurrent: string): Promise<string | undefined> {
1756
+ if (idOrCurrent === "current") {
1757
+ return (await this.currentWorkSession())?.id;
1758
+ }
1759
+ const exists = await this.adapter.query(`SELECT id FROM work_sessions WHERE id = $1 LIMIT 1`, [idOrCurrent]);
1760
+ return typeof exists.rows[0]?.id === "string" ? exists.rows[0].id : undefined;
1761
+ }
1762
+
1763
+ private async rebuildWorkSessionFromOperations(workSessionId: string): Promise<void> {
1764
+ const links = await this.adapter.query(
1765
+ `SELECT operation_id FROM work_session_operations WHERE work_session_id = $1 ORDER BY created_at`,
1766
+ [workSessionId],
1767
+ );
1768
+ if (links.rows.length === 0) {
1769
+ await this.adapter.query(
1770
+ `UPDATE work_sessions SET status = 'closed', ended_at = COALESCE(ended_at, updated_at), updated_at = $1 WHERE id = $2`,
1771
+ [new Date().toISOString(), workSessionId],
1772
+ );
1773
+ return;
1774
+ }
1775
+ let metadata = emptyWorkSessionMetadata();
1776
+ let first: DeltaOperationContext | undefined;
1777
+ let last: DeltaOperationContext | undefined;
1778
+ for (const row of links.rows) {
1779
+ const context = await this.loadOperationContext(String(row.operation_id));
1780
+ if (!context) {
1781
+ continue;
1782
+ }
1783
+ first ??= context;
1784
+ last = context;
1785
+ metadata = mergeWorkSessionMetadata(metadata, contextToWorkSessionMetadata(context));
1786
+ }
1787
+ if (!first || !last) {
1788
+ return;
1789
+ }
1790
+ const existing = await this.getWorkSessionDetails(workSessionId);
1791
+ metadata = mergeWorkSessionMetadata(existing?.metadata ?? emptyWorkSessionMetadata(), metadata);
1792
+ const title = existing?.metadata.manualTitle ? existing.title : inferWorkSessionTitle(last, metadata);
1793
+ await this.adapter.query(
1794
+ `UPDATE work_sessions
1795
+ SET title = $1, inferred_intent = $2, started_at = $3, actor_ids_json = $4, git_branch = $5,
1796
+ git_head_start = $6, git_head_end = $7, summary = $8, metadata_json = $9, updated_at = $10
1797
+ WHERE id = $11`,
1798
+ [
1799
+ title,
1800
+ inferIntent(last, metadata),
1801
+ first.timestamp,
1802
+ JSON.stringify(metadata.actorIds),
1803
+ first.branch ?? null,
1804
+ first.gitHead ?? null,
1805
+ last.gitHead ?? null,
1806
+ summarizeWorkSession(metadata),
1807
+ JSON.stringify(metadata),
1808
+ last.timestamp,
1809
+ workSessionId,
1810
+ ],
1811
+ );
1812
+ }
1813
+
1814
+ private async insertFileChange(operationId: string, fileChange: DeltaFileChangeInput): Promise<void> {
1815
+ await this.adapter.query(
1816
+ `INSERT INTO file_changes (id, operation_id, path, change_type, hash_before, hash_after, diff_summary, semantic_hints_json)
1817
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
1818
+ [
1819
+ createDeltaId("filechg"),
1820
+ operationId,
1821
+ normalizePath(fileChange.path),
1822
+ fileChange.changeType,
1823
+ fileChange.hashBefore ?? null,
1824
+ fileChange.hashAfter ?? null,
1825
+ fileChange.diffSummary ?? null,
1826
+ JSON.stringify(fileChange.semanticHints ?? classifyDeltaPath(fileChange.path)),
1827
+ ],
1828
+ );
1829
+ }
1830
+
1831
+ private async insertCommandRun(operationId: string, commandRun: DeltaCommandRunInput): Promise<void> {
1832
+ const redacted = redactDeltaPayload({ argv: commandRun.argv ?? [] });
1833
+ await this.adapter.query(
1834
+ `INSERT INTO command_runs (id, operation_id, command_name, argv_redacted_json, exit_code, duration_ms, diagnostics_json)
1835
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
1836
+ [
1837
+ createDeltaId("cmdrun"),
1838
+ operationId,
1839
+ commandRun.commandName,
1840
+ JSON.stringify(redacted.value.argv),
1841
+ commandRun.exitCode ?? null,
1842
+ commandRun.durationMs ?? null,
1843
+ JSON.stringify(commandRun.diagnostics ?? []),
1844
+ ],
1845
+ );
1846
+ }
1847
+
1848
+ private async insertRuntimeCall(operationId: string, runtimeCall: DeltaRuntimeCallInput): Promise<void> {
1849
+ await this.adapter.query(
1850
+ `INSERT INTO runtime_calls (id, operation_id, entry_name, entry_kind, risk, policy, tenant_scoped, result, diagnostic_code, trace_id, service, language, needs_approval)
1851
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
1852
+ [
1853
+ createDeltaId("rtcall"),
1854
+ operationId,
1855
+ runtimeCall.entryName,
1856
+ runtimeCall.entryKind ?? null,
1857
+ runtimeCall.risk ?? null,
1858
+ runtimeCall.policy ?? null,
1859
+ runtimeCall.tenantScoped === undefined ? null : runtimeCall.tenantScoped ? 1 : 0,
1860
+ runtimeCall.result ?? null,
1861
+ runtimeCall.diagnosticCode ?? null,
1862
+ runtimeCall.traceId ?? null,
1863
+ runtimeCall.service ?? null,
1864
+ runtimeCall.language ?? null,
1865
+ runtimeCall.needsApproval === undefined ? null : runtimeCall.needsApproval ? 1 : 0,
1866
+ ],
1867
+ );
1868
+ }
1869
+
1870
+ private async insertProof(operationId: string, proof: DeltaProofInput): Promise<void> {
1871
+ await this.adapter.query(
1872
+ `INSERT INTO proofs (id, operation_id, proof_kind, command, result, assurance, diagnostics_json, artifact_paths_json)
1873
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
1874
+ [
1875
+ createDeltaId("proof"),
1876
+ operationId,
1877
+ proof.proofKind,
1878
+ proof.command ?? null,
1879
+ proof.result,
1880
+ proof.assurance ?? null,
1881
+ JSON.stringify(proof.diagnostics ?? []),
1882
+ JSON.stringify(proof.artifactPaths ?? []),
1883
+ ],
1884
+ );
1885
+ }
1886
+
1887
+ private async insertArtifact(operationId: string, artifact: DeltaArtifactInput): Promise<void> {
1888
+ await this.adapter.query(
1889
+ `INSERT INTO artifacts (id, operation_id, path, artifact_kind, hash, generated)
1890
+ VALUES ($1, $2, $3, $4, $5, $6)`,
1891
+ [
1892
+ createDeltaId("artifact"),
1893
+ operationId,
1894
+ normalizePath(artifact.path),
1895
+ artifact.artifactKind ?? classifyArtifactKind(artifact.path),
1896
+ artifact.hash ?? null,
1897
+ artifact.generated === false ? 0 : 1,
1898
+ ],
1899
+ );
1900
+ }
1901
+
1902
+ private async ensureSemanticTimelineFresh(): Promise<void> {
1903
+ const latest = await this.latestOperationId();
1904
+ const state = await this.timelineProjectionState();
1905
+ if (state.lastOperationId === latest && state.version === DELTA_SCHEMA_VERSION) {
1906
+ return;
1907
+ }
1908
+ await this.rebuildSemanticTimeline();
1909
+ }
1910
+
1911
+ private async emptySemanticTimeline(entity?: DeltaTimelineEntityRef): Promise<DeltaSemanticTimelineResult> {
1912
+ return {
1913
+ entity,
1914
+ currentState: {},
1915
+ events: [],
1916
+ causalEdges: [],
1917
+ openQuestions: entity ? [`No timeline events found for ${entity.kind}:${entity.name}`] : [],
1918
+ projection: await this.timelineProjectionState(),
1919
+ };
1920
+ }
1921
+
1922
+ private async latestOperationId(): Promise<string | undefined> {
1923
+ const result = await this.adapter.query(`SELECT id FROM operations ORDER BY timestamp DESC, id DESC LIMIT 1`);
1924
+ return typeof result.rows[0]?.id === "string" ? result.rows[0].id : undefined;
1925
+ }
1926
+
1927
+ private async timelineProjectionState(): Promise<DeltaSemanticTimelineResult["projection"]> {
1928
+ const result = await this.adapter.query(`SELECT * FROM timeline_projection_state WHERE id = 'semantic' LIMIT 1`);
1929
+ const row = result.rows[0];
1930
+ return {
1931
+ version: typeof row?.projection_version === "string" ? row.projection_version : DELTA_SCHEMA_VERSION,
1932
+ lastOperationId: typeof row?.last_operation_id === "string" ? row.last_operation_id : undefined,
1933
+ lastRebuildAt: typeof row?.last_rebuild_at === "string" ? row.last_rebuild_at : undefined,
1934
+ };
1935
+ }
1936
+
1937
+ private async projectOperationToSemanticEvent(context: DeltaOperationContext): Promise<ProjectedTimelineEvent | undefined> {
1938
+ if (context.kind === "session.started" || context.kind === "session.ended") {
1939
+ return undefined;
1940
+ }
1941
+ const runtimeResult = await this.adapter.query(`SELECT * FROM runtime_calls WHERE operation_id = $1`, [context.id]);
1942
+ const proofResult = await this.adapter.query(`SELECT * FROM proofs WHERE operation_id = $1`, [context.id]);
1943
+ const artifactResult = await this.adapter.query(`SELECT * FROM artifacts WHERE operation_id = $1`, [context.id]);
1944
+ const fileResult = await this.adapter.query(`SELECT * FROM file_changes WHERE operation_id = $1`, [context.id]);
1945
+ const runtime = runtimeResult.rows[0];
1946
+ const proof = proofResult.rows[0];
1947
+ const eventKind = semanticEventKindForOperation(context, runtime, proof);
1948
+ if (!eventKind) {
1949
+ return undefined;
1950
+ }
1951
+ const title = semanticTitleForOperation(context, eventKind, runtime, proof);
1952
+ const severity = semanticSeverity(eventKind);
1953
+ const artifacts = summarizeTimelineArtifacts(artifactResult.rows.map(normalizeRow));
1954
+ const event: DeltaSemanticTimelineEvent = {
1955
+ id: deterministicTimelineId("tle", [context.id, eventKind]),
1956
+ operationId: context.id,
1957
+ sessionId: context.sessionId,
1958
+ timestamp: context.timestamp,
1959
+ kind: eventKind,
1960
+ title,
1961
+ summary: context.summary,
1962
+ severity,
1963
+ confidence: confidenceForSemanticEvent(context, eventKind),
1964
+ data: redactedTimelineData({
1965
+ operationKind: context.kind,
1966
+ ...context.data,
1967
+ runtime: runtime ? normalizeRow(runtime) : undefined,
1968
+ proof: proof ? normalizeRow(proof) : undefined,
1969
+ artifacts,
1970
+ }),
1971
+ entities: [],
1972
+ };
1973
+ const entities = timelineEntitiesFromContext(context, eventKind, runtimeResult.rows, proofResult.rows, fileResult.rows, artifactResult.rows)
1974
+ .map((entity, index) => ({
1975
+ ...entity,
1976
+ id: deterministicTimelineId("tlent", [event.id, entity.kind, entity.name, entity.role, String(index)]),
1977
+ eventId: event.id,
1978
+ }));
1979
+ event.entities = entities;
1980
+ return { event, entities };
1981
+ }
1982
+
1983
+ private async insertSemanticTimelineEvent(projected: ProjectedTimelineEvent): Promise<void> {
1984
+ const event = projected.event;
1985
+ await this.adapter.query(
1986
+ `INSERT INTO timeline_events (id, operation_id, session_id, change_id, timestamp, event_kind, title, summary, severity, confidence, data_json)
1987
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
1988
+ [
1989
+ event.id,
1990
+ event.operationId ?? null,
1991
+ event.sessionId ?? null,
1992
+ event.changeId ?? null,
1993
+ event.timestamp,
1994
+ event.kind,
1995
+ event.title,
1996
+ event.summary ?? null,
1997
+ event.severity ?? null,
1998
+ event.confidence,
1999
+ JSON.stringify(event.data),
2000
+ ],
2001
+ );
2002
+ for (const entity of projected.entities) {
2003
+ await this.adapter.query(
2004
+ `INSERT INTO timeline_entities (id, timeline_event_id, entity_kind, entity_name, role, confidence)
2005
+ VALUES ($1, $2, $3, $4, $5, $6)`,
2006
+ [entity.id, event.id, entity.kind, entity.name, entity.role, entity.confidence],
2007
+ );
2008
+ }
2009
+ }
2010
+
2011
+ private async insertSemanticTimelineEdges(projected: ProjectedTimelineEvent[]): Promise<void> {
2012
+ const edges: DeltaSemanticTimelineEdge[] = [];
2013
+ for (const denied of projected.filter((item) => item.event.kind === "denied" || item.event.kind === "diagnostic.emitted")) {
2014
+ const entry = denied.entities.find((entity) => entity.kind === "runtime-entry")?.name;
2015
+ const diagnostic = denied.entities.find((entity) => entity.kind === "diagnostic")?.name;
2016
+ const policy = denied.entities.find((entity) => entity.kind === "policy")?.name;
2017
+ if (!entry && !diagnostic) {
2018
+ continue;
2019
+ }
2020
+ const repair = projected.find((item) =>
2021
+ item.event.timestamp >= denied.event.timestamp &&
2022
+ item.event.kind === "policy.changed" &&
2023
+ (!policy || item.entities.some((entity) => entity.kind === "policy" && entity.name === policy)),
2024
+ );
2025
+ if (!repair) {
2026
+ continue;
2027
+ }
2028
+ edges.push({
2029
+ id: deterministicTimelineId("tledge", [denied.event.id, repair.event.id, "fixed"]),
2030
+ from: denied.event.id,
2031
+ to: repair.event.id,
2032
+ kind: "fixed",
2033
+ confidence: 0.82,
2034
+ reason: { diagnostic, entry, policy, rule: "diagnostic-to-policy-repair" },
2035
+ });
2036
+ const success = projected.find((item) =>
2037
+ item.event.timestamp >= repair.event.timestamp &&
2038
+ item.event.kind === "executed" &&
2039
+ (!entry || item.entities.some((entity) => entity.kind === "runtime-entry" && entity.name === entry)),
2040
+ );
2041
+ if (success) {
2042
+ edges.push({
2043
+ id: deterministicTimelineId("tledge", [repair.event.id, success.event.id, "validated"]),
2044
+ from: repair.event.id,
2045
+ to: success.event.id,
2046
+ kind: "validated",
2047
+ confidence: 0.86,
2048
+ reason: { diagnostic, entry, policy, rule: "repair-to-success" },
2049
+ });
2050
+ }
2051
+ }
2052
+ for (const proof of projected.filter((item) => item.event.kind === "proof.passed" || item.event.kind === "proof.failed")) {
2053
+ const previous = [...projected]
2054
+ .reverse()
2055
+ .find((item) =>
2056
+ item.event.timestamp < proof.event.timestamp &&
2057
+ item.event.kind !== "proof.passed" &&
2058
+ item.event.kind !== "proof.failed" &&
2059
+ hasSharedSemanticEntity(item.entities, proof.entities),
2060
+ );
2061
+ if (previous) {
2062
+ edges.push({
2063
+ id: deterministicTimelineId("tledge", [previous.event.id, proof.event.id, "validated"]),
2064
+ from: previous.event.id,
2065
+ to: proof.event.id,
2066
+ kind: proof.event.kind === "proof.passed" ? "validated" : "failed",
2067
+ confidence: 0.74,
2068
+ reason: { rule: "related-change-to-proof" },
2069
+ });
2070
+ }
2071
+ }
2072
+ for (const edge of uniqueEdges(edges)) {
2073
+ await this.adapter.query(
2074
+ `INSERT INTO timeline_edges (id, from_event_id, to_event_id, edge_kind, confidence, reason_json)
2075
+ VALUES ($1, $2, $3, $4, $5, $6)`,
2076
+ [edge.id, edge.from, edge.to, edge.kind, edge.confidence, JSON.stringify(edge.reason ?? {})],
2077
+ );
2078
+ }
2079
+ }
2080
+
2081
+ private async timelineEntitiesForEvent(eventId: string): Promise<DeltaSemanticTimelineEntity[]> {
2082
+ const result = await this.adapter.query(
2083
+ `SELECT * FROM timeline_entities WHERE timeline_event_id = $1 ORDER BY role, entity_kind, entity_name`,
2084
+ [eventId],
2085
+ );
2086
+ return result.rows.map(rowToSemanticTimelineEntity);
2087
+ }
2088
+
2089
+ private async timelineEdgesForEvents(eventIds: string[]): Promise<DeltaSemanticTimelineEdge[]> {
2090
+ const values = eventIds.map((_, index) => `$${index + 1}`).join(", ");
2091
+ const result = await this.adapter.query(
2092
+ `SELECT * FROM timeline_edges
2093
+ WHERE from_event_id IN (${values}) AND to_event_id IN (${values})
2094
+ ORDER BY edge_kind, id`,
2095
+ eventIds,
2096
+ );
2097
+ return result.rows.map(rowToSemanticTimelineEdge);
2098
+ }
2099
+
2100
+ private async timelineEdgesTouchingEvents(eventIds: string[]): Promise<DeltaSemanticTimelineEdge[]> {
2101
+ const values = eventIds.map((_, index) => `$${index + 1}`).join(", ");
2102
+ const result = await this.adapter.query(
2103
+ `SELECT * FROM timeline_edges
2104
+ WHERE from_event_id IN (${values}) OR to_event_id IN (${values})
2105
+ ORDER BY edge_kind, id`,
2106
+ eventIds,
2107
+ );
2108
+ return result.rows.map(rowToSemanticTimelineEdge);
2109
+ }
2110
+
2111
+ private async semanticEventsByIds(eventIds: string[]): Promise<DeltaSemanticTimelineEvent[]> {
2112
+ const values = eventIds.map((_, index) => `$${index + 1}`).join(", ");
2113
+ const result = await this.adapter.query(
2114
+ `SELECT * FROM timeline_events WHERE id IN (${values}) ORDER BY timestamp, id`,
2115
+ eventIds,
2116
+ );
2117
+ const events = result.rows.map(rowToSemanticTimelineEvent);
2118
+ for (const event of events) {
2119
+ event.entities = await this.timelineEntitiesForEvent(event.id);
2120
+ }
2121
+ return events;
2122
+ }
2123
+
2124
+ private async semanticCurrentState(
2125
+ entity: DeltaTimelineEntityRef | undefined,
2126
+ events: DeltaSemanticTimelineEvent[],
2127
+ edges: DeltaSemanticTimelineEdge[],
2128
+ ): Promise<Record<string, unknown>> {
2129
+ if (!entity) {
2130
+ return {
2131
+ eventCount: events.length,
2132
+ latestEventKind: events[events.length - 1]?.kind,
2133
+ };
2134
+ }
2135
+ if (entity.kind === "runtime-entry" || entity.kind === "agent-tool") {
2136
+ const runtime = await this.adapter.query(`SELECT * FROM runtime_calls WHERE entry_name = $1 ORDER BY operation_id DESC LIMIT 1`, [entity.name]);
2137
+ const row = runtime.rows[0];
2138
+ const latestRelevantChange = latestEventTimestamp(events, ["modified", "policy.changed", "imported", "generated"]);
2139
+ const latestProof = latestEventTimestamp(events, ["proof.passed"]);
2140
+ return {
2141
+ kind: row ? row.entry_kind : undefined,
2142
+ service: row ? row.service : undefined,
2143
+ language: row ? row.language : undefined,
2144
+ risk: row ? row.risk : undefined,
2145
+ policy: row ? row.policy : undefined,
2146
+ tenantScoped: row?.tenant_scoped === 1 || row?.tenant_scoped === true,
2147
+ needsApproval: row?.needs_approval === 1 || row?.needs_approval === true,
2148
+ lastResult: row ? row.result : undefined,
2149
+ lastDiagnostic: row ? row.diagnostic_code : undefined,
2150
+ proofStatus: latestProof && latestRelevantChange && Date.parse(latestRelevantChange) > Date.parse(latestProof) ? "stale" : latestProof ? "fresh" : "unknown",
2151
+ exportedToGit: events.some((event) => event.kind === "git.exported"),
2152
+ };
2153
+ }
2154
+ if (entity.kind === "policy") {
2155
+ const entries = await this.adapter.query(
2156
+ `SELECT DISTINCT entry_name FROM runtime_calls WHERE policy = $1 ORDER BY entry_name LIMIT 50`,
2157
+ [entity.name],
2158
+ );
2159
+ return {
2160
+ entries: entries.rows.map((row) => String(row.entry_name)),
2161
+ lastChangedAt: latestEventTimestamp(events, ["policy.changed"]),
2162
+ lastDenialAt: latestEventTimestamp(events, ["denied"]),
2163
+ resolved: edges.some((edge) => edge.kind === "validated" || edge.kind === "fixed"),
2164
+ };
2165
+ }
2166
+ if (entity.kind === "proof") {
2167
+ const latestProofResult = await this.adapter.query(
2168
+ `SELECT te.timestamp, te.event_kind
2169
+ FROM timeline_events te
2170
+ JOIN timeline_entities ten ON ten.timeline_event_id = te.id
2171
+ WHERE ten.entity_kind = 'proof' AND ten.entity_name = $1
2172
+ ORDER BY te.timestamp DESC, te.id DESC
2173
+ LIMIT 1`,
2174
+ [entity.name],
2175
+ );
2176
+ const latestProof = typeof latestProofResult.rows[0]?.timestamp === "string" ? latestProofResult.rows[0].timestamp : undefined;
2177
+ const latestProofKind = typeof latestProofResult.rows[0]?.event_kind === "string" ? latestProofResult.rows[0].event_kind : undefined;
2178
+ const latestChangeResult = await this.adapter.query(
2179
+ `SELECT timestamp FROM timeline_events
2180
+ WHERE event_kind IN ('modified', 'policy.changed', 'generated', 'imported')
2181
+ ORDER BY timestamp DESC, id DESC
2182
+ LIMIT 1`,
2183
+ );
2184
+ const latestChange = typeof latestChangeResult.rows[0]?.timestamp === "string" ? latestChangeResult.rows[0].timestamp : undefined;
2185
+ return {
2186
+ lastRunAt: latestProof,
2187
+ proofStatus: latestProof && latestChange && Date.parse(latestChange) > Date.parse(latestProof) ? "stale" : latestProof ? "fresh" : "unknown",
2188
+ lastResult: latestProofKind,
2189
+ };
2190
+ }
2191
+ if (entity.kind === "diagnostic") {
2192
+ return {
2193
+ occurrences: events.filter((event) => event.kind === "denied" || event.kind === "diagnostic.emitted").length,
2194
+ resolved: edges.some((edge) => edge.kind === "fixed" || edge.kind === "validated"),
2195
+ };
2196
+ }
2197
+ if (entity.kind === "external-service") {
2198
+ return {
2199
+ entries: uniqueStrings(events.flatMap((event) => event.entities.filter((item) => item.kind === "runtime-entry").map((item) => item.name))),
2200
+ lastFailureAt: latestEventTimestamp(events, ["failed", "denied"]),
2201
+ lastSuccessAt: latestEventTimestamp(events, ["executed"]),
2202
+ };
2203
+ }
2204
+ return {
2205
+ eventCount: events.length,
2206
+ latestEventKind: events[events.length - 1]?.kind,
2207
+ latestEventAt: events[events.length - 1]?.timestamp,
2208
+ };
2209
+ }
2210
+
2211
+ private async latestGitMapping(): Promise<Record<string, unknown> | null> {
2212
+ const result = await this.adapter.query(`SELECT * FROM git_mappings ORDER BY detected_at DESC LIMIT 1`);
2213
+ return result.rows[0] ? normalizeRow(result.rows[0]) : null;
2214
+ }
2215
+ }
2216
+
2217
+ export function getDeltaStorePath(workspaceRoot: string): string {
2218
+ return join(workspaceRoot, ".forge", "delta", "delta.db");
2219
+ }
2220
+
2221
+ interface ProjectedTimelineEvent {
2222
+ event: DeltaSemanticTimelineEvent;
2223
+ entities: DeltaSemanticTimelineEntity[];
2224
+ }
2225
+
2226
+ function semanticEventKindForOperation(
2227
+ context: DeltaOperationContext,
2228
+ runtime: Record<string, unknown> | undefined,
2229
+ proof: Record<string, unknown> | undefined,
2230
+ ): string | undefined {
2231
+ if (context.kind === "manifest.imported" || context.kind === "manifest.validated") {
2232
+ return "imported";
2233
+ }
2234
+ if (context.kind === "artifact.generated" || context.kind === "generate.completed") {
2235
+ return "generated";
2236
+ }
2237
+ if (context.kind === "proof.run" || proof) {
2238
+ const result = String(proof?.result ?? context.data.result ?? context.data.exitCode ?? "");
2239
+ return result === "passed" || result === "success" || result === "0" || result === "true" ? "proof.passed" : "proof.failed";
2240
+ }
2241
+ if (context.kind.startsWith("runtime.entry") || runtime) {
2242
+ const result = String(runtime?.result ?? context.data.result ?? context.kind);
2243
+ if (result === "denied" || context.kind.includes("denied")) {
2244
+ return "denied";
2245
+ }
2246
+ if (result === "failed" || result === "error" || context.kind.includes("failed")) {
2247
+ return "failed";
2248
+ }
2249
+ return "executed";
2250
+ }
2251
+ if (context.kind === "diagnostic.emitted" || context.diagnostics.length > 0) {
2252
+ return "diagnostic.emitted";
2253
+ }
2254
+ if (context.kind === "git.commit.detected" || context.kind === "git.mapping.detected") {
2255
+ return "git.exported";
2256
+ }
2257
+ if (context.kind.startsWith("agent.") || context.kind.startsWith("approval.")) {
2258
+ return context.kind;
2259
+ }
2260
+ if (context.kind.startsWith("file.")) {
2261
+ return context.fileClusters.includes("policy.change") ? "policy.changed" : "modified";
2262
+ }
2263
+ if (context.kind.startsWith("command.")) {
2264
+ return context.data.exitCode === 0 ? "executed" : "failed";
2265
+ }
2266
+ return undefined;
2267
+ }
2268
+
2269
+ function semanticTitleForOperation(
2270
+ context: DeltaOperationContext,
2271
+ eventKind: string,
2272
+ runtime: Record<string, unknown> | undefined,
2273
+ proof: Record<string, unknown> | undefined,
2274
+ ): string {
2275
+ const entry = context.entries[0] ?? stringOrUndefined(runtime?.entry_name);
2276
+ const file = context.files[0];
2277
+ const diagnostic = context.diagnostics[0] ?? stringOrUndefined(runtime?.diagnostic_code);
2278
+ const proofKind = context.proofs[0] ?? stringOrUndefined(proof?.proof_kind);
2279
+ const service = context.services[0];
2280
+ if (eventKind === "imported") {
2281
+ return service ? `Imported ${service}` : `Imported ${file ?? "manifest"}`;
2282
+ }
2283
+ if (eventKind === "generated") {
2284
+ return file ? `Generated ${file}` : "Generated artifacts";
2285
+ }
2286
+ if (eventKind === "denied") {
2287
+ return `${entry ?? "runtime entry"} denied${diagnostic ? `: ${diagnostic}` : ""}`;
2288
+ }
2289
+ if (eventKind === "executed") {
2290
+ return `${entry ?? context.commands[0] ?? "operation"} executed`;
2291
+ }
2292
+ if (eventKind === "failed") {
2293
+ return `${entry ?? context.commands[0] ?? "operation"} failed`;
2294
+ }
2295
+ if (eventKind === "policy.changed") {
2296
+ return context.data.policy ? `Policy ${String(context.data.policy)} changed` : `Policy source changed${file ? ` in ${file}` : ""}`;
2297
+ }
2298
+ if (eventKind === "proof.passed" || eventKind === "proof.failed") {
2299
+ return `${proofKind ?? "proof"} ${eventKind === "proof.passed" ? "passed" : "failed"}`;
2300
+ }
2301
+ if (eventKind === "diagnostic.emitted") {
2302
+ return `Diagnostic emitted${diagnostic ? `: ${diagnostic}` : ""}`;
2303
+ }
2304
+ if (eventKind === "git.exported") {
2305
+ return "Exported to Git";
2306
+ }
2307
+ if (eventKind === "agent.prompt.submitted") {
2308
+ return `${agentNameFromContext(context) ?? "Agent"} submitted a prompt`;
2309
+ }
2310
+ if (eventKind.startsWith("agent.tool")) {
2311
+ return `${agentNameFromContext(context) ?? "Agent"} ${String(context.data.toolName ?? context.commands[0] ?? "tool")} ${eventKind.split(".").pop()}`;
2312
+ }
2313
+ if (eventKind.startsWith("approval.")) {
2314
+ return `${agentNameFromContext(context) ?? "Agent"} approval ${eventKind.split(".").pop()}`;
2315
+ }
2316
+ if (eventKind.startsWith("agent.")) {
2317
+ return `${agentNameFromContext(context) ?? "Agent"} ${eventKind.replace(/^agent\./, "").replace(/\./g, " ")}`;
2318
+ }
2319
+ return context.summary ?? context.kind;
2320
+ }
2321
+
2322
+ function semanticSeverity(eventKind: string): string {
2323
+ if (eventKind === "failed" || eventKind === "denied" || eventKind === "proof.failed" || eventKind.endsWith(".failed") || eventKind.endsWith(".denied")) {
2324
+ return "error";
2325
+ }
2326
+ if (eventKind === "proof.passed" || eventKind === "executed" || eventKind.endsWith(".completed")) {
2327
+ return "success";
2328
+ }
2329
+ if (eventKind === "policy.changed" || eventKind === "dependency.added" || eventKind === "dependency.upgraded") {
2330
+ return "warning";
2331
+ }
2332
+ return "info";
2333
+ }
2334
+
2335
+ function confidenceForSemanticEvent(context: DeltaOperationContext, eventKind: string): number {
2336
+ if (eventKind === "modified" && context.fileClusters.some((cluster) => cluster.startsWith("file."))) {
2337
+ return 0.72;
2338
+ }
2339
+ if (eventKind === "policy.changed" && !context.data.policy) {
2340
+ return 0.78;
2341
+ }
2342
+ if (context.kind.startsWith("agent.") || context.kind.startsWith("approval.")) {
2343
+ const capture = context.data.capture && typeof context.data.capture === "object"
2344
+ ? context.data.capture as Record<string, unknown>
2345
+ : {};
2346
+ return typeof capture.confidence === "number" ? capture.confidence : 0.86;
2347
+ }
2348
+ return 0.95;
2349
+ }
2350
+
2351
+ function timelineEntitiesFromContext(
2352
+ context: DeltaOperationContext,
2353
+ eventKind: string,
2354
+ runtimeRows: Record<string, unknown>[],
2355
+ proofRows: Record<string, unknown>[],
2356
+ fileRows: Record<string, unknown>[],
2357
+ artifactRows: Record<string, unknown>[],
2358
+ ): Array<Omit<DeltaSemanticTimelineEntity, "id" | "eventId">> {
2359
+ const entities: Array<Omit<DeltaSemanticTimelineEntity, "id" | "eventId">> = [];
2360
+ const add = (kind: string, name: unknown, role: string, confidence = 0.9) => {
2361
+ if (typeof name !== "string" || name.length === 0) {
2362
+ return;
2363
+ }
2364
+ const normalizedName = kind === "file" || kind === "manifest" ? normalizePath(name) : name;
2365
+ if (!entities.some((entity) => entity.kind === kind && entity.name === normalizedName && entity.role === role)) {
2366
+ entities.push({ kind, name: normalizedName, role, confidence });
2367
+ }
2368
+ };
2369
+ for (const entry of context.entries) {
2370
+ add("runtime-entry", entry, eventKind === "executed" || eventKind === "denied" || eventKind === "failed" ? "primary" : "affected", 0.95);
2371
+ add("agent-tool", entry, "affected", 0.7);
2372
+ }
2373
+ add("agent", agentNameFromContext(context), "source", 0.95);
2374
+ add("agent-tool", context.data.toolName, eventKind.startsWith("agent.tool") ? "primary" : "affected", 0.95);
2375
+ for (const service of context.services) {
2376
+ add("external-service", service, eventKind === "imported" ? "primary" : "source", 0.88);
2377
+ }
2378
+ for (const file of context.files) {
2379
+ add(file.endsWith(".manifest.json") || file === "forge.manifest.json" ? "manifest" : "file", file, eventKind === "generated" ? "generated" : "affected", 0.9);
2380
+ }
2381
+ for (const file of fileRows) {
2382
+ add("file", file.path, eventKind === "policy.changed" ? "source" : "affected", 0.95);
2383
+ for (const hint of parseSemanticHints(file.semantic_hints_json)) {
2384
+ if (hint.kind === "dependency.change") {
2385
+ add("dependency", stringOrUndefined(context.data.dependency) ?? stringOrUndefined(context.data.packageName), "affected", 0.65);
2386
+ }
2387
+ if (hint.kind === "policy.change") {
2388
+ add("policy", context.data.policy, eventKind === "policy.changed" ? "primary" : "affected", context.data.policy ? 0.86 : 0.55);
2389
+ }
2390
+ }
2391
+ }
2392
+ for (const artifact of artifactRows) {
2393
+ add("file", artifact.path, "generated", 0.86);
2394
+ }
2395
+ for (const runtime of runtimeRows) {
2396
+ add("runtime-entry", runtime.entry_name, "primary", 0.98);
2397
+ add("policy", runtime.policy, eventKind === "denied" ? "requires" : "affected", 0.9);
2398
+ add("diagnostic", runtime.diagnostic_code, eventKind === "denied" ? "failed" : "affected", 0.94);
2399
+ add("external-service", runtime.service, "source", 0.9);
2400
+ }
2401
+ for (const diagnostic of context.diagnostics) {
2402
+ add("diagnostic", diagnostic, eventKind === "denied" || eventKind === "failed" ? "failed" : "affected", 0.9);
2403
+ }
2404
+ for (const proof of proofRows) {
2405
+ add("proof", proof.proof_kind, eventKind === "proof.passed" ? "validated" : "failed", 0.98);
2406
+ for (const path of arrayOfStrings(parseJsonUnknown(proof.artifact_paths_json))) {
2407
+ add("file", path, "validated", 0.75);
2408
+ }
2409
+ }
2410
+ for (const proof of context.proofs) {
2411
+ add("proof", proof, eventKind === "proof.passed" ? "validated" : "failed", 0.9);
2412
+ }
2413
+ if (context.sessionId) {
2414
+ add("session", context.sessionId, "source", 0.75);
2415
+ }
2416
+ if (context.data.commitSha) {
2417
+ add("git-commit", context.data.commitSha, "exported", 0.85);
2418
+ }
2419
+ return entities.length > 0
2420
+ ? entities
2421
+ : [{ kind: "session", name: context.sessionId ?? context.id, role: "source", confidence: 0.5 }];
2422
+ }
2423
+
2424
+ function agentNameFromContext(context: DeltaOperationContext): string | undefined {
2425
+ const source = context.data.source;
2426
+ if (source && typeof source === "object" && !Array.isArray(source)) {
2427
+ const agent = (source as Record<string, unknown>).agent;
2428
+ return typeof agent === "string" && agent.length > 0 ? agent : undefined;
2429
+ }
2430
+ return undefined;
2431
+ }
2432
+
2433
+ function parseTimelineEntityTarget(target: string | undefined): DeltaTimelineEntityRef | undefined {
2434
+ if (!target) {
2435
+ return undefined;
2436
+ }
2437
+ const [prefix, ...tail] = target.split(":");
2438
+ if (tail.length > 0) {
2439
+ const name = tail.join(":");
2440
+ const kind = timelineEntityKindFromPrefix(prefix);
2441
+ return { kind, name: kind === "file" || kind === "manifest" ? normalizePath(name) : name };
2442
+ }
2443
+ if (target.includes("/") || target.startsWith(".") || hasKnownFileExtension(target)) {
2444
+ return { kind: "file", name: normalizePath(target) };
2445
+ }
2446
+ if (/^[A-Z0-9_]+$/.test(target)) {
2447
+ return { kind: "diagnostic", name: target };
2448
+ }
2449
+ return { kind: "runtime-entry", name: target };
2450
+ }
2451
+
2452
+ function hasKnownFileExtension(target: string): boolean {
2453
+ return /\.(ts|tsx|js|jsx|mjs|cjs|json|md|mdx|sql|css|html|yml|yaml|toml|lock)$/i.test(target);
2454
+ }
2455
+
2456
+ function timelineEntityKindFromPrefix(prefix: string): string {
2457
+ switch (prefix) {
2458
+ case "entry":
2459
+ case "runtime":
2460
+ return "runtime-entry";
2461
+ case "tool":
2462
+ return "agent-tool";
2463
+ case "service":
2464
+ return "external-service";
2465
+ case "commit":
2466
+ case "git":
2467
+ return "git-commit";
2468
+ default:
2469
+ return prefix;
2470
+ }
2471
+ }
2472
+
2473
+ function normalizeSemanticKindFilter(kind: string): string[] {
2474
+ if (kind === "proof.run") {
2475
+ return ["proof.passed", "proof.failed"];
2476
+ }
2477
+ if (kind === "runtime.entry.executed") {
2478
+ return ["executed"];
2479
+ }
2480
+ if (kind === "runtime.entry.denied") {
2481
+ return ["denied"];
2482
+ }
2483
+ if (kind === "file.changed") {
2484
+ return ["modified", "policy.changed"];
2485
+ }
2486
+ return [kind];
2487
+ }
2488
+
2489
+ function deterministicTimelineId(prefix: string, parts: string[]): string {
2490
+ return `${prefix}_${hashStable(parts.join("\0")).slice(0, 24)}`;
2491
+ }
2492
+
2493
+ function summarizeTimelineArtifacts(artifacts: Record<string, unknown>[]): Record<string, unknown> | undefined {
2494
+ if (artifacts.length === 0) {
2495
+ return undefined;
2496
+ }
2497
+ const sample = artifacts.slice(0, 10).map((artifact) => ({
2498
+ path: artifact.path,
2499
+ artifactKind: artifact.artifact_kind,
2500
+ hash: artifact.hash,
2501
+ generated: artifact.generated,
2502
+ }));
2503
+ return {
2504
+ count: artifacts.length,
2505
+ hash: hashStable(JSON.stringify(artifacts.map((artifact) => ({
2506
+ path: artifact.path,
2507
+ artifactKind: artifact.artifact_kind,
2508
+ hash: artifact.hash,
2509
+ generated: artifact.generated,
2510
+ })))),
2511
+ sample,
2512
+ omitted: Math.max(0, artifacts.length - sample.length),
2513
+ };
2514
+ }
2515
+
2516
+ function redactedTimelineData(data: Record<string, unknown>): Record<string, unknown> {
2517
+ const cleaned = Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined));
2518
+ return redactDeltaPayload(cleaned).value;
2519
+ }
2520
+
2521
+ function rowToSemanticTimelineEvent(row: Record<string, unknown>): DeltaSemanticTimelineEvent {
2522
+ return {
2523
+ id: String(row.id),
2524
+ operationId: typeof row.operation_id === "string" ? row.operation_id : undefined,
2525
+ sessionId: typeof row.session_id === "string" ? row.session_id : undefined,
2526
+ changeId: typeof row.change_id === "string" ? row.change_id : undefined,
2527
+ timestamp: String(row.timestamp),
2528
+ kind: String(row.event_kind),
2529
+ title: String(row.title),
2530
+ summary: typeof row.summary === "string" ? row.summary : undefined,
2531
+ severity: typeof row.severity === "string" ? row.severity : undefined,
2532
+ confidence: Number(row.confidence ?? 0),
2533
+ data: parseJsonRecord(row.data_json),
2534
+ entities: [],
2535
+ };
2536
+ }
2537
+
2538
+ function rowToSemanticTimelineEntity(row: Record<string, unknown>): DeltaSemanticTimelineEntity {
2539
+ return {
2540
+ id: String(row.id),
2541
+ eventId: String(row.timeline_event_id),
2542
+ kind: String(row.entity_kind),
2543
+ name: String(row.entity_name),
2544
+ role: String(row.role),
2545
+ confidence: Number(row.confidence ?? 0),
2546
+ };
2547
+ }
2548
+
2549
+ function rowToSemanticTimelineEdge(row: Record<string, unknown>): DeltaSemanticTimelineEdge {
2550
+ return {
2551
+ id: String(row.id),
2552
+ from: String(row.from_event_id),
2553
+ to: String(row.to_event_id),
2554
+ kind: String(row.edge_kind),
2555
+ confidence: Number(row.confidence ?? 0),
2556
+ reason: parseJsonRecord(row.reason_json),
2557
+ };
2558
+ }
2559
+
2560
+ function hasSharedSemanticEntity(left: DeltaSemanticTimelineEntity[], right: DeltaSemanticTimelineEntity[]): boolean {
2561
+ return left.some((leftEntity) =>
2562
+ right.some((rightEntity) =>
2563
+ leftEntity.kind === rightEntity.kind &&
2564
+ leftEntity.name === rightEntity.name &&
2565
+ leftEntity.kind !== "session",
2566
+ ),
2567
+ );
2568
+ }
2569
+
2570
+ function uniqueEdges(edges: DeltaSemanticTimelineEdge[]): DeltaSemanticTimelineEdge[] {
2571
+ const seen = new Set<string>();
2572
+ return edges.filter((edge) => {
2573
+ if (seen.has(edge.id)) {
2574
+ return false;
2575
+ }
2576
+ seen.add(edge.id);
2577
+ return true;
2578
+ });
2579
+ }
2580
+
2581
+ function latestEventTimestamp(events: DeltaSemanticTimelineEvent[], kinds: string[]): string | undefined {
2582
+ return [...events].reverse().find((event) => kinds.includes(event.kind))?.timestamp;
2583
+ }
2584
+
2585
+ function semanticOpenQuestions(
2586
+ entity: DeltaTimelineEntityRef | undefined,
2587
+ currentState: Record<string, unknown>,
2588
+ events: DeltaSemanticTimelineEvent[],
2589
+ ): string[] {
2590
+ const questions: string[] = [];
2591
+ if (entity && events.length === 0) {
2592
+ questions.push(`No semantic history found for ${entity.kind}:${entity.name}`);
2593
+ }
2594
+ if (entity?.kind === "runtime-entry" && currentState.exportedToGit === false) {
2595
+ questions.push("No Git export linked yet");
2596
+ }
2597
+ if (currentState.proofStatus === "stale") {
2598
+ questions.push("Proof is stale after the latest relevant change");
2599
+ }
2600
+ return questions;
2601
+ }
2602
+
2603
+ function shouldInferWorkSession(context: DeltaOperationContext): boolean {
2604
+ return !context.kind.startsWith("session.") && context.kind !== "git.mapping.detected";
2605
+ }
2606
+
2607
+ function scoreWorkSessionCandidate(
2608
+ context: DeltaOperationContext,
2609
+ candidate: DeltaWorkSessionSummary,
2610
+ ): { score: number; signals: DeltaWorkSessionSignal[] } {
2611
+ const signals: DeltaWorkSessionSignal[] = [];
2612
+ const metadata = candidate.metadata;
2613
+ addSignalIfOverlap(signals, "sameTraceId", 0.4, context.traces, metadata.traces);
2614
+ addSignalIfOverlap(signals, "sameManifestService", 0.35, context.services, metadata.services);
2615
+ addSignalIfOverlap(signals, "sameRuntimeEntry", 0.3, context.entries, metadata.entries);
2616
+ if (isDiagnosticRepairChain(context, metadata)) {
2617
+ signals.push({ signal: "diagnostic-repair", weight: 0.3, value: context.diagnostics[0] ?? metadata.diagnostics[0] });
2618
+ }
2619
+ if (context.proofs.length > 0 && hasAnyOverlap(context.entries, metadata.entries, context.files, metadata.files, context.services, metadata.services)) {
2620
+ signals.push({ signal: "proof-after-related-change", weight: 0.25, value: context.proofs[0] });
2621
+ }
2622
+ addSignalIfOverlap(signals, "sameFileCluster", 0.2, context.fileClusters, metadata.fileClusters);
2623
+ if (context.branch && candidate.gitBranch && context.branch === candidate.gitBranch) {
2624
+ signals.push({ signal: "same-branch", weight: 0.15, value: context.branch });
2625
+ } else if (context.branch && candidate.gitBranch && context.branch !== candidate.gitBranch) {
2626
+ signals.push({ signal: "branch-changed", weight: -0.3, value: `${candidate.gitBranch} -> ${context.branch}` });
2627
+ }
2628
+ if (context.actorId && metadata.actorIds.includes(context.actorId)) {
2629
+ signals.push({ signal: "same-actor", weight: 0.1, value: context.actorId });
2630
+ }
2631
+ const gapMinutes = metadata.lastOperationAt
2632
+ ? Math.abs(Date.parse(context.timestamp) - Date.parse(metadata.lastOperationAt)) / 60_000
2633
+ : Number.POSITIVE_INFINITY;
2634
+ if (gapMinutes < 10) {
2635
+ signals.push({ signal: "time-proximity", weight: 0.15, value: "<10m" });
2636
+ } else if (gapMinutes < 30) {
2637
+ signals.push({ signal: "time-proximity", weight: 0.1, value: "<30m" });
2638
+ } else if (gapMinutes > 120) {
2639
+ signals.push({ signal: "time-gap", weight: -0.3, value: ">2h" });
2640
+ }
2641
+ if (
2642
+ context.fileClusters.length > 0 &&
2643
+ metadata.fileClusters.length > 0 &&
2644
+ !intersects(context.fileClusters, metadata.fileClusters) &&
2645
+ !intersects(context.entries, metadata.entries) &&
2646
+ !intersects(context.services, metadata.services) &&
2647
+ !intersects(context.traces, metadata.traces)
2648
+ ) {
2649
+ signals.push({ signal: "unrelated-file-cluster", weight: -0.2, value: context.fileClusters[0] });
2650
+ }
2651
+ const score = roundConfidence(Math.max(0, Math.min(1, signals.reduce((total, signal) => total + signal.weight, 0))));
2652
+ return { score, signals };
2653
+ }
2654
+
2655
+ function seedSignalsForContext(context: DeltaOperationContext): DeltaWorkSessionSignal[] {
2656
+ const signals: DeltaWorkSessionSignal[] = [{ signal: "session-seed", weight: 0.5, value: context.kind }];
2657
+ if (context.kind === "manifest.imported") {
2658
+ signals.push({ signal: "manifest-import-chain", weight: 0.35, value: context.services[0] ?? context.summary });
2659
+ }
2660
+ if (context.entries[0]) {
2661
+ signals.push({ signal: "sameRuntimeEntry", weight: 0.3, value: context.entries[0] });
2662
+ }
2663
+ if (context.fileClusters[0]) {
2664
+ signals.push({ signal: "sameFileCluster", weight: 0.2, value: context.fileClusters[0] });
2665
+ }
2666
+ if (context.branch) {
2667
+ signals.push({ signal: "same-branch", weight: 0.15, value: context.branch });
2668
+ }
2669
+ return signals;
2670
+ }
2671
+
2672
+ function addSignalIfOverlap(
2673
+ signals: DeltaWorkSessionSignal[],
2674
+ signal: string,
2675
+ weight: number,
2676
+ left: string[],
2677
+ right: string[],
2678
+ ): void {
2679
+ const value = left.find((item) => right.includes(item));
2680
+ if (value) {
2681
+ signals.push({ signal, weight, value });
2682
+ }
2683
+ }
2684
+
2685
+ function contextToWorkSessionMetadata(context: DeltaOperationContext): DeltaWorkSessionMetadata {
2686
+ return {
2687
+ files: context.files,
2688
+ fileClusters: context.fileClusters,
2689
+ entries: context.entries,
2690
+ diagnostics: context.diagnostics,
2691
+ proofs: context.proofs,
2692
+ services: context.services,
2693
+ traces: context.traces,
2694
+ commands: context.commands,
2695
+ operationKinds: [context.kind],
2696
+ actorIds: uniqueStrings([context.actorId]),
2697
+ lastOperationAt: context.timestamp,
2698
+ };
2699
+ }
2700
+
2701
+ function emptyWorkSessionMetadata(): DeltaWorkSessionMetadata {
2702
+ return {
2703
+ files: [],
2704
+ fileClusters: [],
2705
+ entries: [],
2706
+ diagnostics: [],
2707
+ proofs: [],
2708
+ services: [],
2709
+ traces: [],
2710
+ commands: [],
2711
+ operationKinds: [],
2712
+ actorIds: [],
2713
+ };
2714
+ }
2715
+
2716
+ function mergeWorkSessionMetadata(left: DeltaWorkSessionMetadata, right: DeltaWorkSessionMetadata): DeltaWorkSessionMetadata {
2717
+ return {
2718
+ files: capStrings(uniqueStrings([...left.files, ...right.files]), 50),
2719
+ fileClusters: capStrings(uniqueStrings([...left.fileClusters, ...right.fileClusters]), 30),
2720
+ entries: capStrings(uniqueStrings([...left.entries, ...right.entries]), 50),
2721
+ diagnostics: capStrings(uniqueStrings([...left.diagnostics, ...right.diagnostics]), 30),
2722
+ proofs: capStrings(uniqueStrings([...left.proofs, ...right.proofs]), 30),
2723
+ services: capStrings(uniqueStrings([...left.services, ...right.services]), 30),
2724
+ traces: capStrings(uniqueStrings([...left.traces, ...right.traces]), 30),
2725
+ commands: capStrings(uniqueStrings([...left.commands, ...right.commands]), 30),
2726
+ operationKinds: capStrings(uniqueStrings([...left.operationKinds, ...right.operationKinds]), 50),
2727
+ actorIds: capStrings(uniqueStrings([...left.actorIds, ...right.actorIds]), 30),
2728
+ lastOperationAt: maxIso(left.lastOperationAt, right.lastOperationAt),
2729
+ mergedFrom: capStrings(uniqueStrings([...(left.mergedFrom ?? []), ...(right.mergedFrom ?? [])]), 20),
2730
+ splitFrom: left.splitFrom ?? right.splitFrom,
2731
+ manualTitle: left.manualTitle || right.manualTitle || undefined,
2732
+ };
2733
+ }
2734
+
2735
+ function normalizeWorkSessionMetadata(value: Record<string, unknown>): DeltaWorkSessionMetadata {
2736
+ return {
2737
+ files: arrayOfStrings(value.files),
2738
+ fileClusters: arrayOfStrings(value.fileClusters),
2739
+ entries: arrayOfStrings(value.entries),
2740
+ diagnostics: arrayOfStrings(value.diagnostics),
2741
+ proofs: arrayOfStrings(value.proofs),
2742
+ services: arrayOfStrings(value.services),
2743
+ traces: arrayOfStrings(value.traces),
2744
+ commands: arrayOfStrings(value.commands),
2745
+ operationKinds: arrayOfStrings(value.operationKinds),
2746
+ actorIds: arrayOfStrings(value.actorIds),
2747
+ lastOperationAt: stringOrUndefined(value.lastOperationAt),
2748
+ mergedFrom: arrayOfStrings(value.mergedFrom),
2749
+ splitFrom: stringOrUndefined(value.splitFrom),
2750
+ manualTitle: value.manualTitle === true,
2751
+ };
2752
+ }
2753
+
2754
+ function inferWorkSessionTitle(context: DeltaOperationContext, metadata: DeltaWorkSessionMetadata): string {
2755
+ if (context.kind === "manifest.imported" && metadata.services[0]) {
2756
+ return `Import ${metadata.services[0]} external service`;
2757
+ }
2758
+ if (metadata.diagnostics[0] && metadata.entries[0]) {
2759
+ return `Fix ${metadata.diagnostics[0]} for ${metadata.entries[0]}`;
2760
+ }
2761
+ if (metadata.entries[0]) {
2762
+ return `${context.kind.includes("failed") ? "Repair" : "Update"} ${metadata.entries[0]}`;
2763
+ }
2764
+ if (metadata.proofs[0]) {
2765
+ return `Validate ${metadata.proofs[0]}`;
2766
+ }
2767
+ if (metadata.fileClusters.includes("policy.change")) {
2768
+ return "Update policies";
2769
+ }
2770
+ if (metadata.files.length > 1) {
2771
+ return "Update files and artifacts";
2772
+ }
2773
+ if (metadata.files[0]) {
2774
+ return `Update ${metadata.files[0]}`;
2775
+ }
2776
+ if (metadata.commands[0]) {
2777
+ return metadata.commands[0];
2778
+ }
2779
+ return `Work session ${context.timestamp.slice(0, 16).replace("T", " ")}`;
2780
+ }
2781
+
2782
+ function inferIntent(context: DeltaOperationContext, metadata: DeltaWorkSessionMetadata): string {
2783
+ if (context.kind === "manifest.imported" || metadata.fileClusters.includes("manifest.change")) {
2784
+ return "external-runtime-import";
2785
+ }
2786
+ if (metadata.diagnostics.length > 0) {
2787
+ return "diagnostic-repair";
2788
+ }
2789
+ if (metadata.proofs.length > 0) {
2790
+ return "proof-validation";
2791
+ }
2792
+ if (metadata.entries.length > 0) {
2793
+ return "runtime-entry-work";
2794
+ }
2795
+ if (metadata.fileClusters.some((cluster) => cluster.endsWith(".change"))) {
2796
+ return "source-change";
2797
+ }
2798
+ return "general-work";
2799
+ }
2800
+
2801
+ function summarizeWorkSession(metadata: DeltaWorkSessionMetadata): string {
2802
+ const parts: string[] = [];
2803
+ if (metadata.services[0]) {
2804
+ parts.push(`worked on ${metadata.services[0]}`);
2805
+ }
2806
+ if (metadata.entries.length > 0) {
2807
+ parts.push(`touched ${metadata.entries.slice(0, 3).join(", ")}`);
2808
+ }
2809
+ if (metadata.files.length > 0) {
2810
+ parts.push(`changed ${metadata.files.slice(0, 3).join(", ")}`);
2811
+ }
2812
+ if (metadata.diagnostics.length > 0) {
2813
+ parts.push(`observed ${metadata.diagnostics.slice(0, 3).join(", ")}`);
2814
+ }
2815
+ if (metadata.proofs.length > 0) {
2816
+ parts.push(`ran ${metadata.proofs.slice(0, 3).join(", ")}`);
2817
+ }
2818
+ return parts.length === 0 ? "Recorded a Forge work session." : `Session ${parts.join("; ")}.`;
2819
+ }
2820
+
2821
+ function initialWorkSessionConfidence(context: DeltaOperationContext): number {
2822
+ if (context.kind === "manifest.imported") {
2823
+ return 0.78;
2824
+ }
2825
+ if (context.entries.length > 0) {
2826
+ return 0.72;
2827
+ }
2828
+ if (context.proofs.length > 0 || context.kind === "proof.run") {
2829
+ return 0.7;
2830
+ }
2831
+ if (context.files.length > 1) {
2832
+ return 0.68;
2833
+ }
2834
+ if (context.files.length > 0) {
2835
+ return 0.62;
2836
+ }
2837
+ return 0.55;
2838
+ }
2839
+
2840
+ function isDiagnosticRepairChain(context: DeltaOperationContext, metadata: DeltaWorkSessionMetadata): boolean {
2841
+ return (
2842
+ (context.diagnostics.length > 0 && metadata.fileClusters.some((cluster) => cluster.includes("policy") || cluster.includes("command") || cluster.includes("query"))) ||
2843
+ (metadata.diagnostics.length > 0 && context.fileClusters.some((cluster) => cluster.includes("policy") || cluster.includes("command") || cluster.includes("query")))
2844
+ );
2845
+ }
2846
+
2847
+ function hasAnyOverlap(...groups: string[][]): boolean {
2848
+ for (let index = 0; index < groups.length; index += 2) {
2849
+ if (intersects(groups[index] ?? [], groups[index + 1] ?? [])) {
2850
+ return true;
2851
+ }
2852
+ }
2853
+ return false;
2854
+ }
2855
+
2856
+ function intersects(left: string[], right: string[]): boolean {
2857
+ return left.some((item) => right.includes(item));
2858
+ }
2859
+
2860
+ function uniqueStrings(values: unknown[]): string[] {
2861
+ return [...new Set(values.filter((value): value is string => typeof value === "string" && value.length > 0))];
2862
+ }
2863
+
2864
+ function capStrings(values: string[], limit: number): string[] {
2865
+ return values.slice(0, limit);
2866
+ }
2867
+
2868
+ function arrayOfStrings(value: unknown): string[] {
2869
+ return Array.isArray(value) ? uniqueStrings(value) : [];
2870
+ }
2871
+
2872
+ function maxIso(left?: string, right?: string): string | undefined {
2873
+ if (!left) {
2874
+ return right;
2875
+ }
2876
+ if (!right) {
2877
+ return left;
2878
+ }
2879
+ return Date.parse(left) >= Date.parse(right) ? left : right;
2880
+ }
2881
+
2882
+ function stringOrUndefined(value: unknown): string | undefined {
2883
+ return typeof value === "string" && value.length > 0 ? value : undefined;
2884
+ }
2885
+
2886
+ function roundConfidence(value: number): number {
2887
+ return Math.round(value * 100) / 100;
2888
+ }
2889
+
2890
+ function parseSemanticHints(value: unknown): DeltaSemanticHint[] {
2891
+ const parsed = typeof value === "string" ? safeJson<unknown>(value || "[]", []) : value;
2892
+ return Array.isArray(parsed)
2893
+ ? parsed.filter((item): item is DeltaSemanticHint => Boolean(item) && typeof item === "object" && "kind" in item)
2894
+ : [];
2895
+ }
2896
+
2897
+ function diagnosticCodesFromJson(value: unknown): string[] {
2898
+ const parsed = typeof value === "string" ? safeJson(value, []) : value;
2899
+ if (!Array.isArray(parsed)) {
2900
+ return [];
2901
+ }
2902
+ return uniqueStrings(parsed.map((item) => item && typeof item === "object" && "code" in item ? (item as { code?: unknown }).code : undefined));
2903
+ }
2904
+
2905
+ function safeJson<T>(value: string, fallback: T): T {
2906
+ try {
2907
+ return JSON.parse(value) as T;
2908
+ } catch {
2909
+ return fallback;
2910
+ }
2911
+ }
2912
+
2913
+ function parseJsonUnknown(value: unknown): unknown {
2914
+ if (typeof value !== "string") {
2915
+ return value;
2916
+ }
2917
+ try {
2918
+ return JSON.parse(value) as unknown;
2919
+ } catch {
2920
+ return undefined;
2921
+ }
2922
+ }
2923
+
2924
+ function clusterForPath(path: string): string {
2925
+ return classifyDeltaPath(path)[0]?.kind ?? "file.unknown";
2926
+ }
2927
+
2928
+ function serviceFromManifestPath(path: string): string | undefined {
2929
+ const normalized = normalizePath(path);
2930
+ const fileName = normalized.split("/").pop() ?? "";
2931
+ return fileName.endsWith(".manifest.json") ? fileName.replace(/\.manifest\.json$/, "") : undefined;
2932
+ }
2933
+
2934
+ function normalizeWorkSessionKind(value: unknown): DeltaWorkSessionKind {
2935
+ return value === "agent" || value === "human" || value === "ci" || value === "git" || value === "manual-corrected" ? value : "auto";
2936
+ }
2937
+
2938
+ function normalizeWorkSessionStatus(value: unknown): DeltaWorkSessionStatus {
2939
+ return value === "idle" || value === "closed" || value === "merged" || value === "split" || value === "needs-review" ? value : "open";
2940
+ }
2941
+
2942
+ function rowToTimelineEntry(row: Record<string, unknown>): DeltaTimelineEntry {
2943
+ return {
2944
+ id: String(row.id),
2945
+ kind: String(row.kind),
2946
+ timestamp: String(row.timestamp),
2947
+ summary: typeof row.summary === "string" ? row.summary : undefined,
2948
+ data: parseJsonRecord(row.data_json),
2949
+ };
2950
+ }
2951
+
2952
+ function parseJsonRecord(value: unknown): Record<string, unknown> {
2953
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2954
+ return value as Record<string, unknown>;
2955
+ }
2956
+ if (typeof value !== "string") {
2957
+ return {};
2958
+ }
2959
+ try {
2960
+ return JSON.parse(value) as Record<string, unknown>;
2961
+ } catch {
2962
+ return {};
2963
+ }
2964
+ }
2965
+
2966
+ function normalizeRow(row: Record<string, unknown>): Record<string, unknown> {
2967
+ return Object.fromEntries(
2968
+ Object.entries(row).map(([key, value]) => [
2969
+ key,
2970
+ typeof value === "string" && (key.endsWith("_json") || key === "data_json")
2971
+ ? parseJsonRecord(value)
2972
+ : value,
2973
+ ]),
2974
+ );
2975
+ }