forge-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/worktree-create.sh +64 -0
- package/.claude/hooks/worktree-remove.sh +57 -0
- package/.claude/settings.local.json +29 -0
- package/.forge/knowledge/conventions.yaml +1 -0
- package/.forge/knowledge/decisions.yaml +1 -0
- package/.forge/knowledge/gotchas.yaml +1 -0
- package/.forge/knowledge/patterns.yaml +1 -0
- package/.forge/manifest.yaml +6 -0
- package/CLAUDE.md +144 -0
- package/bin/setup-forge.sh +132 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +553 -0
- package/dist/cli.js.map +1 -0
- package/dist/context/codebase.d.ts +57 -0
- package/dist/context/codebase.d.ts.map +1 -0
- package/dist/context/codebase.js +301 -0
- package/dist/context/codebase.js.map +1 -0
- package/dist/context/injector.d.ts +147 -0
- package/dist/context/injector.d.ts.map +1 -0
- package/dist/context/injector.js +533 -0
- package/dist/context/injector.js.map +1 -0
- package/dist/context/memory.d.ts +32 -0
- package/dist/context/memory.d.ts.map +1 -0
- package/dist/context/memory.js +140 -0
- package/dist/context/memory.js.map +1 -0
- package/dist/context/session-index.d.ts +54 -0
- package/dist/context/session-index.d.ts.map +1 -0
- package/dist/context/session-index.js +265 -0
- package/dist/context/session-index.js.map +1 -0
- package/dist/context/session.d.ts +42 -0
- package/dist/context/session.d.ts.map +1 -0
- package/dist/context/session.js +121 -0
- package/dist/context/session.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/ingestion/chunker.d.ts +19 -0
- package/dist/ingestion/chunker.d.ts.map +1 -0
- package/dist/ingestion/chunker.js +189 -0
- package/dist/ingestion/chunker.js.map +1 -0
- package/dist/ingestion/embedder.d.ts +45 -0
- package/dist/ingestion/embedder.d.ts.map +1 -0
- package/dist/ingestion/embedder.js +152 -0
- package/dist/ingestion/embedder.js.map +1 -0
- package/dist/ingestion/git-analyzer.d.ts +77 -0
- package/dist/ingestion/git-analyzer.d.ts.map +1 -0
- package/dist/ingestion/git-analyzer.js +437 -0
- package/dist/ingestion/git-analyzer.js.map +1 -0
- package/dist/ingestion/indexer.d.ts +79 -0
- package/dist/ingestion/indexer.d.ts.map +1 -0
- package/dist/ingestion/indexer.js +766 -0
- package/dist/ingestion/indexer.js.map +1 -0
- package/dist/ingestion/markdown-chunker.d.ts +19 -0
- package/dist/ingestion/markdown-chunker.d.ts.map +1 -0
- package/dist/ingestion/markdown-chunker.js +243 -0
- package/dist/ingestion/markdown-chunker.js.map +1 -0
- package/dist/ingestion/markdown-knowledge.d.ts +21 -0
- package/dist/ingestion/markdown-knowledge.d.ts.map +1 -0
- package/dist/ingestion/markdown-knowledge.js +129 -0
- package/dist/ingestion/markdown-knowledge.js.map +1 -0
- package/dist/ingestion/parser.d.ts +20 -0
- package/dist/ingestion/parser.d.ts.map +1 -0
- package/dist/ingestion/parser.js +429 -0
- package/dist/ingestion/parser.js.map +1 -0
- package/dist/ingestion/watcher.d.ts +28 -0
- package/dist/ingestion/watcher.d.ts.map +1 -0
- package/dist/ingestion/watcher.js +147 -0
- package/dist/ingestion/watcher.js.map +1 -0
- package/dist/knowledge/hydrator.d.ts +37 -0
- package/dist/knowledge/hydrator.d.ts.map +1 -0
- package/dist/knowledge/hydrator.js +220 -0
- package/dist/knowledge/hydrator.js.map +1 -0
- package/dist/knowledge/registry.d.ts +129 -0
- package/dist/knowledge/registry.d.ts.map +1 -0
- package/dist/knowledge/registry.js +361 -0
- package/dist/knowledge/registry.js.map +1 -0
- package/dist/knowledge/search.d.ts +114 -0
- package/dist/knowledge/search.d.ts.map +1 -0
- package/dist/knowledge/search.js +428 -0
- package/dist/knowledge/search.js.map +1 -0
- package/dist/knowledge/store.d.ts +76 -0
- package/dist/knowledge/store.d.ts.map +1 -0
- package/dist/knowledge/store.js +230 -0
- package/dist/knowledge/store.js.map +1 -0
- package/dist/learning/confidence.d.ts +30 -0
- package/dist/learning/confidence.d.ts.map +1 -0
- package/dist/learning/confidence.js +165 -0
- package/dist/learning/confidence.js.map +1 -0
- package/dist/learning/patterns.d.ts +52 -0
- package/dist/learning/patterns.d.ts.map +1 -0
- package/dist/learning/patterns.js +290 -0
- package/dist/learning/patterns.js.map +1 -0
- package/dist/learning/trajectory.d.ts +55 -0
- package/dist/learning/trajectory.d.ts.map +1 -0
- package/dist/learning/trajectory.js +200 -0
- package/dist/learning/trajectory.js.map +1 -0
- package/dist/memory/memory-compat.d.ts +100 -0
- package/dist/memory/memory-compat.d.ts.map +1 -0
- package/dist/memory/memory-compat.js +146 -0
- package/dist/memory/memory-compat.js.map +1 -0
- package/dist/memory/observation-store.d.ts +57 -0
- package/dist/memory/observation-store.d.ts.map +1 -0
- package/dist/memory/observation-store.js +154 -0
- package/dist/memory/observation-store.js.map +1 -0
- package/dist/memory/session-tracker.d.ts +81 -0
- package/dist/memory/session-tracker.d.ts.map +1 -0
- package/dist/memory/session-tracker.js +262 -0
- package/dist/memory/session-tracker.js.map +1 -0
- package/dist/pipeline/engine.d.ts +179 -0
- package/dist/pipeline/engine.d.ts.map +1 -0
- package/dist/pipeline/engine.js +691 -0
- package/dist/pipeline/engine.js.map +1 -0
- package/dist/pipeline/events.d.ts +54 -0
- package/dist/pipeline/events.d.ts.map +1 -0
- package/dist/pipeline/events.js +157 -0
- package/dist/pipeline/events.js.map +1 -0
- package/dist/pipeline/parallel.d.ts +83 -0
- package/dist/pipeline/parallel.d.ts.map +1 -0
- package/dist/pipeline/parallel.js +277 -0
- package/dist/pipeline/parallel.js.map +1 -0
- package/dist/pipeline/state-machine.d.ts +65 -0
- package/dist/pipeline/state-machine.d.ts.map +1 -0
- package/dist/pipeline/state-machine.js +176 -0
- package/dist/pipeline/state-machine.js.map +1 -0
- package/dist/query/graph-queries.d.ts +84 -0
- package/dist/query/graph-queries.d.ts.map +1 -0
- package/dist/query/graph-queries.js +216 -0
- package/dist/query/graph-queries.js.map +1 -0
- package/dist/query/hybrid-search.d.ts +34 -0
- package/dist/query/hybrid-search.d.ts.map +1 -0
- package/dist/query/hybrid-search.js +263 -0
- package/dist/query/hybrid-search.js.map +1 -0
- package/dist/query/intent-detector.d.ts +35 -0
- package/dist/query/intent-detector.d.ts.map +1 -0
- package/dist/query/intent-detector.js +115 -0
- package/dist/query/intent-detector.js.map +1 -0
- package/dist/query/ranking.d.ts +57 -0
- package/dist/query/ranking.d.ts.map +1 -0
- package/dist/query/ranking.js +109 -0
- package/dist/query/ranking.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +291 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/falkordb-store.d.ts +73 -0
- package/dist/storage/falkordb-store.d.ts.map +1 -0
- package/dist/storage/falkordb-store.js +346 -0
- package/dist/storage/falkordb-store.js.map +1 -0
- package/dist/storage/file-cache.d.ts +32 -0
- package/dist/storage/file-cache.d.ts.map +1 -0
- package/dist/storage/file-cache.js +115 -0
- package/dist/storage/file-cache.js.map +1 -0
- package/dist/storage/interfaces.d.ts +151 -0
- package/dist/storage/interfaces.d.ts.map +1 -0
- package/dist/storage/interfaces.js +7 -0
- package/dist/storage/interfaces.js.map +1 -0
- package/dist/storage/qdrant-store.d.ts +110 -0
- package/dist/storage/qdrant-store.d.ts.map +1 -0
- package/dist/storage/qdrant-store.js +467 -0
- package/dist/storage/qdrant-store.js.map +1 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +136 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/sqlite.d.ts +35 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +132 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/tools/collaboration-tools.d.ts +111 -0
- package/dist/tools/collaboration-tools.d.ts.map +1 -0
- package/dist/tools/collaboration-tools.js +174 -0
- package/dist/tools/collaboration-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +293 -0
- package/dist/tools/context-tools.d.ts.map +1 -0
- package/dist/tools/context-tools.js +437 -0
- package/dist/tools/context-tools.js.map +1 -0
- package/dist/tools/graph-tools.d.ts +129 -0
- package/dist/tools/graph-tools.d.ts.map +1 -0
- package/dist/tools/graph-tools.js +237 -0
- package/dist/tools/graph-tools.js.map +1 -0
- package/dist/tools/ingestion-tools.d.ts +96 -0
- package/dist/tools/ingestion-tools.d.ts.map +1 -0
- package/dist/tools/ingestion-tools.js +90 -0
- package/dist/tools/ingestion-tools.js.map +1 -0
- package/dist/tools/learning-tools.d.ts +168 -0
- package/dist/tools/learning-tools.d.ts.map +1 -0
- package/dist/tools/learning-tools.js +158 -0
- package/dist/tools/learning-tools.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +183 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +197 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/phase-tools.d.ts +954 -0
- package/dist/tools/phase-tools.d.ts.map +1 -0
- package/dist/tools/phase-tools.js +1215 -0
- package/dist/tools/phase-tools.js.map +1 -0
- package/dist/tools/pipeline-tools.d.ts +140 -0
- package/dist/tools/pipeline-tools.d.ts.map +1 -0
- package/dist/tools/pipeline-tools.js +162 -0
- package/dist/tools/pipeline-tools.js.map +1 -0
- package/dist/tools/registration-tools.d.ts +220 -0
- package/dist/tools/registration-tools.d.ts.map +1 -0
- package/dist/tools/registration-tools.js +391 -0
- package/dist/tools/registration-tools.js.map +1 -0
- package/dist/util/circuit-breaker.d.ts +75 -0
- package/dist/util/circuit-breaker.d.ts.map +1 -0
- package/dist/util/circuit-breaker.js +159 -0
- package/dist/util/circuit-breaker.js.map +1 -0
- package/dist/util/config.d.ts +23 -0
- package/dist/util/config.d.ts.map +1 -0
- package/dist/util/config.js +164 -0
- package/dist/util/config.js.map +1 -0
- package/dist/util/logger.d.ts +13 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +45 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/token-counter.d.ts +24 -0
- package/dist/util/token-counter.d.ts.map +1 -0
- package/dist/util/token-counter.js +48 -0
- package/dist/util/token-counter.js.map +1 -0
- package/dist/util/types.d.ts +525 -0
- package/dist/util/types.d.ts.map +1 -0
- package/dist/util/types.js +5 -0
- package/dist/util/types.js.map +1 -0
- package/docker-compose.yml +20 -0
- package/docs/plans/2026-02-27-swarm-coordination/architecture.md +203 -0
- package/docs/plans/2026-02-27-swarm-coordination/vision.md +57 -0
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/architecture.md +1 -0
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/vision.md +300 -0
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/architecture.md +480 -0
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/verification-checklist.md +462 -0
- package/docs/plans/completed/2026-02-27-git-history-atlassian/git-jira-plan.md +181 -0
- package/package.json +39 -0
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/README.md +134 -0
- package/plugin/agents/architect.md +367 -0
- package/plugin/agents/backend-specialist.md +263 -0
- package/plugin/agents/brainstormer.md +122 -0
- package/plugin/agents/data-specialist.md +266 -0
- package/plugin/agents/designer.md +408 -0
- package/plugin/agents/frontend-specialist.md +241 -0
- package/plugin/agents/inspector.md +406 -0
- package/plugin/agents/knowledge-keeper.md +443 -0
- package/plugin/agents/platform-engineer.md +326 -0
- package/plugin/agents/product-manager.md +268 -0
- package/plugin/agents/product-owner.md +438 -0
- package/plugin/agents/pulse-checker.md +73 -0
- package/plugin/agents/qa-strategist.md +500 -0
- package/plugin/agents/self-improver.md +310 -0
- package/plugin/agents/strategist.md +360 -0
- package/plugin/agents/supervisor.md +380 -0
- package/plugin/commands/brainstorm.md +25 -0
- package/plugin/commands/forge.md +88 -0
- package/plugin/docs/atlassian-integration.md +110 -0
- package/plugin/docs/workflow.md +126 -0
- package/plugin/skills/agent-development/.skillfish.json +10 -0
- package/plugin/skills/agent-development/SKILL.md +415 -0
- package/plugin/skills/agent-development/examples/agent-creation-prompt.md +238 -0
- package/plugin/skills/agent-development/examples/complete-agent-examples.md +427 -0
- package/plugin/skills/agent-development/references/agent-creation-system-prompt.md +207 -0
- package/plugin/skills/agent-development/references/system-prompt-design.md +411 -0
- package/plugin/skills/agent-development/references/triggering-examples.md +491 -0
- package/plugin/skills/agent-development/scripts/validate-agent.sh +217 -0
- package/plugin/skills/agent-handoff/SKILL.md +335 -0
- package/plugin/skills/anti-stub/SKILL.md +317 -0
- package/plugin/skills/brainstorm/SKILL.md +31 -0
- package/plugin/skills/debugging/SKILL.md +276 -0
- package/plugin/skills/fix/SKILL.md +62 -0
- package/plugin/skills/frontend-design/.skillfish.json +10 -0
- package/plugin/skills/frontend-design/SKILL.md +42 -0
- package/plugin/skills/gotchas/SKILL.md +61 -0
- package/plugin/skills/graph-orchestrator/SKILL.md +38 -0
- package/plugin/skills/history/SKILL.md +58 -0
- package/plugin/skills/impact/SKILL.md +59 -0
- package/plugin/skills/implementation-execution/SKILL.md +291 -0
- package/plugin/skills/index-repo/SKILL.md +55 -0
- package/plugin/skills/interviewing/SKILL.md +225 -0
- package/plugin/skills/knowledge-curation/SKILL.md +393 -0
- package/plugin/skills/learn/SKILL.md +69 -0
- package/plugin/skills/mcp-integration/.skillfish.json +10 -0
- package/plugin/skills/mcp-integration/SKILL.md +554 -0
- package/plugin/skills/mcp-integration/examples/http-server.json +20 -0
- package/plugin/skills/mcp-integration/examples/sse-server.json +19 -0
- package/plugin/skills/mcp-integration/examples/stdio-server.json +26 -0
- package/plugin/skills/mcp-integration/references/authentication.md +549 -0
- package/plugin/skills/mcp-integration/references/server-types.md +536 -0
- package/plugin/skills/mcp-integration/references/tool-usage.md +538 -0
- package/plugin/skills/nestjs/.skillfish.json +10 -0
- package/plugin/skills/nestjs/SKILL.md +669 -0
- package/plugin/skills/nestjs/drizzle-reference.md +1894 -0
- package/plugin/skills/nestjs/reference.md +1447 -0
- package/plugin/skills/nestjs/workflow-optimization.md +229 -0
- package/plugin/skills/parallel-dispatch/SKILL.md +308 -0
- package/plugin/skills/project-discovery/SKILL.md +304 -0
- package/plugin/skills/search/SKILL.md +56 -0
- package/plugin/skills/security-audit/SKILL.md +362 -0
- package/plugin/skills/skill-development/.skillfish.json +10 -0
- package/plugin/skills/skill-development/SKILL.md +637 -0
- package/plugin/skills/skill-development/references/skill-creator-original.md +209 -0
- package/plugin/skills/tdd/SKILL.md +273 -0
- package/plugin/skills/terminal-presentation/SKILL.md +395 -0
- package/plugin/skills/test-strategy/SKILL.md +365 -0
- package/plugin/skills/verification-protocol/SKILL.md +256 -0
- package/plugin/skills/visual-explainer/CHANGELOG.md +97 -0
- package/plugin/skills/visual-explainer/LICENSE +21 -0
- package/plugin/skills/visual-explainer/README.md +137 -0
- package/plugin/skills/visual-explainer/SKILL.md +352 -0
- package/plugin/skills/visual-explainer/banner.png +0 -0
- package/plugin/skills/visual-explainer/package.json +11 -0
- package/plugin/skills/visual-explainer/prompts/diff-review.md +68 -0
- package/plugin/skills/visual-explainer/prompts/fact-check.md +63 -0
- package/plugin/skills/visual-explainer/prompts/generate-slides.md +18 -0
- package/plugin/skills/visual-explainer/prompts/generate-web-diagram.md +10 -0
- package/plugin/skills/visual-explainer/prompts/plan-review.md +86 -0
- package/plugin/skills/visual-explainer/prompts/project-recap.md +61 -0
- package/plugin/skills/visual-explainer/references/css-patterns.md +1188 -0
- package/plugin/skills/visual-explainer/references/libraries.md +470 -0
- package/plugin/skills/visual-explainer/references/responsive-nav.md +212 -0
- package/plugin/skills/visual-explainer/references/slide-patterns.md +1403 -0
- package/plugin/skills/visual-explainer/templates/architecture.html +596 -0
- package/plugin/skills/visual-explainer/templates/data-table.html +540 -0
- package/plugin/skills/visual-explainer/templates/mermaid-flowchart.html +435 -0
- package/plugin/skills/visual-explainer/templates/slide-deck.html +913 -0
- package/src/cli.ts +655 -0
- package/src/context/.gitkeep +0 -0
- package/src/context/codebase.ts +393 -0
- package/src/context/injector.ts +797 -0
- package/src/context/memory.ts +187 -0
- package/src/context/session-index.ts +327 -0
- package/src/context/session.ts +152 -0
- package/src/index.ts +47 -0
- package/src/ingestion/.gitkeep +0 -0
- package/src/ingestion/chunker.ts +277 -0
- package/src/ingestion/embedder.ts +167 -0
- package/src/ingestion/git-analyzer.ts +545 -0
- package/src/ingestion/indexer.ts +984 -0
- package/src/ingestion/markdown-chunker.ts +337 -0
- package/src/ingestion/markdown-knowledge.ts +175 -0
- package/src/ingestion/parser.ts +475 -0
- package/src/ingestion/watcher.ts +182 -0
- package/src/knowledge/.gitkeep +0 -0
- package/src/knowledge/hydrator.ts +246 -0
- package/src/knowledge/registry.ts +463 -0
- package/src/knowledge/search.ts +565 -0
- package/src/knowledge/store.ts +262 -0
- package/src/learning/.gitkeep +0 -0
- package/src/learning/confidence.ts +193 -0
- package/src/learning/patterns.ts +360 -0
- package/src/learning/trajectory.ts +268 -0
- package/src/memory/.gitkeep +0 -0
- package/src/memory/memory-compat.ts +233 -0
- package/src/memory/observation-store.ts +224 -0
- package/src/memory/session-tracker.ts +332 -0
- package/src/pipeline/.gitkeep +0 -0
- package/src/pipeline/engine.ts +1139 -0
- package/src/pipeline/events.ts +253 -0
- package/src/pipeline/parallel.ts +394 -0
- package/src/pipeline/state-machine.ts +199 -0
- package/src/query/.gitkeep +0 -0
- package/src/query/graph-queries.ts +262 -0
- package/src/query/hybrid-search.ts +337 -0
- package/src/query/intent-detector.ts +131 -0
- package/src/query/ranking.ts +161 -0
- package/src/server.ts +352 -0
- package/src/storage/.gitkeep +0 -0
- package/src/storage/falkordb-store.ts +388 -0
- package/src/storage/file-cache.ts +141 -0
- package/src/storage/interfaces.ts +201 -0
- package/src/storage/qdrant-store.ts +557 -0
- package/src/storage/schema.ts +139 -0
- package/src/storage/sqlite.ts +168 -0
- package/src/tools/.gitkeep +0 -0
- package/src/tools/collaboration-tools.ts +208 -0
- package/src/tools/context-tools.ts +493 -0
- package/src/tools/graph-tools.ts +295 -0
- package/src/tools/ingestion-tools.ts +122 -0
- package/src/tools/learning-tools.ts +181 -0
- package/src/tools/memory-tools.ts +234 -0
- package/src/tools/phase-tools.ts +1452 -0
- package/src/tools/pipeline-tools.ts +188 -0
- package/src/tools/registration-tools.ts +450 -0
- package/src/util/.gitkeep +0 -0
- package/src/util/circuit-breaker.ts +193 -0
- package/src/util/config.ts +177 -0
- package/src/util/logger.ts +53 -0
- package/src/util/token-counter.ts +52 -0
- package/src/util/types.ts +710 -0
- package/tests/context/.gitkeep +0 -0
- package/tests/integration/.gitkeep +0 -0
- package/tests/knowledge/.gitkeep +0 -0
- package/tests/learning/.gitkeep +0 -0
- package/tests/pipeline/.gitkeep +0 -0
- package/tests/tools/.gitkeep +0 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +10 -0
- package/vscode-extension/.vscodeignore +7 -0
- package/vscode-extension/README.md +43 -0
- package/vscode-extension/out/edge-collector.js +274 -0
- package/vscode-extension/out/edge-collector.js.map +1 -0
- package/vscode-extension/out/extension.js +264 -0
- package/vscode-extension/out/extension.js.map +1 -0
- package/vscode-extension/out/forge-client.js +318 -0
- package/vscode-extension/out/forge-client.js.map +1 -0
- package/vscode-extension/package-lock.json +59 -0
- package/vscode-extension/package.json +71 -0
- package/vscode-extension/src/edge-collector.ts +320 -0
- package/vscode-extension/src/extension.ts +269 -0
- package/vscode-extension/src/forge-client.ts +364 -0
- package/vscode-extension/tsconfig.json +19 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// MemoryBridge — B8
|
|
2
|
+
//
|
|
3
|
+
// Wraps the Qdrant vector store's observation collection to provide
|
|
4
|
+
// memory search and save operations for the context injection layer.
|
|
5
|
+
//
|
|
6
|
+
// Uses the QdrantVectorStore directly (not the VectorStore interface) because
|
|
7
|
+
// we need the searchObservations + upsertObservation methods that are Qdrant-specific.
|
|
8
|
+
// When vectorStore is null or unhealthy, all methods degrade gracefully.
|
|
9
|
+
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import type { QdrantVectorStore } from '../storage/qdrant-store.js';
|
|
12
|
+
import type { ObservationPayload } from '../util/types.js';
|
|
13
|
+
import { embedText } from '../ingestion/embedder.js';
|
|
14
|
+
import { logger } from '../util/logger.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Public types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface MemorySearchResult {
|
|
21
|
+
content: string;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
relevanceScore: number;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MemorySearchOptions {
|
|
28
|
+
repoId?: string;
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
limit?: number;
|
|
31
|
+
scoreThreshold?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// MemoryBridge
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export class MemoryBridge {
|
|
39
|
+
constructor(private readonly vectorStore: QdrantVectorStore | null) {}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Search observations by semantic similarity.
|
|
43
|
+
* Optionally filters by repoId and/or sessionId.
|
|
44
|
+
* Returns [] gracefully when Qdrant is unavailable.
|
|
45
|
+
*/
|
|
46
|
+
async searchMemories(
|
|
47
|
+
query: string,
|
|
48
|
+
options: MemorySearchOptions = {},
|
|
49
|
+
): Promise<MemorySearchResult[]> {
|
|
50
|
+
if (!this.vectorStore) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const healthy = await this.vectorStore.isHealthy();
|
|
55
|
+
if (!healthy) {
|
|
56
|
+
logger.debug('MemoryBridge: Qdrant unavailable — returning empty memories');
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
repoId,
|
|
62
|
+
sessionId,
|
|
63
|
+
limit = 10,
|
|
64
|
+
scoreThreshold = 0.3,
|
|
65
|
+
} = options;
|
|
66
|
+
|
|
67
|
+
let queryVector: number[];
|
|
68
|
+
try {
|
|
69
|
+
queryVector = await embedText(query);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
logger.warn('MemoryBridge: embedding failed', { error: String(err) });
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Build filter — only include conditions for fields that are specified
|
|
76
|
+
const mustConditions: Record<string, unknown>[] = [];
|
|
77
|
+
if (repoId) {
|
|
78
|
+
mustConditions.push({ key: 'repo_id', match: { value: repoId } });
|
|
79
|
+
}
|
|
80
|
+
if (sessionId) {
|
|
81
|
+
mustConditions.push({ key: 'session_id', match: { value: sessionId } });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Exclude session markers from memory results
|
|
85
|
+
const mustNotConditions: Record<string, unknown>[] = [
|
|
86
|
+
{ key: 'tags', match: { value: 'session_marker' } },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const filter: Record<string, unknown> =
|
|
90
|
+
mustConditions.length > 0
|
|
91
|
+
? { must: mustConditions, must_not: mustNotConditions }
|
|
92
|
+
: { must_not: mustNotConditions };
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const results = await this.vectorStore.searchObservations(queryVector, {
|
|
96
|
+
limit,
|
|
97
|
+
scoreThreshold,
|
|
98
|
+
filter,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return results
|
|
102
|
+
.map((r) => {
|
|
103
|
+
const payload = r.payload as ObservationPayload;
|
|
104
|
+
return {
|
|
105
|
+
content: payload.content,
|
|
106
|
+
createdAt: payload.created_at,
|
|
107
|
+
relevanceScore: Math.round(r.score * 1000) / 1000,
|
|
108
|
+
sessionId: payload.session_id || undefined,
|
|
109
|
+
};
|
|
110
|
+
})
|
|
111
|
+
.filter((m) => m.content && m.content.trim().length > 0);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logger.warn('MemoryBridge: search failed', { error: String(err) });
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Save a memory (observation) to the vector store.
|
|
120
|
+
* Returns the UUID of the stored observation.
|
|
121
|
+
* Throws when storage fails so the caller can decide whether to propagate.
|
|
122
|
+
*/
|
|
123
|
+
async saveMemory(
|
|
124
|
+
content: string,
|
|
125
|
+
repoId: string,
|
|
126
|
+
sessionId?: string,
|
|
127
|
+
): Promise<string> {
|
|
128
|
+
if (!this.vectorStore) {
|
|
129
|
+
throw new Error('MemoryBridge: vector store not configured');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const healthy = await this.vectorStore.isHealthy();
|
|
133
|
+
if (!healthy) {
|
|
134
|
+
throw new Error('MemoryBridge: Qdrant is unavailable');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const id = randomUUID();
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
const effectiveSessionId = sessionId ?? `session-${now}`;
|
|
140
|
+
|
|
141
|
+
let vector: number[];
|
|
142
|
+
try {
|
|
143
|
+
vector = await embedText(content);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
logger.warn('MemoryBridge: embedding failed, using zero vector', {
|
|
146
|
+
error: String(err),
|
|
147
|
+
});
|
|
148
|
+
vector = new Array(384).fill(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const payload: ObservationPayload = {
|
|
152
|
+
id,
|
|
153
|
+
repo_id: repoId,
|
|
154
|
+
session_id: effectiveSessionId,
|
|
155
|
+
content,
|
|
156
|
+
type: 'semantic',
|
|
157
|
+
category: null,
|
|
158
|
+
tags: [],
|
|
159
|
+
importance: 0.5,
|
|
160
|
+
created_at: now,
|
|
161
|
+
accessed_at: now,
|
|
162
|
+
access_count: 0,
|
|
163
|
+
is_stale: false,
|
|
164
|
+
linked_symbols: [],
|
|
165
|
+
source: 'forge',
|
|
166
|
+
metadata: {},
|
|
167
|
+
key: null,
|
|
168
|
+
namespace: 'default',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
await this.vectorStore.upsertObservation(id, vector, payload);
|
|
172
|
+
|
|
173
|
+
logger.debug('MemoryBridge: memory saved', {
|
|
174
|
+
id,
|
|
175
|
+
repoId,
|
|
176
|
+
sessionId: effectiveSessionId,
|
|
177
|
+
contentLength: content.length,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** True when the vector store is configured (does not perform a health check). */
|
|
184
|
+
isAvailable(): boolean {
|
|
185
|
+
return this.vectorStore !== null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// session-index.ts
|
|
2
|
+
//
|
|
3
|
+
// Session-scoped context index — virtual memory for LLM context windows.
|
|
4
|
+
// Instead of pruning old context (losing it), offloads to Qdrant for
|
|
5
|
+
// semantic retrieval. The injector searches this before assembling
|
|
6
|
+
// context, retrieving only what's relevant to the current task.
|
|
7
|
+
//
|
|
8
|
+
// Uses existing forge_observations collection with type='working' to
|
|
9
|
+
// avoid creating new infrastructure.
|
|
10
|
+
|
|
11
|
+
import type { QdrantVectorStore } from '../storage/qdrant-store.js';
|
|
12
|
+
import type { GraphStore } from '../storage/interfaces.js';
|
|
13
|
+
import { saveObservation } from '../memory/observation-store.js';
|
|
14
|
+
import { embedText } from '../ingestion/embedder.js';
|
|
15
|
+
import { logger } from '../util/logger.js';
|
|
16
|
+
|
|
17
|
+
// No-op graph store for saveObservation calls (graph linking not needed for cache)
|
|
18
|
+
const noOpGraph: GraphStore = {
|
|
19
|
+
connect: async () => {},
|
|
20
|
+
disconnect: async () => {},
|
|
21
|
+
isHealthy: async () => false,
|
|
22
|
+
query: async () => ({ nodes: [], edges: [] }),
|
|
23
|
+
upsertNode: async () => {},
|
|
24
|
+
upsertEdge: async () => {},
|
|
25
|
+
deleteFile: async () => {},
|
|
26
|
+
deleteRepo: async () => {},
|
|
27
|
+
getCounts: async () => ({ totalNodes: 0, totalEdges: 0, byLabel: {} }),
|
|
28
|
+
ensureIndexes: async () => {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Phase names for tag extraction
|
|
32
|
+
const KNOWN_PHASES = [
|
|
33
|
+
'interview',
|
|
34
|
+
'requirements',
|
|
35
|
+
'architecture',
|
|
36
|
+
'design',
|
|
37
|
+
'qa_strategy',
|
|
38
|
+
'implementation',
|
|
39
|
+
'inspection',
|
|
40
|
+
'knowledge_collection',
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
// Event types for tag extraction
|
|
44
|
+
const KNOWN_EVENT_TYPES = [
|
|
45
|
+
'phase_output',
|
|
46
|
+
'broadcast',
|
|
47
|
+
'tool_result',
|
|
48
|
+
'agent_summary',
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
export interface SessionEvent {
|
|
52
|
+
type: 'phase_output' | 'broadcast' | 'tool_result' | 'agent_summary';
|
|
53
|
+
phase: string;
|
|
54
|
+
agent: string;
|
|
55
|
+
projectId: string;
|
|
56
|
+
content: string;
|
|
57
|
+
created_at: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TieredContext {
|
|
61
|
+
/** Tier 0: critical directives, contracts, blockers — always injected */
|
|
62
|
+
alwaysInject: string[];
|
|
63
|
+
/** Tier 1: top-K semantic matches for this module — smart-injected */
|
|
64
|
+
smartInjected: string[];
|
|
65
|
+
/** Tier 2: count of remaining searchable items (for the agent's awareness) */
|
|
66
|
+
searchableCount: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class SessionContextIndex {
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly vectorStore: QdrantVectorStore,
|
|
72
|
+
private readonly sessionId: string,
|
|
73
|
+
) {}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Index a session event for later retrieval.
|
|
77
|
+
* Fire-and-forget — never blocks the caller.
|
|
78
|
+
*/
|
|
79
|
+
async index(event: SessionEvent): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
await saveObservation(
|
|
82
|
+
{
|
|
83
|
+
content: event.content.slice(0, 2000),
|
|
84
|
+
sessionId: this.sessionId,
|
|
85
|
+
type: 'working',
|
|
86
|
+
category: `session_${event.type}`,
|
|
87
|
+
importance: event.type === 'broadcast' ? 0.7 : 0.5,
|
|
88
|
+
tags: [
|
|
89
|
+
'session_cache',
|
|
90
|
+
this.sessionId,
|
|
91
|
+
event.projectId,
|
|
92
|
+
event.phase,
|
|
93
|
+
event.agent,
|
|
94
|
+
event.type,
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
this.vectorStore,
|
|
98
|
+
noOpGraph,
|
|
99
|
+
);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
logger.debug('SessionContextIndex.index: failed (non-fatal)', {
|
|
102
|
+
type: event.type,
|
|
103
|
+
error: String(err),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Semantic search scoped to this session + project.
|
|
110
|
+
* Returns the most relevant cached items for a query.
|
|
111
|
+
*/
|
|
112
|
+
async retrieve(
|
|
113
|
+
query: string,
|
|
114
|
+
projectId: string,
|
|
115
|
+
limit: number = 5,
|
|
116
|
+
): Promise<SessionEvent[]> {
|
|
117
|
+
try {
|
|
118
|
+
// Attempt semantic search first — requires embedding the query
|
|
119
|
+
let queryVector: number[] | null = null;
|
|
120
|
+
try {
|
|
121
|
+
queryVector = await embedText(query);
|
|
122
|
+
} catch {
|
|
123
|
+
// Embedding unavailable — fall back to filter-only below
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (queryVector) {
|
|
127
|
+
// Semantic search: ranked by vector similarity
|
|
128
|
+
const results = await this.vectorStore.searchObservations(queryVector, {
|
|
129
|
+
limit,
|
|
130
|
+
scoreThreshold: 0.2,
|
|
131
|
+
filter: {
|
|
132
|
+
must: [
|
|
133
|
+
{ key: 'tags', match: { value: 'session_cache' } },
|
|
134
|
+
{ key: 'tags', match: { value: this.sessionId } },
|
|
135
|
+
{ key: 'tags', match: { value: projectId } },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return results.map((r) => this.resultToSessionEvent(r.payload as unknown as Record<string, unknown>, projectId));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fallback: filter-only retrieval (no semantic ranking)
|
|
143
|
+
const filter = {
|
|
144
|
+
must: [
|
|
145
|
+
{ key: 'tags', match: { value: 'session_cache' } },
|
|
146
|
+
{ key: 'tags', match: { value: this.sessionId } },
|
|
147
|
+
{ key: 'tags', match: { value: projectId } },
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
const results = await this.vectorStore.filterObservations(filter, limit);
|
|
151
|
+
return results.map((r) => this.resultToSessionEvent(r.payload as unknown as Record<string, unknown>, projectId));
|
|
152
|
+
} catch (err) {
|
|
153
|
+
logger.debug('SessionContextIndex.retrieve: failed (non-fatal)', { error: String(err) });
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build tiered context for a module using semantic search.
|
|
160
|
+
*
|
|
161
|
+
* Tier 0: Items tagged as critical/directive (importance >= 0.8) — always injected
|
|
162
|
+
* Tier 1: Top-K semantic matches for the module description — smart-injected
|
|
163
|
+
* Tier 2: Count of remaining items (awareness only)
|
|
164
|
+
*/
|
|
165
|
+
async buildTieredContext(
|
|
166
|
+
moduleDescription: string,
|
|
167
|
+
projectId: string,
|
|
168
|
+
): Promise<TieredContext> {
|
|
169
|
+
const defaultCtx: TieredContext = {
|
|
170
|
+
alwaysInject: [],
|
|
171
|
+
smartInjected: [],
|
|
172
|
+
searchableCount: 0,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// Tier 0: All session cache items for this project (we filter high-importance in memory)
|
|
177
|
+
const allFilter = {
|
|
178
|
+
must: [
|
|
179
|
+
{ key: 'tags', match: { value: 'session_cache' } },
|
|
180
|
+
{ key: 'tags', match: { value: this.sessionId } },
|
|
181
|
+
{ key: 'tags', match: { value: projectId } },
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
const allResults = await this.vectorStore.filterObservations(allFilter, 50);
|
|
185
|
+
|
|
186
|
+
// Extract high-importance items for Tier 0
|
|
187
|
+
const criticals = allResults.filter((r) => {
|
|
188
|
+
const payload = r.payload as unknown as Record<string, unknown>;
|
|
189
|
+
return (payload['importance'] as number) >= 0.8;
|
|
190
|
+
});
|
|
191
|
+
defaultCtx.alwaysInject = criticals.map((r) => {
|
|
192
|
+
const payload = r.payload as unknown as Record<string, unknown>;
|
|
193
|
+
return (payload['content'] as string) ?? '';
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Tier 1: Semantic search for this module's description
|
|
197
|
+
// Requires embedding the module description into a query vector
|
|
198
|
+
let smartResults: string[] = [];
|
|
199
|
+
if (moduleDescription) {
|
|
200
|
+
let queryVector: number[] | null = null;
|
|
201
|
+
try {
|
|
202
|
+
queryVector = await embedText(moduleDescription);
|
|
203
|
+
} catch {
|
|
204
|
+
// Embedding unavailable — skip semantic search
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (queryVector) {
|
|
208
|
+
const searchResults = await this.vectorStore.searchObservations(
|
|
209
|
+
queryVector,
|
|
210
|
+
{
|
|
211
|
+
limit: 5,
|
|
212
|
+
scoreThreshold: 0.3,
|
|
213
|
+
filter: {
|
|
214
|
+
must: [
|
|
215
|
+
{ key: 'tags', match: { value: 'session_cache' } },
|
|
216
|
+
{ key: 'tags', match: { value: this.sessionId } },
|
|
217
|
+
{ key: 'tags', match: { value: projectId } },
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
smartResults = searchResults.map((r) => {
|
|
223
|
+
const payload = r.payload as unknown as Record<string, unknown>;
|
|
224
|
+
return (payload['content'] as string) ?? '';
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
// Fallback: use the non-critical filter results as Tier 1
|
|
228
|
+
const nonCriticalItems = allResults.filter((r) => {
|
|
229
|
+
const payload = r.payload as unknown as Record<string, unknown>;
|
|
230
|
+
return (payload['importance'] as number) < 0.8;
|
|
231
|
+
});
|
|
232
|
+
smartResults = nonCriticalItems.slice(0, 5).map((r) => {
|
|
233
|
+
const payload = r.payload as unknown as Record<string, unknown>;
|
|
234
|
+
return (payload['content'] as string) ?? '';
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
defaultCtx.smartInjected = smartResults;
|
|
239
|
+
|
|
240
|
+
// Tier 2: Total count minus what we already injected
|
|
241
|
+
defaultCtx.searchableCount = Math.max(
|
|
242
|
+
0,
|
|
243
|
+
allResults.length - defaultCtx.alwaysInject.length - defaultCtx.smartInjected.length,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return defaultCtx;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
logger.debug('SessionContextIndex.buildTieredContext: failed (non-fatal)', { error: String(err) });
|
|
249
|
+
return defaultCtx;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* End-of-session cleanup: promote high-value items to permanent
|
|
255
|
+
* observations, let the rest expire naturally.
|
|
256
|
+
*/
|
|
257
|
+
async promoteAndExpire(): Promise<{ promoted: number; expired: number }> {
|
|
258
|
+
try {
|
|
259
|
+
const filter = {
|
|
260
|
+
must: [
|
|
261
|
+
{ key: 'tags', match: { value: 'session_cache' } },
|
|
262
|
+
{ key: 'tags', match: { value: this.sessionId } },
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
const all = await this.vectorStore.filterObservations(filter, 100);
|
|
266
|
+
|
|
267
|
+
let promoted = 0;
|
|
268
|
+
for (const item of all) {
|
|
269
|
+
const payload = item.payload as unknown as Record<string, unknown>;
|
|
270
|
+
const importance = (payload['importance'] as number) ?? 0;
|
|
271
|
+
// Promote items with high importance to permanent episodic memory
|
|
272
|
+
if (importance >= 0.7) {
|
|
273
|
+
await saveObservation(
|
|
274
|
+
{
|
|
275
|
+
content: (payload['content'] as string) ?? '',
|
|
276
|
+
sessionId: this.sessionId,
|
|
277
|
+
type: 'episodic',
|
|
278
|
+
category: (payload['category'] as string) ?? 'session_promoted',
|
|
279
|
+
importance,
|
|
280
|
+
tags: ((payload['tags'] as string[]) ?? []).filter(t => t !== 'session_cache'),
|
|
281
|
+
},
|
|
282
|
+
this.vectorStore,
|
|
283
|
+
noOpGraph,
|
|
284
|
+
);
|
|
285
|
+
promoted++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { promoted, expired: all.length - promoted };
|
|
290
|
+
} catch (err) {
|
|
291
|
+
logger.debug('SessionContextIndex.promoteAndExpire: failed (non-fatal)', { error: String(err) });
|
|
292
|
+
return { promoted: 0, expired: 0 };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Convert a raw Qdrant payload into a SessionEvent.
|
|
298
|
+
* Extracts type, phase, and agent from the tags array.
|
|
299
|
+
*/
|
|
300
|
+
private resultToSessionEvent(
|
|
301
|
+
payload: Record<string, unknown>,
|
|
302
|
+
projectId: string,
|
|
303
|
+
): SessionEvent {
|
|
304
|
+
const tags = (payload['tags'] as string[]) ?? [];
|
|
305
|
+
|
|
306
|
+
const eventType = tags.find((t): t is SessionEvent['type'] =>
|
|
307
|
+
(KNOWN_EVENT_TYPES as readonly string[]).includes(t),
|
|
308
|
+
) ?? 'phase_output';
|
|
309
|
+
|
|
310
|
+
const phase = tags.find((t) =>
|
|
311
|
+
(KNOWN_PHASES as readonly string[]).includes(t),
|
|
312
|
+
) ?? 'unknown';
|
|
313
|
+
|
|
314
|
+
// Agent is stored at index 4 in the tags array:
|
|
315
|
+
// ['session_cache', sessionId, projectId, phase, agent, type]
|
|
316
|
+
const agent = tags[4] ?? 'unknown';
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
type: eventType,
|
|
320
|
+
phase,
|
|
321
|
+
agent,
|
|
322
|
+
projectId,
|
|
323
|
+
content: (payload['content'] as string) ?? '',
|
|
324
|
+
created_at: (payload['created_at'] as number) ?? 0,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// SessionManager — B9
|
|
2
|
+
//
|
|
3
|
+
// Manages session lifecycle against the SQLite sessions table.
|
|
4
|
+
// Sessions are lightweight records that tie memory observations and
|
|
5
|
+
// phase transitions to a chronological window so the context injector can
|
|
6
|
+
// fetch "what was learned in this session" when assembling phase context.
|
|
7
|
+
//
|
|
8
|
+
// Design:
|
|
9
|
+
// - All operations are synchronous SQLite reads/writes (better-sqlite3).
|
|
10
|
+
// - Session IDs follow the format "session-{timestamp}-{random4}".
|
|
11
|
+
// - State is stored as a JSON blob in the `state` column; callers can
|
|
12
|
+
// stash arbitrary resumption data there.
|
|
13
|
+
// - observation_count is incremented via a single UPDATE statement so
|
|
14
|
+
// concurrent callers in the same process do not race.
|
|
15
|
+
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import type { PipelineDB } from '../storage/sqlite.js';
|
|
18
|
+
import type { Session, SessionRow, SessionState } from '../util/types.js';
|
|
19
|
+
import { logger } from '../util/logger.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Row deserialization helper
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function rowToSession(row: SessionRow): Session {
|
|
26
|
+
let state: SessionState = {};
|
|
27
|
+
try {
|
|
28
|
+
state = JSON.parse(row.state) as SessionState;
|
|
29
|
+
} catch {
|
|
30
|
+
state = {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
id: row.id,
|
|
35
|
+
projectId: row.project_id ?? null,
|
|
36
|
+
repoId: row.repo_id ?? null,
|
|
37
|
+
startedAt: row.started_at,
|
|
38
|
+
endedAt: row.ended_at ?? null,
|
|
39
|
+
state,
|
|
40
|
+
observationCount: row.observation_count,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// SessionManager
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export class SessionManager {
|
|
49
|
+
constructor(private readonly db: PipelineDB) {}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create and persist a new session.
|
|
53
|
+
* Returns the new session ID.
|
|
54
|
+
*/
|
|
55
|
+
startSession(repoId?: string, projectId?: string): string {
|
|
56
|
+
const id = `session-${Date.now()}-${randomUUID().slice(0, 4)}`;
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
|
|
59
|
+
this.db.run(
|
|
60
|
+
`INSERT INTO sessions (id, project_id, repo_id, started_at, ended_at, state, observation_count)
|
|
61
|
+
VALUES (?, ?, ?, ?, NULL, '{}', 0)`,
|
|
62
|
+
[id, projectId ?? null, repoId ?? null, now],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
logger.debug('SessionManager: session started', { id, repoId, projectId });
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Mark a session as ended. Idempotent — silently succeeds if already ended.
|
|
71
|
+
*/
|
|
72
|
+
endSession(sessionId: string): void {
|
|
73
|
+
this.db.run(
|
|
74
|
+
`UPDATE sessions SET ended_at = ? WHERE id = ? AND ended_at IS NULL`,
|
|
75
|
+
[Date.now(), sessionId],
|
|
76
|
+
);
|
|
77
|
+
logger.debug('SessionManager: session ended', { sessionId });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Retrieve a session by ID.
|
|
82
|
+
* Returns null if no session with that ID exists.
|
|
83
|
+
*/
|
|
84
|
+
getSession(sessionId: string): Session | null {
|
|
85
|
+
const row = this.db.get<SessionRow>(
|
|
86
|
+
`SELECT * FROM sessions WHERE id = ?`,
|
|
87
|
+
[sessionId],
|
|
88
|
+
);
|
|
89
|
+
if (!row) return null;
|
|
90
|
+
return rowToSession(row);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* List sessions, optionally filtered by repoId.
|
|
95
|
+
* Results are ordered most-recent first.
|
|
96
|
+
*/
|
|
97
|
+
listSessions(repoId?: string): Session[] {
|
|
98
|
+
let rows: SessionRow[];
|
|
99
|
+
if (repoId) {
|
|
100
|
+
rows = this.db.all<SessionRow>(
|
|
101
|
+
`SELECT * FROM sessions WHERE repo_id = ? ORDER BY started_at DESC`,
|
|
102
|
+
[repoId],
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
rows = this.db.all<SessionRow>(
|
|
106
|
+
`SELECT * FROM sessions ORDER BY started_at DESC`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return rows.map(rowToSession);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Overwrite the JSON state blob for a session.
|
|
114
|
+
* Callers use this to persist resumption context (current task, agent
|
|
115
|
+
* outputs, phase position) so the supervisor can restore a paused session.
|
|
116
|
+
*/
|
|
117
|
+
saveState(sessionId: string, state: Record<string, unknown>): void {
|
|
118
|
+
this.db.run(
|
|
119
|
+
`UPDATE sessions SET state = ? WHERE id = ?`,
|
|
120
|
+
[JSON.stringify(state), sessionId],
|
|
121
|
+
);
|
|
122
|
+
logger.debug('SessionManager: state saved', { sessionId });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Read the JSON state blob for a session.
|
|
127
|
+
* Returns null if the session does not exist.
|
|
128
|
+
*/
|
|
129
|
+
restoreState(sessionId: string): Record<string, unknown> | null {
|
|
130
|
+
const row = this.db.get<{ state: string }>(
|
|
131
|
+
`SELECT state FROM sessions WHERE id = ?`,
|
|
132
|
+
[sessionId],
|
|
133
|
+
);
|
|
134
|
+
if (!row) return null;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(row.state) as Record<string, unknown>;
|
|
137
|
+
} catch {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Increment the observation_count for a session by 1.
|
|
144
|
+
* Called by the observation save path each time a memory is stored.
|
|
145
|
+
*/
|
|
146
|
+
incrementObservations(sessionId: string): void {
|
|
147
|
+
this.db.run(
|
|
148
|
+
`UPDATE sessions SET observation_count = observation_count + 1 WHERE id = ?`,
|
|
149
|
+
[sessionId],
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|