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.
- package/.npmignore +4 -0
- package/AGENTS.md +168 -81
- package/CHANGELOG.md +199 -0
- package/README.md +88 -14
- package/adapters/go/README.md +23 -0
- package/adapters/go/go.mod +3 -0
- package/adapters/go/http.go +149 -0
- package/adapters/go/registry.go +234 -0
- package/adapters/go/types.go +136 -0
- package/adapters/java/README.md +68 -0
- package/adapters/java/pom.xml +34 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Auth.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Diagnostic.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Entry.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/EntryKind.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ErrorInfo.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Forge.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeCall.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeContext.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHandler.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHttpHandler.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$EntryOption.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegisteredEntry.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegistryOption.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Json.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Manifest.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/RequestEnvelope.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ResponseEnvelope.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Risk.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Schemas.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Service.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/TransactionMode.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/TypedForgeHandler.class +0 -0
- package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
- package/adapters/java/target/maven-archiver/pom.properties +3 -0
- package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +23 -0
- package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +20 -0
- package/adapters/java-spring-boot-starter/README.md +32 -0
- package/adapters/java-spring-boot-starter/pom.xml +36 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
- package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
- package/adapters/java-spring-boot-starter/target/classes/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeCommand.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeExternalService.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeQuery.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringRuntime.class +0 -0
- package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
- package/adapters/java-spring-boot-starter/target/maven-archiver/pom.properties +3 -0
- package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +6 -0
- package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +6 -0
- package/bin/forge.mjs +18 -0
- package/docs/changelog.md +212 -0
- package/docs/forge-protocol.md +189 -0
- package/examples/go-billing/go.mod +7 -0
- package/examples/go-billing/main.go +120 -0
- package/examples/java-billing/pom.xml +52 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/CreateInvoiceInput.class +0 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Invoice.class +0 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$EmptyInput.class +0 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$Options.class +0 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main.class +0 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
- package/examples/java-billing/target/maven-archiver/pom.properties +3 -0
- package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +5 -0
- package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +3 -0
- package/package.json +29 -7
- package/schemas/forge-manifest.schema.json +57 -0
- package/src/forge/_generated/releaseManifest.json +1 -2
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/index.ts +1511 -123
- package/src/forge/agent-adapters/types.ts +216 -1
- package/src/forge/agent-memory/bridge.ts +1192 -0
- package/src/forge/agent-memory/context-pack.ts +151 -0
- package/src/forge/agent-memory/hook-runner.ts +312 -0
- package/src/forge/agent-memory/mcp.ts +224 -0
- package/src/forge/agent-memory/normalize.ts +498 -0
- package/src/forge/agent-memory/redaction.ts +103 -0
- package/src/forge/agent-memory/sources/claude-code.ts +51 -0
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +84 -0
- package/src/forge/agent-memory/sources/codex.ts +119 -0
- package/src/forge/agent-memory/sources/cursor.ts +35 -0
- package/src/forge/agent-memory/types.ts +191 -0
- package/src/forge/bench.ts +248 -0
- package/src/forge/brownfield-import/index.ts +736 -0
- package/src/forge/brownfield-import/types.ts +127 -0
- package/src/forge/cair/action-journal.ts +61 -0
- package/src/forge/cair/action-parser.ts +314 -0
- package/src/forge/cair/action-validator.ts +40 -0
- package/src/forge/cair/actions.ts +1818 -0
- package/src/forge/cair/format.ts +77 -0
- package/src/forge/cair/index.ts +106 -0
- package/src/forge/cair/query.ts +478 -0
- package/src/forge/cair/snapshot.ts +315 -0
- package/src/forge/cair/types.ts +248 -0
- package/src/forge/cli/ai.ts +671 -3
- package/src/forge/cli/auth.ts +36 -1
- package/src/forge/cli/build.ts +20 -4
- package/src/forge/cli/changed.ts +300 -0
- package/src/forge/cli/codex-app-server.ts +877 -0
- package/src/forge/cli/commands.ts +1285 -7
- package/src/forge/cli/db.ts +121 -2
- package/src/forge/cli/deps.ts +79 -12
- package/src/forge/cli/dev.ts +502 -38
- package/src/forge/cli/docs.ts +265 -0
- package/src/forge/cli/handoff.ts +250 -0
- package/src/forge/cli/index.ts +1 -0
- package/src/forge/cli/main.ts +49 -3
- package/src/forge/cli/new.ts +3 -1
- package/src/forge/cli/next-actions.ts +23 -0
- package/src/forge/cli/output.ts +290 -1
- package/src/forge/cli/parse.ts +770 -36
- package/src/forge/cli/query.ts +32 -0
- package/src/forge/cli/release.ts +35 -11
- package/src/forge/cli/rls.ts +568 -17
- package/src/forge/cli/run.ts +41 -0
- package/src/forge/cli/secrets.ts +46 -1
- package/src/forge/cli/security.ts +381 -0
- package/src/forge/cli/self-host.ts +56 -14
- package/src/forge/cli/studio.ts +2163 -0
- package/src/forge/cli/verify.ts +1422 -32
- package/src/forge/compiler/agent-contract/build.ts +725 -41
- package/src/forge/compiler/agent-contract/types.ts +85 -0
- package/src/forge/compiler/ai-registry/build.ts +62 -1
- package/src/forge/compiler/ai-registry/constants.ts +1 -1
- package/src/forge/compiler/ai-registry/parse.ts +168 -5
- package/src/forge/compiler/api-surface/build.ts +47 -0
- package/src/forge/compiler/app-graph/build.ts +68 -8
- package/src/forge/compiler/app-graph/extract.ts +107 -0
- package/src/forge/compiler/app-graph/forge-apis.ts +1 -0
- package/src/forge/compiler/app-graph/module-graph.ts +73 -78
- package/src/forge/compiler/app-graph/parser.ts +24 -24
- package/src/forge/compiler/app-graph/profile.ts +26 -0
- package/src/forge/compiler/app-graph/versions.ts +1 -1
- package/src/forge/compiler/classifier/capabilities.ts +3 -2
- package/src/forge/compiler/classifier/classify.ts +32 -8
- package/src/forge/compiler/classifier/secrets.ts +3 -2
- package/src/forge/compiler/classifier/signals.ts +91 -1
- package/src/forge/compiler/client-sdk/build-manifest.ts +59 -0
- package/src/forge/compiler/client-sdk/render-client.ts +188 -13
- package/src/forge/compiler/data-graph/parse.ts +3 -3
- package/src/forge/compiler/data-graph/sql/ddl.ts +60 -2
- package/src/forge/compiler/data-graph/sql/serialize.ts +4 -0
- package/src/forge/compiler/data-graph/sql/types.ts +1 -0
- package/src/forge/compiler/dev-manifest/build.ts +3 -0
- package/src/forge/compiler/diagnostics/codes.ts +35 -0
- package/src/forge/compiler/diagnostics/create.ts +8 -3
- package/src/forge/compiler/diagnostics/index.ts +2 -0
- package/src/forge/compiler/emitter/barrel.ts +3 -0
- package/src/forge/compiler/emitter/render.ts +5 -0
- package/src/forge/compiler/external-manifest/registry.ts +205 -0
- package/src/forge/compiler/external-manifest/types.ts +91 -0
- package/src/forge/compiler/external-manifest/validate.ts +373 -0
- package/src/forge/compiler/frontend-graph/build.ts +85 -13
- package/src/forge/compiler/integration/add.ts +498 -22
- package/src/forge/compiler/integration/snapshot.ts +2 -0
- package/src/forge/compiler/make-registry/build.ts +19 -7
- package/src/forge/compiler/orchestrator/plan-profile.ts +23 -0
- package/src/forge/compiler/orchestrator/plan.ts +78 -7
- package/src/forge/compiler/orchestrator/profile.ts +65 -0
- package/src/forge/compiler/orchestrator/run.ts +97 -31
- package/src/forge/compiler/orchestrator/serialize.ts +101 -8
- package/src/forge/compiler/package-graph/compiler.ts +13 -3
- package/src/forge/compiler/package-manager/adapter.ts +4 -1
- package/src/forge/compiler/package-manager/commands.ts +4 -0
- package/src/forge/compiler/package-manager/executor.ts +30 -1
- package/src/forge/compiler/policy-registry/build.ts +44 -1
- package/src/forge/compiler/test-graph/build.ts +11 -3
- package/src/forge/compiler/types/ai-registry.ts +25 -1
- package/src/forge/compiler/types/app-graph.ts +9 -2
- package/src/forge/compiler/types/cli.ts +76 -1
- package/src/forge/compiler/types/dev-manifest.ts +3 -0
- package/src/forge/compiler/types/frontend-graph.ts +2 -2
- package/src/forge/delta/classifier.ts +52 -0
- package/src/forge/delta/explain.ts +126 -0
- package/src/forge/delta/git-observer.ts +43 -0
- package/src/forge/delta/ids.ts +44 -0
- package/src/forge/delta/index.ts +13 -0
- package/src/forge/delta/recorder.ts +402 -0
- package/src/forge/delta/redaction.ts +50 -0
- package/src/forge/delta/schema.ts +240 -0
- package/src/forge/delta/session.ts +142 -0
- package/src/forge/delta/status.ts +489 -0
- package/src/forge/delta/store.ts +2975 -0
- package/src/forge/delta/timeline.ts +104 -0
- package/src/forge/dev/server.ts +768 -15
- package/src/forge/dev/types.ts +15 -1
- package/src/forge/dev/watch.ts +17 -7
- package/src/forge/dev-console/cycle.ts +233 -21
- package/src/forge/dev-console/types.ts +46 -1
- package/src/forge/impact/index.ts +46 -8
- package/src/forge/impact/types.ts +6 -0
- package/src/forge/intent/index.ts +35 -16
- package/src/forge/make/index.ts +149 -6
- package/src/forge/make/templates.ts +343 -2
- package/src/forge/make/types.ts +3 -1
- package/src/forge/refactor/index.ts +1 -0
- package/src/forge/repair/rules/index.ts +2 -2
- package/src/forge/review/index.ts +158 -12
- package/src/forge/review/types.ts +15 -0
- package/src/forge/runtime/ai/context.ts +210 -5
- package/src/forge/runtime/ai/types.ts +70 -0
- package/src/forge/runtime/auth/claims.ts +32 -0
- package/src/forge/runtime/auth/errors.ts +2 -0
- package/src/forge/runtime/context/create-context.ts +30 -6
- package/src/forge/runtime/db/generated-client.ts +13 -2
- package/src/forge/runtime/db/memory-adapter.ts +2 -2
- package/src/forge/runtime/db/pglite-adapter.ts +77 -2
- package/src/forge/runtime/db/postgres-adapter.ts +6 -3
- package/src/forge/runtime/executor.ts +112 -2
- package/src/forge/runtime/external/bridge.ts +649 -0
- package/src/forge/runtime/runner/run-entry.ts +16 -7
- package/src/forge/runtime/telemetry/scrubber.ts +91 -10
- package/src/forge/runtime/webhooks/security.ts +184 -0
- package/src/forge/server.ts +100 -2
- package/src/forge/version.ts +1 -1
- package/src/forge/vue/index.ts +407 -0
- package/src/forge/workspace/change-summary.ts +209 -0
- package/src/forge/workspace/forge-cli.ts +14 -0
- package/src/forge/workspace/git-summary.ts +279 -0
- package/templates/agent-workroom/AGENTS.md +29 -0
- package/templates/agent-workroom/README.md +34 -0
- package/templates/agent-workroom/forge.config.ts +3 -0
- package/templates/agent-workroom/package.json +33 -0
- package/templates/agent-workroom/src/actions/indexAgentSignal.ts +10 -0
- package/templates/agent-workroom/src/commands/openWorkroom.ts +61 -0
- package/templates/agent-workroom/src/commands/recordAgentSignal.ts +119 -0
- package/templates/agent-workroom/src/commands/recordCheckRun.ts +52 -0
- package/templates/agent-workroom/src/forge/schema.ts +54 -0
- package/templates/agent-workroom/src/policies.ts +6 -0
- package/templates/agent-workroom/src/queries/listWorkrooms.ts +11 -0
- package/templates/agent-workroom/src/queries/liveWorkroom.ts +63 -0
- package/templates/agent-workroom/tsconfig.json +16 -0
- package/templates/agent-workroom/web/index.html +12 -0
- package/templates/agent-workroom/web/package.json +21 -0
- package/templates/agent-workroom/web/src/App.tsx +345 -0
- package/templates/agent-workroom/web/src/lib/forge.ts +13 -0
- package/templates/agent-workroom/web/src/main.tsx +13 -0
- package/templates/agent-workroom/web/src/styles.css +545 -0
- package/templates/agent-workroom/web/tsconfig.json +27 -0
- package/templates/b2b-support-web/package.json +2 -0
- package/templates/b2b-support-web/tsconfig.json +4 -1
- package/templates/b2b-support-web/web/package.json +1 -1
- package/templates/minimal-web/package.json +2 -1
- package/templates/minimal-web/tsconfig.json +3 -1
- package/templates/minimal-web/web/package.json +2 -2
- package/src/forge/_generated/actionSubscriptions.json +0 -2
- package/src/forge/_generated/actionSubscriptions.ts +0 -10
- package/src/forge/_generated/agentAdapterManifest.json +0 -2
- package/src/forge/_generated/agentAdapterManifest.ts +0 -73
- package/src/forge/_generated/agentContract.json +0 -2
- package/src/forge/_generated/agentContract.ts +0 -7696
- package/src/forge/_generated/agentQuickstart.md +0 -32
- package/src/forge/_generated/aiContext.ts +0 -59
- package/src/forge/_generated/aiModels.json +0 -2
- package/src/forge/_generated/aiModels.ts +0 -35
- package/src/forge/_generated/aiProviders.json +0 -2
- package/src/forge/_generated/aiProviders.ts +0 -23
- package/src/forge/_generated/aiRegistry.json +0 -2
- package/src/forge/_generated/aiRegistry.ts +0 -29
- package/src/forge/_generated/api.json +0 -2
- package/src/forge/_generated/api.ts +0 -8
- package/src/forge/_generated/appGraph.json +0 -2
- package/src/forge/_generated/appGraph.ts +0 -14667
- package/src/forge/_generated/appMap.md +0 -35
- package/src/forge/_generated/artifactManifest.json +0 -2
- package/src/forge/_generated/artifactManifest.ts +0 -7
- package/src/forge/_generated/authClaims.json +0 -2
- package/src/forge/_generated/authClaims.ts +0 -13
- package/src/forge/_generated/authConfig.json +0 -2
- package/src/forge/_generated/authConfig.ts +0 -17
- package/src/forge/_generated/authContext.ts +0 -23
- package/src/forge/_generated/authRegistry.json +0 -2
- package/src/forge/_generated/authRegistry.ts +0 -25
- package/src/forge/_generated/buildInfo.json +0 -2
- package/src/forge/_generated/buildInfo.ts +0 -9
- package/src/forge/_generated/capabilityMap.json +0 -2
- package/src/forge/_generated/capabilityMap.md +0 -15
- package/src/forge/_generated/capabilityMap.ts +0 -17
- package/src/forge/_generated/client.ts +0 -282
- package/src/forge/_generated/clientApi.ts +0 -9
- package/src/forge/_generated/clientManifest.json +0 -2
- package/src/forge/_generated/clientManifest.ts +0 -39
- package/src/forge/_generated/clientTypes.ts +0 -78
- package/src/forge/_generated/configRegistry.json +0 -2
- package/src/forge/_generated/configRegistry.ts +0 -4
- package/src/forge/_generated/dataGraph.json +0 -2
- package/src/forge/_generated/dataGraph.ts +0 -8
- package/src/forge/_generated/db.json +0 -2
- package/src/forge/_generated/db.ts +0 -2
- package/src/forge/_generated/dbSecurityManifest.json +0 -2
- package/src/forge/_generated/dbSecurityManifest.ts +0 -15
- package/src/forge/_generated/dbSessionContext.json +0 -2
- package/src/forge/_generated/dbSessionContext.ts +0 -39
- package/src/forge/_generated/deployManifest.json +0 -2
- package/src/forge/_generated/deployManifest.ts +0 -14
- package/src/forge/_generated/devManifest.json +0 -2
- package/src/forge/_generated/devManifest.ts +0 -47
- package/src/forge/_generated/envSchema.json +0 -2
- package/src/forge/_generated/envSchema.ts +0 -59
- package/src/forge/_generated/frontendGraph.json +0 -2
- package/src/forge/_generated/frontendGraph.ts +0 -27
- package/src/forge/_generated/importGuards.json +0 -2
- package/src/forge/_generated/importGuards.ts +0 -686
- package/src/forge/_generated/index.ts +0 -67
- package/src/forge/_generated/liveProductionManifest.json +0 -2
- package/src/forge/_generated/liveProductionManifest.ts +0 -23
- package/src/forge/_generated/liveProtocol.json +0 -2
- package/src/forge/_generated/liveProtocol.ts +0 -21
- package/src/forge/_generated/liveQueryRegistry.json +0 -2
- package/src/forge/_generated/liveQueryRegistry.ts +0 -9
- package/src/forge/_generated/liveTransportConfig.json +0 -2
- package/src/forge/_generated/liveTransportConfig.ts +0 -19
- package/src/forge/_generated/makeRegistry.json +0 -2
- package/src/forge/_generated/makeRegistry.ts +0 -163
- package/src/forge/_generated/makeTemplates.json +0 -2
- package/src/forge/_generated/makeTemplates.ts +0 -61
- package/src/forge/_generated/mockMap.json +0 -2
- package/src/forge/_generated/mockMap.ts +0 -7
- package/src/forge/_generated/operationPlaybooks.md +0 -147
- package/src/forge/_generated/packageGraph.json +0 -2
- package/src/forge/_generated/packageGraph.ts +0 -245249
- package/src/forge/_generated/packageUpgradeRegistry.json +0 -2
- package/src/forge/_generated/packageUpgradeRegistry.ts +0 -15
- package/src/forge/_generated/permissionMatrix.json +0 -2
- package/src/forge/_generated/permissionMatrix.ts +0 -7
- package/src/forge/_generated/policyRegistry.json +0 -2
- package/src/forge/_generated/policyRegistry.ts +0 -11
- package/src/forge/_generated/queryRegistry.json +0 -2
- package/src/forge/_generated/queryRegistry.ts +0 -9
- package/src/forge/_generated/react.d.ts +0 -22
- package/src/forge/_generated/react.ts +0 -29
- package/src/forge/_generated/reactManifest.json +0 -2
- package/src/forge/_generated/reactManifest.ts +0 -19
- package/src/forge/_generated/rlsPolicies.json +0 -2
- package/src/forge/_generated/rlsPolicies.sql +0 -34
- package/src/forge/_generated/rlsPolicies.ts +0 -6
- package/src/forge/_generated/runtimeGraph.json +0 -2
- package/src/forge/_generated/runtimeGraph.ts +0 -8
- package/src/forge/_generated/runtimeMatrix.json +0 -2
- package/src/forge/_generated/runtimeMatrix.ts +0 -327385
- package/src/forge/_generated/runtimeRegistry.ts +0 -2
- package/src/forge/_generated/runtimeRules.md +0 -79
- package/src/forge/_generated/secretRegistry.json +0 -2
- package/src/forge/_generated/secretRegistry.ts +0 -50
- package/src/forge/_generated/secretsContext.ts +0 -11
- package/src/forge/_generated/serverApi.ts +0 -10
- package/src/forge/_generated/sourceMapManifest.json +0 -2
- package/src/forge/_generated/sourceMapManifest.ts +0 -7
- package/src/forge/_generated/sqlPlan.json +0 -2
- package/src/forge/_generated/sqlPlan.ts +0 -88
- package/src/forge/_generated/subscriptionManifest.json +0 -2
- package/src/forge/_generated/subscriptionManifest.ts +0 -7
- package/src/forge/_generated/symbolicationManifest.json +0 -2
- package/src/forge/_generated/symbolicationManifest.ts +0 -17
- package/src/forge/_generated/telemetryRegistry.json +0 -2
- package/src/forge/_generated/telemetryRegistry.ts +0 -9
- package/src/forge/_generated/telemetrySinks.json +0 -2
- package/src/forge/_generated/telemetrySinks.ts +0 -11
- package/src/forge/_generated/tenantScope.json +0 -2
- package/src/forge/_generated/tenantScope.ts +0 -8
- package/src/forge/_generated/testGraph.json +0 -2
- package/src/forge/_generated/testGraph.ts +0 -3108
- package/src/forge/_generated/testPlanRegistry.json +0 -2
- package/src/forge/_generated/testPlanRegistry.ts +0 -33
- package/src/forge/_generated/uiRoutes.json +0 -2
- package/src/forge/_generated/uiRoutes.ts +0 -16
- package/src/forge/_generated/uiScenarios.json +0 -2
- package/src/forge/_generated/uiScenarios.ts +0 -30
- package/src/forge/_generated/uiTestManifest.json +0 -2
- package/src/forge/_generated/uiTestManifest.ts +0 -27
- package/src/forge/_generated/workflowRegistry.json +0 -2
- package/src/forge/_generated/workflowRegistry.ts +0 -9
- package/src/forge/_generated/workflowSubscriptions.json +0 -2
- 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
|
+
}
|