@mrc2204/agent-smart-memo 5.0.2 → 5.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +209 -375
  2. package/bin/asm.mjs +365 -0
  3. package/bin/opencode-mcp-server.mjs +320 -0
  4. package/dist/core/contracts/adapter-contracts.d.ts +1 -1
  5. package/dist/core/contracts/adapter-contracts.d.ts.map +1 -1
  6. package/dist/core/contracts/change-overlay-contracts.d.ts +69 -0
  7. package/dist/core/contracts/change-overlay-contracts.d.ts.map +1 -0
  8. package/dist/core/contracts/change-overlay-contracts.js +2 -0
  9. package/dist/core/contracts/change-overlay-contracts.js.map +1 -0
  10. package/dist/core/contracts/feature-pack-contracts.d.ts +37 -0
  11. package/dist/core/contracts/feature-pack-contracts.d.ts.map +1 -0
  12. package/dist/core/contracts/feature-pack-contracts.js +8 -0
  13. package/dist/core/contracts/feature-pack-contracts.js.map +1 -0
  14. package/dist/core/contracts/project-query-contracts.d.ts +84 -0
  15. package/dist/core/contracts/project-query-contracts.d.ts.map +1 -0
  16. package/dist/core/contracts/project-query-contracts.js +2 -0
  17. package/dist/core/contracts/project-query-contracts.js.map +1 -0
  18. package/dist/core/graph/code-graph-model.d.ts +9 -0
  19. package/dist/core/graph/code-graph-model.d.ts.map +1 -0
  20. package/dist/core/graph/code-graph-model.js +70 -0
  21. package/dist/core/graph/code-graph-model.js.map +1 -0
  22. package/dist/core/graph/code-graph-populator.d.ts +20 -0
  23. package/dist/core/graph/code-graph-populator.d.ts.map +1 -0
  24. package/dist/core/graph/code-graph-populator.js +760 -0
  25. package/dist/core/graph/code-graph-populator.js.map +1 -0
  26. package/dist/core/graph/contracts.d.ts +29 -0
  27. package/dist/core/graph/contracts.d.ts.map +1 -0
  28. package/dist/core/graph/contracts.js +47 -0
  29. package/dist/core/graph/contracts.js.map +1 -0
  30. package/dist/core/ingest/contracts.d.ts +44 -0
  31. package/dist/core/ingest/contracts.d.ts.map +1 -0
  32. package/dist/core/ingest/contracts.js +2 -0
  33. package/dist/core/ingest/contracts.js.map +1 -0
  34. package/dist/core/ingest/ids.d.ts +5 -0
  35. package/dist/core/ingest/ids.d.ts.map +1 -0
  36. package/dist/core/ingest/ids.js +17 -0
  37. package/dist/core/ingest/ids.js.map +1 -0
  38. package/dist/core/ingest/ingest-pipeline.d.ts +4 -0
  39. package/dist/core/ingest/ingest-pipeline.d.ts.map +1 -0
  40. package/dist/core/ingest/ingest-pipeline.js +105 -0
  41. package/dist/core/ingest/ingest-pipeline.js.map +1 -0
  42. package/dist/core/ingest/semantic-block-extractor.d.ts +9 -0
  43. package/dist/core/ingest/semantic-block-extractor.d.ts.map +1 -0
  44. package/dist/core/ingest/semantic-block-extractor.js +171 -0
  45. package/dist/core/ingest/semantic-block-extractor.js.map +1 -0
  46. package/dist/core/usecases/default-memory-usecase-port.d.ts +38 -0
  47. package/dist/core/usecases/default-memory-usecase-port.d.ts.map +1 -1
  48. package/dist/core/usecases/default-memory-usecase-port.js +1686 -12
  49. package/dist/core/usecases/default-memory-usecase-port.js.map +1 -1
  50. package/dist/db/graph-db.d.ts +24 -0
  51. package/dist/db/graph-db.d.ts.map +1 -1
  52. package/dist/db/graph-db.js +81 -2
  53. package/dist/db/graph-db.js.map +1 -1
  54. package/dist/db/slot-db.d.ts +235 -2
  55. package/dist/db/slot-db.d.ts.map +1 -1
  56. package/dist/db/slot-db.js +840 -18
  57. package/dist/db/slot-db.js.map +1 -1
  58. package/dist/index.d.ts +7 -247
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +32 -119
  61. package/dist/index.js.map +1 -1
  62. package/dist/shared/asm-config.d.ts +82 -0
  63. package/dist/shared/asm-config.d.ts.map +1 -0
  64. package/dist/shared/asm-config.js +254 -0
  65. package/dist/shared/asm-config.js.map +1 -0
  66. package/dist/shared/slotdb-path.d.ts +4 -3
  67. package/dist/shared/slotdb-path.d.ts.map +1 -1
  68. package/dist/shared/slotdb-path.js +15 -6
  69. package/dist/shared/slotdb-path.js.map +1 -1
  70. package/dist/tools/graph-tools.d.ts.map +1 -1
  71. package/dist/tools/graph-tools.js +131 -0
  72. package/dist/tools/graph-tools.js.map +1 -1
  73. package/dist/tools/project-tools.d.ts.map +1 -1
  74. package/dist/tools/project-tools.js +543 -0
  75. package/dist/tools/project-tools.js.map +1 -1
  76. package/openclaw.plugin.json +5 -164
  77. package/package.json +61 -26
  78. package/scripts/init-openclaw.mjs +727 -0
@@ -10,6 +10,10 @@ import { DatabaseSync } from "node:sqlite";
10
10
  import { randomUUID } from "node:crypto";
11
11
  import { join } from "node:path";
12
12
  import { mkdirSync, existsSync } from "node:fs";
13
+ import { buildChunkArtifacts } from "../core/ingest/ingest-pipeline.js";
14
+ import { extractSemanticBlocks } from "../core/ingest/semantic-block-extractor.js";
15
+ import { buildSymbolId } from "../core/ingest/ids.js";
16
+ import { populateUniversalCodeGraphForFile } from "../core/graph/code-graph-populator.js";
13
17
  import { GraphDB } from "./graph-db.js";
14
18
  import { getSlotTTL } from "../shared/memory-config.js";
15
19
  import { resolveLegacyStateDirInput, resolveSlotDbDir } from "../shared/slotdb-path.js";
@@ -629,6 +633,278 @@ export class SlotDB {
629
633
  updated_at: row.updated_at,
630
634
  };
631
635
  }
