openclaw-hybrid-memory 2026.5.310 → 2026.6.10
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/api/plugin-runtime.ts +2 -0
- package/backends/facts-db/contradictions.ts +1 -1
- package/cli/cmd-extract-directives.ts +225 -11
- package/cli/cmd-extract-proposals.ts +5 -6
- package/cli/cmd-extract-reinforcement.ts +71 -0
- package/cli/cmd-feedback.ts +15 -9
- package/cli/commands/manage/register-reflection-pipeline.ts +247 -13
- package/cli/commands/manage/register-storage-maintenance.ts +224 -15
- package/cli/commands/manage/storage-stats-helpers.ts +13 -2
- package/cli/context.ts +9 -19
- package/cli/distill.ts +31 -1
- package/cli/register.ts +28 -38
- package/dist/api/plugin-runtime.js.map +1 -1
- package/dist/backends/agent-health-store.js.map +1 -1
- package/dist/backends/apitap-store.js.map +1 -1
- package/dist/backends/audit-store.js.map +1 -1
- package/dist/backends/base-sqlite-store.js.map +1 -1
- package/dist/backends/cost-tracker.js.map +1 -1
- package/dist/backends/credentials-db.js +2 -3
- package/dist/backends/credentials-db.js.map +1 -1
- package/dist/backends/crystallization-store.js.map +1 -1
- package/dist/backends/edict-store.js.map +1 -1
- package/dist/backends/event-bus.js.map +1 -1
- package/dist/backends/event-log.js.map +1 -1
- package/dist/backends/facts-db/cache-manager.js.map +1 -1
- package/dist/backends/facts-db/clusters.js.map +1 -1
- package/dist/backends/facts-db/contradictions.js +1 -1
- package/dist/backends/facts-db/contradictions.js.map +1 -1
- package/dist/backends/facts-db/crud.js.map +1 -1
- package/dist/backends/facts-db/db-connection.js.map +1 -1
- package/dist/backends/facts-db/entity-autolink.js.map +1 -1
- package/dist/backends/facts-db/entity-layer.js.map +1 -1
- package/dist/backends/facts-db/episodes.js.map +1 -1
- package/dist/backends/facts-db/fact-queries.js.map +1 -1
- package/dist/backends/facts-db/fact-read-queries.js.map +1 -1
- package/dist/backends/facts-db/facts-db-layer1.js.map +1 -1
- package/dist/backends/facts-db/facts-db-layer2.js.map +1 -1
- package/dist/backends/facts-db/facts-db-layer3.js.map +1 -1
- package/dist/backends/facts-db/fts-text.js.map +1 -1
- package/dist/backends/facts-db/generated-skills/policy.js.map +1 -1
- package/dist/backends/facts-db/generated-skills.js.map +1 -1
- package/dist/backends/facts-db/housekeeping.js.map +1 -1
- package/dist/backends/facts-db/links.js.map +1 -1
- package/dist/backends/facts-db/maintenance.js.map +1 -1
- package/dist/backends/facts-db/procedures/crud.js.map +1 -1
- package/dist/backends/facts-db/procedures/internal.js.map +1 -1
- package/dist/backends/facts-db/procedures/promotion.js.map +1 -1
- package/dist/backends/facts-db/procedures/search.js.map +1 -1
- package/dist/backends/facts-db/procedures/stats.js.map +1 -1
- package/dist/backends/facts-db/reinforcement.js.map +1 -1
- package/dist/backends/facts-db/row-mapper.js.map +1 -1
- package/dist/backends/facts-db/scan-cursors.js.map +1 -1
- package/dist/backends/facts-db/schema-bootstrap.js.map +1 -1
- package/dist/backends/facts-db/scope-sql.js.map +1 -1
- package/dist/backends/facts-db/search.js.map +1 -1
- package/dist/backends/facts-db/stats.js.map +1 -1
- package/dist/backends/facts-db/types.js.map +1 -1
- package/dist/backends/facts-db/variants.js.map +1 -1
- package/dist/backends/identity-reflection-store.js.map +1 -1
- package/dist/backends/issue-store.js.map +1 -1
- package/dist/backends/learnings-db.js.map +1 -1
- package/dist/backends/migrations/facts-migrations.js.map +1 -1
- package/dist/backends/migrations/procedures.js.map +1 -1
- package/dist/backends/narratives-db.js.map +1 -1
- package/dist/backends/persona-state-store.js.map +1 -1
- package/dist/backends/proposals-db.js.map +1 -1
- package/dist/backends/scope-filter-sql.js.map +1 -1
- package/dist/backends/sqlite-schema-meta.js.map +1 -1
- package/dist/backends/tool-proposal-store.js.map +1 -1
- package/dist/backends/vector-db/constants.js.map +1 -1
- package/dist/backends/vector-db/path-utils.js.map +1 -1
- package/dist/backends/vector-db/runtime-locks.js.map +1 -1
- package/dist/backends/vector-db/vector-db-class.js.map +1 -1
- package/dist/backends/wal.js.map +1 -1
- package/dist/backends/workflow-store.js.map +1 -1
- package/dist/benchmark/shadow-eval.js.map +1 -1
- package/dist/cli/active-tasks.js.map +1 -1
- package/dist/cli/backup.js.map +1 -1
- package/dist/cli/benchmark.js.map +1 -1
- package/dist/cli/cmd-backfill.js.map +1 -1
- package/dist/cli/cmd-config.js.map +1 -1
- package/dist/cli/cmd-credentials.js.map +1 -1
- package/dist/cli/cmd-demo.js.map +1 -1
- package/dist/cli/cmd-distill.js.map +1 -1
- package/dist/cli/cmd-doctor.js.map +1 -1
- package/dist/cli/cmd-examples.js.map +1 -1
- package/dist/cli/cmd-extract-daily.js.map +1 -1
- package/dist/cli/cmd-extract-directives.js +141 -10
- package/dist/cli/cmd-extract-directives.js.map +1 -1
- package/dist/cli/cmd-extract-procedures.js.map +1 -1
- package/dist/cli/cmd-extract-proposals.js +3 -2
- package/dist/cli/cmd-extract-proposals.js.map +1 -1
- package/dist/cli/cmd-extract-reinforcement.js +39 -0
- package/dist/cli/cmd-extract-reinforcement.js.map +1 -1
- package/dist/cli/cmd-extract-sessions.js.map +1 -1
- package/dist/cli/cmd-feedback.js +9 -4
- package/dist/cli/cmd-feedback.js.map +1 -1
- package/dist/cli/cmd-health.js.map +1 -1
- package/dist/cli/cmd-providers.js.map +1 -1
- package/dist/cli/cmd-selfcorrection.js.map +1 -1
- package/dist/cli/cmd-setup.js.map +1 -1
- package/dist/cli/cmd-status.js.map +1 -1
- package/dist/cli/cmd-store.js.map +1 -1
- package/dist/cli/cmd-user-friendly.js.map +1 -1
- package/dist/cli/cmd-verify.js.map +1 -1
- package/dist/cli/commands/manage/bindings.js.map +1 -1
- package/dist/cli/commands/manage/dream-cycle-followup.js.map +1 -1
- package/dist/cli/commands/manage/maintenance-heartbeat.js.map +1 -1
- package/dist/cli/commands/manage/register-agents-audit-runall.js.map +1 -1
- package/dist/cli/commands/manage/register-analyze-maintenance-logs.js.map +1 -1
- package/dist/cli/commands/manage/register-budget-proposals.js.map +1 -1
- package/dist/cli/commands/manage/register-config-cli.js.map +1 -1
- package/dist/cli/commands/manage/register-corrections-and-pipeline.js.map +1 -1
- package/dist/cli/commands/manage/register-corrections.js.map +1 -1
- package/dist/cli/commands/manage/register-council.js.map +1 -1
- package/dist/cli/commands/manage/register-credentials-scope.js.map +1 -1
- package/dist/cli/commands/manage/register-digest.js.map +1 -1
- package/dist/cli/commands/manage/register-lifecycle.js.map +1 -1
- package/dist/cli/commands/manage/register-procedure-lifecycle.js.map +1 -1
- package/dist/cli/commands/manage/register-reconcile-cron-ledgers.js.map +1 -1
- package/dist/cli/commands/manage/register-reflection-pipeline.js +144 -7
- package/dist/cli/commands/manage/register-reflection-pipeline.js.map +1 -1
- package/dist/cli/commands/manage/register-self-correction-feedback.js.map +1 -1
- package/dist/cli/commands/manage/register-storage-and-stats.js.map +1 -1
- package/dist/cli/commands/manage/register-storage-entities-decay.js.map +1 -1
- package/dist/cli/commands/manage/register-storage-graph-audit.js.map +1 -1
- package/dist/cli/commands/manage/register-storage-maintenance.js +152 -9
- package/dist/cli/commands/manage/register-storage-maintenance.js.map +1 -1
- package/dist/cli/commands/manage/register-validate-cron-exit.js.map +1 -1
- package/dist/cli/commands/manage/storage-stats-helpers.js +10 -3
- package/dist/cli/commands/manage/storage-stats-helpers.js.map +1 -1
- package/dist/cli/commands/register-manage-commands.js.map +1 -1
- package/dist/cli/config-feature-summaries.js.map +1 -1
- package/dist/cli/config-output-sink.js.map +1 -1
- package/dist/cli/distill-session-jsonl.js.map +1 -1
- package/dist/cli/distill.js +10 -1
- package/dist/cli/distill.js.map +1 -1
- package/dist/cli/global-verbose.js.map +1 -1
- package/dist/cli/goals.js.map +1 -1
- package/dist/cli/hybrid-mem-commander-utils.js.map +1 -1
- package/dist/cli/install/config-merge.js.map +1 -1
- package/dist/cli/install/cron-jobs.js.map +1 -1
- package/dist/cli/install/embedding-detect.js.map +1 -1
- package/dist/cli/install/run-install.js.map +1 -1
- package/dist/cli/install/workspace.js.map +1 -1
- package/dist/cli/proposals.js.map +1 -1
- package/dist/cli/register.js.map +1 -1
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/task-queue-status.js.map +1 -1
- package/dist/cli/verified.js.map +1 -1
- package/dist/cli/verify/fact-count.js.map +1 -1
- package/dist/cli/verify/openclaw-config.js.map +1 -1
- package/dist/cli/verify/plugin-config-credentials.js.map +1 -1
- package/dist/cli/verify/sections/config-cron.js.map +1 -1
- package/dist/cli/verify/sections/embeddings.js.map +1 -1
- package/dist/cli/verify/sections/infrastructure.js.map +1 -1
- package/dist/cli/verify/sections/llm-models.js.map +1 -1
- package/dist/cli/verify/sections/reconcile.js.map +1 -1
- package/dist/cli/verify/verify-run-state.js.map +1 -1
- package/dist/cli/verify-llm-azure-auth.js.map +1 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/config/hybrid-schema.js.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/maintenance-fallback-policy.js.map +1 -1
- package/dist/config/parsers/capture.js.map +1 -1
- package/dist/config/parsers/core.js.map +1 -1
- package/dist/config/parsers/features.js.map +1 -1
- package/dist/config/parsers/index.js.map +1 -1
- package/dist/config/parsers/maintenance.js.map +1 -1
- package/dist/config/parsers/retrieval.js.map +1 -1
- package/dist/config/parsers/sensors.js.map +1 -1
- package/dist/config/skill-sections.js.map +1 -1
- package/dist/config/skill-size-limits.js.map +1 -1
- package/dist/config/types/agents.js.map +1 -1
- package/dist/config/types/bootstrap.js.map +1 -1
- package/dist/config/types/core.js.map +1 -1
- package/dist/config/types/index.js.map +1 -1
- package/dist/config/utils.js.map +1 -1
- package/dist/index-help.js.map +1 -1
- package/dist/index-testing-exports.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/hook-resolution-api.js.map +1 -1
- package/dist/lifecycle/hooks.js +0 -1
- package/dist/lifecycle/hooks.js.map +1 -1
- package/dist/lifecycle/resolve-agent-id.js.map +1 -1
- package/dist/lifecycle/session-state.js.map +1 -1
- package/dist/lifecycle/stage-active-task.js.map +1 -1
- package/dist/lifecycle/stage-auth-failure.js.map +1 -1
- package/dist/lifecycle/stage-capture/run-capture.js.map +1 -1
- package/dist/lifecycle/stage-capture.js.map +1 -1
- package/dist/lifecycle/stage-cleanup.js.map +1 -1
- package/dist/lifecycle/stage-credential-hint.js.map +1 -1
- package/dist/lifecycle/stage-frustration.js.map +1 -1
- package/dist/lifecycle/stage-goal-stewardship.js.map +1 -1
- package/dist/lifecycle/stage-goal-subagent.js.map +1 -1
- package/dist/lifecycle/stage-injection.js +1 -1
- package/dist/lifecycle/stage-injection.js.map +1 -1
- package/dist/lifecycle/stage-recall/run-recall.js.map +1 -1
- package/dist/lifecycle/stage-recall.js.map +1 -1
- package/dist/lifecycle/stage-setup.js.map +1 -1
- package/dist/routes/dashboard/collectors.js.map +1 -1
- package/dist/routes/dashboard/html.js.map +1 -1
- package/dist/routes/dashboard/server.js.map +1 -1
- package/dist/routes/dashboard-graph.js.map +1 -1
- package/dist/routes/graphql-resolvers.js.map +1 -1
- package/dist/routes/graphql-server.js.map +1 -1
- package/dist/services/active-task-checkpoint.js.map +1 -1
- package/dist/services/active-task-injection.js.map +1 -1
- package/dist/services/active-task.js.map +1 -1
- package/dist/services/adaptive-catch-up-pacing.js +25 -0
- package/dist/services/adaptive-catch-up-pacing.js.map +1 -0
- package/dist/services/adaptive-maintenance-llm.js.map +1 -1
- package/dist/services/adaptive-model-limits.js.map +1 -1
- package/dist/services/ambient-retrieval.js.map +1 -1
- package/dist/services/apitap-service.js.map +1 -1
- package/dist/services/audit-health-exit-info.js.map +1 -1
- package/dist/services/audit-health-json.js.map +1 -1
- package/dist/services/auth-failure-detect.js.map +1 -1
- package/dist/services/auto-capture.js.map +1 -1
- package/dist/services/auto-classifier.js.map +1 -1
- package/dist/services/auto-skills-audit.js.map +1 -1
- package/dist/services/bootstrap-optional.js.map +1 -1
- package/dist/services/bootstrap-priority.js.map +1 -1
- package/dist/services/bootstrap.js.map +1 -1
- package/dist/services/capture-provenance.js.map +1 -1
- package/dist/services/capture-utils.js.map +1 -1
- package/dist/services/chat.js +22 -3
- package/dist/services/chat.js.map +1 -1
- package/dist/services/classification-scope.js.map +1 -1
- package/dist/services/classification.js.map +1 -1
- package/dist/services/cli-sql-dump.js.map +1 -1
- package/dist/services/consolidation.js.map +1 -1
- package/dist/services/context-audit.js +1 -1
- package/dist/services/context-audit.js.map +1 -1
- package/dist/services/context-budget.js.map +1 -1
- package/dist/services/context-engine.js.map +1 -1
- package/dist/services/contextual-variants.js.map +1 -1
- package/dist/services/continuous-verifier.js.map +1 -1
- package/dist/services/contradiction-adjudicator.js.map +1 -1
- package/dist/services/cost-context.js.map +1 -1
- package/dist/services/cost-feature-labels.js.map +1 -1
- package/dist/services/credential-migration.js.map +1 -1
- package/dist/services/credential-scanner.js.map +1 -1
- package/dist/services/credential-validation.js.map +1 -1
- package/dist/services/cron-exit-validator.js.map +1 -1
- package/dist/services/cron-guard.js.map +1 -1
- package/dist/services/cron-job-bash-harness.js +52 -5
- package/dist/services/cron-job-bash-harness.js.map +1 -1
- package/dist/services/cron-maintenance-reconciler.js +1 -3
- package/dist/services/cron-maintenance-reconciler.js.map +1 -1
- package/dist/services/cross-agent-learning.js.map +1 -1
- package/dist/services/crystallization-proposer.js.map +1 -1
- package/dist/services/dedupe-policy.js.map +1 -1
- package/dist/services/deprecated-cron-commands.js.map +1 -1
- package/dist/services/directive-extract.js.map +1 -1
- package/dist/services/document-chunker.js.map +1 -1
- package/dist/services/document-grader.js.map +1 -1
- package/dist/services/dream-cycle.js.map +1 -1
- package/dist/services/embedding-migration.js.map +1 -1
- package/dist/services/embedding-registry.js.map +1 -1
- package/dist/services/embeddings/chain-provider.js.map +1 -1
- package/dist/services/embeddings/factory.js.map +1 -1
- package/dist/services/embeddings/fallback-provider.js.map +1 -1
- package/dist/services/embeddings/ollama-provider.js.map +1 -1
- package/dist/services/embeddings/onnx-provider.js.map +1 -1
- package/dist/services/embeddings/openai-provider.js.map +1 -1
- package/dist/services/embeddings/shared.js +3 -3
- package/dist/services/embeddings/shared.js.map +1 -1
- package/dist/services/embeddings/types.js.map +1 -1
- package/dist/services/entity-enrichment-adaptive.js +128 -0
- package/dist/services/entity-enrichment-adaptive.js.map +1 -0
- package/dist/services/entity-enrichment-cli.js +389 -42
- package/dist/services/entity-enrichment-cli.js.map +1 -1
- package/dist/services/entity-enrichment.js +31 -5
- package/dist/services/entity-enrichment.js.map +1 -1
- package/dist/services/error-reporter/noisy-errors.js.map +1 -1
- package/dist/services/error-reporter/sanitize.js.map +1 -1
- package/dist/services/error-reporter.js.map +1 -1
- package/dist/services/event-hub-repair.js.map +1 -1
- package/dist/services/export-memory.js.map +1 -1
- package/dist/services/fact-extraction.js.map +1 -1
- package/dist/services/feedback-effectiveness.js.map +1 -1
- package/dist/services/find-duplicates.js.map +1 -1
- package/dist/services/frustration-detector.js.map +1 -1
- package/dist/services/fts-search.js.map +1 -1
- package/dist/services/gap-detector.js.map +1 -1
- package/dist/services/generated-skill-lifecycle.js.map +1 -1
- package/dist/services/generated-skill-validation.js.map +1 -1
- package/dist/services/goal-active-task-mirror.js.map +1 -1
- package/dist/services/goal-circuit-breaker.js.map +1 -1
- package/dist/services/goal-health.js.map +1 -1
- package/dist/services/goal-registry.js.map +1 -1
- package/dist/services/goal-stewardship-heartbeat.js.map +1 -1
- package/dist/services/goal-stewardship-llm-triage.js.map +1 -1
- package/dist/services/goal-stewardship-verify-cron.js.map +1 -1
- package/dist/services/goal-stewardship.js.map +1 -1
- package/dist/services/goal-subagent.js.map +1 -1
- package/dist/services/graph-retrieval.js.map +1 -1
- package/dist/services/humanizer-score.js.map +1 -1
- package/dist/services/hybrid-mem-cron-default-job-steps.js.map +1 -1
- package/dist/services/hyde-helper.js.map +1 -1
- package/dist/services/identity-reflection.js.map +1 -1
- package/dist/services/implicit-feedback-extract.js.map +1 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/ingest-utils.js.map +1 -1
- package/dist/services/intent-template.js.map +1 -1
- package/dist/services/json-array-parser.js.map +1 -1
- package/dist/services/knowledge-gaps.js.map +1 -1
- package/dist/services/language-keywords-build.js.map +1 -1
- package/dist/services/lifecycle/github-adapter.js.map +1 -1
- package/dist/services/llm-rate-limit-headers.js +1 -2
- package/dist/services/llm-rate-limit-headers.js.map +1 -1
- package/dist/services/maintenance-auto-fix.js.map +1 -1
- package/dist/services/maintenance-log-analyzer.js +7 -1
- package/dist/services/maintenance-log-analyzer.js.map +1 -1
- package/dist/services/maintenance-timestamp.js.map +1 -1
- package/dist/services/memory-diagnostics.js.map +1 -1
- package/dist/services/memory-index.js.map +1 -1
- package/dist/services/merge-results.js.map +1 -1
- package/dist/services/model-capabilities.js.map +1 -1
- package/dist/services/model-pricing.js.map +1 -1
- package/dist/services/narrative-recall.js.map +1 -1
- package/dist/services/openclaw-session-artifact.js.map +1 -1
- package/dist/services/passive-observer.js.map +1 -1
- package/dist/services/pattern-detector-hash.js.map +1 -1
- package/dist/services/pattern-detector.js.map +1 -1
- package/dist/services/pending-autopilot/foundation.js.map +1 -1
- package/dist/services/pending-autopilot/redaction.js.map +1 -1
- package/dist/services/pending-autopilot/store.js.map +1 -1
- package/dist/services/pending-autopilot/types.js.map +1 -1
- package/dist/services/pending-digest-autopilot-cron.js.map +1 -1
- package/dist/services/pending-digest-autopilot.js.map +1 -1
- package/dist/services/pending-review-digest.js.map +1 -1
- package/dist/services/persona-proposal-triage.js.map +1 -1
- package/dist/services/persona-state-promotion.js.map +1 -1
- package/dist/services/post-compaction-recall.js.map +1 -1
- package/dist/services/pre-consolidation-flush.js.map +1 -1
- package/dist/services/pre-finalization-guard.js.map +1 -1
- package/dist/services/procedure-cluster.js.map +1 -1
- package/dist/services/procedure-extractor.js.map +1 -1
- package/dist/services/procedure-promotion/duplicate-skill-cache.js.map +1 -1
- package/dist/services/procedure-promotion-policy.js.map +1 -1
- package/dist/services/procedure-selection-metrics.js.map +1 -1
- package/dist/services/procedure-skill-eval.js.map +1 -1
- package/dist/services/procedure-skill-generator.js.map +1 -1
- package/dist/services/procedure-skill-recipe.js.map +1 -1
- package/dist/services/procedure-skill-shrink.js.map +1 -1
- package/dist/services/procedure-skill-workflow.js.map +1 -1
- package/dist/services/provenance.js.map +1 -1
- package/dist/services/public-export-bundle.js.map +1 -1
- package/dist/services/python-bridge.js.map +1 -1
- package/dist/services/query-expander.js.map +1 -1
- package/dist/services/query-validator.js.map +1 -1
- package/dist/services/recall-pipeline.js.map +1 -1
- package/dist/services/recall-timing.js.map +1 -1
- package/dist/services/recent-http-attempts.js.map +1 -1
- package/dist/services/reflection/shared.js.map +1 -1
- package/dist/services/reflection.js.map +1 -1
- package/dist/services/reinforcement-extract.js.map +1 -1
- package/dist/services/reranker.js.map +1 -1
- package/dist/services/responses-adapter.js.map +1 -1
- package/dist/services/retrieval-aliases.js.map +1 -1
- package/dist/services/retrieval-mode-policy.js.map +1 -1
- package/dist/services/retrieval-orchestrator/packing.js.map +1 -1
- package/dist/services/retrieval-orchestrator.d.ts +2 -3
- package/dist/services/retrieval-orchestrator.js.map +1 -1
- package/dist/services/rrf-fusion.js.map +1 -1
- package/dist/services/self-correction-extract.js.map +1 -1
- package/dist/services/session-observability.js.map +1 -1
- package/dist/services/session-pre-filter.js.map +1 -1
- package/dist/services/shortest-path.js.map +1 -1
- package/dist/services/skill-allowed-tools.js.map +1 -1
- package/dist/services/skill-creator-validator.js.map +1 -1
- package/dist/services/skill-crystallizer-helpers.js.map +1 -1
- package/dist/services/skill-crystallizer.js.map +1 -1
- package/dist/services/skill-description-builder.js.map +1 -1
- package/dist/services/skill-eval-synthesizer.js.map +1 -1
- package/dist/services/skill-examples-builder.js.map +1 -1
- package/dist/services/skill-frontmatter.js.map +1 -1
- package/dist/services/skill-name-validator.js.map +1 -1
- package/dist/services/skill-prompt-injection.js.map +1 -1
- package/dist/services/skill-reference-sidecar.js.map +1 -1
- package/dist/services/skill-script-bundler.js.map +1 -1
- package/dist/services/skill-validator.js.map +1 -1
- package/dist/services/startup-memory-attribution.js.map +1 -1
- package/dist/services/task-hygiene.js.map +1 -1
- package/dist/services/task-ledger/canonical.js.map +1 -1
- package/dist/services/task-ledger-facts.js.map +1 -1
- package/dist/services/task-queue-leases.js.map +1 -1
- package/dist/services/task-queue-watchdog.js.map +1 -1
- package/dist/services/tool-effectiveness.js.map +1 -1
- package/dist/services/tool-proposer.js.map +1 -1
- package/dist/services/tools-md-section.js.map +1 -1
- package/dist/services/topic-clusters.js.map +1 -1
- package/dist/services/trajectory-tracker.js.map +1 -1
- package/dist/services/vector-backend-observability.js.map +1 -1
- package/dist/services/vector-lifecycle-audit.js.map +1 -1
- package/dist/services/vector-maintenance.js.map +1 -1
- package/dist/services/vector-search.js.map +1 -1
- package/dist/services/verification-store.js.map +1 -1
- package/dist/services/verified-fact-triage.js.map +1 -1
- package/dist/services/wal-helpers.js.map +1 -1
- package/dist/services/workflow-tracker.js.map +1 -1
- package/dist/setup/bootstrap-databases.js.map +1 -1
- package/dist/setup/cli-context/cli-services.js.map +1 -1
- package/dist/setup/cli-context/help-text.js.map +1 -1
- package/dist/setup/cli-context/metadata.js.map +1 -1
- package/dist/setup/cli-context/register-cli-with-help.js.map +1 -1
- package/dist/setup/cli-context/register-full.js.map +1 -1
- package/dist/setup/cli-context/register-help.js.map +1 -1
- package/dist/setup/cost-instrumentation.js.map +1 -1
- package/dist/setup/hybrid-memory-generation-state.js.map +1 -1
- package/dist/setup/hybrid-memory-reload-coordinator.js +13 -13
- package/dist/setup/hybrid-memory-reload-coordinator.js.map +1 -1
- package/dist/setup/plugin-service.js.map +1 -1
- package/dist/setup/provider-router.js.map +1 -1
- package/dist/setup/register-context-engine.js.map +1 -1
- package/dist/setup/register-hooks.js.map +1 -1
- package/dist/setup/register-plugin.js +25 -21
- package/dist/setup/register-plugin.js.map +1 -1
- package/dist/setup/register-tools.js.map +1 -1
- package/dist/setup/reregister-policy.js +2 -2
- package/dist/setup/reregister-policy.js.map +1 -1
- package/dist/setup/tool-installers.js.map +1 -1
- package/dist/setup/workspace-bootstrap.js.map +1 -1
- package/dist/src/worker/narratives.js.map +1 -1
- package/dist/tools/apitap-tools.js.map +1 -1
- package/dist/tools/credential-tools.js.map +1 -1
- package/dist/tools/crystallization-tools.js.map +1 -1
- package/dist/tools/dashboard-routes.js.map +1 -1
- package/dist/tools/document-tools.js.map +1 -1
- package/dist/tools/goal-tools.js.map +1 -1
- package/dist/tools/graph-tools.js.map +1 -1
- package/dist/tools/issue-tools.js.map +1 -1
- package/dist/tools/memory/build-runtime.js.map +1 -1
- package/dist/tools/memory/helpers.js.map +1 -1
- package/dist/tools/memory/register-checkpoint-tools.js.map +1 -1
- package/dist/tools/memory/register-directory-tools.js.map +1 -1
- package/dist/tools/memory/register-edict-tools.js.map +1 -1
- package/dist/tools/memory/register-episode-tools.js.map +1 -1
- package/dist/tools/memory/register-recall-tools.js.map +1 -1
- package/dist/tools/memory/register-store-tools.js.map +1 -1
- package/dist/tools/memory-tools.js.map +1 -1
- package/dist/tools/persona-tools.js.map +1 -1
- package/dist/tools/provenance-tools.js.map +1 -1
- package/dist/tools/public-api-routes.js.map +1 -1
- package/dist/tools/safe-register-http-route.js.map +1 -1
- package/dist/tools/self-extension-tools.js.map +1 -1
- package/dist/tools/task-hygiene-tools.js.map +1 -1
- package/dist/tools/utility-tools.js.map +1 -1
- package/dist/tools/verification-tools.js.map +1 -1
- package/dist/tools/workflow-tools.js.map +1 -1
- package/dist/types/issue-types.js.map +1 -1
- package/dist/types/learnings-types.js.map +1 -1
- package/dist/types/memory.js.map +1 -1
- package/dist/utils/apim-gateway-fetch.js.map +1 -1
- package/dist/utils/atomic-write.js.map +1 -1
- package/dist/utils/auth-failover.js.map +1 -1
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/compaction-model-watchdog.js.map +1 -1
- package/dist/utils/consolidation-controls.js.map +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/date-detector.js.map +1 -1
- package/dist/utils/dates.js.map +1 -1
- package/dist/utils/decay.js.map +1 -1
- package/dist/utils/duration.js.map +1 -1
- package/dist/utils/embed-call.js.map +1 -1
- package/dist/utils/entity-lookup-resolve.js.map +1 -1
- package/dist/utils/entity-mention-quality.js.map +1 -1
- package/dist/utils/entity-stopwords.js.map +1 -1
- package/dist/utils/env-manager.js.map +1 -1
- package/dist/utils/error-tracking.js.map +1 -1
- package/dist/utils/event-loop-yield.js.map +1 -1
- package/dist/utils/extract-last-user-message.js.map +1 -1
- package/dist/utils/extraction-from-template.js.map +1 -1
- package/dist/utils/fact-embeddings.js.map +1 -1
- package/dist/utils/file-snapshot.js.map +1 -1
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/gh-repo-arg.js.map +1 -1
- package/dist/utils/hybrid-mem-json-cli.js.map +1 -1
- package/dist/utils/language-keywords.js.map +1 -1
- package/dist/utils/lifecycle-generation.js.map +1 -1
- package/dist/utils/llm-json-array.js.map +1 -1
- package/dist/utils/llm-selection.js.map +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/model-provider-family.js.map +1 -1
- package/dist/utils/model-tier.js.map +1 -1
- package/dist/utils/openclaw-agent-defaults.js.map +1 -1
- package/dist/utils/path.js.map +1 -1
- package/dist/utils/plugin-root.js.map +1 -1
- package/dist/utils/plugin-update-check.js.map +1 -1
- package/dist/utils/procedure-risk.js.map +1 -1
- package/dist/utils/progress-indicators.js.map +1 -1
- package/dist/utils/prompt-loader.js.map +1 -1
- package/dist/utils/provenance.js.map +1 -1
- package/dist/utils/provider-detection.js.map +1 -1
- package/dist/utils/registration-superseded.js.map +1 -1
- package/dist/utils/salience.js.map +1 -1
- package/dist/utils/sanitize-messages.js.map +1 -1
- package/dist/utils/scope-filter.js.map +1 -1
- package/dist/utils/skill-discovery.js.map +1 -1
- package/dist/utils/sqlite-file-perms.js.map +1 -1
- package/dist/utils/sqlite-outcome-compat.js.map +1 -1
- package/dist/utils/sqlite-transaction.js.map +1 -1
- package/dist/utils/stable-stringify.js.map +1 -1
- package/dist/utils/subagent-ended-utils.js.map +1 -1
- package/dist/utils/tags.js.map +1 -1
- package/dist/utils/text.js.map +1 -1
- package/dist/utils/timeout.js.map +1 -1
- package/dist/utils/typebox.js.map +1 -1
- package/dist/utils/version-check.js.map +1 -1
- package/dist/utils/wal-replay.js.map +1 -1
- package/dist/versionInfo.js.map +1 -1
- package/index.ts +2 -2
- package/lifecycle/hooks.ts +0 -1
- package/npm-shrinkwrap.json +487 -186
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/services/adaptive-catch-up-pacing.ts +28 -0
- package/services/chat.ts +34 -1
- package/services/cron-job-bash-harness.ts +52 -5
- package/services/embeddings/shared.ts +5 -2
- package/services/entity-enrichment-adaptive.ts +245 -0
- package/services/entity-enrichment-cli.ts +553 -47
- package/services/entity-enrichment.ts +43 -2
- package/services/llm-rate-limit-headers.ts +1 -4
- package/services/maintenance-log-analyzer.ts +13 -9
- package/services/reinforcement-extract.ts +19 -0
- package/setup/hybrid-memory-reload-coordinator.ts +26 -0
- package/setup/register-plugin.ts +62 -32
- package/setup/reregister-policy.ts +10 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-sql-dump.js","names":[],"sources":["../../services/cli-sql-dump.ts"],"sourcesContent":["/**\n * Read-only SQLite table dumps for `hybrid-mem dump` (debug / inspection).\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\n\n/** User-facing type -> physical table */\nconst TYPE_TO_TABLE: Record<string, string> = {\n fact_entity: \"fact_entity_mentions\",\n fact_entities: \"fact_entity_mentions\",\n entity_mentions: \"fact_entity_mentions\",\n fact_entity_mentions: \"fact_entity_mentions\",\n fem: \"fact_entity_mentions\",\n org: \"organizations\",\n orgs: \"organizations\",\n organizations: \"organizations\",\n contact: \"contacts\",\n contacts: \"contacts\",\n org_fact_links: \"org_fact_links\",\n org_links: \"org_fact_links\",\n facts: \"facts\",\n links: \"memory_links\",\n memory_links: \"memory_links\",\n procedures: \"procedures\",\n contradictions: \"contradictions\",\n clusters: \"clusters\",\n cluster_members: \"cluster_members\",\n episodes: \"episodes\",\n episode_relations: \"episode_relations\",\n recall_log: \"recall_log\",\n implicit_signals: \"implicit_signals\",\n reinforcement_log: \"reinforcement_log\",\n scan_cursors: \"scan_cursors\",\n embedding_meta: \"embedding_meta\",\n feedback_trajectories: \"feedback_trajectories\",\n trajectories: \"feedback_trajectories\",\n feedback_effectiveness: \"feedback_effectiveness\",\n};\n\nconst PREFERRED_ORDER: Record<string, string> = {\n fact_entity_mentions: \"created_at\",\n organizations: \"updated_at\",\n contacts: \"updated_at\",\n org_fact_links: \"created_at\",\n facts: \"created_at\",\n memory_links: \"created_at\",\n procedures: \"updated_at\",\n contradictions: \"detected_at\",\n clusters: \"updated_at\",\n cluster_members: \"rowid\",\n episodes: \"created_at\",\n episode_relations: \"created_at\",\n recall_log: \"occurred_at\",\n implicit_signals: \"created_at\",\n reinforcement_log: \"occurred_at\",\n scan_cursors: \"last_run_at\",\n embedding_meta: \"updated_at\",\n feedback_trajectories: \"created_at\",\n feedback_effectiveness: \"measured_at\",\n};\n\nconst TRUNCATE_TEXT_TABLES = new Set([\"facts\", \"procedures\"]);\n\nfunction assertSafeIdent(name: string): string {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n throw new Error(`Invalid SQL identifier: ${name}`);\n }\n return name;\n}\n\nfunction tableColumns(db: DatabaseSync, table: string): Set<string> {\n const safe = assertSafeIdent(table);\n const rows = db.prepare(`PRAGMA table_info(${safe})`).all() as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\nfunction resolveOrderColumn(db: DatabaseSync, table: string): string {\n const preferred = PREFERRED_ORDER[table] ?? \"created_at\";\n const cols = tableColumns(db, table);\n if (preferred === \"rowid\") return \"rowid\";\n if (cols.has(preferred)) return preferred;\n if (cols.has(\"created_at\")) return \"created_at\";\n if (cols.has(\"updated_at\")) return \"updated_at\";\n if (cols.has(\"occurred_at\")) return \"occurred_at\";\n if (cols.has(\"timestamp\")) return \"timestamp\";\n return \"rowid\";\n}\n\nexport function resolveDumpTableType(type: string): string | null {\n const k = type.trim().toLowerCase().replace(/-/g, \"_\");\n return TYPE_TO_TABLE[k] ?? null;\n}\n\nexport function listDumpTypeAliases(): string[] {\n const out = new Set<string>();\n for (const [alias, table] of Object.entries(TYPE_TO_TABLE)) {\n out.add(alias);\n out.add(table);\n }\n return [...out].sort((a, b) => a.localeCompare(b));\n}\n\nexport type SqlDumpOrder = \"first\" | \"last\";\n\nexport type SqlDumpResult = { ok: true; table: string; rows: Record<string, unknown>[] } | { ok: false; error: string };\n\nfunction truncateValue(key: string, value: unknown, table: string, maxLen: number): unknown {\n if (typeof value !== \"string\") return value;\n if (!TRUNCATE_TEXT_TABLES.has(table)) return value;\n const textKeys =\n table === \"facts\"\n ? [\"text\", \"why\", \"key\", \"value\"]\n : table === \"procedures\"\n ? [\"task_pattern\", \"recipe_json\", \"source_sessions\"]\n : [];\n if (!textKeys.includes(key)) return value;\n if (value.length <= maxLen) return value;\n return `${value.slice(0, maxLen)}… (${value.length} chars)`;\n}\n\nexport function runSqliteTableDump(\n db: DatabaseSync,\n opts: { type: string; limit: number; order: SqlDumpOrder; json: boolean },\n): SqlDumpResult {\n const rawLimit = Number.isFinite(opts.limit) ? opts.limit : 20;\n const limit = Math.max(1, Math.min(5_000, Math.floor(rawLimit)));\n const table = resolveDumpTableType(opts.type);\n if (!table) {\n return {\n ok: false,\n error: `Unknown --type \"${opts.type}\". Use --list-types or see hybrid-mem dump --help.`,\n };\n }\n\n const exists = db.prepare(\"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?\").get(table) as\n | { 1?: number }\n | undefined;\n if (!exists) {\n return { ok: false, error: `Table \"${table}\" does not exist in this database (feature may be unused yet).` };\n }\n\n const orderCol = resolveOrderColumn(db, table);\n const dir = opts.order === \"last\" ? \"DESC\" : \"ASC\";\n const qTable = assertSafeIdent(table);\n const qOrder = orderCol === \"rowid\" ? \"rowid\" : assertSafeIdent(orderCol);\n\n const rows = db.prepare(`SELECT * FROM ${qTable} ORDER BY ${qOrder} ${dir} LIMIT ?`).all(limit) as Record<\n string,\n unknown\n >[];\n\n if (opts.json) {\n return { ok: true, table, rows };\n }\n\n const maxLen = 240;\n const display = rows.map((row) => {\n const o: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(row)) {\n o[k] = truncateValue(k, v, table, maxLen);\n }\n return o;\n });\n return { ok: true, table, rows: display };\n}\n"],"mappings":";;AAOA,MAAM,gBAAwC;CAC5C,aAAa;CACb,eAAe;CACf,iBAAiB;CACjB,sBAAsB;CACtB,KAAK;CACL,KAAK;CACL,MAAM;CACN,eAAe;CACf,SAAS;CACT,UAAU;CACV,gBAAgB;CAChB,WAAW;CACX,OAAO;CACP,OAAO;CACP,cAAc;CACd,YAAY;CACZ,gBAAgB;CAChB,UAAU;CACV,iBAAiB;CACjB,UAAU;CACV,mBAAmB;CACnB,YAAY;CACZ,kBAAkB;CAClB,mBAAmB;CACnB,cAAc;CACd,gBAAgB;CAChB,uBAAuB;CACvB,cAAc;CACd,wBAAwB;
|
|
1
|
+
{"version":3,"file":"cli-sql-dump.js","names":[],"sources":["../../services/cli-sql-dump.ts"],"sourcesContent":["/**\n * Read-only SQLite table dumps for `hybrid-mem dump` (debug / inspection).\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\n\n/** User-facing type -> physical table */\nconst TYPE_TO_TABLE: Record<string, string> = {\n fact_entity: \"fact_entity_mentions\",\n fact_entities: \"fact_entity_mentions\",\n entity_mentions: \"fact_entity_mentions\",\n fact_entity_mentions: \"fact_entity_mentions\",\n fem: \"fact_entity_mentions\",\n org: \"organizations\",\n orgs: \"organizations\",\n organizations: \"organizations\",\n contact: \"contacts\",\n contacts: \"contacts\",\n org_fact_links: \"org_fact_links\",\n org_links: \"org_fact_links\",\n facts: \"facts\",\n links: \"memory_links\",\n memory_links: \"memory_links\",\n procedures: \"procedures\",\n contradictions: \"contradictions\",\n clusters: \"clusters\",\n cluster_members: \"cluster_members\",\n episodes: \"episodes\",\n episode_relations: \"episode_relations\",\n recall_log: \"recall_log\",\n implicit_signals: \"implicit_signals\",\n reinforcement_log: \"reinforcement_log\",\n scan_cursors: \"scan_cursors\",\n embedding_meta: \"embedding_meta\",\n feedback_trajectories: \"feedback_trajectories\",\n trajectories: \"feedback_trajectories\",\n feedback_effectiveness: \"feedback_effectiveness\",\n};\n\nconst PREFERRED_ORDER: Record<string, string> = {\n fact_entity_mentions: \"created_at\",\n organizations: \"updated_at\",\n contacts: \"updated_at\",\n org_fact_links: \"created_at\",\n facts: \"created_at\",\n memory_links: \"created_at\",\n procedures: \"updated_at\",\n contradictions: \"detected_at\",\n clusters: \"updated_at\",\n cluster_members: \"rowid\",\n episodes: \"created_at\",\n episode_relations: \"created_at\",\n recall_log: \"occurred_at\",\n implicit_signals: \"created_at\",\n reinforcement_log: \"occurred_at\",\n scan_cursors: \"last_run_at\",\n embedding_meta: \"updated_at\",\n feedback_trajectories: \"created_at\",\n feedback_effectiveness: \"measured_at\",\n};\n\nconst TRUNCATE_TEXT_TABLES = new Set([\"facts\", \"procedures\"]);\n\nfunction assertSafeIdent(name: string): string {\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n throw new Error(`Invalid SQL identifier: ${name}`);\n }\n return name;\n}\n\nfunction tableColumns(db: DatabaseSync, table: string): Set<string> {\n const safe = assertSafeIdent(table);\n const rows = db.prepare(`PRAGMA table_info(${safe})`).all() as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\nfunction resolveOrderColumn(db: DatabaseSync, table: string): string {\n const preferred = PREFERRED_ORDER[table] ?? \"created_at\";\n const cols = tableColumns(db, table);\n if (preferred === \"rowid\") return \"rowid\";\n if (cols.has(preferred)) return preferred;\n if (cols.has(\"created_at\")) return \"created_at\";\n if (cols.has(\"updated_at\")) return \"updated_at\";\n if (cols.has(\"occurred_at\")) return \"occurred_at\";\n if (cols.has(\"timestamp\")) return \"timestamp\";\n return \"rowid\";\n}\n\nexport function resolveDumpTableType(type: string): string | null {\n const k = type.trim().toLowerCase().replace(/-/g, \"_\");\n return TYPE_TO_TABLE[k] ?? null;\n}\n\nexport function listDumpTypeAliases(): string[] {\n const out = new Set<string>();\n for (const [alias, table] of Object.entries(TYPE_TO_TABLE)) {\n out.add(alias);\n out.add(table);\n }\n return [...out].sort((a, b) => a.localeCompare(b));\n}\n\nexport type SqlDumpOrder = \"first\" | \"last\";\n\nexport type SqlDumpResult = { ok: true; table: string; rows: Record<string, unknown>[] } | { ok: false; error: string };\n\nfunction truncateValue(key: string, value: unknown, table: string, maxLen: number): unknown {\n if (typeof value !== \"string\") return value;\n if (!TRUNCATE_TEXT_TABLES.has(table)) return value;\n const textKeys =\n table === \"facts\"\n ? [\"text\", \"why\", \"key\", \"value\"]\n : table === \"procedures\"\n ? [\"task_pattern\", \"recipe_json\", \"source_sessions\"]\n : [];\n if (!textKeys.includes(key)) return value;\n if (value.length <= maxLen) return value;\n return `${value.slice(0, maxLen)}… (${value.length} chars)`;\n}\n\nexport function runSqliteTableDump(\n db: DatabaseSync,\n opts: { type: string; limit: number; order: SqlDumpOrder; json: boolean },\n): SqlDumpResult {\n const rawLimit = Number.isFinite(opts.limit) ? opts.limit : 20;\n const limit = Math.max(1, Math.min(5_000, Math.floor(rawLimit)));\n const table = resolveDumpTableType(opts.type);\n if (!table) {\n return {\n ok: false,\n error: `Unknown --type \"${opts.type}\". Use --list-types or see hybrid-mem dump --help.`,\n };\n }\n\n const exists = db.prepare(\"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?\").get(table) as\n | { 1?: number }\n | undefined;\n if (!exists) {\n return { ok: false, error: `Table \"${table}\" does not exist in this database (feature may be unused yet).` };\n }\n\n const orderCol = resolveOrderColumn(db, table);\n const dir = opts.order === \"last\" ? \"DESC\" : \"ASC\";\n const qTable = assertSafeIdent(table);\n const qOrder = orderCol === \"rowid\" ? \"rowid\" : assertSafeIdent(orderCol);\n\n const rows = db.prepare(`SELECT * FROM ${qTable} ORDER BY ${qOrder} ${dir} LIMIT ?`).all(limit) as Record<\n string,\n unknown\n >[];\n\n if (opts.json) {\n return { ok: true, table, rows };\n }\n\n const maxLen = 240;\n const display = rows.map((row) => {\n const o: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(row)) {\n o[k] = truncateValue(k, v, table, maxLen);\n }\n return o;\n });\n return { ok: true, table, rows: display };\n}\n"],"mappings":";;AAOA,MAAM,gBAAwC;CAC5C,aAAa;CACb,eAAe;CACf,iBAAiB;CACjB,sBAAsB;CACtB,KAAK;CACL,KAAK;CACL,MAAM;CACN,eAAe;CACf,SAAS;CACT,UAAU;CACV,gBAAgB;CAChB,WAAW;CACX,OAAO;CACP,OAAO;CACP,cAAc;CACd,YAAY;CACZ,gBAAgB;CAChB,UAAU;CACV,iBAAiB;CACjB,UAAU;CACV,mBAAmB;CACnB,YAAY;CACZ,kBAAkB;CAClB,mBAAmB;CACnB,cAAc;CACd,gBAAgB;CAChB,uBAAuB;CACvB,cAAc;CACd,wBAAwB;AAC1B;AAEA,MAAM,kBAA0C;CAC9C,sBAAsB;CACtB,eAAe;CACf,UAAU;CACV,gBAAgB;CAChB,OAAO;CACP,cAAc;CACd,YAAY;CACZ,gBAAgB;CAChB,UAAU;CACV,iBAAiB;CACjB,UAAU;CACV,mBAAmB;CACnB,YAAY;CACZ,kBAAkB;CAClB,mBAAmB;CACnB,cAAc;CACd,gBAAgB;CAChB,uBAAuB;CACvB,wBAAwB;AAC1B;AAEA,MAAM,uBAAuB,IAAI,IAAI,CAAC,SAAS,YAAY,CAAC;AAE5D,SAAS,gBAAgB,MAAsB;CAC7C,IAAI,CAAC,2BAA2B,KAAK,IAAI,GACvC,MAAM,IAAI,MAAM,2BAA2B,MAAM;CAEnD,OAAO;AACT;AAEA,SAAS,aAAa,IAAkB,OAA4B;CAClE,MAAM,OAAO,gBAAgB,KAAK;CAClC,MAAM,OAAO,GAAG,QAAQ,qBAAqB,KAAK,EAAE,EAAE,IAAI;CAC1D,OAAO,IAAI,IAAI,KAAK,KAAK,MAAM,EAAE,IAAI,CAAC;AACxC;AAEA,SAAS,mBAAmB,IAAkB,OAAuB;CACnE,MAAM,YAAY,gBAAgB,UAAU;CAC5C,MAAM,OAAO,aAAa,IAAI,KAAK;CACnC,IAAI,cAAc,SAAS,OAAO;CAClC,IAAI,KAAK,IAAI,SAAS,GAAG,OAAO;CAChC,IAAI,KAAK,IAAI,YAAY,GAAG,OAAO;CACnC,IAAI,KAAK,IAAI,YAAY,GAAG,OAAO;CACnC,IAAI,KAAK,IAAI,aAAa,GAAG,OAAO;CACpC,IAAI,KAAK,IAAI,WAAW,GAAG,OAAO;CAClC,OAAO;AACT;AAEA,SAAgB,qBAAqB,MAA6B;CAEhE,OAAO,cADG,KAAK,KAAK,EAAE,YAAY,EAAE,QAAQ,MAAM,GAC7B,MAAM;AAC7B;AAEA,SAAgB,sBAAgC;CAC9C,MAAM,sBAAM,IAAI,IAAY;CAC5B,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,aAAa,GAAG;EAC1D,IAAI,IAAI,KAAK;EACb,IAAI,IAAI,KAAK;CACf;CACA,OAAO,CAAC,GAAG,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AACnD;AAMA,SAAS,cAAc,KAAa,OAAgB,OAAe,QAAyB;CAC1F,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,CAAC,qBAAqB,IAAI,KAAK,GAAG,OAAO;CAO7C,IAAI,EALF,UAAU,UACN;EAAC;EAAQ;EAAO;EAAO;CAAO,IAC9B,UAAU,eACR;EAAC;EAAgB;EAAe;CAAiB,IACjD,CAAC,GACK,SAAS,GAAG,GAAG,OAAO;CACpC,IAAI,MAAM,UAAU,QAAQ,OAAO;CACnC,OAAO,GAAG,MAAM,MAAM,GAAG,MAAM,EAAE,KAAK,MAAM,OAAO;AACrD;AAEA,SAAgB,mBACd,IACA,MACe;CACf,MAAM,WAAW,OAAO,SAAS,KAAK,KAAK,IAAI,KAAK,QAAQ;CAC5D,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,KAAO,KAAK,MAAM,QAAQ,CAAC,CAAC;CAC/D,MAAM,QAAQ,qBAAqB,KAAK,IAAI;CAC5C,IAAI,CAAC,OACH,OAAO;EACL,IAAI;EACJ,OAAO,mBAAmB,KAAK,KAAK;CACtC;CAMF,IAAI,CAHW,GAAG,QAAQ,+DAA+D,EAAE,IAAI,KAGrF,GACR,OAAO;EAAE,IAAI;EAAO,OAAO,UAAU,MAAM;CAAgE;CAG7G,MAAM,WAAW,mBAAmB,IAAI,KAAK;CAC7C,MAAM,MAAM,KAAK,UAAU,SAAS,SAAS;CAC7C,MAAM,SAAS,gBAAgB,KAAK;CACpC,MAAM,SAAS,aAAa,UAAU,UAAU,gBAAgB,QAAQ;CAExE,MAAM,OAAO,GAAG,QAAQ,iBAAiB,OAAO,YAAY,OAAO,GAAG,IAAI,SAAS,EAAE,IAAI,KAAK;CAK9F,IAAI,KAAK,MACP,OAAO;EAAE,IAAI;EAAM;EAAO;CAAK;CAGjC,MAAM,SAAS;CAQf,OAAO;EAAE,IAAI;EAAM;EAAO,MAPV,KAAK,KAAK,QAAQ;GAChC,MAAM,IAA6B,CAAC;GACpC,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,GAAG,GACrC,EAAE,KAAK,cAAc,GAAG,GAAG,OAAO,MAAM;GAE1C,OAAO;EACT,CACsC;CAAE;AAC1C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consolidation.js","names":[],"sources":["../../services/consolidation.ts"],"sourcesContent":["/**\n * Consolidation service (2.4): find clusters of similar facts (by embedding),\n * merge each cluster with LLM, store one fact and delete cluster.\n *\n * Uses SQLite as source; re-embeds to compute similarity (no Lance scan).\n * Merged fact is stored in both SQLite and Lance.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type OpenAI from \"openai\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport type { VectorDB } from \"../backends/vector-db.js\";\nimport type { MemoryCategory, MemoryEntry } from \"../types/memory.js\";\nimport { CONSOLIDATED_FACT_DECAY_CLASS } from \"../utils/consolidation-controls.js\";\nimport { BATCH_STORE_IMPORTANCE, CONSOLIDATION_MERGE_MAX_CHARS } from \"../utils/constants.js\";\nimport { fillPrompt, loadPrompt } from \"../utils/prompt-loader.js\";\nimport { SENSITIVE_PATTERNS } from \"./auto-capture.js\";\nimport { withCostFeature } from \"./cost-context.js\";\nimport type { EmbeddingProvider } from \"./embeddings.js\";\nimport { shouldSuppressEmbeddingError } from \"./embeddings.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { chatCompletionTokenParams } from \"./model-capabilities.js\";\nimport type { ProvenanceService } from \"./provenance.js\";\nimport { dotProductSimilarity, loadReflectionDedupeCorpusVectors } from \"./reflection.js\";\nimport { deleteVectorsForFactIds } from \"./vector-maintenance.js\";\n\ninterface ConsolidateOptions {\n threshold: number;\n includeStructured: boolean;\n dryRun: boolean;\n limit: number;\n model: string;\n}\n\ninterface ConsolidateResult {\n clustersFound: number;\n merged: number;\n deleted: number;\n}\n\n/**\n * Union-Find data structure for clustering.\n */\nexport function unionFind(ids: string[], edges: Array<[string, string]>): Map<string, string> {\n const parent = new Map<string, string>();\n for (const id of ids) parent.set(id, id);\n function find(x: string): string {\n const p = parent.get(x)!;\n if (p !== x) parent.set(x, find(p));\n return parent.get(x)!;\n }\n function union(a: string, b: string): void {\n const ra = find(a);\n const rb = find(b);\n if (ra !== rb) parent.set(ra, rb);\n }\n for (const [a, b] of edges) union(a, b);\n return parent;\n}\n\n/**\n * Get root ID for a cluster.\n */\nexport function getRoot(parent: Map<string, string>, id: string): string {\n let r = id;\n while (parent.get(r) !== r) r = parent.get(r)!;\n return r;\n}\n\n/**\n * True if fact looks like identifier/number (IP, email, phone, UUID, etc.).\n * Used by consolidate to skip by default (2.2/2.4).\n */\nexport function isStructuredForConsolidation(text: string, entity: string | null, key: string | null): boolean {\n if (/\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/.test(text)) return true;\n if (/[\\w.-]+@[\\w.-]+\\.\\w+/.test(text)) return true;\n if (/\\+\\d{10,}/.test(text) || /\\b\\d{10,}\\b/.test(text)) return true;\n if (/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(text)) return true;\n const k = (key ?? \"\").toLowerCase();\n const e = (entity ?? \"\").toLowerCase();\n if ([\"email\", \"phone\", \"api_key\", \"ip\", \"uuid\", \"password\"].some((x) => k.includes(x) || e.includes(x))) return true;\n // Check for sensitive patterns to prevent credential leakage in consolidation\n if (SENSITIVE_PATTERNS.some((pattern) => pattern.test(text))) return true;\n return false;\n}\n\nfunction selectConsolidatedKeyValue(facts: MemoryEntry[]): { key: string | null; value: string | null } {\n if (facts.length === 0) return { key: null, value: null };\n const highestConfidence = facts.reduce((best, f) => (f.confidence > best.confidence ? f : best), facts[0]);\n const factsWithKey = facts.filter(\n (f): f is MemoryEntry & { key: string } => typeof f.key === \"string\" && f.key.trim().length > 0,\n );\n if (factsWithKey.length === 0) return { key: null, value: null };\n\n const keyToBest = new Map<string, MemoryEntry>();\n for (const fact of factsWithKey) {\n const key = fact.key.trim();\n const prev = keyToBest.get(key);\n if (!prev || fact.confidence > prev.confidence) {\n keyToBest.set(key, fact);\n }\n }\n\n const keys = [...keyToBest.keys()];\n let selectedKey = keys[0];\n if (keys.length > 1) {\n selectedKey = keys.reduce((bestKey, candidate) => {\n const bestFact = keyToBest.get(bestKey)!;\n const candidateFact = keyToBest.get(candidate)!;\n if (candidateFact.confidence > bestFact.confidence) return candidate;\n if (candidateFact.confidence < bestFact.confidence) return bestKey;\n return candidate.length > bestKey.length ? candidate : bestKey;\n }, selectedKey);\n }\n\n const bestForKey = keyToBest.get(selectedKey)!;\n const selectedValue =\n highestConfidence.key?.trim() === selectedKey && highestConfidence.value != null\n ? highestConfidence.value\n : (bestForKey.value ?? null);\n return { key: selectedKey, value: selectedValue };\n}\n\n/**\n * Run consolidation: cluster similar facts and merge them with LLM.\n */\nexport async function runConsolidate(\n factsDb: FactsDB,\n vectorDb: VectorDB,\n embeddings: EmbeddingProvider,\n openai: OpenAI,\n opts: ConsolidateOptions,\n logger: { info: (msg: string) => void; warn: (msg: string) => void },\n aliasDb?: import(\"./retrieval-aliases.js\").AliasDB | null,\n provenanceService?: ProvenanceService | null,\n): Promise<ConsolidateResult> {\n const facts = factsDb.getFactsForConsolidation(opts.limit);\n const candidateFacts = opts.includeStructured\n ? facts\n : facts.filter((f) => !isStructuredForConsolidation(f.text, f.entity, f.key));\n if (candidateFacts.length < 2) {\n logger.info(\"memory-hybrid: consolidate — fewer than 2 candidate facts\");\n return { clustersFound: 0, merged: 0, deleted: 0 };\n }\n\n const idToFact = new Map(candidateFacts.map((f) => [f.id, f]));\n const ids = candidateFacts.map((f) => f.id);\n\n logger.info(`memory-hybrid: consolidate — loading vectors for ${ids.length} facts (Lance index + embedding API)...`);\n const vectorResults = await loadReflectionDedupeCorpusVectors(\n candidateFacts as MemoryEntry[],\n embeddings,\n vectorDb,\n logger,\n \"memory-hybrid: consolidate\",\n \"consolidate-embed\",\n undefined,\n opts.dryRun,\n );\n const vectors = vectorResults.map((v) => (v === null ? [] : v));\n\n const _idToIndex = new Map(ids.map((id, idx) => [id, idx]));\n const edges: Array<[string, string]> = [];\n for (let i = 0; i < ids.length; i++) {\n const vi = vectors[i];\n if (vi.length === 0) continue;\n for (let j = i + 1; j < ids.length; j++) {\n const vj = vectors[j];\n if (vj.length === 0) continue;\n const score = dotProductSimilarity(vi, vj);\n if (score >= opts.threshold) edges.push([ids[i], ids[j]]);\n }\n }\n\n const parent = unionFind(ids, edges);\n const rootToCluster = new Map<string, string[]>();\n for (const id of ids) {\n const r = getRoot(parent, id);\n if (!rootToCluster.has(r)) rootToCluster.set(r, []);\n rootToCluster.get(r)?.push(id);\n }\n const clusters = [...rootToCluster.values()].filter((c) => c.length >= 2);\n logger.info(`memory-hybrid: consolidate — ${clusters.length} clusters (≥2 facts)`);\n\n if (clusters.length === 0) return { clustersFound: 0, merged: 0, deleted: 0 };\n\n let merged = 0;\n let deleted = 0;\n let storeDedupeVectorFallbackSuppressed = 0;\n const consolidationRunId = provenanceService ? randomUUID() : null;\n for (const clusterIds of clusters) {\n const texts = clusterIds.map((id) => idToFact.get(id)?.text);\n const factsList = texts.map((t, i) => `${i + 1}. ${t}`).join(\"\\n\");\n const prompt = fillPrompt(loadPrompt(\"consolidate\"), { facts_list: factsList });\n let mergedText: string;\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n // Uses withCostFeature context-wrapper (rather than a feature: param) because\n // openai.chat.completions.create is called directly here, not via chatCompleteWithRetry.\n const resp = await withCostFeature(\"consolidation\", () =>\n withLLMRetry(\n () =>\n openai.chat.completions.create({\n model: opts.model,\n messages: [{ role: \"user\", content: prompt }],\n temperature: 0,\n ...chatCompletionTokenParams(opts.model, 300),\n }),\n { maxRetries: 2 },\n ),\n );\n mergedText = (resp.choices[0]?.message?.content ?? \"\").trim().slice(0, CONSOLIDATION_MERGE_MAX_CHARS);\n } catch (err) {\n logger.warn(`memory-hybrid: consolidate LLM failed for cluster: ${err}`);\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-llm\",\n subsystem: \"openai\",\n clusterSize: clusterIds.length,\n });\n continue;\n }\n if (!mergedText) continue;\n\n const clusterFacts = clusterIds.map((id) => factsDb.getById(id)).filter(Boolean) as MemoryEntry[];\n const first = clusterFacts[0];\n const category = (first?.category as MemoryCategory) ?? \"other\";\n const maxSourceDate = clusterFacts.reduce(\n (acc, f) => (f.sourceDate != null && (acc == null || f.sourceDate > acc) ? f.sourceDate : acc),\n null as number | null,\n );\n const mergedTags = [...new Set([...clusterFacts.flatMap((f) => f.tags ?? []), \"consolidated\"])];\n const { key: mergedKey, value: mergedValue } = selectConsolidatedKeyValue(clusterFacts);\n\n if (opts.dryRun) {\n logger.info(\n `memory-hybrid: consolidate [dry-run] would merge ${clusterIds.length} facts → \"${mergedText.slice(0, 80)}...\"`,\n );\n merged++;\n continue;\n }\n\n storeDedupeVectorFallbackSuppressed++;\n const entry = factsDb.store(\n {\n text: mergedText,\n category,\n importance: BATCH_STORE_IMPORTANCE,\n entity: first?.entity ?? null,\n key: mergedKey,\n value: mergedValue,\n source: \"consolidation\",\n decayClass: CONSOLIDATED_FACT_DECAY_CLASS,\n sourceDate: maxSourceDate,\n tags: mergedTags.length > 0 ? mergedTags : undefined,\n extractionMethod: \"consolidation\",\n extractionConfidence: BATCH_STORE_IMPORTANCE,\n provenanceJson: JSON.stringify({\n method: \"consolidation\",\n consolidatedAt: Math.floor(Date.now() / 1000),\n sourceFactIds: clusterIds,\n sourceFacts: clusterFacts.map((sourceFact) => ({\n id: sourceFact.id,\n text: sourceFact.text.slice(0, 300),\n source: sourceFact.source,\n category: sourceFact.category,\n })),\n }),\n },\n {\n warnContext: \"consolidation\",\n suppressVectorFallbackWarning: true,\n },\n );\n if (provenanceService && consolidationRunId) {\n try {\n provenanceService.addEdge(entry.id, {\n edgeType: \"DERIVED_FROM\",\n sourceType: \"consolidation\",\n sourceId: consolidationRunId,\n });\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-provenance-derived\",\n subsystem: \"provenance\",\n factId: entry.id,\n });\n }\n }\n let vector: number[] | undefined;\n try {\n vector = await embeddings.embed(mergedText);\n } catch (err) {\n logger.warn(`memory-hybrid: consolidate embed failed: ${err}`);\n // AllEmbeddingProvidersFailed is expected when all providers are unavailable — don't report (#486)\n if (!shouldSuppressEmbeddingError(err)) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-embed\",\n subsystem: \"embeddings\",\n factId: entry.id,\n });\n }\n }\n if (vector !== undefined) {\n try {\n await vectorDb.store({ text: mergedText, vector, importance: BATCH_STORE_IMPORTANCE, category, id: entry.id });\n factsDb.setEmbeddingModel(entry.id, embeddings.modelName);\n } catch (err) {\n logger.warn(`memory-hybrid: consolidate vector store failed: ${err}`);\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-vector-store\",\n subsystem: \"vector\",\n factId: entry.id,\n });\n }\n }\n // Source lineage is stored on facts.provenance_json. memory_links is reserved for semantic edges.\n if (provenanceService) {\n for (const sourceFact of clusterFacts) {\n try {\n provenanceService.addEdge(entry.id, {\n edgeType: \"CONSOLIDATED_FROM\",\n sourceType: \"consolidation\",\n sourceId: sourceFact.id,\n sourceText: sourceFact.text,\n });\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-provenance-chain\",\n subsystem: \"provenance\",\n factId: entry.id,\n });\n }\n }\n }\n // Delete source vectors before removing SQLite rows: if we deleted SQLite first and then\n // crashed, the vectors would be permanent orphans (no matching SQL row, unreachable by\n // reconciliation). Deleting vectors first is safe — if we crash after vector delete but\n // before SQL delete, the next run will find the SQLite rows still present and can retry.\n await deleteVectorsForFactIds(vectorDb, clusterIds, {\n operation: \"consolidate-cleanup\",\n logger,\n });\n for (const id of clusterIds) {\n factsDb.delete(id);\n aliasDb?.deleteByFactId(id);\n deleted++;\n }\n merged++;\n }\n\n if (storeDedupeVectorFallbackSuppressed > 0) {\n logger.info(\n `memory-hybrid: consolidate — store dedupe used lexical-only for ${storeDedupeVectorFallbackSuppressed} store(s) (vectors are embedded/stored after insert in this pipeline)`,\n );\n }\n return { clustersFound: clusters.length, merged, deleted };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"consolidation.js","names":[],"sources":["../../services/consolidation.ts"],"sourcesContent":["/**\n * Consolidation service (2.4): find clusters of similar facts (by embedding),\n * merge each cluster with LLM, store one fact and delete cluster.\n *\n * Uses SQLite as source; re-embeds to compute similarity (no Lance scan).\n * Merged fact is stored in both SQLite and Lance.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type OpenAI from \"openai\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport type { VectorDB } from \"../backends/vector-db.js\";\nimport type { MemoryCategory, MemoryEntry } from \"../types/memory.js\";\nimport { CONSOLIDATED_FACT_DECAY_CLASS } from \"../utils/consolidation-controls.js\";\nimport { BATCH_STORE_IMPORTANCE, CONSOLIDATION_MERGE_MAX_CHARS } from \"../utils/constants.js\";\nimport { fillPrompt, loadPrompt } from \"../utils/prompt-loader.js\";\nimport { SENSITIVE_PATTERNS } from \"./auto-capture.js\";\nimport { withCostFeature } from \"./cost-context.js\";\nimport type { EmbeddingProvider } from \"./embeddings.js\";\nimport { shouldSuppressEmbeddingError } from \"./embeddings.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { chatCompletionTokenParams } from \"./model-capabilities.js\";\nimport type { ProvenanceService } from \"./provenance.js\";\nimport { dotProductSimilarity, loadReflectionDedupeCorpusVectors } from \"./reflection.js\";\nimport { deleteVectorsForFactIds } from \"./vector-maintenance.js\";\n\ninterface ConsolidateOptions {\n threshold: number;\n includeStructured: boolean;\n dryRun: boolean;\n limit: number;\n model: string;\n}\n\ninterface ConsolidateResult {\n clustersFound: number;\n merged: number;\n deleted: number;\n}\n\n/**\n * Union-Find data structure for clustering.\n */\nexport function unionFind(ids: string[], edges: Array<[string, string]>): Map<string, string> {\n const parent = new Map<string, string>();\n for (const id of ids) parent.set(id, id);\n function find(x: string): string {\n const p = parent.get(x)!;\n if (p !== x) parent.set(x, find(p));\n return parent.get(x)!;\n }\n function union(a: string, b: string): void {\n const ra = find(a);\n const rb = find(b);\n if (ra !== rb) parent.set(ra, rb);\n }\n for (const [a, b] of edges) union(a, b);\n return parent;\n}\n\n/**\n * Get root ID for a cluster.\n */\nexport function getRoot(parent: Map<string, string>, id: string): string {\n let r = id;\n while (parent.get(r) !== r) r = parent.get(r)!;\n return r;\n}\n\n/**\n * True if fact looks like identifier/number (IP, email, phone, UUID, etc.).\n * Used by consolidate to skip by default (2.2/2.4).\n */\nexport function isStructuredForConsolidation(text: string, entity: string | null, key: string | null): boolean {\n if (/\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/.test(text)) return true;\n if (/[\\w.-]+@[\\w.-]+\\.\\w+/.test(text)) return true;\n if (/\\+\\d{10,}/.test(text) || /\\b\\d{10,}\\b/.test(text)) return true;\n if (/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(text)) return true;\n const k = (key ?? \"\").toLowerCase();\n const e = (entity ?? \"\").toLowerCase();\n if ([\"email\", \"phone\", \"api_key\", \"ip\", \"uuid\", \"password\"].some((x) => k.includes(x) || e.includes(x))) return true;\n // Check for sensitive patterns to prevent credential leakage in consolidation\n if (SENSITIVE_PATTERNS.some((pattern) => pattern.test(text))) return true;\n return false;\n}\n\nfunction selectConsolidatedKeyValue(facts: MemoryEntry[]): { key: string | null; value: string | null } {\n if (facts.length === 0) return { key: null, value: null };\n const highestConfidence = facts.reduce((best, f) => (f.confidence > best.confidence ? f : best), facts[0]);\n const factsWithKey = facts.filter(\n (f): f is MemoryEntry & { key: string } => typeof f.key === \"string\" && f.key.trim().length > 0,\n );\n if (factsWithKey.length === 0) return { key: null, value: null };\n\n const keyToBest = new Map<string, MemoryEntry>();\n for (const fact of factsWithKey) {\n const key = fact.key.trim();\n const prev = keyToBest.get(key);\n if (!prev || fact.confidence > prev.confidence) {\n keyToBest.set(key, fact);\n }\n }\n\n const keys = [...keyToBest.keys()];\n let selectedKey = keys[0];\n if (keys.length > 1) {\n selectedKey = keys.reduce((bestKey, candidate) => {\n const bestFact = keyToBest.get(bestKey)!;\n const candidateFact = keyToBest.get(candidate)!;\n if (candidateFact.confidence > bestFact.confidence) return candidate;\n if (candidateFact.confidence < bestFact.confidence) return bestKey;\n return candidate.length > bestKey.length ? candidate : bestKey;\n }, selectedKey);\n }\n\n const bestForKey = keyToBest.get(selectedKey)!;\n const selectedValue =\n highestConfidence.key?.trim() === selectedKey && highestConfidence.value != null\n ? highestConfidence.value\n : (bestForKey.value ?? null);\n return { key: selectedKey, value: selectedValue };\n}\n\n/**\n * Run consolidation: cluster similar facts and merge them with LLM.\n */\nexport async function runConsolidate(\n factsDb: FactsDB,\n vectorDb: VectorDB,\n embeddings: EmbeddingProvider,\n openai: OpenAI,\n opts: ConsolidateOptions,\n logger: { info: (msg: string) => void; warn: (msg: string) => void },\n aliasDb?: import(\"./retrieval-aliases.js\").AliasDB | null,\n provenanceService?: ProvenanceService | null,\n): Promise<ConsolidateResult> {\n const facts = factsDb.getFactsForConsolidation(opts.limit);\n const candidateFacts = opts.includeStructured\n ? facts\n : facts.filter((f) => !isStructuredForConsolidation(f.text, f.entity, f.key));\n if (candidateFacts.length < 2) {\n logger.info(\"memory-hybrid: consolidate — fewer than 2 candidate facts\");\n return { clustersFound: 0, merged: 0, deleted: 0 };\n }\n\n const idToFact = new Map(candidateFacts.map((f) => [f.id, f]));\n const ids = candidateFacts.map((f) => f.id);\n\n logger.info(`memory-hybrid: consolidate — loading vectors for ${ids.length} facts (Lance index + embedding API)...`);\n const vectorResults = await loadReflectionDedupeCorpusVectors(\n candidateFacts as MemoryEntry[],\n embeddings,\n vectorDb,\n logger,\n \"memory-hybrid: consolidate\",\n \"consolidate-embed\",\n undefined,\n opts.dryRun,\n );\n const vectors = vectorResults.map((v) => (v === null ? [] : v));\n\n const _idToIndex = new Map(ids.map((id, idx) => [id, idx]));\n const edges: Array<[string, string]> = [];\n for (let i = 0; i < ids.length; i++) {\n const vi = vectors[i];\n if (vi.length === 0) continue;\n for (let j = i + 1; j < ids.length; j++) {\n const vj = vectors[j];\n if (vj.length === 0) continue;\n const score = dotProductSimilarity(vi, vj);\n if (score >= opts.threshold) edges.push([ids[i], ids[j]]);\n }\n }\n\n const parent = unionFind(ids, edges);\n const rootToCluster = new Map<string, string[]>();\n for (const id of ids) {\n const r = getRoot(parent, id);\n if (!rootToCluster.has(r)) rootToCluster.set(r, []);\n rootToCluster.get(r)?.push(id);\n }\n const clusters = [...rootToCluster.values()].filter((c) => c.length >= 2);\n logger.info(`memory-hybrid: consolidate — ${clusters.length} clusters (≥2 facts)`);\n\n if (clusters.length === 0) return { clustersFound: 0, merged: 0, deleted: 0 };\n\n let merged = 0;\n let deleted = 0;\n let storeDedupeVectorFallbackSuppressed = 0;\n const consolidationRunId = provenanceService ? randomUUID() : null;\n for (const clusterIds of clusters) {\n const texts = clusterIds.map((id) => idToFact.get(id)?.text);\n const factsList = texts.map((t, i) => `${i + 1}. ${t}`).join(\"\\n\");\n const prompt = fillPrompt(loadPrompt(\"consolidate\"), { facts_list: factsList });\n let mergedText: string;\n try {\n const { withLLMRetry } = await import(\"./chat.js\");\n // Uses withCostFeature context-wrapper (rather than a feature: param) because\n // openai.chat.completions.create is called directly here, not via chatCompleteWithRetry.\n const resp = await withCostFeature(\"consolidation\", () =>\n withLLMRetry(\n () =>\n openai.chat.completions.create({\n model: opts.model,\n messages: [{ role: \"user\", content: prompt }],\n temperature: 0,\n ...chatCompletionTokenParams(opts.model, 300),\n }),\n { maxRetries: 2 },\n ),\n );\n mergedText = (resp.choices[0]?.message?.content ?? \"\").trim().slice(0, CONSOLIDATION_MERGE_MAX_CHARS);\n } catch (err) {\n logger.warn(`memory-hybrid: consolidate LLM failed for cluster: ${err}`);\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-llm\",\n subsystem: \"openai\",\n clusterSize: clusterIds.length,\n });\n continue;\n }\n if (!mergedText) continue;\n\n const clusterFacts = clusterIds.map((id) => factsDb.getById(id)).filter(Boolean) as MemoryEntry[];\n const first = clusterFacts[0];\n const category = (first?.category as MemoryCategory) ?? \"other\";\n const maxSourceDate = clusterFacts.reduce(\n (acc, f) => (f.sourceDate != null && (acc == null || f.sourceDate > acc) ? f.sourceDate : acc),\n null as number | null,\n );\n const mergedTags = [...new Set([...clusterFacts.flatMap((f) => f.tags ?? []), \"consolidated\"])];\n const { key: mergedKey, value: mergedValue } = selectConsolidatedKeyValue(clusterFacts);\n\n if (opts.dryRun) {\n logger.info(\n `memory-hybrid: consolidate [dry-run] would merge ${clusterIds.length} facts → \"${mergedText.slice(0, 80)}...\"`,\n );\n merged++;\n continue;\n }\n\n storeDedupeVectorFallbackSuppressed++;\n const entry = factsDb.store(\n {\n text: mergedText,\n category,\n importance: BATCH_STORE_IMPORTANCE,\n entity: first?.entity ?? null,\n key: mergedKey,\n value: mergedValue,\n source: \"consolidation\",\n decayClass: CONSOLIDATED_FACT_DECAY_CLASS,\n sourceDate: maxSourceDate,\n tags: mergedTags.length > 0 ? mergedTags : undefined,\n extractionMethod: \"consolidation\",\n extractionConfidence: BATCH_STORE_IMPORTANCE,\n provenanceJson: JSON.stringify({\n method: \"consolidation\",\n consolidatedAt: Math.floor(Date.now() / 1000),\n sourceFactIds: clusterIds,\n sourceFacts: clusterFacts.map((sourceFact) => ({\n id: sourceFact.id,\n text: sourceFact.text.slice(0, 300),\n source: sourceFact.source,\n category: sourceFact.category,\n })),\n }),\n },\n {\n warnContext: \"consolidation\",\n suppressVectorFallbackWarning: true,\n },\n );\n if (provenanceService && consolidationRunId) {\n try {\n provenanceService.addEdge(entry.id, {\n edgeType: \"DERIVED_FROM\",\n sourceType: \"consolidation\",\n sourceId: consolidationRunId,\n });\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-provenance-derived\",\n subsystem: \"provenance\",\n factId: entry.id,\n });\n }\n }\n let vector: number[] | undefined;\n try {\n vector = await embeddings.embed(mergedText);\n } catch (err) {\n logger.warn(`memory-hybrid: consolidate embed failed: ${err}`);\n // AllEmbeddingProvidersFailed is expected when all providers are unavailable — don't report (#486)\n if (!shouldSuppressEmbeddingError(err)) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-embed\",\n subsystem: \"embeddings\",\n factId: entry.id,\n });\n }\n }\n if (vector !== undefined) {\n try {\n await vectorDb.store({ text: mergedText, vector, importance: BATCH_STORE_IMPORTANCE, category, id: entry.id });\n factsDb.setEmbeddingModel(entry.id, embeddings.modelName);\n } catch (err) {\n logger.warn(`memory-hybrid: consolidate vector store failed: ${err}`);\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-vector-store\",\n subsystem: \"vector\",\n factId: entry.id,\n });\n }\n }\n // Source lineage is stored on facts.provenance_json. memory_links is reserved for semantic edges.\n if (provenanceService) {\n for (const sourceFact of clusterFacts) {\n try {\n provenanceService.addEdge(entry.id, {\n edgeType: \"CONSOLIDATED_FROM\",\n sourceType: \"consolidation\",\n sourceId: sourceFact.id,\n sourceText: sourceFact.text,\n });\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"consolidate-provenance-chain\",\n subsystem: \"provenance\",\n factId: entry.id,\n });\n }\n }\n }\n // Delete source vectors before removing SQLite rows: if we deleted SQLite first and then\n // crashed, the vectors would be permanent orphans (no matching SQL row, unreachable by\n // reconciliation). Deleting vectors first is safe — if we crash after vector delete but\n // before SQL delete, the next run will find the SQLite rows still present and can retry.\n await deleteVectorsForFactIds(vectorDb, clusterIds, {\n operation: \"consolidate-cleanup\",\n logger,\n });\n for (const id of clusterIds) {\n factsDb.delete(id);\n aliasDb?.deleteByFactId(id);\n deleted++;\n }\n merged++;\n }\n\n if (storeDedupeVectorFallbackSuppressed > 0) {\n logger.info(\n `memory-hybrid: consolidate — store dedupe used lexical-only for ${storeDedupeVectorFallbackSuppressed} store(s) (vectors are embedded/stored after insert in this pipeline)`,\n );\n }\n return { clustersFound: clusters.length, merged, deleted };\n}\n"],"mappings":"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"}
|
|
@@ -142,7 +142,7 @@ async function runContextAudit(opts) {
|
|
|
142
142
|
const narrativeEstimateTokens = 0;
|
|
143
143
|
const activeTaskEstimateTokens = cfg.activeTask.enabled ? activeTaskMaxTokens : 0;
|
|
144
144
|
const staleWarningEstimateTokens = cfg.activeTask.enabled && cfg.activeTask.staleWarning.enabled ? staleWarningMaxTokens : 0;
|
|
145
|
-
const fixedBlockEstimatedTokens =
|
|
145
|
+
const fixedBlockEstimatedTokens = 0 + Math.min(hotTokens, hotMaxTokens) + Math.min(proceduresTokens, procedureMaxTokens) + activeTaskEstimateTokens + staleWarningEstimateTokens;
|
|
146
146
|
const remainingForRecall = Math.max(0, autoRecallBudget - fixedBlockEstimatedTokens);
|
|
147
147
|
const wouldExhaustRecall = cfg.autoRecall.enabled && autoRecallBudget > 0 && remainingForRecall === 0;
|
|
148
148
|
const totalTokens = autoRecallBudget + hotTokens + proceduresTokens + activeTasksTokens + staleWarningTokens + workspaceTokens;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-audit.js","names":[],"sources":["../../services/context-audit.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join } from \"node:path\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport type { HybridMemoryConfig } from \"../config.js\";\nimport { parseDuration } from \"../utils/duration.js\";\nimport { getEnv } from \"../utils/env-manager.js\";\nimport { estimateTokens, sanitizeRecallFactText } from \"../utils/text.js\";\nimport { readActiveTaskFile } from \"./active-task.js\";\nimport { buildActiveTaskContextBundle } from \"./active-task-injection.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { readActiveTaskRowsFromFacts } from \"./task-ledger-facts.js\";\n\ntype ContextAuditResult = {\n autoRecall: {\n enabled: boolean;\n budgetTokens: number;\n hotTokens: number;\n injectionFormat: string;\n fixedBlocks: {\n caps: {\n issueMaxTokens: number;\n narrativeMaxTokens: number;\n hotMaxTokens: number;\n procedureMaxTokens: number;\n activeTaskMaxTokens: number;\n staleWarningMaxTokens: number;\n };\n estimatedTokens: {\n issue: number;\n narrative: number;\n hot: number;\n procedures: number;\n activeTasks: number;\n staleWarnings: number;\n total: number;\n remainingForRecall: number;\n wouldExhaustRecall: boolean;\n };\n };\n };\n procedures: { enabled: boolean; tokens: number; lines: number };\n activeTasks: {\n enabled: boolean;\n tokens: number;\n /** Rows in ledger with non-terminal status (before projection). */\n ledgerActiveCount: number;\n /** Rows after projection/sort caps (candidates for injection). */\n filteredActiveCount: number;\n /** Tasks listed in `<active-tasks>` summary. */\n injectedTaskCount: number;\n /** @deprecated Use ledgerActiveCount instead. Kept for backwards compatibility. */\n count: number;\n /** @deprecated Use injectedTaskCount instead. Kept for older machine-readable consumers. */\n injectedCount: number;\n stale: number;\n injectionBudget: number;\n };\n workspaceFiles: { totalTokens: number; files: Array<{ file: string; tokens: number }> };\n totalTokens: number;\n recommendations: string[];\n};\n\nconst DEFAULT_BOOTSTRAP_FILES = [\n \"AGENTS.md\",\n \"SOUL.md\",\n \"USER.md\",\n \"TOOLS.md\",\n \"MEMORY.md\",\n \"HEARTBEAT.md\",\n \"IDENTITY.md\",\n];\n\nexport async function runContextAudit(opts: {\n cfg: HybridMemoryConfig;\n factsDb: FactsDB;\n workspaceRoot?: string;\n}): Promise<ContextAuditResult> {\n const { cfg, factsDb } = opts;\n const workspaceRoot = opts.workspaceRoot ?? getEnv(\"OPENCLAW_WORKSPACE\") ?? join(homedir(), \".openclaw\", \"workspace\");\n\n const workspaceFiles: Array<{ file: string; tokens: number }> = [];\n for (const file of DEFAULT_BOOTSTRAP_FILES) {\n const fp = isAbsolute(file) ? file : join(workspaceRoot, file);\n if (!existsSync(fp)) continue;\n const content = readFileSync(fp, \"utf-8\");\n workspaceFiles.push({ file, tokens: estimateTokens(content) });\n }\n const workspaceTokens = workspaceFiles.reduce((sum, f) => sum + f.tokens, 0);\n\n let activeTasksTokens = 0;\n let staleWarningTokens = 0;\n let ledgerActiveCount = 0;\n let filteredActiveCount = 0;\n let injectedTaskCount = 0;\n let activeTasksStale = 0;\n if (cfg.activeTask.enabled) {\n try {\n const staleMinutes = parseDuration(cfg.activeTask.staleThreshold);\n let activeRows: import(\"./active-task.js\").ActiveTaskEntry[] = [];\n if (cfg.activeTask.ledger === \"facts\") {\n const { active } = readActiveTaskRowsFromFacts(factsDb, staleMinutes);\n activeRows = active;\n } else {\n const taskFile = await readActiveTaskFile(\n isAbsolute(cfg.activeTask.filePath) ? cfg.activeTask.filePath : join(workspaceRoot, cfg.activeTask.filePath),\n staleMinutes,\n );\n if (taskFile?.active.length) activeRows = taskFile.active;\n }\n const bundle = buildActiveTaskContextBundle({\n ledgerTasks: activeRows,\n injectionBudgetTokens: cfg.activeTask.injectionBudget,\n staleMinutes,\n staleWarningEnabled: cfg.activeTask.staleWarning.enabled,\n projection: cfg.activeTask.projection,\n injectionMaxTasks: cfg.activeTask.injectionMaxTasks,\n });\n activeTasksTokens = bundle.activeTaskTokens;\n staleWarningTokens = bundle.staleWarningTokens;\n ledgerActiveCount = bundle.ledgerActiveCount;\n filteredActiveCount = bundle.filteredActiveCount;\n injectedTaskCount = bundle.injectedTaskCount;\n activeTasksStale = activeRows.filter((t) => t.stale).length;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-audit\",\n operation: \"active-task\",\n });\n }\n }\n\n let proceduresTokens = 0;\n let proceduresLines = 0;\n if (cfg.procedures.enabled) {\n try {\n const procedures = factsDb.getProceduresForAudit(5);\n const positive = procedures.filter((p) => p.procedureType === \"positive\");\n const negative = procedures.filter((p) => p.procedureType === \"negative\");\n const lines: string[] = [];\n\n if (positive.length > 0) {\n lines.push(\"Last time this worked:\");\n for (const p of positive.slice(0, 3)) {\n try {\n const steps = (JSON.parse(p.recipeJson) as Array<{ tool?: string }>)\n .map((s) => s.tool)\n .filter(Boolean)\n .join(\" → \");\n const emoji = p.confidence >= 0.7 ? \"✅\" : \"⚠️\";\n const confidence = Math.round(p.confidence * 100);\n lines.push(`- ${emoji} [${confidence}%] ${p.taskPattern.slice(0, 50)}… (${steps})`);\n } catch {\n const emoji = p.confidence >= 0.7 ? \"✅\" : \"⚠️\";\n const confidence = Math.round(p.confidence * 100);\n lines.push(`- ${emoji} [${confidence}%] ${p.taskPattern.slice(0, 70)}…`);\n }\n }\n }\n\n if (negative.length > 0) {\n lines.push(\"⚠️ Known issue (avoid):\");\n for (const n of negative.slice(0, 2)) {\n try {\n const steps = (JSON.parse(n.recipeJson) as Array<{ tool?: string }>)\n .map((s) => s.tool)\n .filter(Boolean)\n .join(\" → \");\n const emoji = n.confidence >= 0.7 ? \"❌\" : \"⚠️\";\n const confidence = Math.round(n.confidence * 100);\n lines.push(`- ${emoji} [${confidence}%] ${n.taskPattern.slice(0, 50)}… (${steps})`);\n } catch {\n const emoji = n.confidence >= 0.7 ? \"❌\" : \"⚠️\";\n const confidence = Math.round(n.confidence * 100);\n lines.push(`- ${emoji} [${confidence}%] ${n.taskPattern.slice(0, 70)}…`);\n }\n }\n }\n\n if (lines.length > 0) {\n const block = `<relevant-procedures>\\n${lines.join(\"\\n\")}\\n</relevant-procedures>`;\n proceduresTokens = estimateTokens(block);\n proceduresLines = lines.length;\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-audit\",\n operation: \"procedures\",\n });\n }\n }\n\n let hotTokens = 0;\n if (cfg.memoryTiering.enabled && cfg.memoryTiering.hotMaxTokens > 0) {\n try {\n const hotResults = factsDb.getHotFacts(cfg.memoryTiering.hotMaxTokens);\n if (hotResults.length > 0) {\n const hotLines = hotResults\n .map((r) => {\n const text = sanitizeRecallFactText(r.entry.summary || r.entry.text);\n if (!text) return \"\";\n const clipped = `${text.slice(0, 200)}${text.length > 200 ? \"…\" : \"\"}`;\n return `- [hot/${r.entry.category}] ${clipped}`;\n })\n .filter(Boolean);\n if (hotLines.length > 0) {\n const hotBlock = `<hot-memories>\\n${hotLines.join(\"\\n\")}\\n</hot-memories>`;\n hotTokens = estimateTokens(hotBlock);\n }\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-audit\",\n operation: \"hot-memories\",\n });\n }\n }\n\n const autoRecallBudget = cfg.autoRecall.enabled\n ? Math.min(cfg.autoRecall.maxTokens, cfg.retrieval.ambientBudgetTokens)\n : 0;\n const autoRecallHasBudget = cfg.autoRecall.enabled && autoRecallBudget > 0;\n const issueCapTokens = autoRecallHasBudget ? Math.max(80, Math.floor(autoRecallBudget * 0.15)) : 0;\n const narrativeMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.narrativeMaxTokens ?? Math.max(100, Math.floor(autoRecallBudget * 0.2)))\n : 0;\n const hotMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.hotMaxTokens ?? (hotTokens > 0 ? Math.max(100, Math.floor(autoRecallBudget * 0.25)) : 0))\n : 0;\n const defaultProcedureCap =\n autoRecallHasBudget && proceduresTokens > 0 ? Math.max(100, Math.floor(autoRecallBudget * 0.2)) : 0;\n const procedureMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.procedureMaxTokens ??\n (cfg.procedures.enabled ? Math.min(cfg.procedures.maxInjectionTokens, defaultProcedureCap) : 0))\n : 0;\n const activeTaskMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.activeTaskMaxTokens ??\n (cfg.activeTask.enabled\n ? Math.min(cfg.activeTask.injectionBudget, Math.max(80, Math.floor(autoRecallBudget * 0.2)))\n : 0))\n : 0;\n const staleWarningMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.staleWarningMaxTokens ??\n (cfg.activeTask.enabled && cfg.activeTask.staleWarning.enabled\n ? Math.max(40, Math.floor(autoRecallBudget * 0.08))\n : 0))\n : 0;\n\n const issueEstimateTokens = 0;\n const narrativeEstimateTokens = 0;\n const activeTaskEstimateTokens = cfg.activeTask.enabled ? activeTaskMaxTokens : 0;\n const staleWarningEstimateTokens =\n cfg.activeTask.enabled && cfg.activeTask.staleWarning.enabled ? staleWarningMaxTokens : 0;\n const fixedBlockEstimatedTokens =\n issueEstimateTokens +\n narrativeEstimateTokens +\n Math.min(hotTokens, hotMaxTokens) +\n Math.min(proceduresTokens, procedureMaxTokens) +\n activeTaskEstimateTokens +\n staleWarningEstimateTokens;\n const remainingForRecall = Math.max(0, autoRecallBudget - fixedBlockEstimatedTokens);\n const wouldExhaustRecall = cfg.autoRecall.enabled && autoRecallBudget > 0 && remainingForRecall === 0;\n\n const totalTokens =\n autoRecallBudget + hotTokens + proceduresTokens + activeTasksTokens + staleWarningTokens + workspaceTokens;\n\n const recommendations: string[] = [];\n if (workspaceTokens > 3000) {\n recommendations.push(\"Trim bootstrap files (AGENTS.md / TOOLS.md / MEMORY.md) to keep total under ~3000 tokens.\");\n }\n if (cfg.autoRecall.enabled && autoRecallBudget > 1200) {\n recommendations.push(\"Lower autoRecall.maxTokens or switch to progressive injection to save context.\");\n }\n if (wouldExhaustRecall) {\n recommendations.push(\n \"Fixed block caps consume the full recall budget. Lower hot/narrative/procedure/activeTask caps or increase autoRecall.maxTokens.\",\n );\n }\n if (cfg.activeTask.enabled && activeTasksTokens > cfg.activeTask.injectionBudget) {\n recommendations.push(\n \"Active tasks exceed the injection budget; consider summarizing or lowering activeTask.injectionBudget.\",\n );\n }\n if (proceduresTokens > 600) {\n recommendations.push(\n \"Procedure injection is sizable; consider pruning procedures or raising the relevance threshold.\",\n );\n }\n if (totalTokens > 8000) {\n recommendations.push(\n \"Total injected context is high; reduce auto-recall, bootstrap files, or active tasks to avoid compaction.\",\n );\n }\n\n return {\n autoRecall: {\n enabled: cfg.autoRecall.enabled,\n budgetTokens: autoRecallBudget,\n hotTokens,\n injectionFormat: cfg.autoRecall.injectionFormat,\n fixedBlocks: {\n caps: {\n issueMaxTokens: issueCapTokens,\n narrativeMaxTokens,\n hotMaxTokens,\n procedureMaxTokens,\n activeTaskMaxTokens,\n staleWarningMaxTokens,\n },\n estimatedTokens: {\n issue: issueEstimateTokens,\n narrative: narrativeEstimateTokens,\n hot: Math.min(hotTokens, hotMaxTokens),\n procedures: Math.min(proceduresTokens, procedureMaxTokens),\n activeTasks: activeTaskEstimateTokens,\n staleWarnings: staleWarningEstimateTokens,\n total: fixedBlockEstimatedTokens,\n remainingForRecall,\n wouldExhaustRecall,\n },\n },\n },\n procedures: { enabled: cfg.procedures.enabled, tokens: proceduresTokens, lines: proceduresLines },\n activeTasks: {\n enabled: cfg.activeTask.enabled,\n tokens: activeTasksTokens + staleWarningTokens,\n ledgerActiveCount,\n filteredActiveCount,\n injectedTaskCount,\n count: ledgerActiveCount,\n injectedCount: injectedTaskCount,\n stale: activeTasksStale,\n injectionBudget: cfg.activeTask.injectionBudget,\n },\n workspaceFiles: { totalTokens: workspaceTokens, files: workspaceFiles },\n totalTokens,\n recommendations,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"context-audit.js","names":[],"sources":["../../services/context-audit.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join } from \"node:path\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport type { HybridMemoryConfig } from \"../config.js\";\nimport { parseDuration } from \"../utils/duration.js\";\nimport { getEnv } from \"../utils/env-manager.js\";\nimport { estimateTokens, sanitizeRecallFactText } from \"../utils/text.js\";\nimport { readActiveTaskFile } from \"./active-task.js\";\nimport { buildActiveTaskContextBundle } from \"./active-task-injection.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { readActiveTaskRowsFromFacts } from \"./task-ledger-facts.js\";\n\ntype ContextAuditResult = {\n autoRecall: {\n enabled: boolean;\n budgetTokens: number;\n hotTokens: number;\n injectionFormat: string;\n fixedBlocks: {\n caps: {\n issueMaxTokens: number;\n narrativeMaxTokens: number;\n hotMaxTokens: number;\n procedureMaxTokens: number;\n activeTaskMaxTokens: number;\n staleWarningMaxTokens: number;\n };\n estimatedTokens: {\n issue: number;\n narrative: number;\n hot: number;\n procedures: number;\n activeTasks: number;\n staleWarnings: number;\n total: number;\n remainingForRecall: number;\n wouldExhaustRecall: boolean;\n };\n };\n };\n procedures: { enabled: boolean; tokens: number; lines: number };\n activeTasks: {\n enabled: boolean;\n tokens: number;\n /** Rows in ledger with non-terminal status (before projection). */\n ledgerActiveCount: number;\n /** Rows after projection/sort caps (candidates for injection). */\n filteredActiveCount: number;\n /** Tasks listed in `<active-tasks>` summary. */\n injectedTaskCount: number;\n /** @deprecated Use ledgerActiveCount instead. Kept for backwards compatibility. */\n count: number;\n /** @deprecated Use injectedTaskCount instead. Kept for older machine-readable consumers. */\n injectedCount: number;\n stale: number;\n injectionBudget: number;\n };\n workspaceFiles: { totalTokens: number; files: Array<{ file: string; tokens: number }> };\n totalTokens: number;\n recommendations: string[];\n};\n\nconst DEFAULT_BOOTSTRAP_FILES = [\n \"AGENTS.md\",\n \"SOUL.md\",\n \"USER.md\",\n \"TOOLS.md\",\n \"MEMORY.md\",\n \"HEARTBEAT.md\",\n \"IDENTITY.md\",\n];\n\nexport async function runContextAudit(opts: {\n cfg: HybridMemoryConfig;\n factsDb: FactsDB;\n workspaceRoot?: string;\n}): Promise<ContextAuditResult> {\n const { cfg, factsDb } = opts;\n const workspaceRoot = opts.workspaceRoot ?? getEnv(\"OPENCLAW_WORKSPACE\") ?? join(homedir(), \".openclaw\", \"workspace\");\n\n const workspaceFiles: Array<{ file: string; tokens: number }> = [];\n for (const file of DEFAULT_BOOTSTRAP_FILES) {\n const fp = isAbsolute(file) ? file : join(workspaceRoot, file);\n if (!existsSync(fp)) continue;\n const content = readFileSync(fp, \"utf-8\");\n workspaceFiles.push({ file, tokens: estimateTokens(content) });\n }\n const workspaceTokens = workspaceFiles.reduce((sum, f) => sum + f.tokens, 0);\n\n let activeTasksTokens = 0;\n let staleWarningTokens = 0;\n let ledgerActiveCount = 0;\n let filteredActiveCount = 0;\n let injectedTaskCount = 0;\n let activeTasksStale = 0;\n if (cfg.activeTask.enabled) {\n try {\n const staleMinutes = parseDuration(cfg.activeTask.staleThreshold);\n let activeRows: import(\"./active-task.js\").ActiveTaskEntry[] = [];\n if (cfg.activeTask.ledger === \"facts\") {\n const { active } = readActiveTaskRowsFromFacts(factsDb, staleMinutes);\n activeRows = active;\n } else {\n const taskFile = await readActiveTaskFile(\n isAbsolute(cfg.activeTask.filePath) ? cfg.activeTask.filePath : join(workspaceRoot, cfg.activeTask.filePath),\n staleMinutes,\n );\n if (taskFile?.active.length) activeRows = taskFile.active;\n }\n const bundle = buildActiveTaskContextBundle({\n ledgerTasks: activeRows,\n injectionBudgetTokens: cfg.activeTask.injectionBudget,\n staleMinutes,\n staleWarningEnabled: cfg.activeTask.staleWarning.enabled,\n projection: cfg.activeTask.projection,\n injectionMaxTasks: cfg.activeTask.injectionMaxTasks,\n });\n activeTasksTokens = bundle.activeTaskTokens;\n staleWarningTokens = bundle.staleWarningTokens;\n ledgerActiveCount = bundle.ledgerActiveCount;\n filteredActiveCount = bundle.filteredActiveCount;\n injectedTaskCount = bundle.injectedTaskCount;\n activeTasksStale = activeRows.filter((t) => t.stale).length;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-audit\",\n operation: \"active-task\",\n });\n }\n }\n\n let proceduresTokens = 0;\n let proceduresLines = 0;\n if (cfg.procedures.enabled) {\n try {\n const procedures = factsDb.getProceduresForAudit(5);\n const positive = procedures.filter((p) => p.procedureType === \"positive\");\n const negative = procedures.filter((p) => p.procedureType === \"negative\");\n const lines: string[] = [];\n\n if (positive.length > 0) {\n lines.push(\"Last time this worked:\");\n for (const p of positive.slice(0, 3)) {\n try {\n const steps = (JSON.parse(p.recipeJson) as Array<{ tool?: string }>)\n .map((s) => s.tool)\n .filter(Boolean)\n .join(\" → \");\n const emoji = p.confidence >= 0.7 ? \"✅\" : \"⚠️\";\n const confidence = Math.round(p.confidence * 100);\n lines.push(`- ${emoji} [${confidence}%] ${p.taskPattern.slice(0, 50)}… (${steps})`);\n } catch {\n const emoji = p.confidence >= 0.7 ? \"✅\" : \"⚠️\";\n const confidence = Math.round(p.confidence * 100);\n lines.push(`- ${emoji} [${confidence}%] ${p.taskPattern.slice(0, 70)}…`);\n }\n }\n }\n\n if (negative.length > 0) {\n lines.push(\"⚠️ Known issue (avoid):\");\n for (const n of negative.slice(0, 2)) {\n try {\n const steps = (JSON.parse(n.recipeJson) as Array<{ tool?: string }>)\n .map((s) => s.tool)\n .filter(Boolean)\n .join(\" → \");\n const emoji = n.confidence >= 0.7 ? \"❌\" : \"⚠️\";\n const confidence = Math.round(n.confidence * 100);\n lines.push(`- ${emoji} [${confidence}%] ${n.taskPattern.slice(0, 50)}… (${steps})`);\n } catch {\n const emoji = n.confidence >= 0.7 ? \"❌\" : \"⚠️\";\n const confidence = Math.round(n.confidence * 100);\n lines.push(`- ${emoji} [${confidence}%] ${n.taskPattern.slice(0, 70)}…`);\n }\n }\n }\n\n if (lines.length > 0) {\n const block = `<relevant-procedures>\\n${lines.join(\"\\n\")}\\n</relevant-procedures>`;\n proceduresTokens = estimateTokens(block);\n proceduresLines = lines.length;\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-audit\",\n operation: \"procedures\",\n });\n }\n }\n\n let hotTokens = 0;\n if (cfg.memoryTiering.enabled && cfg.memoryTiering.hotMaxTokens > 0) {\n try {\n const hotResults = factsDb.getHotFacts(cfg.memoryTiering.hotMaxTokens);\n if (hotResults.length > 0) {\n const hotLines = hotResults\n .map((r) => {\n const text = sanitizeRecallFactText(r.entry.summary || r.entry.text);\n if (!text) return \"\";\n const clipped = `${text.slice(0, 200)}${text.length > 200 ? \"…\" : \"\"}`;\n return `- [hot/${r.entry.category}] ${clipped}`;\n })\n .filter(Boolean);\n if (hotLines.length > 0) {\n const hotBlock = `<hot-memories>\\n${hotLines.join(\"\\n\")}\\n</hot-memories>`;\n hotTokens = estimateTokens(hotBlock);\n }\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-audit\",\n operation: \"hot-memories\",\n });\n }\n }\n\n const autoRecallBudget = cfg.autoRecall.enabled\n ? Math.min(cfg.autoRecall.maxTokens, cfg.retrieval.ambientBudgetTokens)\n : 0;\n const autoRecallHasBudget = cfg.autoRecall.enabled && autoRecallBudget > 0;\n const issueCapTokens = autoRecallHasBudget ? Math.max(80, Math.floor(autoRecallBudget * 0.15)) : 0;\n const narrativeMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.narrativeMaxTokens ?? Math.max(100, Math.floor(autoRecallBudget * 0.2)))\n : 0;\n const hotMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.hotMaxTokens ?? (hotTokens > 0 ? Math.max(100, Math.floor(autoRecallBudget * 0.25)) : 0))\n : 0;\n const defaultProcedureCap =\n autoRecallHasBudget && proceduresTokens > 0 ? Math.max(100, Math.floor(autoRecallBudget * 0.2)) : 0;\n const procedureMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.procedureMaxTokens ??\n (cfg.procedures.enabled ? Math.min(cfg.procedures.maxInjectionTokens, defaultProcedureCap) : 0))\n : 0;\n const activeTaskMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.activeTaskMaxTokens ??\n (cfg.activeTask.enabled\n ? Math.min(cfg.activeTask.injectionBudget, Math.max(80, Math.floor(autoRecallBudget * 0.2)))\n : 0))\n : 0;\n const staleWarningMaxTokens = autoRecallHasBudget\n ? (cfg.autoRecall.staleWarningMaxTokens ??\n (cfg.activeTask.enabled && cfg.activeTask.staleWarning.enabled\n ? Math.max(40, Math.floor(autoRecallBudget * 0.08))\n : 0))\n : 0;\n\n const issueEstimateTokens = 0;\n const narrativeEstimateTokens = 0;\n const activeTaskEstimateTokens = cfg.activeTask.enabled ? activeTaskMaxTokens : 0;\n const staleWarningEstimateTokens =\n cfg.activeTask.enabled && cfg.activeTask.staleWarning.enabled ? staleWarningMaxTokens : 0;\n const fixedBlockEstimatedTokens =\n issueEstimateTokens +\n narrativeEstimateTokens +\n Math.min(hotTokens, hotMaxTokens) +\n Math.min(proceduresTokens, procedureMaxTokens) +\n activeTaskEstimateTokens +\n staleWarningEstimateTokens;\n const remainingForRecall = Math.max(0, autoRecallBudget - fixedBlockEstimatedTokens);\n const wouldExhaustRecall = cfg.autoRecall.enabled && autoRecallBudget > 0 && remainingForRecall === 0;\n\n const totalTokens =\n autoRecallBudget + hotTokens + proceduresTokens + activeTasksTokens + staleWarningTokens + workspaceTokens;\n\n const recommendations: string[] = [];\n if (workspaceTokens > 3000) {\n recommendations.push(\"Trim bootstrap files (AGENTS.md / TOOLS.md / MEMORY.md) to keep total under ~3000 tokens.\");\n }\n if (cfg.autoRecall.enabled && autoRecallBudget > 1200) {\n recommendations.push(\"Lower autoRecall.maxTokens or switch to progressive injection to save context.\");\n }\n if (wouldExhaustRecall) {\n recommendations.push(\n \"Fixed block caps consume the full recall budget. Lower hot/narrative/procedure/activeTask caps or increase autoRecall.maxTokens.\",\n );\n }\n if (cfg.activeTask.enabled && activeTasksTokens > cfg.activeTask.injectionBudget) {\n recommendations.push(\n \"Active tasks exceed the injection budget; consider summarizing or lowering activeTask.injectionBudget.\",\n );\n }\n if (proceduresTokens > 600) {\n recommendations.push(\n \"Procedure injection is sizable; consider pruning procedures or raising the relevance threshold.\",\n );\n }\n if (totalTokens > 8000) {\n recommendations.push(\n \"Total injected context is high; reduce auto-recall, bootstrap files, or active tasks to avoid compaction.\",\n );\n }\n\n return {\n autoRecall: {\n enabled: cfg.autoRecall.enabled,\n budgetTokens: autoRecallBudget,\n hotTokens,\n injectionFormat: cfg.autoRecall.injectionFormat,\n fixedBlocks: {\n caps: {\n issueMaxTokens: issueCapTokens,\n narrativeMaxTokens,\n hotMaxTokens,\n procedureMaxTokens,\n activeTaskMaxTokens,\n staleWarningMaxTokens,\n },\n estimatedTokens: {\n issue: issueEstimateTokens,\n narrative: narrativeEstimateTokens,\n hot: Math.min(hotTokens, hotMaxTokens),\n procedures: Math.min(proceduresTokens, procedureMaxTokens),\n activeTasks: activeTaskEstimateTokens,\n staleWarnings: staleWarningEstimateTokens,\n total: fixedBlockEstimatedTokens,\n remainingForRecall,\n wouldExhaustRecall,\n },\n },\n },\n procedures: { enabled: cfg.procedures.enabled, tokens: proceduresTokens, lines: proceduresLines },\n activeTasks: {\n enabled: cfg.activeTask.enabled,\n tokens: activeTasksTokens + staleWarningTokens,\n ledgerActiveCount,\n filteredActiveCount,\n injectedTaskCount,\n count: ledgerActiveCount,\n injectedCount: injectedTaskCount,\n stale: activeTasksStale,\n injectionBudget: cfg.activeTask.injectionBudget,\n },\n workspaceFiles: { totalTokens: workspaceTokens, files: workspaceFiles },\n totalTokens,\n recommendations,\n };\n}\n"],"mappings":";;;;;;;;;;;AA+DA,MAAM,0BAA0B;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,eAAsB,gBAAgB,MAIN;CAC9B,MAAM,EAAE,KAAK,YAAY;CACzB,MAAM,gBAAgB,KAAK,iBAAiB,OAAO,oBAAoB,KAAK,KAAK,QAAQ,GAAG,aAAa,WAAW;CAEpH,MAAM,iBAA0D,CAAC;CACjE,KAAK,MAAM,QAAQ,yBAAyB;EAC1C,MAAM,KAAK,WAAW,IAAI,IAAI,OAAO,KAAK,eAAe,IAAI;EAC7D,IAAI,CAAC,WAAW,EAAE,GAAG;EACrB,MAAM,UAAU,aAAa,IAAI,OAAO;EACxC,eAAe,KAAK;GAAE;GAAM,QAAQ,eAAe,OAAO;EAAE,CAAC;CAC/D;CACA,MAAM,kBAAkB,eAAe,QAAQ,KAAK,MAAM,MAAM,EAAE,QAAQ,CAAC;CAE3E,IAAI,oBAAoB;CACxB,IAAI,qBAAqB;CACzB,IAAI,oBAAoB;CACxB,IAAI,sBAAsB;CAC1B,IAAI,oBAAoB;CACxB,IAAI,mBAAmB;CACvB,IAAI,IAAI,WAAW,SACjB,IAAI;EACF,MAAM,eAAe,cAAc,IAAI,WAAW,cAAc;EAChE,IAAI,aAA2D,CAAC;EAChE,IAAI,IAAI,WAAW,WAAW,SAAS;GACrC,MAAM,EAAE,WAAW,4BAA4B,SAAS,YAAY;GACpE,aAAa;EACf,OAAO;GACL,MAAM,WAAW,MAAM,mBACrB,WAAW,IAAI,WAAW,QAAQ,IAAI,IAAI,WAAW,WAAW,KAAK,eAAe,IAAI,WAAW,QAAQ,GAC3G,YACF;GACA,IAAI,UAAU,OAAO,QAAQ,aAAa,SAAS;EACrD;EACA,MAAM,SAAS,6BAA6B;GAC1C,aAAa;GACb,uBAAuB,IAAI,WAAW;GACtC;GACA,qBAAqB,IAAI,WAAW,aAAa;GACjD,YAAY,IAAI,WAAW;GAC3B,mBAAmB,IAAI,WAAW;EACpC,CAAC;EACD,oBAAoB,OAAO;EAC3B,qBAAqB,OAAO;EAC5B,oBAAoB,OAAO;EAC3B,sBAAsB,OAAO;EAC7B,oBAAoB,OAAO;EAC3B,mBAAmB,WAAW,QAAQ,MAAM,EAAE,KAAK,EAAE;CACvD,SAAS,KAAK;EACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,GAAG;GACtE,WAAW;GACX,WAAW;EACb,CAAC;CACH;CAGF,IAAI,mBAAmB;CACvB,IAAI,kBAAkB;CACtB,IAAI,IAAI,WAAW,SACjB,IAAI;EACF,MAAM,aAAa,QAAQ,sBAAsB,CAAC;EAClD,MAAM,WAAW,WAAW,QAAQ,MAAM,EAAE,kBAAkB,UAAU;EACxE,MAAM,WAAW,WAAW,QAAQ,MAAM,EAAE,kBAAkB,UAAU;EACxE,MAAM,QAAkB,CAAC;EAEzB,IAAI,SAAS,SAAS,GAAG;GACvB,MAAM,KAAK,wBAAwB;GACnC,KAAK,MAAM,KAAK,SAAS,MAAM,GAAG,CAAC,GACjC,IAAI;IACF,MAAM,QAAS,KAAK,MAAM,EAAE,UAAU,EACnC,KAAK,MAAM,EAAE,IAAI,EACjB,OAAO,OAAO,EACd,KAAK,KAAK;IACb,MAAM,QAAQ,EAAE,cAAc,KAAM,MAAM;IAC1C,MAAM,aAAa,KAAK,MAAM,EAAE,aAAa,GAAG;IAChD,MAAM,KAAK,KAAK,MAAM,IAAI,WAAW,KAAK,EAAE,YAAY,MAAM,GAAG,EAAE,EAAE,KAAK,MAAM,EAAE;GACpF,QAAQ;IACN,MAAM,QAAQ,EAAE,cAAc,KAAM,MAAM;IAC1C,MAAM,aAAa,KAAK,MAAM,EAAE,aAAa,GAAG;IAChD,MAAM,KAAK,KAAK,MAAM,IAAI,WAAW,KAAK,EAAE,YAAY,MAAM,GAAG,EAAE,EAAE,EAAE;GACzE;EAEJ;EAEA,IAAI,SAAS,SAAS,GAAG;GACvB,MAAM,KAAK,yBAAyB;GACpC,KAAK,MAAM,KAAK,SAAS,MAAM,GAAG,CAAC,GACjC,IAAI;IACF,MAAM,QAAS,KAAK,MAAM,EAAE,UAAU,EACnC,KAAK,MAAM,EAAE,IAAI,EACjB,OAAO,OAAO,EACd,KAAK,KAAK;IACb,MAAM,QAAQ,EAAE,cAAc,KAAM,MAAM;IAC1C,MAAM,aAAa,KAAK,MAAM,EAAE,aAAa,GAAG;IAChD,MAAM,KAAK,KAAK,MAAM,IAAI,WAAW,KAAK,EAAE,YAAY,MAAM,GAAG,EAAE,EAAE,KAAK,MAAM,EAAE;GACpF,QAAQ;IACN,MAAM,QAAQ,EAAE,cAAc,KAAM,MAAM;IAC1C,MAAM,aAAa,KAAK,MAAM,EAAE,aAAa,GAAG;IAChD,MAAM,KAAK,KAAK,MAAM,IAAI,WAAW,KAAK,EAAE,YAAY,MAAM,GAAG,EAAE,EAAE,EAAE;GACzE;EAEJ;EAEA,IAAI,MAAM,SAAS,GAAG;GAEpB,mBAAmB,eAAe,0BADM,MAAM,KAAK,IAAI,EAAE,yBAClB;GACvC,kBAAkB,MAAM;EAC1B;CACF,SAAS,KAAK;EACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,GAAG;GACtE,WAAW;GACX,WAAW;EACb,CAAC;CACH;CAGF,IAAI,YAAY;CAChB,IAAI,IAAI,cAAc,WAAW,IAAI,cAAc,eAAe,GAChE,IAAI;EACF,MAAM,aAAa,QAAQ,YAAY,IAAI,cAAc,YAAY;EACrE,IAAI,WAAW,SAAS,GAAG;GACzB,MAAM,WAAW,WACd,KAAK,MAAM;IACV,MAAM,OAAO,uBAAuB,EAAE,MAAM,WAAW,EAAE,MAAM,IAAI;IACnE,IAAI,CAAC,MAAM,OAAO;IAClB,MAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG,IAAI,KAAK,SAAS,MAAM,MAAM;IAClE,OAAO,UAAU,EAAE,MAAM,SAAS,IAAI;GACxC,CAAC,EACA,OAAO,OAAO;GACjB,IAAI,SAAS,SAAS,GAEpB,YAAY,eAAe,mBADS,SAAS,KAAK,IAAI,EAAE,kBACrB;EAEvC;CACF,SAAS,KAAK;EACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,GAAG;GACtE,WAAW;GACX,WAAW;EACb,CAAC;CACH;CAGF,MAAM,mBAAmB,IAAI,WAAW,UACpC,KAAK,IAAI,IAAI,WAAW,WAAW,IAAI,UAAU,mBAAmB,IACpE;CACJ,MAAM,sBAAsB,IAAI,WAAW,WAAW,mBAAmB;CACzE,MAAM,iBAAiB,sBAAsB,KAAK,IAAI,IAAI,KAAK,MAAM,mBAAmB,GAAI,CAAC,IAAI;CACjG,MAAM,qBAAqB,sBACtB,IAAI,WAAW,sBAAsB,KAAK,IAAI,KAAK,KAAK,MAAM,mBAAmB,EAAG,CAAC,IACtF;CACJ,MAAM,eAAe,sBAChB,IAAI,WAAW,iBAAiB,YAAY,IAAI,KAAK,IAAI,KAAK,KAAK,MAAM,mBAAmB,GAAI,CAAC,IAAI,KACtG;CACJ,MAAM,sBACJ,uBAAuB,mBAAmB,IAAI,KAAK,IAAI,KAAK,KAAK,MAAM,mBAAmB,EAAG,CAAC,IAAI;CACpG,MAAM,qBAAqB,sBACtB,IAAI,WAAW,uBACf,IAAI,WAAW,UAAU,KAAK,IAAI,IAAI,WAAW,oBAAoB,mBAAmB,IAAI,KAC7F;CACJ,MAAM,sBAAsB,sBACvB,IAAI,WAAW,wBACf,IAAI,WAAW,UACZ,KAAK,IAAI,IAAI,WAAW,iBAAiB,KAAK,IAAI,IAAI,KAAK,MAAM,mBAAmB,EAAG,CAAC,CAAC,IACzF,KACJ;CACJ,MAAM,wBAAwB,sBACzB,IAAI,WAAW,0BACf,IAAI,WAAW,WAAW,IAAI,WAAW,aAAa,UACnD,KAAK,IAAI,IAAI,KAAK,MAAM,mBAAmB,GAAI,CAAC,IAChD,KACJ;CAEJ,MAAM,sBAAsB;CAC5B,MAAM,0BAA0B;CAChC,MAAM,2BAA2B,IAAI,WAAW,UAAU,sBAAsB;CAChF,MAAM,6BACJ,IAAI,WAAW,WAAW,IAAI,WAAW,aAAa,UAAU,wBAAwB;CAC1F,MAAM,4BACJ,IAEA,KAAK,IAAI,WAAW,YAAY,IAChC,KAAK,IAAI,kBAAkB,kBAAkB,IAC7C,2BACA;CACF,MAAM,qBAAqB,KAAK,IAAI,GAAG,mBAAmB,yBAAyB;CACnF,MAAM,qBAAqB,IAAI,WAAW,WAAW,mBAAmB,KAAK,uBAAuB;CAEpG,MAAM,cACJ,mBAAmB,YAAY,mBAAmB,oBAAoB,qBAAqB;CAE7F,MAAM,kBAA4B,CAAC;CACnC,IAAI,kBAAkB,KACpB,gBAAgB,KAAK,2FAA2F;CAElH,IAAI,IAAI,WAAW,WAAW,mBAAmB,MAC/C,gBAAgB,KAAK,gFAAgF;CAEvG,IAAI,oBACF,gBAAgB,KACd,kIACF;CAEF,IAAI,IAAI,WAAW,WAAW,oBAAoB,IAAI,WAAW,iBAC/D,gBAAgB,KACd,wGACF;CAEF,IAAI,mBAAmB,KACrB,gBAAgB,KACd,iGACF;CAEF,IAAI,cAAc,KAChB,gBAAgB,KACd,2GACF;CAGF,OAAO;EACL,YAAY;GACV,SAAS,IAAI,WAAW;GACxB,cAAc;GACd;GACA,iBAAiB,IAAI,WAAW;GAChC,aAAa;IACX,MAAM;KACJ,gBAAgB;KAChB;KACA;KACA;KACA;KACA;IACF;IACA,iBAAiB;KACf,OAAO;KACP,WAAW;KACX,KAAK,KAAK,IAAI,WAAW,YAAY;KACrC,YAAY,KAAK,IAAI,kBAAkB,kBAAkB;KACzD,aAAa;KACb,eAAe;KACf,OAAO;KACP;KACA;IACF;GACF;EACF;EACA,YAAY;GAAE,SAAS,IAAI,WAAW;GAAS,QAAQ;GAAkB,OAAO;EAAgB;EAChG,aAAa;GACX,SAAS,IAAI,WAAW;GACxB,QAAQ,oBAAoB;GAC5B;GACA;GACA;GACA,OAAO;GACP,eAAe;GACf,OAAO;GACP,iBAAiB,IAAI,WAAW;EAClC;EACA,gBAAgB;GAAE,aAAa;GAAiB,OAAO;EAAe;EACtE;EACA;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-budget.js","names":[],"sources":["../../services/context-budget.ts"],"sourcesContent":["/**\n * Plugin-visible token budget hints until OpenClaw SDK #274 exposes native context APIs (issue #908).\n */\n\nimport type { HybridMemoryConfig } from \"../config.js\";\n\nexport function getHybridMemoryContextBudgetHint(cfg: HybridMemoryConfig): {\n autoRecallMaxTokens: number;\n proceduresMaxInjectionTokens: number;\n retrievalAmbientBudgetTokens: number;\n retrievalExplicitBudgetTokens: number;\n} {\n return {\n autoRecallMaxTokens: cfg.autoRecall.maxTokens,\n proceduresMaxInjectionTokens: cfg.procedures.maxInjectionTokens,\n retrievalAmbientBudgetTokens: cfg.retrieval.ambientBudgetTokens,\n retrievalExplicitBudgetTokens: cfg.retrieval.explicitBudgetTokens,\n };\n}\n"],"mappings":";AAMA,SAAgB,iCAAiC,KAK/C;CACA,OAAO;EACL,qBAAqB,IAAI,WAAW;EACpC,8BAA8B,IAAI,WAAW;EAC7C,8BAA8B,IAAI,UAAU;EAC5C,+BAA+B,IAAI,UAAU;
|
|
1
|
+
{"version":3,"file":"context-budget.js","names":[],"sources":["../../services/context-budget.ts"],"sourcesContent":["/**\n * Plugin-visible token budget hints until OpenClaw SDK #274 exposes native context APIs (issue #908).\n */\n\nimport type { HybridMemoryConfig } from \"../config.js\";\n\nexport function getHybridMemoryContextBudgetHint(cfg: HybridMemoryConfig): {\n autoRecallMaxTokens: number;\n proceduresMaxInjectionTokens: number;\n retrievalAmbientBudgetTokens: number;\n retrievalExplicitBudgetTokens: number;\n} {\n return {\n autoRecallMaxTokens: cfg.autoRecall.maxTokens,\n proceduresMaxInjectionTokens: cfg.procedures.maxInjectionTokens,\n retrievalAmbientBudgetTokens: cfg.retrieval.ambientBudgetTokens,\n retrievalExplicitBudgetTokens: cfg.retrieval.explicitBudgetTokens,\n };\n}\n"],"mappings":";AAMA,SAAgB,iCAAiC,KAK/C;CACA,OAAO;EACL,qBAAqB,IAAI,WAAW;EACpC,8BAA8B,IAAI,WAAW;EAC7C,8BAA8B,IAAI,UAAU;EAC5C,+BAA+B,IAAI,UAAU;CAC/C;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-engine.js","names":[],"sources":["../../services/context-engine.ts"],"sourcesContent":["/**\n * ContextEngine Plugin Slot Adoption (Issue #273)\n *\n * Registers the hybrid-memory plugin as an OpenClaw ContextEngine when the\n * registerContextEngine API is available (OpenClaw ≥ 2026.3.8).\n *\n * Capabilities:\n * - compact: flush WAL + snapshot session-scoped facts before compaction\n * - prepareSubagentSpawn: inject relevant parent memories into sub-agent context\n * - onSubagentEnded: capture facts from finished sub-agent session\n * - assemble: pass-through (autoRecall handles injection via before_agent_start)\n * - ingest: no-op (SessionManager owns persistence)\n *\n * Feature detection: if registerContextEngine is absent the module exports a\n * no-op so callers do not need to guard on their side.\n */\n\nimport { access, readFile } from \"node:fs/promises\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport type { VectorDB } from \"../backends/vector-db.js\";\nimport type { WriteAheadLog } from \"../backends/wal.js\";\nimport type { HybridMemoryConfig } from \"../config.js\";\nimport type { MemoryEntry } from \"../types/memory.js\";\nimport type { EmbeddingProvider } from \"./embeddings.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { runPreConsolidationFlush } from \"./pre-consolidation-flush.js\";\nimport { estimateTokenCount, serializeFactForContext } from \"./retrieval-orchestrator.js\";\nimport { RECALLED_CONTEXT_BOUNDARY } from \"./skill-prompt-injection.js\";\n\n// ---------------------------------------------------------------------------\n// Auto-capture: outcome phrase patterns for episodic memory (#781)\n// ---------------------------------------------------------------------------\n\n/**\n * Outcome phrase → EpisodeOutcome mapping.\n * Scanned in session JSONL during compaction to auto-create episode records.\n * Order matters: more specific patterns should come before generic ones.\n */\nconst OUTCOME_PATTERNS: Array<{\n pattern: RegExp;\n outcome: \"success\" | \"failure\" | \"partial\" | \"unknown\";\n eventTemplate: (phrase: string, context: string) => string;\n}> = [\n // Success patterns\n {\n pattern: /✅?\\s*merged\\b/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `PR/branch merged: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /✅?\\s*(?:successfully\\s+)?(?:deployed|deploy\\b)/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Deployment succeeded: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /✅?\\s*(?:passed|tests?\\s+passed|test\\s+pass)/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Tests passed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /🔧\\s*fixed\\b/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Issue fixed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /\\b(?:successfully\\s+)?completed\\b/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Task completed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /✅?\\s*(?:resolved|fix(?:ed)?)\\b/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Resolved: ${ctx.slice(0, 120).trim()}`,\n },\n // Failure patterns\n {\n pattern: /❌\\s*(?:failed|failure|error\\b)/i,\n outcome: \"failure\",\n eventTemplate: (_p, ctx) => `Failed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /\\b(?:FAILED|FAILURE|CRASH(?:ed)?)\\b/i,\n outcome: \"failure\",\n eventTemplate: (_p, ctx) => `Failed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /\\b(?:\\berror\\b|\\bfailed\\b).*(?:during|when|in|while)\\b/i,\n outcome: \"failure\",\n eventTemplate: (_p, ctx) => `Error encountered: ${ctx.slice(0, 120).trim()}`,\n },\n // Partial patterns\n {\n pattern: /⚠️?\\s*partial(?:ly)?\\b/i,\n outcome: \"partial\",\n eventTemplate: (_p, ctx) => `Partial result: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /\\b(?:partial(?:ly)?|incomplete)\\b/i,\n outcome: \"partial\",\n eventTemplate: (_p, ctx) => `Partial result: ${ctx.slice(0, 120).trim()}`,\n },\n];\n\n/**\n * Extract readable text from a session JSONL file for episode scanning.\n */\nasync function extractTextFromSessionFile(sessionFile: string): Promise<string> {\n try {\n await access(sessionFile);\n } catch {\n return \"\";\n }\n try {\n const content = await readFile(sessionFile, \"utf-8\");\n const lines = content.split(\"\\n\").filter((l) => l.trim());\n const parts: string[] = [];\n for (const line of lines) {\n let obj: unknown;\n try {\n obj = JSON.parse(line);\n } catch {\n continue;\n }\n if (!obj || typeof obj !== \"object\") continue;\n const rec = obj as Record<string, unknown>;\n // Prefer structured outcome hints when present (less fragile than emoji scans — #899).\n const structuredOutcome = rec.outcome ?? rec.episodeOutcome;\n if (structuredOutcome === \"success\" || structuredOutcome === \"failure\" || structuredOutcome === \"partial\") {\n parts.push(`[structured outcome=${structuredOutcome}]`);\n }\n const msg = rec.message as Record<string, unknown> | undefined;\n if (!msg || typeof msg !== \"object\") continue;\n const text = (msg.content as string) || (msg.text as string) || \"\";\n if (text) parts.push(text);\n }\n return parts.join(\" \");\n } catch {\n return \"\";\n }\n}\n\n/**\n * Scan session text for outcome-indicating phrases and create episode records.\n * Called during session compaction to auto-capture significant events.\n * Returns the number of episodes created.\n */\nasync function autoCaptureEpisodes(\n factsDb: FactsDB,\n sessionFile: string,\n sessionId: string,\n logger: { debug?: (msg: string) => void; warn?: (msg: string) => void },\n): Promise<number> {\n const text = await extractTextFromSessionFile(sessionFile);\n if (!text) return 0;\n\n // Track which patterns have already fired to avoid duplicate episodes for the same phrase\n const seen = new Set<string>();\n let created = 0;\n\n for (const { pattern, outcome, eventTemplate } of OUTCOME_PATTERNS) {\n const match = text.match(pattern);\n if (!match) continue;\n const phrase = match[0];\n const contextKey = phrase.toLowerCase().slice(0, 50);\n if (seen.has(contextKey)) continue;\n seen.add(contextKey);\n\n try {\n const contextSnippet = text.slice(Math.max(0, text.indexOf(phrase) - 20), text.indexOf(phrase) + 100);\n const event = eventTemplate(phrase, contextSnippet);\n // Deduplicate: don't create the same episode event within this session\n factsDb.recordEpisode({\n event,\n outcome,\n context: contextSnippet.slice(0, 500),\n sessionId,\n tags: [\"auto-captured\"],\n });\n created++;\n logger.debug?.(`memory-hybrid: auto-captured episode [${outcome}]: ${event.slice(0, 80)}`);\n } catch (err) {\n logger.warn?.(`memory-hybrid: auto-capture episode failed: ${err}`);\n }\n }\n\n return created;\n}\n\n// ---------------------------------------------------------------------------\n// Types (local stubs if SDK types are unavailable)\n// ---------------------------------------------------------------------------\n\ntype IngestResult = { ingested: boolean };\ntype AssembleResult = { messages: unknown[]; estimatedTokens: number; systemPromptAddition?: string };\ntype CompactResult = { ok: boolean; compacted: boolean; reason?: string; result?: unknown };\ntype SubagentSpawnPreparation = { rollback: () => void | Promise<void> };\n\ninterface MinimalContextEngine {\n readonly info: { id: string; name: string; version?: string; ownsCompaction?: boolean };\n ingest(p: { sessionId: string; message: unknown }): Promise<IngestResult>;\n assemble(p: { sessionId: string; messages: unknown[]; tokenBudget?: number }): Promise<AssembleResult>;\n compact(p: {\n sessionId: string;\n sessionFile: string;\n tokenBudget?: number;\n force?: boolean;\n currentTokenCount?: number;\n }): Promise<CompactResult>;\n prepareSubagentSpawn?(p: {\n parentSessionKey: string;\n childSessionKey: string;\n ttlMs?: number;\n }): Promise<SubagentSpawnPreparation | undefined>;\n onSubagentEnded?(p: { childSessionKey: string; reason: string }): Promise<void>;\n dispose?(): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Engine implementation\n// ---------------------------------------------------------------------------\n\nexport interface ContextEngineOptions {\n factsDb: FactsDB;\n vectorDb: VectorDB;\n wal: WriteAheadLog | null;\n embeddings: EmbeddingProvider;\n cfg: HybridMemoryConfig;\n logger: { info?: (m: string) => void; warn?: (m: string) => void; debug?: (m: string) => void };\n pluginVersion?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Context block builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build a structured memory context block from a list of facts.\n *\n * Uses `serializeFactForContext` for each entry (same format as the\n * retrieval pipeline) and respects the optional `tokenBudget` cap.\n *\n * An explicit untrusted-data boundary comment is prepended so that\n * recalled content is always framed as data, never instructions (Issue #1579).\n *\n * @param facts - Ordered list of MemoryEntry values, best-first.\n * @param header - HTML comment tag used as the opening/closing marker.\n * @param label - Human-readable label for the block heading.\n * @param tokenBudget - Optional max tokens; entries are skipped once the\n * budget is exceeded.\n * @returns The complete context block string, or `null` when `facts` is empty.\n */\nexport function buildContextBlock(\n facts: MemoryEntry[],\n header: string,\n label: string,\n tokenBudget?: number,\n): string | null {\n if (facts.length === 0) return null;\n\n const lines: string[] = [`<!-- memory-hybrid: ${header} -->`, RECALLED_CONTEXT_BOUNDARY, label];\n const closingLine = `<!-- /memory-hybrid: ${header} -->`;\n\n const baseText = [...lines, closingLine].join(\"\\n\");\n let currentTokens = estimateTokenCount(baseText);\n\n if (tokenBudget !== undefined && currentTokens > tokenBudget) {\n return null;\n }\n\n let addedFacts = 0;\n for (const entry of facts) {\n const serialized = serializeFactForContext(entry);\n const entryTokens = estimateTokenCount(`\\n${serialized}`);\n if (tokenBudget !== undefined && currentTokens + entryTokens > tokenBudget) break;\n lines.push(serialized);\n currentTokens += entryTokens;\n addedFacts++;\n }\n\n if (addedFacts === 0) return null;\n\n lines.push(closingLine);\n\n // Ensure the final joined string strictly satisfies the budget.\n // lines = [header, boundaryComment, label, ...facts, closingLine]\n // Minimum structure has 4 fixed items; trim facts from the end while over budget.\n while (lines.length > 4 && tokenBudget !== undefined && estimateTokenCount(lines.join(\"\\n\")) > tokenBudget) {\n lines.splice(lines.length - 2, 1);\n }\n\n if (lines.length <= 4 && tokenBudget !== undefined && estimateTokenCount(lines.join(\"\\n\")) > tokenBudget) {\n return null;\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Minimal ContextEngine that integrates hybrid-memory into the OpenClaw\n * context lifecycle. Designed for backward compatibility — every method\n * degrades gracefully if dependencies are missing.\n */\nexport class HybridMemoryContextEngine implements MinimalContextEngine {\n readonly info = {\n id: \"hybrid-memory\",\n name: \"OpenClaw Hybrid Memory\",\n version: undefined as string | undefined,\n // We do NOT own compaction — the legacy compaction pipeline handles it.\n // We just flush pending state before it runs.\n ownsCompaction: false,\n };\n\n constructor(private readonly opts: ContextEngineOptions) {\n this.info.version = opts.pluginVersion;\n }\n\n /** no-op: SessionManager handles message persistence. */\n async ingest(_params: { sessionId: string; message: unknown }): Promise<IngestResult> {\n return { ingested: false };\n }\n\n /**\n * Budget-aware context assembly.\n *\n * Fetches the top-N most recent/important facts and injects them as a\n * `systemPromptAddition` block so the SDK can place them in the system\n * prompt. The `tokenBudget` parameter (exposed by SDK #274) caps the\n * total context size returned; when absent, a conservative 1000-token\n * default is used.\n *\n * The autoRecall hook already handles per-turn injection via\n * `before_agent_start → prependContext`; this method provides the same\n * information to the ContextEngine slot so future SDK versions can\n * manage injection centrally.\n */\n async assemble(params: { sessionId: string; messages: unknown[]; tokenBudget?: number }): Promise<AssembleResult> {\n const { factsDb, cfg, logger } = this.opts;\n const budget = params.tokenBudget ?? cfg.autoRecall?.maxTokens ?? 1000;\n\n try {\n const limit = cfg.autoRecall?.limit ?? 10;\n const facts = factsDb.list(Math.min(limit, 15));\n\n if (facts.length === 0) {\n return { messages: params.messages, estimatedTokens: 0 };\n }\n\n const block = buildContextBlock(facts, \"session-context\", \"Relevant memories for this session:\", budget);\n if (!block) {\n return { messages: params.messages, estimatedTokens: 0 };\n }\n\n const estimatedTokens = estimateTokenCount(block);\n logger.debug?.(\n `memory-hybrid: context-engine assemble — injecting ${facts.length} facts, ~${estimatedTokens} tokens (budget=${budget})`,\n );\n\n return {\n messages: params.messages,\n estimatedTokens,\n systemPromptAddition: block,\n };\n } catch (err) {\n logger.warn?.(`memory-hybrid: context-engine assemble failed (non-fatal): ${err}`);\n return { messages: params.messages, estimatedTokens: 0 };\n }\n }\n\n /**\n * Pre-compaction flush:\n * 1. Drain the WAL — replay pending entries to SQLite (and LanceDB if embeddings available)\n * 2. Snapshot session-scoped facts to WAL for durability\n */\n async compact(_params: {\n sessionId: string;\n sessionFile: string;\n tokenBudget?: number;\n force?: boolean;\n currentTokenCount?: number;\n }): Promise<CompactResult> {\n const { logger, wal, factsDb, vectorDb, embeddings } = this.opts;\n try {\n // 1. Replay pending WAL entries — commit any writes that didn't complete before crash/compaction\n const { committed: walCommitted, skipped: walSkipped } = await runPreConsolidationFlush(\n { wal, factsDb, vectorDb, embeddings },\n logger,\n \"context-engine compact\",\n );\n\n // 2. Auto-capture episodic events from session log (#781)\n // Scans the session JSONL for outcome-indicating phrases (✅ merged, ❌ failed, etc.)\n // and creates episode records for significant events.\n let episodesCreated = 0;\n try {\n episodesCreated = await autoCaptureEpisodes(factsDb, _params.sessionFile, _params.sessionId, logger);\n if (episodesCreated > 0) {\n logger.info?.(`memory-hybrid: auto-captured ${episodesCreated} episode(s) from session compaction`);\n }\n } catch (err) {\n logger.warn?.(`memory-hybrid: episode auto-capture failed (non-fatal): ${err}`);\n }\n\n // 3. Count total facts and build a brief post-compaction memory summary.\n //\n // The summary is returned in the `result` field so that SDK versions which\n // consume it can inject the top facts into the post-compaction context.\n // On older runtimes that only read `ok`/`compacted`/`reason`, the extra\n // fields are ignored — no behaviour change.\n //\n // TODO(SDK #275): When the SDK exposes a dedicated `contextAddition` field\n // on CompactResult, move the summary string there instead of nesting it in\n // `result`. Until then, `result.memorySummary` serves as the best-effort\n // injection surface.\n let sessionFacts = 0;\n let memorySummary: string | undefined;\n // Declare topFacts in outer scope so it's accessible in the return statement below.\n let topFacts: ReturnType<typeof factsDb.list> = [];\n try {\n sessionFacts = factsDb.getCount();\n topFacts = factsDb.list(8);\n const block = buildContextBlock(\n topFacts,\n \"post-compaction memory summary\",\n \"Key memories retained across compaction:\",\n _params.tokenBudget,\n );\n if (block) memorySummary = block;\n } catch {\n // Non-fatal\n }\n\n logger.debug?.(\n `memory-hybrid: context-engine compact — pre-compaction flush done, sessionFacts≈${sessionFacts}, episodes≈${episodesCreated}`,\n );\n return {\n ok: true,\n compacted: false,\n reason: `flushed pending state (wal: ${walCommitted} committed, ${episodesCreated} episodes auto-captured)`,\n // Extended result field: SDK ≥ 2026.3.8 may inject memorySummary into context.\n result: memorySummary\n ? { topFacts, factCount: sessionFacts, memorySummary, episodesCreated }\n : { factCount: sessionFacts, episodesCreated },\n };\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-engine\",\n operation: \"compact\",\n });\n logger.warn?.(`memory-hybrid: context-engine compact failed: ${err}`);\n return { ok: false, compacted: false, reason: String(err) };\n }\n }\n\n /**\n * Pre-spawn preparation: inject relevant parent memories into sub-agent context.\n *\n * Fetches the top-N most recent/important facts from the parent session and\n * returns them as a `contextAddition` string for the SDK to inject into the\n * sub-agent's initial context.\n *\n * Guard against double-processing: the before_agent_start hook also injects\n * memories for the child session. This method only fires when the SDK calls\n * the ContextEngine API directly (OpenClaw ≥ 2026.3.8). On older runtimes the\n * method is never called, so there is no duplication risk.\n */\n async prepareSubagentSpawn(params: {\n parentSessionKey: string;\n childSessionKey: string;\n ttlMs?: number;\n }): Promise<SubagentSpawnPreparation | undefined> {\n const { factsDb, cfg, logger } = this.opts;\n try {\n // Fetch top-N recent/important facts to seed the sub-agent's context\n const limit = cfg.autoRecall?.limit ?? 10;\n const topFacts = factsDb.list(Math.min(limit, 15));\n\n if (topFacts.length === 0) {\n logger.debug?.(`memory-hybrid: prepareSubagentSpawn — no facts to inject for child=${params.childSessionKey}`);\n return { rollback: async () => {} };\n }\n\n // contextAddition is a non-standard field on the return type — populated so that\n // SDK versions that support it can inject the block; older versions ignore it.\n // Uses serializeFactForContext for consistent formatting with the retrieval pipeline.\n const contextAddition = buildContextBlock(\n topFacts,\n `parent context injected for subagent ${params.childSessionKey}`,\n `Relevant memories from parent session (${params.parentSessionKey}):`,\n );\n\n logger.debug?.(\n `memory-hybrid: prepareSubagentSpawn — injecting ${topFacts.length} facts for child=${params.childSessionKey}`,\n );\n\n return {\n rollback: async () => {\n // No state was mutated; rollback is a no-op.\n logger.debug?.(\n `memory-hybrid: prepareSubagentSpawn rollback — no state to revert for child=${params.childSessionKey}`,\n );\n },\n // Extended field: injected into sub-agent context by SDK ≥ 2026.3.8.\n // null when buildContextBlock returns null (empty facts list — already guarded above).\n ...(contextAddition !== null ? { contextAddition } : {}),\n } as SubagentSpawnPreparation & { contextAddition?: string };\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-engine\",\n operation: \"prepare-subagent-spawn\",\n });\n logger.warn?.(`memory-hybrid: prepareSubagentSpawn failed (non-fatal): ${err}`);\n return undefined;\n }\n }\n\n /**\n * Post-subagent cleanup: capture any session-scoped facts created by the sub-agent\n * and promote them to the appropriate scope.\n *\n * Guard against double-processing: OpenClaw's typed **`subagent_ended`** hook\n * (`lifecycle/stage-cleanup.ts`) only performs ACTIVE-TASKS.md checkpointing — not fact capture.\n * This method provides a lightweight ContextEngine callback; primary fact capture is the\n * child session's **agent_end** autoCapture path. If a fact is already in the store it will be\n * skipped by the hasDuplicate check when that pipeline exists.\n *\n * NOTE: The current SDK interface does not pass the sub-agent's result text here.\n * When the SDK exposes result text, parse it using the existing autoCapture logic\n * (see lifecycle/hooks.ts agent_end handler).\n */\n async onSubagentEnded(params: { childSessionKey: string; reason: string }): Promise<void> {\n const { factsDb, logger } = this.opts;\n try {\n // Count facts captured from the child session (written to the shared store by the\n // child's own agent_end autoCapture hook while the child session was running).\n const capturedCount = factsDb.countBySource(params.childSessionKey);\n\n if (capturedCount > 0) {\n logger.info?.(\n `memory-hybrid: context-engine onSubagentEnded — child=${params.childSessionKey} reason=${params.reason} childFacts=${capturedCount}`,\n );\n } else {\n logger.debug?.(\n `memory-hybrid: context-engine onSubagentEnded — child=${params.childSessionKey} reason=${params.reason} childFacts=0 (no auto-captured facts found for this session key)`,\n );\n }\n\n // TODO(SDK #273): Implement result-text fact extraction once the SDK exposes it.\n //\n // The current hook signature only provides { childSessionKey, reason }.\n // To implement full result-text capture, the SDK must expose one of:\n // params.resultText: string — the final assistant text from the sub-agent\n // params.messages: unknown[] — full message log (same shape as agent_end event)\n //\n // When available, mirror the agent_end autoCapture pipeline from lifecycle/hooks.ts:\n // 1. Iterate messages, extract text/content blocks\n // 2. Filter via shouldCapture() (needs to be added to ContextEngineOptions)\n // 3. Classify with detectCategory() / classifyMemoryOperation()\n // 4. Deduplicate via factsDb.hasDuplicate()\n // 5. Store with source=params.childSessionKey, scope=\"global\"\n //\n // Until then, all sub-agent fact capture is delegated to:\n // (a) The child session's own agent_end autoCapture hook (primary path — runs\n // inside the child's session and writes directly to the shared FactsDB)\n // (b) The typed subagent_ended hook in lifecycle/stage-cleanup.ts (ACTIVE-TASKS.md only; issue #966)\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-engine\",\n operation: \"on-subagent-ended\",\n });\n logger.warn?.(`memory-hybrid: onSubagentEnded failed (non-fatal): ${err}`);\n }\n }\n\n async dispose(): Promise<void> {\n // No owned resources to dispose.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Registration helper (feature-detected)\n// ---------------------------------------------------------------------------\n\n/**\n * Attempt to register HybridMemoryContextEngine with the OpenClaw plugin SDK.\n *\n * Safe to call even when:\n * - The registerContextEngine export doesn't exist (older OpenClaw versions)\n * - The import fails (ESM resolution error in older runtime)\n *\n * Returns true if registration succeeded, false otherwise.\n */\nexport async function registerHybridContextEngine(opts: ContextEngineOptions): Promise<boolean> {\n try {\n // Dynamic import for feature detection — avoids hard dependency on the API.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const sdk: any = await import(\"openclaw/plugin-sdk/core\").catch(() => null);\n if (!sdk) {\n opts.logger.debug?.(\"memory-hybrid: openclaw/plugin-sdk not available; skipping ContextEngine registration\");\n return false;\n }\n\n const { registerContextEngine } = sdk as {\n registerContextEngine?: (id: string, factory: () => MinimalContextEngine) => void;\n };\n\n if (typeof registerContextEngine !== \"function\") {\n opts.logger.debug?.(\"memory-hybrid: registerContextEngine not found in SDK; skipping ContextEngine registration\");\n return false;\n }\n\n const engine = new HybridMemoryContextEngine(opts);\n registerContextEngine(\"hybrid-memory\", () => engine);\n opts.logger.info?.(\"memory-hybrid: ContextEngine registered (id=hybrid-memory)\");\n return true;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-engine\",\n operation: \"register\",\n });\n opts.logger.warn?.(`memory-hybrid: ContextEngine registration failed (non-fatal): ${err}`);\n return false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,MAAM,mBAID;CAEH;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EAC1E;CACD;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,yBAAyB,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EAC9E;CACD;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EACtE;CACD;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EACrE;CACD;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,mBAAmB,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EACxE;CACD;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,aAAa,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EAClE;CAED;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,WAAW,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EAChE;CACD;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,WAAW,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EAChE;CACD;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EAC3E;CAED;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,mBAAmB,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EACxE;CACD;EACE,SAAS;EACT,SAAS;EACT,gBAAgB,IAAI,QAAQ,mBAAmB,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM;EACxE;CACF;;;;AAKD,eAAe,2BAA2B,aAAsC;CAC9E,IAAI;EACF,MAAM,OAAO,YAAY;SACnB;EACN,OAAO;;CAET,IAAI;EAEF,MAAM,SAAQ,MADQ,SAAS,aAAa,QAAQ,EAC9B,MAAM,KAAK,CAAC,QAAQ,MAAM,EAAE,MAAM,CAAC;EACzD,MAAM,QAAkB,EAAE;EAC1B,KAAK,MAAM,QAAQ,OAAO;GACxB,IAAI;GACJ,IAAI;IACF,MAAM,KAAK,MAAM,KAAK;WAChB;IACN;;GAEF,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;GACrC,MAAM,MAAM;GAEZ,MAAM,oBAAoB,IAAI,WAAW,IAAI;GAC7C,IAAI,sBAAsB,aAAa,sBAAsB,aAAa,sBAAsB,WAC9F,MAAM,KAAK,uBAAuB,kBAAkB,GAAG;GAEzD,MAAM,MAAM,IAAI;GAChB,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;GACrC,MAAM,OAAQ,IAAI,WAAuB,IAAI,QAAmB;GAChE,IAAI,MAAM,MAAM,KAAK,KAAK;;EAE5B,OAAO,MAAM,KAAK,IAAI;SAChB;EACN,OAAO;;;;;;;;AASX,eAAe,oBACb,SACA,aACA,WACA,QACiB;CACjB,MAAM,OAAO,MAAM,2BAA2B,YAAY;CAC1D,IAAI,CAAC,MAAM,OAAO;CAGlB,MAAM,uBAAO,IAAI,KAAa;CAC9B,IAAI,UAAU;CAEd,KAAK,MAAM,EAAE,SAAS,SAAS,mBAAmB,kBAAkB;EAClE,MAAM,QAAQ,KAAK,MAAM,QAAQ;EACjC,IAAI,CAAC,OAAO;EACZ,MAAM,SAAS,MAAM;EACrB,MAAM,aAAa,OAAO,aAAa,CAAC,MAAM,GAAG,GAAG;EACpD,IAAI,KAAK,IAAI,WAAW,EAAE;EAC1B,KAAK,IAAI,WAAW;EAEpB,IAAI;GACF,MAAM,iBAAiB,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK,QAAQ,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,OAAO,GAAG,IAAI;GACrG,MAAM,QAAQ,cAAc,QAAQ,eAAe;GAEnD,QAAQ,cAAc;IACpB;IACA;IACA,SAAS,eAAe,MAAM,GAAG,IAAI;IACrC;IACA,MAAM,CAAC,gBAAgB;IACxB,CAAC;GACF;GACA,OAAO,QAAQ,yCAAyC,QAAQ,KAAK,MAAM,MAAM,GAAG,GAAG,GAAG;WACnF,KAAK;GACZ,OAAO,OAAO,+CAA+C,MAAM;;;CAIvE,OAAO;;;;;;;;;;;;;;;;;;AAkET,SAAgB,kBACd,OACA,QACA,OACA,aACe;CACf,IAAI,MAAM,WAAW,GAAG,OAAO;CAE/B,MAAM,QAAkB;EAAC,uBAAuB,OAAO;EAAO;EAA2B;EAAM;CAC/F,MAAM,cAAc,wBAAwB,OAAO;CAGnD,IAAI,gBAAgB,mBADH,CAAC,GAAG,OAAO,YAAY,CAAC,KAAK,KACC,CAAC;CAEhD,IAAI,gBAAgB,KAAA,KAAa,gBAAgB,aAC/C,OAAO;CAGT,IAAI,aAAa;CACjB,KAAK,MAAM,SAAS,OAAO;EACzB,MAAM,aAAa,wBAAwB,MAAM;EACjD,MAAM,cAAc,mBAAmB,KAAK,aAAa;EACzD,IAAI,gBAAgB,KAAA,KAAa,gBAAgB,cAAc,aAAa;EAC5E,MAAM,KAAK,WAAW;EACtB,iBAAiB;EACjB;;CAGF,IAAI,eAAe,GAAG,OAAO;CAE7B,MAAM,KAAK,YAAY;CAKvB,OAAO,MAAM,SAAS,KAAK,gBAAgB,KAAA,KAAa,mBAAmB,MAAM,KAAK,KAAK,CAAC,GAAG,aAC7F,MAAM,OAAO,MAAM,SAAS,GAAG,EAAE;CAGnC,IAAI,MAAM,UAAU,KAAK,gBAAgB,KAAA,KAAa,mBAAmB,MAAM,KAAK,KAAK,CAAC,GAAG,aAC3F,OAAO;CAGT,OAAO,MAAM,KAAK,KAAK;;;;;;;AAQzB,IAAa,4BAAb,MAAuE;CAUxC;CAT7B,OAAgB;EACd,IAAI;EACJ,MAAM;EACN,SAAS,KAAA;EAGT,gBAAgB;EACjB;CAED,YAAY,MAA6C;EAA5B,KAAA,OAAA;EAC3B,KAAK,KAAK,UAAU,KAAK;;;CAI3B,MAAM,OAAO,SAAyE;EACpF,OAAO,EAAE,UAAU,OAAO;;;;;;;;;;;;;;;;CAiB5B,MAAM,SAAS,QAAmG;EAChH,MAAM,EAAE,SAAS,KAAK,WAAW,KAAK;EACtC,MAAM,SAAS,OAAO,eAAe,IAAI,YAAY,aAAa;EAElE,IAAI;GACF,MAAM,QAAQ,IAAI,YAAY,SAAS;GACvC,MAAM,QAAQ,QAAQ,KAAK,KAAK,IAAI,OAAO,GAAG,CAAC;GAE/C,IAAI,MAAM,WAAW,GACnB,OAAO;IAAE,UAAU,OAAO;IAAU,iBAAiB;IAAG;GAG1D,MAAM,QAAQ,kBAAkB,OAAO,mBAAmB,uCAAuC,OAAO;GACxG,IAAI,CAAC,OACH,OAAO;IAAE,UAAU,OAAO;IAAU,iBAAiB;IAAG;GAG1D,MAAM,kBAAkB,mBAAmB,MAAM;GACjD,OAAO,QACL,sDAAsD,MAAM,OAAO,WAAW,gBAAgB,kBAAkB,OAAO,GACxH;GAED,OAAO;IACL,UAAU,OAAO;IACjB;IACA,sBAAsB;IACvB;WACM,KAAK;GACZ,OAAO,OAAO,8DAA8D,MAAM;GAClF,OAAO;IAAE,UAAU,OAAO;IAAU,iBAAiB;IAAG;;;;;;;;CAS5D,MAAM,QAAQ,SAMa;EACzB,MAAM,EAAE,QAAQ,KAAK,SAAS,UAAU,eAAe,KAAK;EAC5D,IAAI;GAEF,MAAM,EAAE,WAAW,cAAc,SAAS,eAAe,MAAM,yBAC7D;IAAE;IAAK;IAAS;IAAU;IAAY,EACtC,QACA,yBACD;GAKD,IAAI,kBAAkB;GACtB,IAAI;IACF,kBAAkB,MAAM,oBAAoB,SAAS,QAAQ,aAAa,QAAQ,WAAW,OAAO;IACpG,IAAI,kBAAkB,GACpB,OAAO,OAAO,gCAAgC,gBAAgB,qCAAqC;YAE9F,KAAK;IACZ,OAAO,OAAO,2DAA2D,MAAM;;GAcjF,IAAI,eAAe;GACnB,IAAI;GAEJ,IAAI,WAA4C,EAAE;GAClD,IAAI;IACF,eAAe,QAAQ,UAAU;IACjC,WAAW,QAAQ,KAAK,EAAE;IAC1B,MAAM,QAAQ,kBACZ,UACA,kCACA,4CACA,QAAQ,YACT;IACD,IAAI,OAAO,gBAAgB;WACrB;GAIR,OAAO,QACL,mFAAmF,aAAa,aAAa,kBAC9G;GACD,OAAO;IACL,IAAI;IACJ,WAAW;IACX,QAAQ,+BAA+B,aAAa,cAAc,gBAAgB;IAElF,QAAQ,gBACJ;KAAE;KAAU,WAAW;KAAc;KAAe;KAAiB,GACrE;KAAE,WAAW;KAAc;KAAiB;IACjD;WACM,KAAK;GACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,EAAE;IACtE,WAAW;IACX,WAAW;IACZ,CAAC;GACF,OAAO,OAAO,iDAAiD,MAAM;GACrE,OAAO;IAAE,IAAI;IAAO,WAAW;IAAO,QAAQ,OAAO,IAAI;IAAE;;;;;;;;;;;;;;;CAgB/D,MAAM,qBAAqB,QAIuB;EAChD,MAAM,EAAE,SAAS,KAAK,WAAW,KAAK;EACtC,IAAI;GAEF,MAAM,QAAQ,IAAI,YAAY,SAAS;GACvC,MAAM,WAAW,QAAQ,KAAK,KAAK,IAAI,OAAO,GAAG,CAAC;GAElD,IAAI,SAAS,WAAW,GAAG;IACzB,OAAO,QAAQ,sEAAsE,OAAO,kBAAkB;IAC9G,OAAO,EAAE,UAAU,YAAY,IAAI;;GAMrC,MAAM,kBAAkB,kBACtB,UACA,wCAAwC,OAAO,mBAC/C,0CAA0C,OAAO,iBAAiB,IACnE;GAED,OAAO,QACL,mDAAmD,SAAS,OAAO,mBAAmB,OAAO,kBAC9F;GAED,OAAO;IACL,UAAU,YAAY;KAEpB,OAAO,QACL,+EAA+E,OAAO,kBACvF;;IAIH,GAAI,oBAAoB,OAAO,EAAE,iBAAiB,GAAG,EAAE;IACxD;WACM,KAAK;GACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,EAAE;IACtE,WAAW;IACX,WAAW;IACZ,CAAC;GACF,OAAO,OAAO,2DAA2D,MAAM;GAC/E;;;;;;;;;;;;;;;;;CAkBJ,MAAM,gBAAgB,QAAoE;EACxF,MAAM,EAAE,SAAS,WAAW,KAAK;EACjC,IAAI;GAGF,MAAM,gBAAgB,QAAQ,cAAc,OAAO,gBAAgB;GAEnE,IAAI,gBAAgB,GAClB,OAAO,OACL,yDAAyD,OAAO,gBAAgB,UAAU,OAAO,OAAO,cAAc,gBACvH;QAED,OAAO,QACL,yDAAyD,OAAO,gBAAgB,UAAU,OAAO,OAAO,mEACzG;WAqBI,KAAK;GACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,EAAE;IACtE,WAAW;IACX,WAAW;IACZ,CAAC;GACF,OAAO,OAAO,sDAAsD,MAAM;;;CAI9E,MAAM,UAAyB;;;;;;;;;;;AAkBjC,eAAsB,4BAA4B,MAA8C;CAC9F,IAAI;EAGF,MAAM,MAAW,MAAM,OAAO,4BAA4B,YAAY,KAAK;EAC3E,IAAI,CAAC,KAAK;GACR,KAAK,OAAO,QAAQ,wFAAwF;GAC5G,OAAO;;EAGT,MAAM,EAAE,0BAA0B;EAIlC,IAAI,OAAO,0BAA0B,YAAY;GAC/C,KAAK,OAAO,QAAQ,6FAA6F;GACjH,OAAO;;EAGT,MAAM,SAAS,IAAI,0BAA0B,KAAK;EAClD,sBAAsB,uBAAuB,OAAO;EACpD,KAAK,OAAO,OAAO,6DAA6D;EAChF,OAAO;UACA,KAAK;EACZ,mBAAmB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC,EAAE;GACtE,WAAW;GACX,WAAW;GACZ,CAAC;EACF,KAAK,OAAO,OAAO,iEAAiE,MAAM;EAC1F,OAAO"}
|
|
1
|
+
{"version":3,"file":"context-engine.js","names":[],"sources":["../../services/context-engine.ts"],"sourcesContent":["/**\n * ContextEngine Plugin Slot Adoption (Issue #273)\n *\n * Registers the hybrid-memory plugin as an OpenClaw ContextEngine when the\n * registerContextEngine API is available (OpenClaw ≥ 2026.3.8).\n *\n * Capabilities:\n * - compact: flush WAL + snapshot session-scoped facts before compaction\n * - prepareSubagentSpawn: inject relevant parent memories into sub-agent context\n * - onSubagentEnded: capture facts from finished sub-agent session\n * - assemble: pass-through (autoRecall handles injection via before_agent_start)\n * - ingest: no-op (SessionManager owns persistence)\n *\n * Feature detection: if registerContextEngine is absent the module exports a\n * no-op so callers do not need to guard on their side.\n */\n\nimport { access, readFile } from \"node:fs/promises\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport type { VectorDB } from \"../backends/vector-db.js\";\nimport type { WriteAheadLog } from \"../backends/wal.js\";\nimport type { HybridMemoryConfig } from \"../config.js\";\nimport type { MemoryEntry } from \"../types/memory.js\";\nimport type { EmbeddingProvider } from \"./embeddings.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { runPreConsolidationFlush } from \"./pre-consolidation-flush.js\";\nimport { estimateTokenCount, serializeFactForContext } from \"./retrieval-orchestrator.js\";\nimport { RECALLED_CONTEXT_BOUNDARY } from \"./skill-prompt-injection.js\";\n\n// ---------------------------------------------------------------------------\n// Auto-capture: outcome phrase patterns for episodic memory (#781)\n// ---------------------------------------------------------------------------\n\n/**\n * Outcome phrase → EpisodeOutcome mapping.\n * Scanned in session JSONL during compaction to auto-create episode records.\n * Order matters: more specific patterns should come before generic ones.\n */\nconst OUTCOME_PATTERNS: Array<{\n pattern: RegExp;\n outcome: \"success\" | \"failure\" | \"partial\" | \"unknown\";\n eventTemplate: (phrase: string, context: string) => string;\n}> = [\n // Success patterns\n {\n pattern: /✅?\\s*merged\\b/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `PR/branch merged: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /✅?\\s*(?:successfully\\s+)?(?:deployed|deploy\\b)/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Deployment succeeded: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /✅?\\s*(?:passed|tests?\\s+passed|test\\s+pass)/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Tests passed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /🔧\\s*fixed\\b/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Issue fixed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /\\b(?:successfully\\s+)?completed\\b/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Task completed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /✅?\\s*(?:resolved|fix(?:ed)?)\\b/i,\n outcome: \"success\",\n eventTemplate: (_p, ctx) => `Resolved: ${ctx.slice(0, 120).trim()}`,\n },\n // Failure patterns\n {\n pattern: /❌\\s*(?:failed|failure|error\\b)/i,\n outcome: \"failure\",\n eventTemplate: (_p, ctx) => `Failed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /\\b(?:FAILED|FAILURE|CRASH(?:ed)?)\\b/i,\n outcome: \"failure\",\n eventTemplate: (_p, ctx) => `Failed: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /\\b(?:\\berror\\b|\\bfailed\\b).*(?:during|when|in|while)\\b/i,\n outcome: \"failure\",\n eventTemplate: (_p, ctx) => `Error encountered: ${ctx.slice(0, 120).trim()}`,\n },\n // Partial patterns\n {\n pattern: /⚠️?\\s*partial(?:ly)?\\b/i,\n outcome: \"partial\",\n eventTemplate: (_p, ctx) => `Partial result: ${ctx.slice(0, 120).trim()}`,\n },\n {\n pattern: /\\b(?:partial(?:ly)?|incomplete)\\b/i,\n outcome: \"partial\",\n eventTemplate: (_p, ctx) => `Partial result: ${ctx.slice(0, 120).trim()}`,\n },\n];\n\n/**\n * Extract readable text from a session JSONL file for episode scanning.\n */\nasync function extractTextFromSessionFile(sessionFile: string): Promise<string> {\n try {\n await access(sessionFile);\n } catch {\n return \"\";\n }\n try {\n const content = await readFile(sessionFile, \"utf-8\");\n const lines = content.split(\"\\n\").filter((l) => l.trim());\n const parts: string[] = [];\n for (const line of lines) {\n let obj: unknown;\n try {\n obj = JSON.parse(line);\n } catch {\n continue;\n }\n if (!obj || typeof obj !== \"object\") continue;\n const rec = obj as Record<string, unknown>;\n // Prefer structured outcome hints when present (less fragile than emoji scans — #899).\n const structuredOutcome = rec.outcome ?? rec.episodeOutcome;\n if (structuredOutcome === \"success\" || structuredOutcome === \"failure\" || structuredOutcome === \"partial\") {\n parts.push(`[structured outcome=${structuredOutcome}]`);\n }\n const msg = rec.message as Record<string, unknown> | undefined;\n if (!msg || typeof msg !== \"object\") continue;\n const text = (msg.content as string) || (msg.text as string) || \"\";\n if (text) parts.push(text);\n }\n return parts.join(\" \");\n } catch {\n return \"\";\n }\n}\n\n/**\n * Scan session text for outcome-indicating phrases and create episode records.\n * Called during session compaction to auto-capture significant events.\n * Returns the number of episodes created.\n */\nasync function autoCaptureEpisodes(\n factsDb: FactsDB,\n sessionFile: string,\n sessionId: string,\n logger: { debug?: (msg: string) => void; warn?: (msg: string) => void },\n): Promise<number> {\n const text = await extractTextFromSessionFile(sessionFile);\n if (!text) return 0;\n\n // Track which patterns have already fired to avoid duplicate episodes for the same phrase\n const seen = new Set<string>();\n let created = 0;\n\n for (const { pattern, outcome, eventTemplate } of OUTCOME_PATTERNS) {\n const match = text.match(pattern);\n if (!match) continue;\n const phrase = match[0];\n const contextKey = phrase.toLowerCase().slice(0, 50);\n if (seen.has(contextKey)) continue;\n seen.add(contextKey);\n\n try {\n const contextSnippet = text.slice(Math.max(0, text.indexOf(phrase) - 20), text.indexOf(phrase) + 100);\n const event = eventTemplate(phrase, contextSnippet);\n // Deduplicate: don't create the same episode event within this session\n factsDb.recordEpisode({\n event,\n outcome,\n context: contextSnippet.slice(0, 500),\n sessionId,\n tags: [\"auto-captured\"],\n });\n created++;\n logger.debug?.(`memory-hybrid: auto-captured episode [${outcome}]: ${event.slice(0, 80)}`);\n } catch (err) {\n logger.warn?.(`memory-hybrid: auto-capture episode failed: ${err}`);\n }\n }\n\n return created;\n}\n\n// ---------------------------------------------------------------------------\n// Types (local stubs if SDK types are unavailable)\n// ---------------------------------------------------------------------------\n\ntype IngestResult = { ingested: boolean };\ntype AssembleResult = { messages: unknown[]; estimatedTokens: number; systemPromptAddition?: string };\ntype CompactResult = { ok: boolean; compacted: boolean; reason?: string; result?: unknown };\ntype SubagentSpawnPreparation = { rollback: () => void | Promise<void> };\n\ninterface MinimalContextEngine {\n readonly info: { id: string; name: string; version?: string; ownsCompaction?: boolean };\n ingest(p: { sessionId: string; message: unknown }): Promise<IngestResult>;\n assemble(p: { sessionId: string; messages: unknown[]; tokenBudget?: number }): Promise<AssembleResult>;\n compact(p: {\n sessionId: string;\n sessionFile: string;\n tokenBudget?: number;\n force?: boolean;\n currentTokenCount?: number;\n }): Promise<CompactResult>;\n prepareSubagentSpawn?(p: {\n parentSessionKey: string;\n childSessionKey: string;\n ttlMs?: number;\n }): Promise<SubagentSpawnPreparation | undefined>;\n onSubagentEnded?(p: { childSessionKey: string; reason: string }): Promise<void>;\n dispose?(): Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// Engine implementation\n// ---------------------------------------------------------------------------\n\nexport interface ContextEngineOptions {\n factsDb: FactsDB;\n vectorDb: VectorDB;\n wal: WriteAheadLog | null;\n embeddings: EmbeddingProvider;\n cfg: HybridMemoryConfig;\n logger: { info?: (m: string) => void; warn?: (m: string) => void; debug?: (m: string) => void };\n pluginVersion?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Context block builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build a structured memory context block from a list of facts.\n *\n * Uses `serializeFactForContext` for each entry (same format as the\n * retrieval pipeline) and respects the optional `tokenBudget` cap.\n *\n * An explicit untrusted-data boundary comment is prepended so that\n * recalled content is always framed as data, never instructions (Issue #1579).\n *\n * @param facts - Ordered list of MemoryEntry values, best-first.\n * @param header - HTML comment tag used as the opening/closing marker.\n * @param label - Human-readable label for the block heading.\n * @param tokenBudget - Optional max tokens; entries are skipped once the\n * budget is exceeded.\n * @returns The complete context block string, or `null` when `facts` is empty.\n */\nexport function buildContextBlock(\n facts: MemoryEntry[],\n header: string,\n label: string,\n tokenBudget?: number,\n): string | null {\n if (facts.length === 0) return null;\n\n const lines: string[] = [`<!-- memory-hybrid: ${header} -->`, RECALLED_CONTEXT_BOUNDARY, label];\n const closingLine = `<!-- /memory-hybrid: ${header} -->`;\n\n const baseText = [...lines, closingLine].join(\"\\n\");\n let currentTokens = estimateTokenCount(baseText);\n\n if (tokenBudget !== undefined && currentTokens > tokenBudget) {\n return null;\n }\n\n let addedFacts = 0;\n for (const entry of facts) {\n const serialized = serializeFactForContext(entry);\n const entryTokens = estimateTokenCount(`\\n${serialized}`);\n if (tokenBudget !== undefined && currentTokens + entryTokens > tokenBudget) break;\n lines.push(serialized);\n currentTokens += entryTokens;\n addedFacts++;\n }\n\n if (addedFacts === 0) return null;\n\n lines.push(closingLine);\n\n // Ensure the final joined string strictly satisfies the budget.\n // lines = [header, boundaryComment, label, ...facts, closingLine]\n // Minimum structure has 4 fixed items; trim facts from the end while over budget.\n while (lines.length > 4 && tokenBudget !== undefined && estimateTokenCount(lines.join(\"\\n\")) > tokenBudget) {\n lines.splice(lines.length - 2, 1);\n }\n\n if (lines.length <= 4 && tokenBudget !== undefined && estimateTokenCount(lines.join(\"\\n\")) > tokenBudget) {\n return null;\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Minimal ContextEngine that integrates hybrid-memory into the OpenClaw\n * context lifecycle. Designed for backward compatibility — every method\n * degrades gracefully if dependencies are missing.\n */\nexport class HybridMemoryContextEngine implements MinimalContextEngine {\n readonly info = {\n id: \"hybrid-memory\",\n name: \"OpenClaw Hybrid Memory\",\n version: undefined as string | undefined,\n // We do NOT own compaction — the legacy compaction pipeline handles it.\n // We just flush pending state before it runs.\n ownsCompaction: false,\n };\n\n constructor(private readonly opts: ContextEngineOptions) {\n this.info.version = opts.pluginVersion;\n }\n\n /** no-op: SessionManager handles message persistence. */\n async ingest(_params: { sessionId: string; message: unknown }): Promise<IngestResult> {\n return { ingested: false };\n }\n\n /**\n * Budget-aware context assembly.\n *\n * Fetches the top-N most recent/important facts and injects them as a\n * `systemPromptAddition` block so the SDK can place them in the system\n * prompt. The `tokenBudget` parameter (exposed by SDK #274) caps the\n * total context size returned; when absent, a conservative 1000-token\n * default is used.\n *\n * The autoRecall hook already handles per-turn injection via\n * `before_agent_start → prependContext`; this method provides the same\n * information to the ContextEngine slot so future SDK versions can\n * manage injection centrally.\n */\n async assemble(params: { sessionId: string; messages: unknown[]; tokenBudget?: number }): Promise<AssembleResult> {\n const { factsDb, cfg, logger } = this.opts;\n const budget = params.tokenBudget ?? cfg.autoRecall?.maxTokens ?? 1000;\n\n try {\n const limit = cfg.autoRecall?.limit ?? 10;\n const facts = factsDb.list(Math.min(limit, 15));\n\n if (facts.length === 0) {\n return { messages: params.messages, estimatedTokens: 0 };\n }\n\n const block = buildContextBlock(facts, \"session-context\", \"Relevant memories for this session:\", budget);\n if (!block) {\n return { messages: params.messages, estimatedTokens: 0 };\n }\n\n const estimatedTokens = estimateTokenCount(block);\n logger.debug?.(\n `memory-hybrid: context-engine assemble — injecting ${facts.length} facts, ~${estimatedTokens} tokens (budget=${budget})`,\n );\n\n return {\n messages: params.messages,\n estimatedTokens,\n systemPromptAddition: block,\n };\n } catch (err) {\n logger.warn?.(`memory-hybrid: context-engine assemble failed (non-fatal): ${err}`);\n return { messages: params.messages, estimatedTokens: 0 };\n }\n }\n\n /**\n * Pre-compaction flush:\n * 1. Drain the WAL — replay pending entries to SQLite (and LanceDB if embeddings available)\n * 2. Snapshot session-scoped facts to WAL for durability\n */\n async compact(_params: {\n sessionId: string;\n sessionFile: string;\n tokenBudget?: number;\n force?: boolean;\n currentTokenCount?: number;\n }): Promise<CompactResult> {\n const { logger, wal, factsDb, vectorDb, embeddings } = this.opts;\n try {\n // 1. Replay pending WAL entries — commit any writes that didn't complete before crash/compaction\n const { committed: walCommitted, skipped: walSkipped } = await runPreConsolidationFlush(\n { wal, factsDb, vectorDb, embeddings },\n logger,\n \"context-engine compact\",\n );\n\n // 2. Auto-capture episodic events from session log (#781)\n // Scans the session JSONL for outcome-indicating phrases (✅ merged, ❌ failed, etc.)\n // and creates episode records for significant events.\n let episodesCreated = 0;\n try {\n episodesCreated = await autoCaptureEpisodes(factsDb, _params.sessionFile, _params.sessionId, logger);\n if (episodesCreated > 0) {\n logger.info?.(`memory-hybrid: auto-captured ${episodesCreated} episode(s) from session compaction`);\n }\n } catch (err) {\n logger.warn?.(`memory-hybrid: episode auto-capture failed (non-fatal): ${err}`);\n }\n\n // 3. Count total facts and build a brief post-compaction memory summary.\n //\n // The summary is returned in the `result` field so that SDK versions which\n // consume it can inject the top facts into the post-compaction context.\n // On older runtimes that only read `ok`/`compacted`/`reason`, the extra\n // fields are ignored — no behaviour change.\n //\n // TODO(SDK #275): When the SDK exposes a dedicated `contextAddition` field\n // on CompactResult, move the summary string there instead of nesting it in\n // `result`. Until then, `result.memorySummary` serves as the best-effort\n // injection surface.\n let sessionFacts = 0;\n let memorySummary: string | undefined;\n // Declare topFacts in outer scope so it's accessible in the return statement below.\n let topFacts: ReturnType<typeof factsDb.list> = [];\n try {\n sessionFacts = factsDb.getCount();\n topFacts = factsDb.list(8);\n const block = buildContextBlock(\n topFacts,\n \"post-compaction memory summary\",\n \"Key memories retained across compaction:\",\n _params.tokenBudget,\n );\n if (block) memorySummary = block;\n } catch {\n // Non-fatal\n }\n\n logger.debug?.(\n `memory-hybrid: context-engine compact — pre-compaction flush done, sessionFacts≈${sessionFacts}, episodes≈${episodesCreated}`,\n );\n return {\n ok: true,\n compacted: false,\n reason: `flushed pending state (wal: ${walCommitted} committed, ${episodesCreated} episodes auto-captured)`,\n // Extended result field: SDK ≥ 2026.3.8 may inject memorySummary into context.\n result: memorySummary\n ? { topFacts, factCount: sessionFacts, memorySummary, episodesCreated }\n : { factCount: sessionFacts, episodesCreated },\n };\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-engine\",\n operation: \"compact\",\n });\n logger.warn?.(`memory-hybrid: context-engine compact failed: ${err}`);\n return { ok: false, compacted: false, reason: String(err) };\n }\n }\n\n /**\n * Pre-spawn preparation: inject relevant parent memories into sub-agent context.\n *\n * Fetches the top-N most recent/important facts from the parent session and\n * returns them as a `contextAddition` string for the SDK to inject into the\n * sub-agent's initial context.\n *\n * Guard against double-processing: the before_agent_start hook also injects\n * memories for the child session. This method only fires when the SDK calls\n * the ContextEngine API directly (OpenClaw ≥ 2026.3.8). On older runtimes the\n * method is never called, so there is no duplication risk.\n */\n async prepareSubagentSpawn(params: {\n parentSessionKey: string;\n childSessionKey: string;\n ttlMs?: number;\n }): Promise<SubagentSpawnPreparation | undefined> {\n const { factsDb, cfg, logger } = this.opts;\n try {\n // Fetch top-N recent/important facts to seed the sub-agent's context\n const limit = cfg.autoRecall?.limit ?? 10;\n const topFacts = factsDb.list(Math.min(limit, 15));\n\n if (topFacts.length === 0) {\n logger.debug?.(`memory-hybrid: prepareSubagentSpawn — no facts to inject for child=${params.childSessionKey}`);\n return { rollback: async () => {} };\n }\n\n // contextAddition is a non-standard field on the return type — populated so that\n // SDK versions that support it can inject the block; older versions ignore it.\n // Uses serializeFactForContext for consistent formatting with the retrieval pipeline.\n const contextAddition = buildContextBlock(\n topFacts,\n `parent context injected for subagent ${params.childSessionKey}`,\n `Relevant memories from parent session (${params.parentSessionKey}):`,\n );\n\n logger.debug?.(\n `memory-hybrid: prepareSubagentSpawn — injecting ${topFacts.length} facts for child=${params.childSessionKey}`,\n );\n\n return {\n rollback: async () => {\n // No state was mutated; rollback is a no-op.\n logger.debug?.(\n `memory-hybrid: prepareSubagentSpawn rollback — no state to revert for child=${params.childSessionKey}`,\n );\n },\n // Extended field: injected into sub-agent context by SDK ≥ 2026.3.8.\n // null when buildContextBlock returns null (empty facts list — already guarded above).\n ...(contextAddition !== null ? { contextAddition } : {}),\n } as SubagentSpawnPreparation & { contextAddition?: string };\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-engine\",\n operation: \"prepare-subagent-spawn\",\n });\n logger.warn?.(`memory-hybrid: prepareSubagentSpawn failed (non-fatal): ${err}`);\n return undefined;\n }\n }\n\n /**\n * Post-subagent cleanup: capture any session-scoped facts created by the sub-agent\n * and promote them to the appropriate scope.\n *\n * Guard against double-processing: OpenClaw's typed **`subagent_ended`** hook\n * (`lifecycle/stage-cleanup.ts`) only performs ACTIVE-TASKS.md checkpointing — not fact capture.\n * This method provides a lightweight ContextEngine callback; primary fact capture is the\n * child session's **agent_end** autoCapture path. If a fact is already in the store it will be\n * skipped by the hasDuplicate check when that pipeline exists.\n *\n * NOTE: The current SDK interface does not pass the sub-agent's result text here.\n * When the SDK exposes result text, parse it using the existing autoCapture logic\n * (see lifecycle/hooks.ts agent_end handler).\n */\n async onSubagentEnded(params: { childSessionKey: string; reason: string }): Promise<void> {\n const { factsDb, logger } = this.opts;\n try {\n // Count facts captured from the child session (written to the shared store by the\n // child's own agent_end autoCapture hook while the child session was running).\n const capturedCount = factsDb.countBySource(params.childSessionKey);\n\n if (capturedCount > 0) {\n logger.info?.(\n `memory-hybrid: context-engine onSubagentEnded — child=${params.childSessionKey} reason=${params.reason} childFacts=${capturedCount}`,\n );\n } else {\n logger.debug?.(\n `memory-hybrid: context-engine onSubagentEnded — child=${params.childSessionKey} reason=${params.reason} childFacts=0 (no auto-captured facts found for this session key)`,\n );\n }\n\n // TODO(SDK #273): Implement result-text fact extraction once the SDK exposes it.\n //\n // The current hook signature only provides { childSessionKey, reason }.\n // To implement full result-text capture, the SDK must expose one of:\n // params.resultText: string — the final assistant text from the sub-agent\n // params.messages: unknown[] — full message log (same shape as agent_end event)\n //\n // When available, mirror the agent_end autoCapture pipeline from lifecycle/hooks.ts:\n // 1. Iterate messages, extract text/content blocks\n // 2. Filter via shouldCapture() (needs to be added to ContextEngineOptions)\n // 3. Classify with detectCategory() / classifyMemoryOperation()\n // 4. Deduplicate via factsDb.hasDuplicate()\n // 5. Store with source=params.childSessionKey, scope=\"global\"\n //\n // Until then, all sub-agent fact capture is delegated to:\n // (a) The child session's own agent_end autoCapture hook (primary path — runs\n // inside the child's session and writes directly to the shared FactsDB)\n // (b) The typed subagent_ended hook in lifecycle/stage-cleanup.ts (ACTIVE-TASKS.md only; issue #966)\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-engine\",\n operation: \"on-subagent-ended\",\n });\n logger.warn?.(`memory-hybrid: onSubagentEnded failed (non-fatal): ${err}`);\n }\n }\n\n async dispose(): Promise<void> {\n // No owned resources to dispose.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Registration helper (feature-detected)\n// ---------------------------------------------------------------------------\n\n/**\n * Attempt to register HybridMemoryContextEngine with the OpenClaw plugin SDK.\n *\n * Safe to call even when:\n * - The registerContextEngine export doesn't exist (older OpenClaw versions)\n * - The import fails (ESM resolution error in older runtime)\n *\n * Returns true if registration succeeded, false otherwise.\n */\nexport async function registerHybridContextEngine(opts: ContextEngineOptions): Promise<boolean> {\n try {\n // Dynamic import for feature detection — avoids hard dependency on the API.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const sdk: any = await import(\"openclaw/plugin-sdk/core\").catch(() => null);\n if (!sdk) {\n opts.logger.debug?.(\"memory-hybrid: openclaw/plugin-sdk not available; skipping ContextEngine registration\");\n return false;\n }\n\n const { registerContextEngine } = sdk as {\n registerContextEngine?: (id: string, factory: () => MinimalContextEngine) => void;\n };\n\n if (typeof registerContextEngine !== \"function\") {\n opts.logger.debug?.(\"memory-hybrid: registerContextEngine not found in SDK; skipping ContextEngine registration\");\n return false;\n }\n\n const engine = new HybridMemoryContextEngine(opts);\n registerContextEngine(\"hybrid-memory\", () => engine);\n opts.logger.info?.(\"memory-hybrid: ContextEngine registered (id=hybrid-memory)\");\n return true;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"context-engine\",\n operation: \"register\",\n });\n opts.logger.warn?.(`memory-hybrid: ContextEngine registration failed (non-fatal): ${err}`);\n return false;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contextual-variants.js","names":[],"sources":["../../services/contextual-variants.ts"],"sourcesContent":["/**\n * Contextual Variant Generator (Issue #159).\n *\n * At fact storage time, generate contextual expansions via LLM and embed\n * them alongside the canonical text. This bridges semantic gaps — a fact about\n * \"HA runs on Proxmox VM 100 at 192.168.1.212\" also gets a variant like\n * \"smart home server infrastructure\" so it matches queries from different angles.\n *\n * Key design:\n * - Variant generation is ASYNC and non-blocking (queue-based).\n * - Graceful degradation: LLM failures return empty arrays, fact still stored.\n * - Rate limiting via sliding window on call timestamps.\n * - Category filtering: optionally restrict to specific categories.\n */\n\nimport type OpenAI from \"openai\";\nimport type { ContextualVariantsConfig } from \"../config.js\";\nimport { chatComplete } from \"./chat.js\";\nimport { CostFeature } from \"./cost-feature-labels.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { extractJsonArray } from \"./json-array-parser.js\";\n\n// ---------------------------------------------------------------------------\n// Prompt templates\n// ---------------------------------------------------------------------------\n\ntype ContextualVariantType = \"contextual-means\" | \"contextual-search\";\n\nconst CONTEXTUAL_MEANS_PROMPT_TEMPLATE = `Given this memory fact:\n\"{text}\"\nCategory: {category}\n\nGenerate {count} brief alternative phrasings that explain what this means using different vocabulary or framing. Each phrasing should be a single concise sentence.\n\nReturn ONLY a JSON array of strings, no other text. Example: [\"meaning one\", \"meaning two\"]`;\n\nconst CONTEXTUAL_SEARCH_PROMPT_TEMPLATE = `Given this memory fact:\n\"{text}\"\nCategory: {category}\n\nList 3-5 different ways someone might search for this information. Each should be a short query or phrase.\n\nReturn ONLY a JSON array of strings, no other text. Example: [\"search phrase one\", \"search phrase two\"]`;\n\nconst VARIANT_TYPES: ContextualVariantType[] = [\"contextual-means\", \"contextual-search\"];\n\n// ---------------------------------------------------------------------------\n// ContextualVariantGenerator\n// ---------------------------------------------------------------------------\n\n/**\n * Generates contextual expansions for a fact text via LLM.\n * Implements rate limiting and graceful degradation.\n */\nexport class ContextualVariantGenerator {\n /** Sliding-window call timestamps for rate limiting (ms epoch), per variant type. */\n private callTimestamps: Map<ContextualVariantType, number[]> = new Map();\n\n constructor(\n private readonly config: ContextualVariantsConfig,\n private readonly openai: OpenAI,\n ) {}\n\n /**\n * Generate 0-N contextual variant phrasings for the given fact text.\n * Returns empty array when disabled, rate-limited, or on LLM error.\n */\n async generateVariants(text: string, category: string, variantType: ContextualVariantType): Promise<string[]> {\n if (!this.config.enabled) return [];\n\n // Category filter: skip if categories list is set and this category is not in it.\n if (\n Array.isArray(this.config.categories) &&\n this.config.categories.length > 0 &&\n !this.config.categories.includes(category)\n ) {\n return [];\n }\n\n // Rate limiting: sliding window of maxPerMinute calls per 60s, per variant type.\n const now = Date.now();\n const windowMs = 60_000;\n let timestamps = this.callTimestamps.get(variantType) ?? [];\n timestamps = timestamps.filter((t) => now - t < windowMs);\n if (timestamps.length >= this.config.maxPerMinute) {\n return [];\n }\n timestamps.push(now);\n this.callTimestamps.set(variantType, timestamps);\n\n const count =\n variantType === \"contextual-search\"\n ? Math.max(1, Math.min(5, this.config.maxVariantsPerFact))\n : Math.max(1, this.config.maxVariantsPerFact);\n const promptTemplate =\n variantType === \"contextual-search\" ? CONTEXTUAL_SEARCH_PROMPT_TEMPLATE : CONTEXTUAL_MEANS_PROMPT_TEMPLATE;\n const prompt = promptTemplate\n .replace(\"{text}\", () => text)\n .replace(\"{category}\", () => category)\n .replace(\"{count}\", () => String(count));\n\n const model = this.config.model ?? \"openai/gpt-4.1-nano\";\n\n try {\n const response = await chatComplete({\n model,\n content: prompt,\n temperature: 0.7,\n maxTokens: 300,\n openai: this.openai,\n timeoutMs: 15_000,\n feature: CostFeature.contextualVariants,\n });\n\n const maxVariants = variantType === \"contextual-search\" ? 5 : this.config.maxVariantsPerFact;\n return parseVariantsFromResponse(response, maxVariants);\n } catch (err) {\n // Graceful degradation — log but never throw (fact already stored)\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"contextual-variants\",\n operation: \"generateVariants\",\n });\n return [];\n }\n }\n\n /** Expose call count in window for testing rate limiting. */\n get callsInWindow(): number {\n const now = Date.now();\n let total = 0;\n for (const timestamps of this.callTimestamps.values()) {\n total += timestamps.filter((t) => now - t < 60_000).length;\n }\n return total;\n }\n\n /** Reset call timestamps (for testing). */\n _resetRateLimit(): void {\n this.callTimestamps.clear();\n }\n}\n\n/**\n * Parse a JSON array of strings from an LLM response.\n * Handles responses that wrap the JSON in prose or code fences.\n * Tries every [...] substring (same approach as query-expander) so that arrays\n * containing literal \"]\" in string values parse correctly.\n */\nexport function parseVariantsFromResponse(response: string, maxVariants: number): string[] {\n return extractJsonArray(response)\n .filter((v): v is string => typeof v === \"string\" && v.trim().length > 0)\n .map((v) => v.trim())\n .slice(0, maxVariants);\n}\n\n// ---------------------------------------------------------------------------\n// VariantGenerationQueue\n// ---------------------------------------------------------------------------\n\n/** An item waiting for variant generation. */\ninterface VariantQueueItem {\n factId: string;\n text: string;\n category: string;\n}\n\n/**\n * Async background queue for variant generation.\n * Processes in batches of 5, rate-limited by the generator.\n * Enqueue returns immediately; processing happens in the background.\n */\nexport class VariantGenerationQueue {\n private queue: VariantQueueItem[] = [];\n private processing = false;\n private readonly batchSize = 5;\n\n constructor(\n private readonly generator: ContextualVariantGenerator,\n /** Called with generated variants for a fact. Should store them in DB. */\n private readonly onVariantsGenerated: (\n factId: string,\n variantType: ContextualVariantType,\n variants: string[],\n ) => Promise<void>,\n ) {}\n\n /** Add a fact to the variant generation queue. Non-blocking. */\n enqueue(item: VariantQueueItem): void {\n this.queue.push(item);\n if (!this.processing) {\n void this.processQueue();\n }\n }\n\n private async processQueue(): Promise<void> {\n if (this.processing) return;\n this.processing = true;\n\n try {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, this.batchSize);\n for (const item of batch) {\n for (const variantType of VARIANT_TYPES) {\n try {\n const variants = await this.generator.generateVariants(item.text, item.category, variantType);\n if (variants.length > 0) {\n await this.onVariantsGenerated(item.factId, variantType, variants);\n }\n } catch {\n // Graceful degradation — skip this variant type\n }\n }\n }\n }\n } finally {\n this.processing = false;\n }\n }\n\n /** Number of items waiting to be processed. */\n get queueLength(): number {\n return this.queue.length;\n }\n\n /** Whether the queue is currently processing. */\n get isProcessing(): boolean {\n return this.processing;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contextual-variants.js","names":[],"sources":["../../services/contextual-variants.ts"],"sourcesContent":["/**\n * Contextual Variant Generator (Issue #159).\n *\n * At fact storage time, generate contextual expansions via LLM and embed\n * them alongside the canonical text. This bridges semantic gaps — a fact about\n * \"HA runs on Proxmox VM 100 at 192.168.1.212\" also gets a variant like\n * \"smart home server infrastructure\" so it matches queries from different angles.\n *\n * Key design:\n * - Variant generation is ASYNC and non-blocking (queue-based).\n * - Graceful degradation: LLM failures return empty arrays, fact still stored.\n * - Rate limiting via sliding window on call timestamps.\n * - Category filtering: optionally restrict to specific categories.\n */\n\nimport type OpenAI from \"openai\";\nimport type { ContextualVariantsConfig } from \"../config.js\";\nimport { chatComplete } from \"./chat.js\";\nimport { CostFeature } from \"./cost-feature-labels.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport { extractJsonArray } from \"./json-array-parser.js\";\n\n// ---------------------------------------------------------------------------\n// Prompt templates\n// ---------------------------------------------------------------------------\n\ntype ContextualVariantType = \"contextual-means\" | \"contextual-search\";\n\nconst CONTEXTUAL_MEANS_PROMPT_TEMPLATE = `Given this memory fact:\n\"{text}\"\nCategory: {category}\n\nGenerate {count} brief alternative phrasings that explain what this means using different vocabulary or framing. Each phrasing should be a single concise sentence.\n\nReturn ONLY a JSON array of strings, no other text. Example: [\"meaning one\", \"meaning two\"]`;\n\nconst CONTEXTUAL_SEARCH_PROMPT_TEMPLATE = `Given this memory fact:\n\"{text}\"\nCategory: {category}\n\nList 3-5 different ways someone might search for this information. Each should be a short query or phrase.\n\nReturn ONLY a JSON array of strings, no other text. Example: [\"search phrase one\", \"search phrase two\"]`;\n\nconst VARIANT_TYPES: ContextualVariantType[] = [\"contextual-means\", \"contextual-search\"];\n\n// ---------------------------------------------------------------------------\n// ContextualVariantGenerator\n// ---------------------------------------------------------------------------\n\n/**\n * Generates contextual expansions for a fact text via LLM.\n * Implements rate limiting and graceful degradation.\n */\nexport class ContextualVariantGenerator {\n /** Sliding-window call timestamps for rate limiting (ms epoch), per variant type. */\n private callTimestamps: Map<ContextualVariantType, number[]> = new Map();\n\n constructor(\n private readonly config: ContextualVariantsConfig,\n private readonly openai: OpenAI,\n ) {}\n\n /**\n * Generate 0-N contextual variant phrasings for the given fact text.\n * Returns empty array when disabled, rate-limited, or on LLM error.\n */\n async generateVariants(text: string, category: string, variantType: ContextualVariantType): Promise<string[]> {\n if (!this.config.enabled) return [];\n\n // Category filter: skip if categories list is set and this category is not in it.\n if (\n Array.isArray(this.config.categories) &&\n this.config.categories.length > 0 &&\n !this.config.categories.includes(category)\n ) {\n return [];\n }\n\n // Rate limiting: sliding window of maxPerMinute calls per 60s, per variant type.\n const now = Date.now();\n const windowMs = 60_000;\n let timestamps = this.callTimestamps.get(variantType) ?? [];\n timestamps = timestamps.filter((t) => now - t < windowMs);\n if (timestamps.length >= this.config.maxPerMinute) {\n return [];\n }\n timestamps.push(now);\n this.callTimestamps.set(variantType, timestamps);\n\n const count =\n variantType === \"contextual-search\"\n ? Math.max(1, Math.min(5, this.config.maxVariantsPerFact))\n : Math.max(1, this.config.maxVariantsPerFact);\n const promptTemplate =\n variantType === \"contextual-search\" ? CONTEXTUAL_SEARCH_PROMPT_TEMPLATE : CONTEXTUAL_MEANS_PROMPT_TEMPLATE;\n const prompt = promptTemplate\n .replace(\"{text}\", () => text)\n .replace(\"{category}\", () => category)\n .replace(\"{count}\", () => String(count));\n\n const model = this.config.model ?? \"openai/gpt-4.1-nano\";\n\n try {\n const response = await chatComplete({\n model,\n content: prompt,\n temperature: 0.7,\n maxTokens: 300,\n openai: this.openai,\n timeoutMs: 15_000,\n feature: CostFeature.contextualVariants,\n });\n\n const maxVariants = variantType === \"contextual-search\" ? 5 : this.config.maxVariantsPerFact;\n return parseVariantsFromResponse(response, maxVariants);\n } catch (err) {\n // Graceful degradation — log but never throw (fact already stored)\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"contextual-variants\",\n operation: \"generateVariants\",\n });\n return [];\n }\n }\n\n /** Expose call count in window for testing rate limiting. */\n get callsInWindow(): number {\n const now = Date.now();\n let total = 0;\n for (const timestamps of this.callTimestamps.values()) {\n total += timestamps.filter((t) => now - t < 60_000).length;\n }\n return total;\n }\n\n /** Reset call timestamps (for testing). */\n _resetRateLimit(): void {\n this.callTimestamps.clear();\n }\n}\n\n/**\n * Parse a JSON array of strings from an LLM response.\n * Handles responses that wrap the JSON in prose or code fences.\n * Tries every [...] substring (same approach as query-expander) so that arrays\n * containing literal \"]\" in string values parse correctly.\n */\nexport function parseVariantsFromResponse(response: string, maxVariants: number): string[] {\n return extractJsonArray(response)\n .filter((v): v is string => typeof v === \"string\" && v.trim().length > 0)\n .map((v) => v.trim())\n .slice(0, maxVariants);\n}\n\n// ---------------------------------------------------------------------------\n// VariantGenerationQueue\n// ---------------------------------------------------------------------------\n\n/** An item waiting for variant generation. */\ninterface VariantQueueItem {\n factId: string;\n text: string;\n category: string;\n}\n\n/**\n * Async background queue for variant generation.\n * Processes in batches of 5, rate-limited by the generator.\n * Enqueue returns immediately; processing happens in the background.\n */\nexport class VariantGenerationQueue {\n private queue: VariantQueueItem[] = [];\n private processing = false;\n private readonly batchSize = 5;\n\n constructor(\n private readonly generator: ContextualVariantGenerator,\n /** Called with generated variants for a fact. Should store them in DB. */\n private readonly onVariantsGenerated: (\n factId: string,\n variantType: ContextualVariantType,\n variants: string[],\n ) => Promise<void>,\n ) {}\n\n /** Add a fact to the variant generation queue. Non-blocking. */\n enqueue(item: VariantQueueItem): void {\n this.queue.push(item);\n if (!this.processing) {\n void this.processQueue();\n }\n }\n\n private async processQueue(): Promise<void> {\n if (this.processing) return;\n this.processing = true;\n\n try {\n while (this.queue.length > 0) {\n const batch = this.queue.splice(0, this.batchSize);\n for (const item of batch) {\n for (const variantType of VARIANT_TYPES) {\n try {\n const variants = await this.generator.generateVariants(item.text, item.category, variantType);\n if (variants.length > 0) {\n await this.onVariantsGenerated(item.factId, variantType, variants);\n }\n } catch {\n // Graceful degradation — skip this variant type\n }\n }\n }\n }\n } finally {\n this.processing = false;\n }\n }\n\n /** Number of items waiting to be processed. */\n get queueLength(): number {\n return this.queue.length;\n }\n\n /** Whether the queue is currently processing. */\n get isProcessing(): boolean {\n return this.processing;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"continuous-verifier.js","names":[],"sources":["../../services/continuous-verifier.ts"],"sourcesContent":["/**\n * Continuous Verification Service (Issue #164).\n *\n * Periodically re-verifies facts in the Verification Store against recent\n * knowledge to catch staleness proactively. Designed to be triggered\n * externally (cron, heartbeat, lifecycle hook) rather than running as a\n * background timer.\n */\n\nimport type OpenAI from \"openai\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport { chatComplete } from \"./chat.js\";\nimport { CostFeature } from \"./cost-feature-labels.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport type { VerificationStore, VerifiedFact } from \"./verification-store.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype VerificationOutcome = \"CONFIRMED\" | \"STALE\" | \"UNCERTAIN\";\n\nexport interface VerificationCycleResult {\n checked: number;\n confirmed: number;\n stale: number;\n uncertain: number;\n errors: number;\n errorSummaries: string[];\n}\n\nexport interface ContinuousVerifierOptions {\n /** Days between verification cycle runs (default: 30). */\n cycleDays?: number;\n /** Model to use for LLM verification calls (default: 'openai/gpt-4.1-nano'). */\n verificationModel?: string;\n /** Per-fact LLM timeout in ms (default: 15000). */\n timeoutMs?: number;\n /**\n * Progress hook for long cycles (e.g. `hybrid-mem dream-cycle --verbose`).\n * Throttled when many facts are due: first, last, every 10th, and all when ≤25 due.\n */\n onProgress?: (message: string) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Prompt construction\n// ---------------------------------------------------------------------------\n\n/**\n * Build the LLM prompt for re-verifying a single fact.\n * Public for testing.\n */\nexport function buildVerificationPrompt(factText: string, entity: string, recentFacts: string[]): string {\n const recentSection =\n recentFacts.length > 0 ? recentFacts.map((f, i) => `${i + 1}. ${f}`).join(\"\\n\") : \"(no recent facts available)\";\n\n return `You are a fact-verification assistant. Determine whether the following verified fact is still accurate based on recent knowledge.\\n\\nVerified fact: ${factText}\\n\\nRecent knowledge about \"${entity}\":\\n${recentSection}\\n\\nIs this still accurate based on recent knowledge?\\nAnswer with exactly one of: CONFIRMED, STALE, or UNCERTAIN, followed by a brief reason.\\nExample: \"CONFIRMED – the IP address is still in use\"\\nExample: \"STALE – the server was decommissioned last month\"\\nExample: \"UNCERTAIN – no recent information available\"`;\n}\n\n// ---------------------------------------------------------------------------\n// Response parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Parse the LLM response into one of the three outcome labels.\n * Uses word-boundary checks and excludes negations (e.g. \"NOT CONFIRMED\", \"UNCONFIRMED\")\n * so that responses like \"UNCONFIRMED\" are not misclassified as CONFIRMED.\n * Falls back to UNCERTAIN for unrecognised responses.\n */\nexport function parseVerificationOutcome(response: string): VerificationOutcome {\n const trimmed = response.trim();\n if (/\\bCONFIRMED\\b/i.test(trimmed) && !/\\bNOT\\s+CONFIRMED\\b/i.test(trimmed) && !/\\bUNCONFIRMED\\b/i.test(trimmed)) {\n return \"CONFIRMED\";\n }\n if (/\\bSTALE\\b/i.test(trimmed)) return \"STALE\";\n return \"UNCERTAIN\";\n}\n\n// ---------------------------------------------------------------------------\n// ContinuousVerifier\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_MODEL = \"openai/gpt-4.1-nano\";\nconst DEFAULT_TIMEOUT_MS = 15_000;\nconst RECENT_FACTS_DAYS = 90;\nconst MAX_CONTEXT_PER_ENTITY = 20;\nconst MAX_CONTEXT_CHARS_PER_FACT = 500;\nconst MAX_ERROR_SUMMARIES = 5;\nconst MAX_ERROR_SUMMARY_LENGTH = 160;\n// Confidence assigned to facts the LLM determines are stale. Kept below 0.3\n// so that natural decay cycles will eventually remove them, while still\n// preventing immediate deletion (threshold is < 0.1). Previously 0.5, which\n// was misleadingly high — stale facts should not appear reliable in recall.\nconst STALE_CONFIDENCE = 0.2;\n\nfunction compactVerificationError(err: unknown): string {\n const raw = err instanceof Error ? err.message : String(err);\n const compact = raw.replace(/\\s+/g, \" \").trim() || \"unknown error\";\n if (compact.length <= MAX_ERROR_SUMMARY_LENGTH) return compact;\n return `${compact.slice(0, MAX_ERROR_SUMMARY_LENGTH - 3)}...`;\n}\n\nfunction recordVerificationError(result: VerificationCycleResult, prefix: string, err: unknown): void {\n if (result.errorSummaries.length >= MAX_ERROR_SUMMARIES) return;\n result.errorSummaries.push(`${prefix}: ${compactVerificationError(err)}`);\n}\n\nexport class ContinuousVerifier {\n private readonly store: VerificationStore;\n private readonly factsDb: FactsDB;\n private readonly openai: OpenAI;\n private readonly model: string;\n private readonly timeoutMs: number;\n private readonly cycleDays: number | undefined;\n private readonly onProgress?: (message: string) => void;\n private lastRunDate: number | null = null;\n\n constructor(store: VerificationStore, factsDb: FactsDB, openai: OpenAI, options?: ContinuousVerifierOptions) {\n this.store = store;\n this.factsDb = factsDb;\n this.openai = openai;\n this.model = options?.verificationModel ?? DEFAULT_MODEL;\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.cycleDays = options?.cycleDays;\n this.onProgress = options?.onProgress;\n }\n\n /**\n * Internal: call the LLM and return the parsed outcome. Can throw on LLM failure.\n * Use verifyFact() for the public, error-safe interface.\n */\n private async _callLLM(factText: string, entity: string, recentFacts: string[]): Promise<VerificationOutcome> {\n const prompt = buildVerificationPrompt(factText, entity, recentFacts);\n const response = await chatComplete({\n model: this.model,\n content: prompt,\n temperature: 0,\n maxTokens: 256,\n openai: this.openai,\n timeoutMs: this.timeoutMs,\n feature: CostFeature.continuousVerifier,\n });\n return parseVerificationOutcome(response);\n }\n\n /**\n * Verify a single fact against recent knowledge.\n * Returns the outcome or UNCERTAIN on any timeout/error.\n */\n async verifyFact(verifiedFact: VerifiedFact, recentFacts: string[]): Promise<VerificationOutcome> {\n // Determine entity label: prefer the entity from FactsDB, fall back to factId.\n const underlying = this.factsDb.getById(verifiedFact.factId);\n const entity = underlying?.entity ?? verifiedFact.factId;\n try {\n return await this._callLLM(verifiedFact.canonicalText, entity, recentFacts);\n } catch {\n return \"UNCERTAIN\";\n }\n }\n\n /**\n * Run a full verification cycle on all facts currently due for re-verification.\n *\n * For each due fact:\n * - Fetches recent (last 90 days) facts about the same entity from FactsDB.\n * - Asks the LLM whether the fact is still accurate.\n * - CONFIRMED → bumps verified_at and next_verification in the store.\n * - STALE → sets confidence to 0.5 and tags the underlying fact 'needs-verification'.\n * - UNCERTAIN → tags the underlying fact 'review-needed'.\n * - Errors are counted but do not abort the cycle.\n */\n async runCycle(): Promise<VerificationCycleResult> {\n const result: VerificationCycleResult = {\n checked: 0,\n confirmed: 0,\n stale: 0,\n uncertain: 0,\n errors: 0,\n errorSummaries: [],\n };\n\n if (this.cycleDays !== undefined && this.lastRunDate !== null) {\n const elapsedDays = (Date.now() - this.lastRunDate) / (24 * 60 * 60 * 1000);\n if (elapsedDays < this.cycleDays) return result;\n }\n\n let due: VerifiedFact[];\n try {\n due = this.store.listDueForReverification();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"continuous-verifier\",\n operation: \"listDueForReverification\",\n });\n result.errors++;\n recordVerificationError(result, \"listDueForReverification\", err);\n return result;\n }\n\n if (due.length === 0) {\n return result;\n }\n\n const recentEntries = this.factsDb.getRecentFacts(RECENT_FACTS_DAYS);\n const recentByEntity = new Map<string, string[]>();\n for (const e of recentEntries) {\n const entity = e.entity?.toLowerCase() ?? \"\";\n if (!recentByEntity.has(entity)) recentByEntity.set(entity, []);\n const arr = recentByEntity.get(entity)!;\n if (arr.length < MAX_CONTEXT_PER_ENTITY) {\n arr.push(e.text.slice(0, MAX_CONTEXT_CHARS_PER_FACT));\n }\n }\n\n this.onProgress?.(\n `memory-hybrid: continuous-verification — ${due.length} fact(s) due, ${recentEntries.length} recent facts loaded for context`,\n );\n\n for (const fact of due) {\n result.checked++;\n try {\n const underlying = this.factsDb.getById(fact.factId);\n const entity = underlying?.entity ?? null;\n const entityKey = entity?.toLowerCase() ?? \"\";\n const recentFacts = recentByEntity.get(entityKey) ?? [];\n\n if (recentFacts.length === 0 && entityKey.length > 0) {\n if (underlying) {\n this.factsDb.addTag(underlying.id, \"review-needed\");\n }\n result.uncertain++;\n const n = result.checked;\n const total = due.length;\n if (this.onProgress && (total <= 25 || n === 1 || n === total || n % 10 === 0)) {\n this.onProgress(\n `memory-hybrid: continuous-verification — ${n}/${total} (UNCERTAIN/no-context) fact=${fact.factId.slice(0, 8)}…`,\n );\n }\n continue;\n }\n\n let outcome: VerificationOutcome;\n try {\n outcome = await this._callLLM(fact.canonicalText, entity ?? fact.factId, recentFacts);\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"continuous-verifier\",\n operation: \"verify-fact\",\n metadata: { factId: fact.factId },\n });\n result.errors++;\n recordVerificationError(result, `fact=${fact.factId.slice(0, 8)}…`, err);\n outcome = \"UNCERTAIN\";\n }\n\n if (outcome === \"CONFIRMED\") {\n this.store.update(fact.id, fact.canonicalText, \"system\");\n result.confirmed++;\n } else if (outcome === \"STALE\") {\n if (underlying) {\n this.factsDb.setConfidenceTo(underlying.id, STALE_CONFIDENCE);\n this.factsDb.addTag(underlying.id, \"needs-verification\");\n }\n result.stale++;\n } else {\n if (underlying) {\n this.factsDb.addTag(underlying.id, \"review-needed\");\n }\n result.uncertain++;\n }\n\n const n = result.checked;\n const total = due.length;\n if (this.onProgress && (total <= 25 || n === 1 || n === total || n % 10 === 0)) {\n this.onProgress(\n `memory-hybrid: continuous-verification — ${n}/${total} (${outcome}) fact=${fact.factId.slice(0, 8)}…`,\n );\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"continuous-verifier\",\n operation: \"runCycle-per-fact\",\n metadata: { factId: fact.factId },\n });\n result.errors++;\n recordVerificationError(result, `fact=${fact.factId.slice(0, 8)}…`, err);\n }\n }\n\n this.lastRunDate = Date.now();\n return result;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Convenience export for lifecycle hooks / cron triggers\n// ---------------------------------------------------------------------------\n\n/**\n * Run a verification cycle and return the result summary.\n * Suitable for calling from plugin lifecycle hooks or external cron jobs.\n */\nexport async function runVerificationCycle(\n store: VerificationStore,\n factsDb: FactsDB,\n openai: OpenAI,\n options?: ContinuousVerifierOptions,\n): Promise<VerificationCycleResult> {\n const verifier = new ContinuousVerifier(store, factsDb, openai, options);\n return verifier.runCycle();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"continuous-verifier.js","names":[],"sources":["../../services/continuous-verifier.ts"],"sourcesContent":["/**\n * Continuous Verification Service (Issue #164).\n *\n * Periodically re-verifies facts in the Verification Store against recent\n * knowledge to catch staleness proactively. Designed to be triggered\n * externally (cron, heartbeat, lifecycle hook) rather than running as a\n * background timer.\n */\n\nimport type OpenAI from \"openai\";\nimport type { FactsDB } from \"../backends/facts-db.js\";\nimport { chatComplete } from \"./chat.js\";\nimport { CostFeature } from \"./cost-feature-labels.js\";\nimport { capturePluginError } from \"./error-reporter.js\";\nimport type { VerificationStore, VerifiedFact } from \"./verification-store.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype VerificationOutcome = \"CONFIRMED\" | \"STALE\" | \"UNCERTAIN\";\n\nexport interface VerificationCycleResult {\n checked: number;\n confirmed: number;\n stale: number;\n uncertain: number;\n errors: number;\n errorSummaries: string[];\n}\n\nexport interface ContinuousVerifierOptions {\n /** Days between verification cycle runs (default: 30). */\n cycleDays?: number;\n /** Model to use for LLM verification calls (default: 'openai/gpt-4.1-nano'). */\n verificationModel?: string;\n /** Per-fact LLM timeout in ms (default: 15000). */\n timeoutMs?: number;\n /**\n * Progress hook for long cycles (e.g. `hybrid-mem dream-cycle --verbose`).\n * Throttled when many facts are due: first, last, every 10th, and all when ≤25 due.\n */\n onProgress?: (message: string) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Prompt construction\n// ---------------------------------------------------------------------------\n\n/**\n * Build the LLM prompt for re-verifying a single fact.\n * Public for testing.\n */\nexport function buildVerificationPrompt(factText: string, entity: string, recentFacts: string[]): string {\n const recentSection =\n recentFacts.length > 0 ? recentFacts.map((f, i) => `${i + 1}. ${f}`).join(\"\\n\") : \"(no recent facts available)\";\n\n return `You are a fact-verification assistant. Determine whether the following verified fact is still accurate based on recent knowledge.\\n\\nVerified fact: ${factText}\\n\\nRecent knowledge about \"${entity}\":\\n${recentSection}\\n\\nIs this still accurate based on recent knowledge?\\nAnswer with exactly one of: CONFIRMED, STALE, or UNCERTAIN, followed by a brief reason.\\nExample: \"CONFIRMED – the IP address is still in use\"\\nExample: \"STALE – the server was decommissioned last month\"\\nExample: \"UNCERTAIN – no recent information available\"`;\n}\n\n// ---------------------------------------------------------------------------\n// Response parsing\n// ---------------------------------------------------------------------------\n\n/**\n * Parse the LLM response into one of the three outcome labels.\n * Uses word-boundary checks and excludes negations (e.g. \"NOT CONFIRMED\", \"UNCONFIRMED\")\n * so that responses like \"UNCONFIRMED\" are not misclassified as CONFIRMED.\n * Falls back to UNCERTAIN for unrecognised responses.\n */\nexport function parseVerificationOutcome(response: string): VerificationOutcome {\n const trimmed = response.trim();\n if (/\\bCONFIRMED\\b/i.test(trimmed) && !/\\bNOT\\s+CONFIRMED\\b/i.test(trimmed) && !/\\bUNCONFIRMED\\b/i.test(trimmed)) {\n return \"CONFIRMED\";\n }\n if (/\\bSTALE\\b/i.test(trimmed)) return \"STALE\";\n return \"UNCERTAIN\";\n}\n\n// ---------------------------------------------------------------------------\n// ContinuousVerifier\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_MODEL = \"openai/gpt-4.1-nano\";\nconst DEFAULT_TIMEOUT_MS = 15_000;\nconst RECENT_FACTS_DAYS = 90;\nconst MAX_CONTEXT_PER_ENTITY = 20;\nconst MAX_CONTEXT_CHARS_PER_FACT = 500;\nconst MAX_ERROR_SUMMARIES = 5;\nconst MAX_ERROR_SUMMARY_LENGTH = 160;\n// Confidence assigned to facts the LLM determines are stale. Kept below 0.3\n// so that natural decay cycles will eventually remove them, while still\n// preventing immediate deletion (threshold is < 0.1). Previously 0.5, which\n// was misleadingly high — stale facts should not appear reliable in recall.\nconst STALE_CONFIDENCE = 0.2;\n\nfunction compactVerificationError(err: unknown): string {\n const raw = err instanceof Error ? err.message : String(err);\n const compact = raw.replace(/\\s+/g, \" \").trim() || \"unknown error\";\n if (compact.length <= MAX_ERROR_SUMMARY_LENGTH) return compact;\n return `${compact.slice(0, MAX_ERROR_SUMMARY_LENGTH - 3)}...`;\n}\n\nfunction recordVerificationError(result: VerificationCycleResult, prefix: string, err: unknown): void {\n if (result.errorSummaries.length >= MAX_ERROR_SUMMARIES) return;\n result.errorSummaries.push(`${prefix}: ${compactVerificationError(err)}`);\n}\n\nexport class ContinuousVerifier {\n private readonly store: VerificationStore;\n private readonly factsDb: FactsDB;\n private readonly openai: OpenAI;\n private readonly model: string;\n private readonly timeoutMs: number;\n private readonly cycleDays: number | undefined;\n private readonly onProgress?: (message: string) => void;\n private lastRunDate: number | null = null;\n\n constructor(store: VerificationStore, factsDb: FactsDB, openai: OpenAI, options?: ContinuousVerifierOptions) {\n this.store = store;\n this.factsDb = factsDb;\n this.openai = openai;\n this.model = options?.verificationModel ?? DEFAULT_MODEL;\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.cycleDays = options?.cycleDays;\n this.onProgress = options?.onProgress;\n }\n\n /**\n * Internal: call the LLM and return the parsed outcome. Can throw on LLM failure.\n * Use verifyFact() for the public, error-safe interface.\n */\n private async _callLLM(factText: string, entity: string, recentFacts: string[]): Promise<VerificationOutcome> {\n const prompt = buildVerificationPrompt(factText, entity, recentFacts);\n const response = await chatComplete({\n model: this.model,\n content: prompt,\n temperature: 0,\n maxTokens: 256,\n openai: this.openai,\n timeoutMs: this.timeoutMs,\n feature: CostFeature.continuousVerifier,\n });\n return parseVerificationOutcome(response);\n }\n\n /**\n * Verify a single fact against recent knowledge.\n * Returns the outcome or UNCERTAIN on any timeout/error.\n */\n async verifyFact(verifiedFact: VerifiedFact, recentFacts: string[]): Promise<VerificationOutcome> {\n // Determine entity label: prefer the entity from FactsDB, fall back to factId.\n const underlying = this.factsDb.getById(verifiedFact.factId);\n const entity = underlying?.entity ?? verifiedFact.factId;\n try {\n return await this._callLLM(verifiedFact.canonicalText, entity, recentFacts);\n } catch {\n return \"UNCERTAIN\";\n }\n }\n\n /**\n * Run a full verification cycle on all facts currently due for re-verification.\n *\n * For each due fact:\n * - Fetches recent (last 90 days) facts about the same entity from FactsDB.\n * - Asks the LLM whether the fact is still accurate.\n * - CONFIRMED → bumps verified_at and next_verification in the store.\n * - STALE → sets confidence to 0.5 and tags the underlying fact 'needs-verification'.\n * - UNCERTAIN → tags the underlying fact 'review-needed'.\n * - Errors are counted but do not abort the cycle.\n */\n async runCycle(): Promise<VerificationCycleResult> {\n const result: VerificationCycleResult = {\n checked: 0,\n confirmed: 0,\n stale: 0,\n uncertain: 0,\n errors: 0,\n errorSummaries: [],\n };\n\n if (this.cycleDays !== undefined && this.lastRunDate !== null) {\n const elapsedDays = (Date.now() - this.lastRunDate) / (24 * 60 * 60 * 1000);\n if (elapsedDays < this.cycleDays) return result;\n }\n\n let due: VerifiedFact[];\n try {\n due = this.store.listDueForReverification();\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"continuous-verifier\",\n operation: \"listDueForReverification\",\n });\n result.errors++;\n recordVerificationError(result, \"listDueForReverification\", err);\n return result;\n }\n\n if (due.length === 0) {\n return result;\n }\n\n const recentEntries = this.factsDb.getRecentFacts(RECENT_FACTS_DAYS);\n const recentByEntity = new Map<string, string[]>();\n for (const e of recentEntries) {\n const entity = e.entity?.toLowerCase() ?? \"\";\n if (!recentByEntity.has(entity)) recentByEntity.set(entity, []);\n const arr = recentByEntity.get(entity)!;\n if (arr.length < MAX_CONTEXT_PER_ENTITY) {\n arr.push(e.text.slice(0, MAX_CONTEXT_CHARS_PER_FACT));\n }\n }\n\n this.onProgress?.(\n `memory-hybrid: continuous-verification — ${due.length} fact(s) due, ${recentEntries.length} recent facts loaded for context`,\n );\n\n for (const fact of due) {\n result.checked++;\n try {\n const underlying = this.factsDb.getById(fact.factId);\n const entity = underlying?.entity ?? null;\n const entityKey = entity?.toLowerCase() ?? \"\";\n const recentFacts = recentByEntity.get(entityKey) ?? [];\n\n if (recentFacts.length === 0 && entityKey.length > 0) {\n if (underlying) {\n this.factsDb.addTag(underlying.id, \"review-needed\");\n }\n result.uncertain++;\n const n = result.checked;\n const total = due.length;\n if (this.onProgress && (total <= 25 || n === 1 || n === total || n % 10 === 0)) {\n this.onProgress(\n `memory-hybrid: continuous-verification — ${n}/${total} (UNCERTAIN/no-context) fact=${fact.factId.slice(0, 8)}…`,\n );\n }\n continue;\n }\n\n let outcome: VerificationOutcome;\n try {\n outcome = await this._callLLM(fact.canonicalText, entity ?? fact.factId, recentFacts);\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"continuous-verifier\",\n operation: \"verify-fact\",\n metadata: { factId: fact.factId },\n });\n result.errors++;\n recordVerificationError(result, `fact=${fact.factId.slice(0, 8)}…`, err);\n outcome = \"UNCERTAIN\";\n }\n\n if (outcome === \"CONFIRMED\") {\n this.store.update(fact.id, fact.canonicalText, \"system\");\n result.confirmed++;\n } else if (outcome === \"STALE\") {\n if (underlying) {\n this.factsDb.setConfidenceTo(underlying.id, STALE_CONFIDENCE);\n this.factsDb.addTag(underlying.id, \"needs-verification\");\n }\n result.stale++;\n } else {\n if (underlying) {\n this.factsDb.addTag(underlying.id, \"review-needed\");\n }\n result.uncertain++;\n }\n\n const n = result.checked;\n const total = due.length;\n if (this.onProgress && (total <= 25 || n === 1 || n === total || n % 10 === 0)) {\n this.onProgress(\n `memory-hybrid: continuous-verification — ${n}/${total} (${outcome}) fact=${fact.factId.slice(0, 8)}…`,\n );\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"continuous-verifier\",\n operation: \"runCycle-per-fact\",\n metadata: { factId: fact.factId },\n });\n result.errors++;\n recordVerificationError(result, `fact=${fact.factId.slice(0, 8)}…`, err);\n }\n }\n\n this.lastRunDate = Date.now();\n return result;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Convenience export for lifecycle hooks / cron triggers\n// ---------------------------------------------------------------------------\n\n/**\n * Run a verification cycle and return the result summary.\n * Suitable for calling from plugin lifecycle hooks or external cron jobs.\n */\nexport async function runVerificationCycle(\n store: VerificationStore,\n factsDb: FactsDB,\n openai: OpenAI,\n options?: ContinuousVerifierOptions,\n): Promise<VerificationCycleResult> {\n const verifier = new ContinuousVerifier(store, factsDb, openai, options);\n return verifier.runCycle();\n}\n"],"mappings":"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"}
|