@mrc2204/agent-smart-memo 5.1.0 → 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 (69) 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 +1 -1
  31. package/dist/core/ingest/contracts.d.ts.map +1 -1
  32. package/dist/core/ingest/ingest-pipeline.js +1 -1
  33. package/dist/core/ingest/ingest-pipeline.js.map +1 -1
  34. package/dist/core/ingest/semantic-block-extractor.d.ts.map +1 -1
  35. package/dist/core/ingest/semantic-block-extractor.js +36 -0
  36. package/dist/core/ingest/semantic-block-extractor.js.map +1 -1
  37. package/dist/core/usecases/default-memory-usecase-port.d.ts +35 -0
  38. package/dist/core/usecases/default-memory-usecase-port.d.ts.map +1 -1
  39. package/dist/core/usecases/default-memory-usecase-port.js +1578 -19
  40. package/dist/core/usecases/default-memory-usecase-port.js.map +1 -1
  41. package/dist/db/graph-db.d.ts +24 -0
  42. package/dist/db/graph-db.d.ts.map +1 -1
  43. package/dist/db/graph-db.js +81 -2
  44. package/dist/db/graph-db.js.map +1 -1
  45. package/dist/db/slot-db.d.ts +227 -1
  46. package/dist/db/slot-db.d.ts.map +1 -1
  47. package/dist/db/slot-db.js +700 -13
  48. package/dist/db/slot-db.js.map +1 -1
  49. package/dist/index.d.ts +7 -247
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +32 -119
  52. package/dist/index.js.map +1 -1
  53. package/dist/shared/asm-config.d.ts +82 -0
  54. package/dist/shared/asm-config.d.ts.map +1 -0
  55. package/dist/shared/asm-config.js +254 -0
  56. package/dist/shared/asm-config.js.map +1 -0
  57. package/dist/shared/slotdb-path.d.ts +4 -3
  58. package/dist/shared/slotdb-path.d.ts.map +1 -1
  59. package/dist/shared/slotdb-path.js +15 -6
  60. package/dist/shared/slotdb-path.js.map +1 -1
  61. package/dist/tools/graph-tools.d.ts.map +1 -1
  62. package/dist/tools/graph-tools.js +131 -0
  63. package/dist/tools/graph-tools.js.map +1 -1
  64. package/dist/tools/project-tools.d.ts.map +1 -1
  65. package/dist/tools/project-tools.js +476 -1
  66. package/dist/tools/project-tools.js.map +1 -1
  67. package/openclaw.plugin.json +5 -164
  68. package/package.json +61 -26
  69. package/scripts/init-openclaw.mjs +727 -0
@@ -13,6 +13,7 @@ import { mkdirSync, existsSync } from "node:fs";
13
13
  import { buildChunkArtifacts } from "../core/ingest/ingest-pipeline.js";
14
14
  import { extractSemanticBlocks } from "../core/ingest/semantic-block-extractor.js";
15
15
  import { buildSymbolId } from "../core/ingest/ids.js";
16
+ import { populateUniversalCodeGraphForFile } from "../core/graph/code-graph-populator.js";
16
17
  import { GraphDB } from "./graph-db.js";
17
18
  import { getSlotTTL } from "../shared/memory-config.js";
18
19
  import { resolveLegacyStateDirInput, resolveSlotDbDir } from "../shared/slotdb-path.js";
@@ -632,6 +633,278 @@ export class SlotDB {
632
633
  updated_at: row.updated_at,
633
634
  };
634
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
+ }
635
908
  reindexProjectByDiff(scopeUserId, scopeAgentId, input) {
636
909
  const now = new Date().toISOString();
637
910
  const runId = randomUUID();
@@ -725,7 +998,7 @@ export class SlotDB {
725
998
  });
726
999
  }
727
1000
  for (const block of blocks) {
728
- if (!block.symbol_name || !["function", "class", "method"].includes(block.kind))
1001
+ if (!block.symbol_name || !["function", "class", "method", "tool"].includes(block.kind))
729
1002
  continue;
730
1003
  const symbolFqn = block.semantic_path || `${block.kind}:${block.symbol_name}`;
731
1004
  this.upsertSymbolRegistry(scopeUserId, scopeAgentId, {
@@ -744,6 +1017,14 @@ export class SlotDB {
744
1017
  indexed_at: nowIso,
745
1018
  });
746
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
+ });
747
1028
  }
748
1029
  }
749
1030
  for (const relativePath of deleted) {
@@ -758,7 +1039,14 @@ export class SlotDB {
758
1039
  last_checksum_snapshot: checksumSnapshotRecord,
759
1040
  updated_at: nowIso,
760
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);
761
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);
762
1050
  return {
763
1051
  run_id: runId,
764
1052
  project_id: input.project_id,
@@ -932,6 +1220,65 @@ export class SlotDB {
932
1220
  if (!project) {
933
1221
  throw new Error(`project_id '${projectId}' is not registered`);
934
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
+ }
935
1282
  const limit = Math.min(Math.max(Number(input.limit || 10), 1), 50);
936
1283
  const queryLc = query.toLowerCase();
937
1284
  const queryTokens = Array.from(new Set(queryLc.split(/[^a-z0-9._/-]+/i).map((t) => t.trim()).filter(Boolean)));
@@ -946,17 +1293,65 @@ export class SlotDB {
946
1293
  }
947
1294
  return matched / queryTokens.length;
948
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('_');
949
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
+ };
950
1321
  let lineageContext = null;
951
- if (taskContextInput && (taskContextInput.task_id || taskContextInput.tracker_issue_key || taskContextInput.task_title)) {
952
- lineageContext = this.getTaskLineageContext(scopeUserId, scopeAgentId, {
953
- project_id: projectId,
954
- task_id: taskContextInput.task_id,
955
- tracker_issue_key: taskContextInput.tracker_issue_key,
956
- task_title: taskContextInput.task_title,
957
- include_parent_chain: taskContextInput.include_parent_chain,
958
- include_related: taskContextInput.include_related,
959
- });
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
+ }
960
1355
  }