636
+ deindexProject(scopeUserId, scopeAgentId, input) {
637
+ const projectId = String(input.project_id || "").trim();
638
+ if (!projectId)
639
+ throw new Error("project_id is required");
640
+ const project = this.getProjectById(scopeUserId, scopeAgentId, projectId);
641
+ if (!project) {
642
+ throw new Error(`project_id '${projectId}' is not registered`);
643
+ }
644
+ const now = new Date().toISOString();
645
+ const reason = input.reason == null ? null : String(input.reason).trim() || null;
646
+ const fileCountStmt = this.db.prepare(`SELECT COUNT(*) as cnt FROM file_index_state
647
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`);
648
+ const chunkCountStmt = this.db.prepare(`SELECT COUNT(*) as cnt FROM chunk_registry
649
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`);
650
+ const symbolCountStmt = this.db.prepare(`SELECT COUNT(*) as cnt FROM symbol_registry
651
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`);
652
+ const files = Number(fileCountStmt.get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
653
+ const chunks = Number(chunkCountStmt.get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
654
+ const symbols = Number(symbolCountStmt.get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
655
+ this.db.prepare(`UPDATE file_index_state
656
+ SET index_state = 'stale', active = 0, tombstone_at = ?, indexed_at = ?
657
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`).run(now, now, scopeUserId, scopeAgentId, projectId);
658
+ this.db.prepare(`UPDATE chunk_registry
659
+ SET index_state = 'stale', active = 0, tombstone_at = ?, indexed_at = ?
660
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`).run(now, now, scopeUserId, scopeAgentId, projectId);
661
+ this.db.prepare(`UPDATE symbol_registry
662
+ SET index_state = 'stale', active = 0, tombstone_at = ?, indexed_at = ?
663
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`).run(now, now, scopeUserId, scopeAgentId, projectId);
664
+ this.db.prepare(`UPDATE projects
665
+ SET lifecycle_status = 'deindexed', updated_at = ?
666
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(now, scopeUserId, scopeAgentId, projectId);
667
+ this.insertIndexRun(scopeUserId, scopeAgentId, {
668
+ run_id: randomUUID(),
669
+ project_id: projectId,
670
+ index_profile: "default",
671
+ trigger_type: "manual",
672
+ state: "indexed",
673
+ started_at: now,
674
+ finished_at: now,
675
+ error_message: reason ? `deindex:${reason}` : "deindex",
676
+ });
677
+ return {
678
+ project_id: projectId,
679
+ lifecycle_status: "deindexed",
680
+ deindexed_at: now,
681
+ reason,
682
+ affected: {
683
+ files,
684
+ chunks,
685
+ symbols,
686
+ },
687
+ searchable: false,
688
+ };
689
+ }
690
+ detachProject(scopeUserId, scopeAgentId, input) {
691
+ const projectId = String(input.project_id || "").trim();
692
+ if (!projectId)
693
+ throw new Error("project_id is required");
694
+ const project = this.getProjectById(scopeUserId, scopeAgentId, projectId);
695
+ if (!project) {
696
+ throw new Error(`project_id '${projectId}' is not registered`);
697
+ }
698
+ const reason = input.reason == null ? null : String(input.reason).trim() || null;
699
+ if (project.lifecycle_status !== "deindexed") {
700
+ this.deindexProject(scopeUserId, scopeAgentId, {
701
+ project_id: projectId,
702
+ reason: reason || "detach_precondition_deindex",
703
+ });
704
+ }
705
+ const now = new Date().toISOString();
706
+ const aliasesRemoved = Number(this.db.prepare(`SELECT COUNT(*) as cnt FROM project_aliases
707
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
708
+ const trackerMappingsRemoved = Number(this.db.prepare(`SELECT COUNT(*) as cnt FROM project_tracker_mappings
709
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
710
+ this.db.prepare(`DELETE FROM project_aliases
711
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
712
+ this.db.prepare(`DELETE FROM project_tracker_mappings
713
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
714
+ this.db.prepare(`UPDATE projects
715
+ SET lifecycle_status = 'detached', repo_root = NULL, repo_remote_primary = NULL, active_version = NULL, updated_at = ?
716
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(now, scopeUserId, scopeAgentId, projectId);
717
+ return {
718
+ project_id: projectId,
719
+ lifecycle_status: "detached",
720
+ detached_at: now,
721
+ reason,
722
+ detached_fields: {
723
+ repo_root: project.repo_root != null,
724
+ repo_remote_primary: project.repo_remote_primary != null,
725
+ active_version: project.active_version != null,
726
+ aliases_removed: aliasesRemoved,
727
+ tracker_mappings_removed: trackerMappingsRemoved,
728
+ },
729
+ searchable: false,
730
+ next_actions: {
731
+ reattach_via_register_or_update: true,
732
+ reversible_by_re_register: true,
733
+ },
734
+ };
735
+ }
736
+ unregisterProject(scopeUserId, scopeAgentId, input) {
737
+ const projectId = String(input.project_id || "").trim();
738
+ if (!projectId)
739
+ throw new Error("project_id is required");
740
+ const mode = input.mode || "safe";
741
+ if (mode !== "safe") {
742
+ throw new Error("project.unregister currently supports mode='safe' only");
743
+ }
744
+ if (input.confirm !== true) {
745
+ throw new Error("project.unregister requires explicit confirm=true");
746
+ }
747
+ const project = this.getProjectById(scopeUserId, scopeAgentId, projectId);
748
+ if (!project) {
749
+ throw new Error(`project_id '${projectId}' is not registered`);
750
+ }
751
+ const reason = input.reason == null ? null : String(input.reason).trim() || null;
752
+ let deindexedFirst = false;
753
+ if (project.lifecycle_status !== "deindexed") {
754
+ this.deindexProject(scopeUserId, scopeAgentId, {
755
+ project_id: projectId,
756
+ reason: reason || "unregister_precondition_deindex",
757
+ });
758
+ deindexedFirst = true;
759
+ }
760
+ const now = new Date().toISOString();
761
+ const aliasesRemoved = Number(this.db.prepare(`SELECT COUNT(*) as cnt FROM project_aliases
762
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
763
+ const trackerMappingsRemoved = Number(this.db.prepare(`SELECT COUNT(*) as cnt FROM project_tracker_mappings
764
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
765
+ this.db.prepare(`DELETE FROM project_aliases
766
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
767
+ this.db.prepare(`DELETE FROM project_tracker_mappings
768
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
769
+ this.db.prepare(`UPDATE projects
770
+ SET lifecycle_status = 'disabled', repo_root = NULL, repo_remote_primary = NULL, active_version = NULL, updated_at = ?
771
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(now, scopeUserId, scopeAgentId, projectId);
772
+ this.upsertProjectRegistrationState(scopeUserId, scopeAgentId, {
773
+ project_id: projectId,
774
+ registration_status: "draft",
775
+ validation_status: "warn",
776
+ validation_notes: reason ? `unregistered:${reason}` : "unregistered",
777
+ completeness_score: 0,
778
+ missing_required_fields: ["project_alias", "repo_root"],
779
+ last_validated_at: now,
780
+ });
781
+ return {
782
+ project_id: projectId,
783
+ lifecycle_status: "disabled",
784
+ unregistered_at: now,
785
+ mode,
786
+ reason,
787
+ detached_fields: {
788
+ aliases_removed: aliasesRemoved,
789
+ tracker_mappings_removed: trackerMappingsRemoved,
790
+ },
791
+ registration_state: {
792
+ registration_status: "draft",
793
+ validation_status: "warn",
794
+ },
795
+ searchable: false,
796
+ audit: {
797
+ deindexed_first: deindexedFirst,
798
+ confirm_required: true,
799
+ },
800
+ };
801
+ }
802
+ purgePreviewProject(scopeUserId, scopeAgentId, input) {
803
+ const projectId = String(input.project_id || "").trim();
804
+ if (!projectId)
805
+ throw new Error("project_id is required");
806
+ const project = this.getProjectById(scopeUserId, scopeAgentId, projectId);
807
+ if (!project) {
808
+ throw new Error(`project_id '${projectId}' is not registered`);
809
+ }
810
+ const countBy = (table) => Number(this.db.prepare(`SELECT COUNT(*) as cnt FROM ${table}
811
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
812
+ const canPurge = project.lifecycle_status === "disabled";
813
+ const reason = canPurge
814
+ ? "safe to purge: lifecycle_status is disabled and explicit confirm is still required"
815
+ : `purge blocked: lifecycle_status must be disabled (current=${project.lifecycle_status})`;
816
+ return {
817
+ project_id: projectId,
818
+ current_lifecycle_status: project.lifecycle_status,
819
+ purge_guard: {
820
+ destructive: true,
821
+ allowed: canPurge,
822
+ reason,
823
+ requires_lifecycle_status: "disabled",
824
+ requires_confirm: true,
825
+ },
826
+ affected: {
827
+ project_row: 1,
828
+ aliases: countBy("project_aliases"),
829
+ tracker_mappings: countBy("project_tracker_mappings"),
830
+ registration_state: countBy("project_registration_state"),
831
+ index_runs: countBy("index_runs"),
832
+ watch_state: countBy("project_index_watch_state"),
833
+ file_index_state: countBy("file_index_state"),
834
+ chunk_registry: countBy("chunk_registry"),
835
+ symbol_registry: countBy("symbol_registry"),
836
+ task_registry: countBy("task_registry"),
837
+ },
838
+ previewed_at: new Date().toISOString(),
839
+ };
840
+ }
841
+ purgeProject(scopeUserId, scopeAgentId, input) {
842
+ const projectId = String(input.project_id || "").trim();
843
+ if (!projectId)
844
+ throw new Error("project_id is required");
845
+ if (input.confirm !== true) {
846
+ throw new Error("project.purge requires explicit confirm=true");
847
+ }
848
+ const preview = this.purgePreviewProject(scopeUserId, scopeAgentId, { project_id: projectId });
849
+ if (!preview.purge_guard.allowed) {
850
+ throw new Error(preview.purge_guard.reason);
851
+ }
852
+ const now = new Date().toISOString();
853
+ const reason = input.reason == null ? null : String(input.reason).trim() || null;
854
+ const deleted = {
855
+ project_row: 1,
856
+ aliases: preview.affected.aliases,
857
+ tracker_mappings: preview.affected.tracker_mappings,
858
+ registration_state: preview.affected.registration_state,
859
+ index_runs: preview.affected.index_runs,
860
+ watch_state: preview.affected.watch_state,
861
+ file_index_state: preview.affected.file_index_state,
862
+ chunk_registry: preview.affected.chunk_registry,
863
+ symbol_registry: preview.affected.symbol_registry,
864
+ task_registry: preview.affected.task_registry,
865
+ };
866
+ this.db.exec("BEGIN");
867
+ try {
868
+ this.db.prepare(`DELETE FROM project_aliases
869
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
870
+ this.db.prepare(`DELETE FROM project_tracker_mappings
871
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
872
+ this.db.prepare(`DELETE FROM project_registration_state
873
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
874
+ this.db.prepare(`DELETE FROM index_runs
875
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
876
+ this.db.prepare(`DELETE FROM project_index_watch_state
877
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
878
+ this.db.prepare(`DELETE FROM file_index_state
879
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
880
+ this.db.prepare(`DELETE FROM chunk_registry
881
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
882
+ this.db.prepare(`DELETE FROM symbol_registry
883
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
884
+ this.db.prepare(`DELETE FROM task_registry
885
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
886
+ this.db.prepare(`DELETE FROM projects
887
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`).run(scopeUserId, scopeAgentId, projectId);
888
+ this.db.exec("COMMIT");
889
+ }
890
+ catch (error) {
891
+ this.db.exec("ROLLBACK");
892
+ throw error;
893
+ }
894
+ return {
895
+ project_id: projectId,
896
+ lifecycle_status: "purged",
897
+ purged_at: now,
898
+ reason,
899
+ deleted,
900
+ searchable: false,
901
+ recoverable: false,
902
+ audit: {
903
+ confirm_required: true,
904
+ allowed_from_lifecycle_status: "disabled",
905
+ },
906
+ };
907
+ }
632
908
  reindexProjectByDiff(scopeUserId, scopeAgentId, input) {
633
909
  const now = new Date().toISOString();
634
910
  const runId = randomUUID();
@@ -662,9 +938,12 @@ export class SlotDB {
662
938
  unchanged.push(relativePath);
663
939
  }
664
940
  const deleted = [];
665
- for (const prevPath of Object.keys(previousSnapshot)) {
666
- if (!currentSnapshot.has(prevPath))
667
- deleted.push(prevPath);
941
+ const treatAsFullSnapshot = input.full_snapshot === true || triggerType === "bootstrap";
942
+ if (treatAsFullSnapshot) {
943
+ for (const prevPath of Object.keys(previousSnapshot)) {
944
+ if (!currentSnapshot.has(prevPath))
945
+ deleted.push(prevPath);
946
+ }
668
947
  }
669
948
  this.insertIndexRun(scopeUserId, scopeAgentId, {
670
949
  run_id: runId,
@@ -680,12 +959,14 @@ export class SlotDB {
680
959
  const nowIso = new Date().toISOString();
681
960
  for (const relativePath of changed) {
682
961
  const item = (input.paths || []).find((p) => this.normalizeRelativePath(p.relative_path) === relativePath);
962
+ const fileId = this.makeScopedId(input.project_id, relativePath);
963
+ const language = item?.language || null;
683
964
  this.upsertFileIndexState(scopeUserId, scopeAgentId, {
684
- file_id: this.makeScopedId(input.project_id, relativePath),
965
+ file_id: fileId,
685
966
  project_id: input.project_id,
686
967
  relative_path: relativePath,
687
968
  module: item?.module || null,
688
- language: item?.language || null,
969
+ language,
689
970
  checksum: currentSnapshot.get(relativePath) || "__missing__",
690
971
  last_commit_sha: sourceRev,
691
972
  index_state: "indexed",
@@ -693,9 +974,63 @@ export class SlotDB {
693
974
  tombstone_at: null,
694
975
  indexed_at: nowIso,
695
976
  });
977
+ this.markProjectChunksByFileDeleted(scopeUserId, scopeAgentId, input.project_id, relativePath, nowIso);
978
+ this.markProjectSymbolsByFileDeleted(scopeUserId, scopeAgentId, input.project_id, relativePath, nowIso);
979
+ const content = String(item?.content || "");
980
+ if (content.trim()) {
981
+ const blocks = extractSemanticBlocks({ relativePath, content });
982
+ const chunks = buildChunkArtifacts(input.project_id, fileId, relativePath, blocks);
983
+ for (const chunk of chunks) {
984
+ this.upsertChunkRegistry(scopeUserId, scopeAgentId, {
985
+ chunk_id: chunk.chunk_id,
986
+ project_id: input.project_id,
987
+ file_id: chunk.file_id,
988
+ relative_path: chunk.relative_path,
989
+ chunk_kind: chunk.chunk_kind,
990
+ symbol_id: chunk.symbol_id,
991
+ task_id: null,
992
+ checksum: chunk.checksum,
993
+ qdrant_point_id: null,
994
+ index_state: "indexed",
995
+ active: 1,
996
+ tombstone_at: null,
997
+ indexed_at: nowIso,
998
+ });
999
+ }
1000
+ for (const block of blocks) {
1001
+ if (!block.symbol_name || !["function", "class", "method", "tool"].includes(block.kind))
1002
+ continue;
1003
+ const symbolFqn = block.semantic_path || `${block.kind}:${block.symbol_name}`;
1004
+ this.upsertSymbolRegistry(scopeUserId, scopeAgentId, {
1005
+ symbol_id: buildSymbolId(input.project_id, relativePath, symbolFqn),
1006
+ project_id: input.project_id,
1007
+ relative_path: relativePath,
1008
+ module: item?.module || null,
1009
+ language: language || "text",
1010
+ symbol_name: block.symbol_name,
1011
+ symbol_fqn: symbolFqn,
1012
+ symbol_kind: block.kind,
1013
+ signature_hash: null,
1014
+ index_state: "indexed",
1015
+ active: 1,
1016
+ tombstone_at: null,
1017
+ indexed_at: nowIso,
1018
+ });
1019
+ }
1020
+ populateUniversalCodeGraphForFile(this.graph, scopeUserId, scopeAgentId, {
1021
+ projectId: input.project_id,
1022
+ relativePath,
1023
+ module: item?.module || null,
1024
+ language: language || "text",
1025
+ content,
1026
+ blocks,
1027
+ });
1028
+ }
696
1029
  }
697
1030
  for (const relativePath of deleted) {
698
1031
  this.markFileIndexStateDeleted(scopeUserId, scopeAgentId, input.project_id, relativePath, nowIso);
1032
+ this.markProjectChunksByFileDeleted(scopeUserId, scopeAgentId, input.project_id, relativePath, nowIso);
1033
+ this.markProjectSymbolsByFileDeleted(scopeUserId, scopeAgentId, input.project_id, relativePath, nowIso);
699
1034
  }
700
1035
  const checksumSnapshotRecord = Object.fromEntries(currentSnapshot.entries());
701
1036
  this.upsertProjectIndexWatchState(scopeUserId, scopeAgentId, {
@@ -704,7 +1039,14 @@ export class SlotDB {
704
1039
  last_checksum_snapshot: checksumSnapshotRecord,
705
1040
  updated_at: nowIso,
706
1041
  });
1042
+ this.db.prepare(`UPDATE projects
1043
+ SET lifecycle_status = 'active', updated_at = ?
1044
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND lifecycle_status = 'deindexed'`).run(nowIso, scopeUserId, scopeAgentId, input.project_id);
707
1045
  this.finishIndexRun(scopeUserId, scopeAgentId, runId, "indexed", null, nowIso);
1046
+ this.db.prepare(`UPDATE projects
1047
+ SET lifecycle_status = 'active', updated_at = ?
1048
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?
1049
+ AND lifecycle_status = 'deindexed'`).run(nowIso, scopeUserId, scopeAgentId, input.project_id);
708
1050
  return {
709
1051
  run_id: runId,
710
1052
  project_id: input.project_id,
@@ -878,19 +1220,138 @@ export class SlotDB {
878
1220
  if (!project) {
879
1221
  throw new Error(`project_id '${projectId}' is not registered`);
880
1222
  }
1223
+ const isSearchDisabled = ["deindexed", "detached", "disabled", "purged"].includes(project.lifecycle_status);
1224
+ if (isSearchDisabled) {
1225
+ const files = Number(this.db.prepare(`SELECT COUNT(*) as cnt FROM file_index_state
1226
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND tombstone_at IS NOT NULL`).get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
1227
+ const chunks = Number(this.db.prepare(`SELECT COUNT(*) as cnt FROM chunk_registry
1228
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND tombstone_at IS NOT NULL`).get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
1229
+ const symbols = Number(this.db.prepare(`SELECT COUNT(*) as cnt FROM symbol_registry
1230
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND tombstone_at IS NOT NULL`).get(scopeUserId, scopeAgentId, projectId)?.cnt || 0);
1231
+ const taskContextSelector = {
1232
+ ...(input.task_context?.task_id ? { task_id: String(input.task_context.task_id).trim() } : {}),
1233
+ ...(input.task_context?.tracker_issue_key ? { tracker_issue_key: String(input.task_context.tracker_issue_key).trim() } : {}),
1234
+ ...(input.task_context?.task_title ? { task_title: String(input.task_context.task_title).trim() } : {}),
1235
+ };
1236
+ const reasonByLifecycle = {
1237
+ deindexed: "project is deindexed; retrieval is disabled until reindex",
1238
+ detached: "project is detached; retrieval is disabled until project is re-attached and reindexed",
1239
+ disabled: "project is unregistered/disabled; retrieval is disabled until re-registration",
1240
+ purged: "project is purged; retrieval is disabled",
1241
+ };
1242
+ return {
1243
+ query,
1244
+ project_id: projectId,
1245
+ project_lifecycle_status: project.lifecycle_status,
1246
+ searchable: false,
1247
+ tombstone_summary: { files, chunks, symbols },
1248
+ count: 0,
1249
+ task_lineage_context: null,
1250
+ task_context_resolution: {
1251
+ status: Object.keys(taskContextSelector).length > 0 ? "selector_not_resolved" : "not_requested",
1252
+ ...(Object.keys(taskContextSelector).length > 0
1253
+ ? { reason: reasonByLifecycle[project.lifecycle_status] || "project lifecycle disables retrieval" }
1254
+ : {}),
1255
+ selector: taskContextSelector,
1256
+ recoverable: project.lifecycle_status !== "purged",
1257
+ },
1258
+ results: [],
1259
+ debug: input.debug
1260
+ ? {
1261
+ query_intent: {
1262
+ looks_code_intent: false,
1263
+ looks_identifier_query: false,
1264
+ query_tokens: [],
1265
+ },
1266
+ candidate_counts: {
1267
+ file_index_state: 0,
1268
+ symbol_registry: 0,
1269
+ chunk_registry: 0,
1270
+ task_registry: 0,
1271
+ },
1272
+ top_candidates: {
1273
+ file_index_state: [],
1274
+ symbol_registry: [],
1275
+ chunk_registry: [],
1276
+ task_registry: [],
1277
+ },
1278
+ }
1279
+ : undefined,
1280
+ };
1281
+ }
881
1282
  const limit = Math.min(Math.max(Number(input.limit || 10), 1), 50);
882
1283
  const queryLc = query.toLowerCase();
1284
+ const queryTokens = Array.from(new Set(queryLc.split(/[^a-z0-9._/-]+/i).map((t) => t.trim()).filter(Boolean)));
1285
+ const tokenScore = (text) => {
1286
+ if (!queryTokens.length)
1287
+ return 0;
1288
+ const hay = text.toLowerCase();
1289
+ let matched = 0;
1290
+ for (const token of queryTokens) {
1291
+ if (hay.includes(token))
1292
+ matched += 1;
1293
+ }
1294
+ return matched / queryTokens.length;
1295
+ };
1296
+ const exactMatchScore = (candidate) => {
1297
+ const value = String(candidate || '').trim().toLowerCase();
1298
+ if (!value)
1299
+ return 0;
1300
+ if (value === queryLc)
1301
+ return 1;
1302
+ if (value.endsWith(`.${queryLc}`))
1303
+ return 0.92;
1304
+ if (value.includes(`/${queryLc}`) || value.includes(`:${queryLc}`) || value.includes(`#${queryLc}`))
1305
+ return 0.78;
1306
+ return 0;
1307
+ };
1308
+ const codeIntentHints = ['function', 'class', 'method', 'symbol', 'route', 'endpoint', 'extractor', 'registry', 'chunk', 'snippet', 'code'];
1309
+ const looksCodeIntent = codeIntentHints.some((hint) => queryLc.includes(hint)) || query.includes('/') || query.includes('_');
1310
+ const looksIdentifierQuery = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(query) ||
1311
+ /^[a-zA-Z_][a-zA-Z0-9_.#:-]*$/.test(query) ||
1312
+ query.includes('::') ||
1313
+ query.includes('.') ||
1314
+ query.includes('_');
883
1315
  const taskContextInput = input.task_context;
1316
+ const taskContextSelector = {
1317
+ ...(taskContextInput?.task_id ? { task_id: String(taskContextInput.task_id).trim() } : {}),
1318
+ ...(taskContextInput?.tracker_issue_key ? { tracker_issue_key: String(taskContextInput.tracker_issue_key).trim() } : {}),
1319
+ ...(taskContextInput?.task_title ? { task_title: String(taskContextInput.task_title).trim() } : {}),
1320
+ };
884
1321
  let lineageContext = null;
885
- if (taskContextInput && (taskContextInput.task_id || taskContextInput.tracker_issue_key || taskContextInput.task_title)) {
886
- lineageContext = this.getTaskLineageContext(scopeUserId, scopeAgentId, {
887
- project_id: projectId,
888
- task_id: taskContextInput.task_id,
889
- tracker_issue_key: taskContextInput.tracker_issue_key,
890
- task_title: taskContextInput.task_title,
891
- include_parent_chain: taskContextInput.include_parent_chain,
892
- include_related: taskContextInput.include_related,
893
- });
1322
+ let taskContextResolution = {
1323
+ status: "not_requested",
1324
+ selector: taskContextSelector,
1325
+ recoverable: false,
1326
+ };
1327
+ if (Object.keys(taskContextSelector).length > 0) {
1328
+ try {
1329
+ lineageContext = this.getTaskLineageContext(scopeUserId, scopeAgentId, {
1330
+ project_id: projectId,
1331
+ task_id: taskContextInput?.task_id,
1332
+ tracker_issue_key: taskContextInput?.tracker_issue_key,
1333
+ task_title: taskContextInput?.task_title,
1334
+ include_parent_chain: taskContextInput?.include_parent_chain,
1335
+ include_related: taskContextInput?.include_related,
1336
+ });
1337
+ taskContextResolution = {
1338
+ status: "resolved",
1339
+ selector: taskContextSelector,
1340
+ recoverable: false,
1341
+ };
1342
+ }
1343
+ catch (error) {
1344
+ const message = error instanceof Error ? error.message : String(error);
1345
+ if (!message.includes("task lineage focus not found for provided selector")) {
1346
+ throw error;
1347
+ }
1348
+ taskContextResolution = {
1349
+ status: "selector_not_resolved",
1350
+ reason: "task lineage focus not found for provided selector",
1351
+ selector: taskContextSelector,
1352
+ recoverable: true,
1353
+ };
1354
+ }
894
1355
  }
895
1356
  const lexicalPathPrefix = this.normalizeStringArray(input.path_prefix).map((p) => this.normalizeRelativePath(p)).filter(Boolean);
896
1357
  const lexicalModules = new Set(this.normalizeStringArray(input.module).map((s) => s.toLowerCase()));
@@ -908,6 +1369,19 @@ export class SlotDB {
908
1369
  }
909
1370
  }
910
1371
  const results = [];
1372
+ const debugEnabled = input.debug === true;
1373
+ const debugBuckets = {
1374
+ file_index_state: [],
1375
+ symbol_registry: [],
1376
+ chunk_registry: [],
1377
+ task_registry: [],
1378
+ };
1379
+ const pushDebug = (bucket, entry) => {
1380
+ if (!debugEnabled)
1381
+ return;
1382
+ debugBuckets[bucket].push(entry);
1383
+ };
1384
+ const symbolRowsById = new Map();
911
1385
  const fileStmt = this.db.prepare(`SELECT * FROM file_index_state
912
1386
  WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`);
913
1387
  const fileRows = fileStmt.all(scopeUserId, scopeAgentId, projectId);
@@ -929,12 +1403,31 @@ export class SlotDB {
929
1403
  let score = 0;
930
1404
  if (text.includes(queryLc))
931
1405
  score += 0.55;
1406
+ score += tokenScore(text) * 0.25;
932
1407
  if (lineageContext && lineageContext.touched_files.includes(relativePath))
933
1408
  score += 0.35;
934
- if (relativePath.includes("README") || relativePath.includes("docs/"))
1409
+ if (looksCodeIntent) {
1410
+ if (relativePath.includes('/docs/') || relativePath.startsWith('docs/') || relativePath.includes('README'))
1411
+ score -= 0.18;
1412
+ if (relativePath.startsWith('src/') || relativePath.startsWith('tests/'))
1413
+ score += 0.14;
1414
+ }
1415
+ else if (relativePath.includes("README") || relativePath.includes("docs/")) {
935
1416
  score += 0.05;
936
- if (score <= 0)
1417
+ }
1418
+ if (looksIdentifierQuery) {
1419
+ score -= 0.12;
1420
+ }
1421
+ if (score <= 0.08)
937
1422
  continue;
1423
+ pushDebug('file_index_state', {
1424
+ relative_path: relativePath,
1425
+ score: Number(score.toFixed(4)),
1426
+ module: moduleName,
1427
+ language,
1428
+ text_exact: text.includes(queryLc),
1429
+ token_score: Number(tokenScore(text).toFixed(4)),
1430
+ });
938
1431
  results.push({
939
1432
  source: "file_index_state",
940
1433
  id: String(row.file_id),
@@ -956,6 +1449,7 @@ export class SlotDB {
956
1449
  const symbolName = String(row.symbol_name || "");
957
1450
  const symbolKind = String(row.symbol_kind || "");
958
1451
  const symbolFqn = String(row.symbol_fqn || "");
1452
+ symbolRowsById.set(String(row.symbol_id), row);
959
1453
  if (lexicalPathPrefix.length > 0 && !lexicalPathPrefix.some((prefix) => relativePath.startsWith(prefix)))
960
1454
  continue;
961
1455
  if (lexicalModules.size > 0 && !moduleName)
@@ -970,12 +1464,38 @@ export class SlotDB {
970
1464
  let score = 0;
971
1465
  if (text.includes(queryLc))
972
1466
  score += 0.62;
1467
+ score += tokenScore(text) * 0.35;
1468
+ score += exactMatchScore(symbolName) * 1.35;
1469
+ score += exactMatchScore(symbolFqn) * 1.1;
1470
+ if (looksIdentifierQuery) {
1471
+ if (symbolName.toLowerCase() === queryLc)
1472
+ score += 1.8;
1473
+ else if (symbolFqn.toLowerCase() === queryLc)
1474
+ score += 1.5;
1475
+ else if (symbolFqn.toLowerCase().endsWith(`.${queryLc}`))
1476
+ score += 1.1;
1477
+ }
973
1478
  if (lineageContext && lineageContext.touched_symbols.includes(symbolName))
974
1479
  score += 0.3;
975
1480
  if (lineageContext && lineageContext.touched_files.includes(relativePath))
976
1481
  score += 0.12;
977
- if (score <= 0)
1482
+ if (looksCodeIntent && (relativePath.startsWith('src/') || relativePath.startsWith('tests/')))
1483
+ score += 0.08;
1484
+ if (looksIdentifierQuery)
1485
+ score += 0.18;
1486
+ if (score <= 0.08)
978
1487
  continue;
1488
+ pushDebug('symbol_registry', {
1489
+ relative_path: relativePath,
1490
+ symbol_name: symbolName,
1491
+ symbol_fqn: symbolFqn,
1492
+ symbol_kind: symbolKind,
1493
+ score: Number(score.toFixed(4)),
1494
+ exact_symbol: symbolName.toLowerCase() === queryLc,
1495
+ exact_fqn: symbolFqn.toLowerCase() === queryLc,
1496
+ suffix_fqn: symbolFqn.toLowerCase().endsWith(`.${queryLc}`),
1497
+ token_score: Number(tokenScore(text).toFixed(4)),
1498
+ });
979
1499
  results.push({
980
1500
  source: "symbol_registry",
981
1501
  id: String(row.symbol_id),
@@ -989,6 +1509,62 @@ export class SlotDB {
989
1509
  snippet: `symbol ${symbolName} (${symbolKind}) in ${relativePath}`,
990
1510
  });
991
1511
  }
1512
+ const chunkStmt = this.db.prepare(`SELECT * FROM chunk_registry
1513
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`);
1514
+ const chunkRows = chunkStmt.all(scopeUserId, scopeAgentId, projectId);
1515
+ for (const row of chunkRows) {
1516
+ const relativePath = String(row.relative_path || "");
1517
+ const chunkKind = String(row.chunk_kind || "");
1518
+ const symbolId = row.symbol_id ? String(row.symbol_id) : null;
1519
+ const symbolRow = symbolId ? symbolRowsById.get(symbolId) : null;
1520
+ const symbolName = symbolRow ? String(symbolRow.symbol_name || '') : '';
1521
+ const symbolFqn = symbolRow ? String(symbolRow.symbol_fqn || '') : '';
1522
+ if (lexicalPathPrefix.length > 0 && !lexicalPathPrefix.some((prefix) => relativePath.startsWith(prefix)))
1523
+ continue;
1524
+ const text = `${relativePath} ${chunkKind} ${symbolId || ""} ${symbolName} ${symbolFqn}`.toLowerCase();
1525
+ let score = 0;
1526
+ if (text.includes(queryLc))
1527
+ score += 0.6;
1528
+ score += tokenScore(text) * 0.4;
1529
+ score += exactMatchScore(symbolName) * 0.9;
1530
+ score += exactMatchScore(symbolFqn) * 0.7;
1531
+ if (looksIdentifierQuery) {
1532
+ if (symbolName.toLowerCase() === queryLc)
1533
+ score += 1.0;
1534
+ else if (symbolFqn.toLowerCase() === queryLc)
1535
+ score += 0.85;
1536
+ }
1537
+ if (lineageContext && lineageContext.touched_files.includes(relativePath))
1538
+ score += 0.15;
1539
+ if (looksCodeIntent) {
1540
+ if (relativePath.includes('/docs/') || relativePath.startsWith('docs/') || relativePath.includes('README'))
1541
+ score -= 0.14;
1542
+ if (relativePath.startsWith('src/') || relativePath.startsWith('tests/'))
1543
+ score += 0.1;
1544
+ }
1545
+ if (looksIdentifierQuery)
1546
+ score += 0.12;
1547
+ if (score <= 0.08)
1548
+ continue;
1549
+ pushDebug('chunk_registry', {
1550
+ relative_path: relativePath,
1551
+ chunk_kind: chunkKind,
1552
+ symbol_name: symbolName || null,
1553
+ symbol_fqn: symbolFqn || null,
1554
+ score: Number(score.toFixed(4)),
1555
+ exact_symbol: symbolName ? symbolName.toLowerCase() === queryLc : false,
1556
+ token_score: Number(tokenScore(text).toFixed(4)),
1557
+ });
1558
+ results.push({
1559
+ source: "chunk_registry",
1560
+ id: String(row.chunk_id),
1561
+ score,
1562
+ project_id: projectId,
1563
+ relative_path: relativePath,
1564
+ symbol_name: symbolName || undefined,
1565
+ snippet: symbolName ? `chunk ${chunkKind} for symbol ${symbolName} in ${relativePath}` : `chunk ${chunkKind} in ${relativePath}`,
1566
+ });
1567
+ }
992
1568
  const taskStmt = this.db.prepare(`SELECT * FROM task_registry
993
1569
  WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?`);
994
1570
  const taskRows = taskStmt.all(scopeUserId, scopeAgentId, projectId);
@@ -1013,6 +1589,7 @@ export class SlotDB {
1013
1589
  let score = 0;
1014
1590
  if (text.includes(queryLc))
1015
1591
  score += 0.58;
1592
+ score += tokenScore(text) * 0.25;
1016
1593
  if (lexicalTaskIds.has(task.task_id))
1017
1594
  score += 0.28;
1018
1595
  if (taskIssueKey && lexicalIssueKeys.has(taskIssueKey))
@@ -1025,8 +1602,15 @@ export class SlotDB {
1025
1602
  if (lineageContext.related_tasks.some((t) => t.task_id === task.task_id))
1026
1603
  score += 0.2;
1027
1604
  }
1028
- if (score <= 0)
1605
+ if (score <= 0.08)
1029
1606
  continue;
1607
+ pushDebug('task_registry', {
1608
+ task_id: task.task_id,
1609
+ tracker_issue_key: task.tracker_issue_key,
1610
+ task_title: task.task_title,
1611
+ score: Number(score.toFixed(4)),
1612
+ token_score: Number(tokenScore(text).toFixed(4)),
1613
+ });
1030
1614
  results.push({
1031
1615
  source: "task_registry",
1032
1616
  id: task.task_id,
@@ -1045,9 +1629,208 @@ export class SlotDB {
1045
1629
  return {
1046
1630
  query,
1047
1631
  project_id: projectId,
1632
+ project_lifecycle_status: project.lifecycle_status,
1633
+ searchable: true,
1048
1634
  count: ranked.length,
1049
1635
  task_lineage_context: lineageContext,
1636
+ task_context_resolution: taskContextResolution,
1050
1637
  results: ranked,
1638
+ debug: debugEnabled
1639
+ ? {
1640
+ query_intent: {
1641
+ looks_code_intent: looksCodeIntent,
1642
+ looks_identifier_query: looksIdentifierQuery,
1643
+ query_tokens: queryTokens,
1644
+ },
1645
+ candidate_counts: {
1646
+ file_index_state: debugBuckets.file_index_state.length,
1647
+ symbol_registry: debugBuckets.symbol_registry.length,
1648
+ chunk_registry: debugBuckets.chunk_registry.length,
1649
+ task_registry: debugBuckets.task_registry.length,
1650
+ },
1651
+ top_candidates: {
1652
+ file_index_state: debugBuckets.file_index_state.sort((a, b) => Number(b.score || 0) - Number(a.score || 0)).slice(0, 8),
1653
+ symbol_registry: debugBuckets.symbol_registry.sort((a, b) => Number(b.score || 0) - Number(a.score || 0)).slice(0, 8),
1654
+ chunk_registry: debugBuckets.chunk_registry.sort((a, b) => Number(b.score || 0) - Number(a.score || 0)).slice(0, 8),
1655
+ task_registry: debugBuckets.task_registry.sort((a, b) => Number(b.score || 0) - Number(a.score || 0)).slice(0, 8),
1656
+ },
1657
+ }
1658
+ : undefined,
1659
+ };
1660
+ }
1661
+ queryProjectChangeOverlay(scopeUserId, scopeAgentId, input) {
1662
+ let lineage = null;
1663
+ const taskIdSelector = String(input.task_id || "").trim();
1664
+ const trackerSelector = String(input.tracker_issue_key || "").trim();
1665
+ const taskTitleSelector = String(input.task_title || "").trim();
1666
+ const selector = {
1667
+ ...(taskIdSelector ? { task_id: taskIdSelector } : {}),
1668
+ ...(trackerSelector ? { tracker_issue_key: trackerSelector } : {}),
1669
+ ...(taskTitleSelector ? { task_title: taskTitleSelector } : {}),
1670
+ };
1671
+ try {
1672
+ lineage = this.getTaskLineageContext(scopeUserId, scopeAgentId, {
1673
+ project_id: input.project_id,
1674
+ task_id: input.task_id,
1675
+ tracker_issue_key: input.tracker_issue_key,
1676
+ task_title: input.task_title,
1677
+ include_related: input.include_related,
1678
+ include_parent_chain: input.include_parent_chain,
1679
+ });
1680
+ }
1681
+ catch (error) {
1682
+ const message = error instanceof Error ? error.message : String(error);
1683
+ if (!message.includes("task lineage focus not found for provided selector")) {
1684
+ throw error;
1685
+ }
1686
+ const unresolvedTaskId = taskIdSelector || `unresolved:${trackerSelector || taskTitleSelector || "selector"}`;
1687
+ const unresolvedTitle = taskTitleSelector || "Unresolved task lineage selector";
1688
+ return {
1689
+ status: "selector_not_resolved",
1690
+ reason: "task lineage focus not found for provided selector",
1691
+ selector,
1692
+ recoverable: true,
1693
+ project_id: input.project_id,
1694
+ focus: {
1695
+ task_id: unresolvedTaskId,
1696
+ task_title: unresolvedTitle,
1697
+ tracker_issue_key: trackerSelector || null,
1698
+ },
1699
+ changed_files: [],
1700
+ related_symbols: [],
1701
+ commit_refs: [],
1702
+ };
1703
+ }
1704
+ const changedFiles = this.uniqueSorted((lineage.touched_files || []).map((p) => this.normalizeRelativePath(p)).filter(Boolean));
1705
+ const relatedSymbolsMap = new Map();
1706
+ for (const symbolName of lineage.touched_symbols || []) {
1707
+ const normalized = String(symbolName || "").trim();
1708
+ if (!normalized)
1709
+ continue;
1710
+ const key = `task_registry:${normalized}`;
1711
+ if (!relatedSymbolsMap.has(key)) {
1712
+ relatedSymbolsMap.set(key, {
1713
+ symbol_name: normalized,
1714
+ source: "task_registry",
1715
+ });
1716
+ }
1717
+ }
1718
+ if (changedFiles.length > 0) {
1719
+ const placeholders = changedFiles.map(() => "?").join(",");
1720
+ const stmt = this.db.prepare(`SELECT symbol_name, symbol_kind, symbol_fqn, relative_path
1721
+ FROM symbol_registry
1722
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1
1723
+ AND relative_path IN (${placeholders})
1724
+ ORDER BY indexed_at DESC, symbol_name ASC`);
1725
+ const rows = stmt.all(scopeUserId, scopeAgentId, input.project_id, ...changedFiles);
1726
+ for (const row of rows) {
1727
+ const symbolName = String(row.symbol_name || "").trim();
1728
+ if (!symbolName)
1729
+ continue;
1730
+ const relPath = row.relative_path ? String(row.relative_path) : undefined;
1731
+ const symbolFqn = row.symbol_fqn ? String(row.symbol_fqn) : undefined;
1732
+ const key = `symbol_registry:${symbolName}:${symbolFqn || ""}:${relPath || ""}`;
1733
+ if (!relatedSymbolsMap.has(key)) {
1734
+ relatedSymbolsMap.set(key, {
1735
+ symbol_name: symbolName,
1736
+ symbol_kind: row.symbol_kind ? String(row.symbol_kind) : undefined,
1737
+ symbol_fqn: symbolFqn,
1738
+ relative_path: relPath,
1739
+ source: "symbol_registry",
1740
+ });
1741
+ }
1742
+ }
1743
+ }
1744
+ return {
1745
+ status: "ok",
1746
+ selector,
1747
+ recoverable: false,
1748
+ project_id: input.project_id,
1749
+ focus: lineage.focus,
1750
+ changed_files: changedFiles,
1751
+ related_symbols: Array.from(relatedSymbolsMap.values()),
1752
+ commit_refs: this.uniqueSorted(lineage.commit_refs || []),
1753
+ };
1754
+ }
1755
+ getProjectFeaturePackProjectOnboardingIndexingSnapshot(scopeUserId, scopeAgentId, projectId) {
1756
+ const project = this.getProjectById(scopeUserId, scopeAgentId, projectId);
1757
+ if (!project) {
1758
+ throw new Error(`project_id '${projectId}' is not registered`);
1759
+ }
1760
+ const aliases = this.listProjects(scopeUserId, scopeAgentId)
1761
+ .find((row) => row.project.project_id === projectId)?.aliases || [];
1762
+ const registration = this.getProjectRegistrationState(scopeUserId, scopeAgentId, projectId);
1763
+ const trackerStmt = this.db.prepare(`SELECT * FROM project_tracker_mappings
1764
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?
1765
+ ORDER BY updated_at DESC`);
1766
+ const trackerMappings = trackerStmt.all(scopeUserId, scopeAgentId, projectId).map((row) => ({
1767
+ id: String(row.id),
1768
+ project_id: String(row.project_id),
1769
+ scope_user_id: String(row.scope_user_id),
1770
+ scope_agent_id: String(row.scope_agent_id),
1771
+ tracker_type: String(row.tracker_type),
1772
+ tracker_space_key: row.tracker_space_key ? String(row.tracker_space_key) : null,
1773
+ tracker_project_id: row.tracker_project_id ? String(row.tracker_project_id) : null,
1774
+ default_epic_key: row.default_epic_key ? String(row.default_epic_key) : null,
1775
+ board_key: row.board_key ? String(row.board_key) : null,
1776
+ active_version: row.active_version ? String(row.active_version) : null,
1777
+ external_project_url: row.external_project_url ? String(row.external_project_url) : null,
1778
+ created_at: String(row.created_at),
1779
+ updated_at: String(row.updated_at),
1780
+ }));
1781
+ const fileStmt = this.db.prepare(`SELECT relative_path, module, language
1782
+ FROM file_index_state
1783
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1
1784
+ ORDER BY indexed_at DESC, relative_path ASC
1785
+ LIMIT 12`);
1786
+ const recentFiles = fileStmt.all(scopeUserId, scopeAgentId, projectId).map((row) => ({
1787
+ relative_path: String(row.relative_path),
1788
+ module: row.module ? String(row.module) : null,
1789
+ language: row.language ? String(row.language) : null,
1790
+ }));
1791
+ const symbolStmt = this.db.prepare(`SELECT symbol_name, symbol_kind, symbol_fqn, relative_path
1792
+ FROM symbol_registry
1793
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1
1794
+ ORDER BY indexed_at DESC, symbol_name ASC
1795
+ LIMIT 16`);
1796
+ const recentSymbols = symbolStmt.all(scopeUserId, scopeAgentId, projectId).map((row) => ({
1797
+ symbol_name: String(row.symbol_name),
1798
+ symbol_kind: String(row.symbol_kind),
1799
+ symbol_fqn: String(row.symbol_fqn),
1800
+ relative_path: String(row.relative_path),
1801
+ }));
1802
+ const taskStmt = this.db.prepare(`SELECT task_id, task_title, tracker_issue_key, task_status
1803
+ FROM task_registry
1804
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?
1805
+ ORDER BY updated_at DESC
1806
+ LIMIT 12`);
1807
+ const recentTasks = taskStmt.all(scopeUserId, scopeAgentId, projectId).map((row) => ({
1808
+ task_id: String(row.task_id),
1809
+ task_title: String(row.task_title),
1810
+ tracker_issue_key: row.tracker_issue_key ? String(row.tracker_issue_key) : null,
1811
+ task_status: row.task_status ? String(row.task_status) : null,
1812
+ }));
1813
+ const runStmt = this.db.prepare(`SELECT run_id, trigger_type, state, started_at, finished_at
1814
+ FROM index_runs
1815
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ?
1816
+ ORDER BY started_at DESC
1817
+ LIMIT 8`);
1818
+ const recentIndexRuns = runStmt.all(scopeUserId, scopeAgentId, projectId).map((row) => ({
1819
+ run_id: String(row.run_id),
1820
+ trigger_type: String(row.trigger_type),
1821
+ state: String(row.state),
1822
+ started_at: String(row.started_at),
1823
+ finished_at: row.finished_at ? String(row.finished_at) : null,
1824
+ }));
1825
+ return {
1826
+ project,
1827
+ aliases,
1828
+ registration,
1829
+ tracker_mappings: trackerMappings,
1830
+ recent_files: recentFiles,
1831
+ recent_symbols: recentSymbols,
1832
+ recent_tasks: recentTasks,
1833
+ recent_index_runs: recentIndexRuns,
1051
1834
  };
1052
1835
  }
1053
1836
  runLegacyCompatibilityBackfill(scopeUserId, scopeAgentId, input = {}) {
@@ -1252,6 +2035,45 @@ export class SlotDB {
1252
2035
  WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND relative_path = ?`);
1253
2036
  stmt.run(tombstoneAt, tombstoneAt, scopeUserId, scopeAgentId, projectId, relativePath);
1254
2037
  }
2038
+ upsertChunkRegistry(scopeUserId, scopeAgentId, input) {
2039
+ const existing = this.db.prepare(`SELECT chunk_id FROM chunk_registry WHERE scope_user_id = ? AND scope_agent_id = ? AND chunk_id = ?`).get(scopeUserId, scopeAgentId, input.chunk_id);
2040
+ if (existing) {
2041
+ this.db.prepare(`UPDATE chunk_registry
2042
+ SET project_id = ?, file_id = ?, relative_path = ?, chunk_kind = ?, symbol_id = ?, task_id = ?, checksum = ?, qdrant_point_id = ?, index_state = ?, active = ?, tombstone_at = ?, indexed_at = ?
2043
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND chunk_id = ?`).run(input.project_id, input.file_id, input.relative_path, input.chunk_kind, input.symbol_id, input.task_id || null, input.checksum, input.qdrant_point_id || null, input.index_state, input.active, input.tombstone_at, input.indexed_at, scopeUserId, scopeAgentId, input.chunk_id);
2044
+ return;
2045
+ }
2046
+ this.db.prepare(`INSERT INTO chunk_registry (
2047
+ chunk_id, scope_user_id, scope_agent_id, project_id, file_id, relative_path, chunk_kind, symbol_id, task_id, checksum, qdrant_point_id, index_state, active, tombstone_at, indexed_at
2048
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.chunk_id, scopeUserId, scopeAgentId, input.project_id, input.file_id, input.relative_path, input.chunk_kind, input.symbol_id, input.task_id || null, input.checksum, input.qdrant_point_id || null, input.index_state, input.active, input.tombstone_at, input.indexed_at);
2049
+ }
2050
+ upsertSymbolRegistry(scopeUserId, scopeAgentId, input) {
2051
+ const existing = this.db.prepare(`SELECT symbol_id FROM symbol_registry WHERE scope_user_id = ? AND scope_agent_id = ? AND symbol_id = ?`).get(scopeUserId, scopeAgentId, input.symbol_id);
2052
+ if (existing) {
2053
+ this.db.prepare(`UPDATE symbol_registry
2054
+ SET project_id = ?, relative_path = ?, module = ?, language = ?, symbol_name = ?, symbol_fqn = ?, symbol_kind = ?, signature_hash = ?, index_state = ?, active = ?, tombstone_at = ?, indexed_at = ?
2055
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND symbol_id = ?`).run(input.project_id, input.relative_path, input.module, input.language, input.symbol_name, input.symbol_fqn, input.symbol_kind, input.signature_hash || null, input.index_state, input.active, input.tombstone_at, input.indexed_at, scopeUserId, scopeAgentId, input.symbol_id);
2056
+ return;
2057
+ }
2058
+ this.db.prepare(`INSERT INTO symbol_registry (
2059
+ symbol_id, scope_user_id, scope_agent_id, project_id, relative_path, module, language, symbol_name, symbol_fqn, symbol_kind, signature_hash, index_state, active, tombstone_at, indexed_at
2060
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.symbol_id, scopeUserId, scopeAgentId, input.project_id, input.relative_path, input.module, input.language, input.symbol_name, input.symbol_fqn, input.symbol_kind, input.signature_hash || null, input.index_state, input.active, input.tombstone_at, input.indexed_at);
2061
+ }
2062
+ markProjectChunksByFileDeleted(scopeUserId, scopeAgentId, projectId, relativePath, tombstoneAt) {
2063
+ this.db.prepare(`UPDATE chunk_registry
2064
+ SET index_state = 'stale', active = 0, tombstone_at = ?, indexed_at = ?
2065
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND relative_path = ?`).run(tombstoneAt, tombstoneAt, scopeUserId, scopeAgentId, projectId, relativePath);
2066
+ }
2067
+ markProjectSymbolsByFileDeleted(scopeUserId, scopeAgentId, projectId, relativePath, tombstoneAt) {
2068
+ this.db.prepare(`UPDATE symbol_registry
2069
+ SET index_state = 'stale', active = 0, tombstone_at = ?, indexed_at = ?
2070
+ WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND relative_path = ?`).run(tombstoneAt, tombstoneAt, scopeUserId, scopeAgentId, projectId, relativePath);
2071
+ }
2072
+ markProjectFileDeletedForEvent(scopeUserId, scopeAgentId, projectId, relativePath, tombstoneAt) {
2073
+ this.markFileIndexStateDeleted(scopeUserId, scopeAgentId, projectId, relativePath, tombstoneAt);
2074
+ this.markProjectChunksByFileDeleted(scopeUserId, scopeAgentId, projectId, relativePath, tombstoneAt);
2075
+ this.markProjectSymbolsByFileDeleted(scopeUserId, scopeAgentId, projectId, relativePath, tombstoneAt);
2076
+ }
1255
2077
  upsertProjectIndexWatchState(scopeUserId, scopeAgentId, input) {
1256
2078
  const existing = this.getProjectIndexWatchState(scopeUserId, scopeAgentId, input.project_id);
1257
2079
  const checksumJson = JSON.stringify(input.last_checksum_snapshot || {});