961
1356
  const lexicalPathPrefix = this.normalizeStringArray(input.path_prefix).map((p) => this.normalizeRelativePath(p)).filter(Boolean);
962
1357
  const lexicalModules = new Set(this.normalizeStringArray(input.module).map((s) => s.toLowerCase()));
@@ -974,6 +1369,19 @@ export class SlotDB {
974
1369
  }
975
1370
  }
976
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();
977
1385
  const fileStmt = this.db.prepare(`SELECT * FROM file_index_state
978
1386
  WHERE scope_user_id = ? AND scope_agent_id = ? AND project_id = ? AND active = 1`);
979
1387
  const fileRows = fileStmt.all(scopeUserId, scopeAgentId, projectId);
@@ -998,10 +1406,28 @@ export class SlotDB {
998
1406
  score += tokenScore(text) * 0.25;
999
1407
  if (lineageContext && lineageContext.touched_files.includes(relativePath))
1000
1408
  score += 0.35;
1001
- 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/")) {
1002
1416
  score += 0.05;
1417
+ }
1418
+ if (looksIdentifierQuery) {
1419
+ score -= 0.12;
1420
+ }
1003
1421
  if (score <= 0.08)
1004
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
+ });
1005
1431
  results.push({
1006
1432
  source: "file_index_state",
1007
1433
  id: String(row.file_id),
@@ -1023,6 +1449,7 @@ export class SlotDB {
1023
1449
  const symbolName = String(row.symbol_name || "");
1024
1450
  const symbolKind = String(row.symbol_kind || "");
1025
1451
  const symbolFqn = String(row.symbol_fqn || "");
1452
+ symbolRowsById.set(String(row.symbol_id), row);
1026
1453
  if (lexicalPathPrefix.length > 0 && !lexicalPathPrefix.some((prefix) => relativePath.startsWith(prefix)))
1027
1454
  continue;
1028
1455
  if (lexicalModules.size > 0 && !moduleName)
@@ -1038,12 +1465,37 @@ export class SlotDB {
1038
1465
  if (text.includes(queryLc))
1039
1466
  score += 0.62;
1040
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
+ }
1041
1478
  if (lineageContext && lineageContext.touched_symbols.includes(symbolName))
1042
1479
  score += 0.3;
1043
1480
  if (lineageContext && lineageContext.touched_files.includes(relativePath))
1044
1481
  score += 0.12;
1482
+ if (looksCodeIntent && (relativePath.startsWith('src/') || relativePath.startsWith('tests/')))
1483
+ score += 0.08;
1484
+ if (looksIdentifierQuery)
1485
+ score += 0.18;
1045
1486
  if (score <= 0.08)
1046
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
+ });
1047
1499
  results.push({
1048
1500
  source: "symbol_registry",
1049
1501
  id: String(row.symbol_id),
@@ -1064,24 +1516,53 @@ export class SlotDB {
1064
1516
  const relativePath = String(row.relative_path || "");
1065
1517
  const chunkKind = String(row.chunk_kind || "");
1066
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 || '') : '';
1067
1522
  if (lexicalPathPrefix.length > 0 && !lexicalPathPrefix.some((prefix) => relativePath.startsWith(prefix)))
1068
1523
  continue;
1069
- const text = `${relativePath} ${chunkKind} ${symbolId || ""}`.toLowerCase();
1524
+ const text = `${relativePath} ${chunkKind} ${symbolId || ""} ${symbolName} ${symbolFqn}`.toLowerCase();
1070
1525
  let score = 0;
1071
1526
  if (text.includes(queryLc))
1072
1527
  score += 0.6;
1073
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
+ }
1074
1537
  if (lineageContext && lineageContext.touched_files.includes(relativePath))
1075
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;
1076
1547
  if (score <= 0.08)
1077
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
+ });
1078
1558
  results.push({
1079
1559
  source: "chunk_registry",
1080
1560
  id: String(row.chunk_id),
1081
1561
  score,
1082
1562
  project_id: projectId,
1083
1563
  relative_path: relativePath,
1084
- snippet: `chunk ${chunkKind} in ${relativePath}`,
1564
+ symbol_name: symbolName || undefined,
1565
+ snippet: symbolName ? `chunk ${chunkKind} for symbol ${symbolName} in ${relativePath}` : `chunk ${chunkKind} in ${relativePath}`,
1085
1566
  });
1086
1567
  }
1087
1568
  const taskStmt = this.db.prepare(`SELECT * FROM task_registry
@@ -1123,6 +1604,13 @@ export class SlotDB {
1123
1604
  }
1124
1605
  if (score <= 0.08)
1125
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
+ });
1126
1614
  results.push({
1127
1615
  source: "task_registry",
1128
1616
  id: task.task_id,
@@ -1141,9 +1629,208 @@ export class SlotDB {
1141
1629
  return {
1142
1630
  query,
1143
1631
  project_id: projectId,
1632
+ project_lifecycle_status: project.lifecycle_status,
1633
+ searchable: true,
1144
1634
  count: ranked.length,
1145
1635
  task_lineage_context: lineageContext,
1636
+ task_context_resolution: taskContextResolution,
1146
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,
1147
1834
  };
1148
1835
  }
1149
1836
  runLegacyCompatibilityBackfill(scopeUserId, scopeAgentId, input = {}) {