opencode-swarm 7.36.0 → 7.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -34,7 +34,7 @@ var package_default;
34
34
  var init_package = __esm(() => {
35
35
  package_default = {
36
36
  name: "opencode-swarm",
37
- version: "7.36.0",
37
+ version: "7.37.0",
38
38
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
39
39
  main: "dist/index.js",
40
40
  types: "dist/index.d.ts",
@@ -45714,6 +45714,102 @@ var init_curator_decision_helpers = __esm(() => {
45714
45714
  init_schema2();
45715
45715
  });
45716
45716
 
45717
+ // src/memory/role-profiles.ts
45718
+ function resolveMemoryRecallProfile(agentRole) {
45719
+ const role = normalizeMemoryAgentRole(agentRole);
45720
+ return MEMORY_RECALL_PROFILES[role] ?? MEMORY_RECALL_PROFILES.coder;
45721
+ }
45722
+ function normalizeMemoryAgentRole(agentRole) {
45723
+ const base = stripKnownSwarmPrefix(agentRole ?? "architect");
45724
+ if (base === "reviewer" || base === "test_engineer")
45725
+ return "qa";
45726
+ if (base === "critic" || base === "critic_sounding_board" || base === "critic_drift_verifier" || base === "critic_hallucination_verifier") {
45727
+ return "security";
45728
+ }
45729
+ if (base === "curator_init" || base === "curator_phase")
45730
+ return "curator";
45731
+ if (base === "docs")
45732
+ return "sme";
45733
+ if (base === "architect" || base === "sme" || base === "coder" || base === "security" || base === "curator") {
45734
+ return base;
45735
+ }
45736
+ return "coder";
45737
+ }
45738
+ var MEMORY_RECALL_PROFILES;
45739
+ var init_role_profiles = __esm(() => {
45740
+ init_schema();
45741
+ MEMORY_RECALL_PROFILES = {
45742
+ architect: {
45743
+ kinds: [
45744
+ "project_fact",
45745
+ "architecture_decision",
45746
+ "repo_convention",
45747
+ "failure_pattern",
45748
+ "security_note"
45749
+ ],
45750
+ maxItems: 10,
45751
+ tokenBudget: 1600
45752
+ },
45753
+ sme: {
45754
+ kinds: [
45755
+ "api_finding",
45756
+ "code_pattern",
45757
+ "repo_convention",
45758
+ "failure_pattern",
45759
+ "evidence"
45760
+ ],
45761
+ maxItems: 8,
45762
+ tokenBudget: 1200
45763
+ },
45764
+ coder: {
45765
+ kinds: [
45766
+ "architecture_decision",
45767
+ "repo_convention",
45768
+ "code_pattern",
45769
+ "test_pattern",
45770
+ "failure_pattern"
45771
+ ],
45772
+ maxItems: 8,
45773
+ tokenBudget: 1200
45774
+ },
45775
+ qa: {
45776
+ kinds: [
45777
+ "test_pattern",
45778
+ "failure_pattern",
45779
+ "repo_convention",
45780
+ "security_note"
45781
+ ],
45782
+ maxItems: 8,
45783
+ tokenBudget: 1200
45784
+ },
45785
+ security: {
45786
+ kinds: [
45787
+ "security_note",
45788
+ "architecture_decision",
45789
+ "repo_convention",
45790
+ "evidence"
45791
+ ],
45792
+ maxItems: 8,
45793
+ tokenBudget: 1200
45794
+ },
45795
+ curator: {
45796
+ kinds: [
45797
+ "project_fact",
45798
+ "architecture_decision",
45799
+ "repo_convention",
45800
+ "api_finding",
45801
+ "code_pattern",
45802
+ "test_pattern",
45803
+ "failure_pattern",
45804
+ "security_note",
45805
+ "evidence"
45806
+ ],
45807
+ maxItems: 20,
45808
+ tokenBudget: 3000
45809
+ }
45810
+ };
45811
+ });
45812
+
45717
45813
  // src/memory/scoring.ts
45718
45814
  function tokenize(text) {
45719
45815
  return new Set(text.toLowerCase().replace(/[^\w\s-]/g, " ").split(/\s+/).map((token) => token.trim()).filter(Boolean));
@@ -45767,13 +45863,16 @@ function kindProfileBoost(kind, request) {
45767
45863
  return 0.5;
45768
45864
  return request.kinds.includes(kind) ? 1 : 0;
45769
45865
  }
45866
+ function roleProfileBoost(kind, context) {
45867
+ return context.roleProfileKinds?.has(kind) ? 1 : 0;
45868
+ }
45770
45869
  function sameScope(a, b) {
45771
45870
  return stableScopeKey(a) === stableScopeKey(b);
45772
45871
  }
45773
45872
  function scopeAllowed(recordScope, allowedScopes) {
45774
45873
  return allowedScopes.some((scope) => sameScope(recordScope, scope));
45775
45874
  }
45776
- function scoreMemoryRecordDetailed(record3, request) {
45875
+ function scoreMemoryRecordDetailed(record3, request, context) {
45777
45876
  if (!request.includeExpired && isExpired(record3)) {
45778
45877
  return { item: null, skipReason: "filtered" };
45779
45878
  }
@@ -45788,7 +45887,7 @@ function scoreMemoryRecordDetailed(record3, request) {
45788
45887
  if (request.kinds && !request.kinds.includes(record3.kind)) {
45789
45888
  return { item: null, skipReason: "filtered" };
45790
45889
  }
45791
- const queryTokens = request.mode === "injection" && request.task ? tokenize(request.task) : tokenize(request.query);
45890
+ const queryTokens = request.mode === "injection" && context.taskTokens ? context.taskTokens : context.queryTokens;
45792
45891
  const textTokens = tokenize(record3.text);
45793
45892
  const tagTokens = tokenize(record3.tags.join(" "));
45794
45893
  const fileTokens = tokenize([
@@ -45801,24 +45900,31 @@ function scoreMemoryRecordDetailed(record3, request) {
45801
45900
  ])
45802
45901
  ].filter((value) => typeof value === "string").join(" "));
45803
45902
  const symbolTokens = tokenize(collectMetadataStrings(record3.metadata, ["symbol", "symbols"]).join(" "));
45804
- const kindQueryOverlap = overlap(queryTokens, tokenize(normalizeKindText(record3.kind)));
45903
+ const kindTokens = tokenize(normalizeKindText(record3.kind));
45904
+ const sourceRefTokens = tokenize(record3.source.ref ?? "");
45905
+ const taskSearchTokens = unionTokens(textTokens, tagTokens, fileTokens, symbolTokens, kindTokens, sourceRefTokens);
45906
+ const taskTermOverlap = context.taskTokens ? overlap(context.taskTokens, taskSearchTokens) : 0;
45907
+ const kindQueryOverlap = overlap(queryTokens, kindTokens);
45805
45908
  const textOverlap = overlap(queryTokens, textTokens);
45806
45909
  const tagOverlap = overlap(queryTokens, tagTokens);
45807
45910
  const fileOverlap = overlap(queryTokens, fileTokens);
45808
45911
  const symbolOverlap = overlap(queryTokens, symbolTokens);
45809
45912
  const kindMatch = request.kinds?.includes(record3.kind) ?? false;
45810
45913
  const scopeMatch = scopeAllowed(record3.scope, request.scopes);
45914
+ const roleBoost = roleProfileBoost(record3.kind, context);
45811
45915
  const hasQuerySignal = textOverlap > 0 || tagOverlap > 0 || fileOverlap > 0 || symbolOverlap > 0 || kindQueryOverlap > 0;
45812
45916
  if (request.mode === "injection" && request.requireQuerySignal !== false && !hasQuerySignal) {
45813
45917
  return { item: null, skipReason: "no_signal" };
45814
45918
  }
45815
- const score = textOverlap * 0.45 + tagOverlap * 0.2 + fileOverlap * 0.05 + symbolOverlap * 0.05 + scopeSpecificityBoost(record3.scope) * 0.15 + kindProfileBoost(record3.kind, request) * 0.1 + record3.confidence * 0.1;
45919
+ const score = textOverlap * 0.38 + tagOverlap * 0.16 + fileOverlap * 0.12 + symbolOverlap * 0.08 + taskTermOverlap * 0.08 + scopeSpecificityBoost(record3.scope) * 0.12 + kindProfileBoost(record3.kind, request) * 0.06 + roleBoost * 0.05 + record3.confidence * 0.08;
45816
45920
  const reasonParts = [
45817
45921
  textOverlap > 0 ? `text_overlap=${textOverlap.toFixed(2)}` : null,
45818
45922
  tagOverlap > 0 ? `tag_overlap=${tagOverlap.toFixed(2)}` : null,
45819
45923
  fileOverlap > 0 ? `file_overlap=${fileOverlap.toFixed(2)}` : null,
45820
45924
  symbolOverlap > 0 ? `symbol_overlap=${symbolOverlap.toFixed(2)}` : null,
45925
+ taskTermOverlap > 0 ? `task_terms=${taskTermOverlap.toFixed(2)}` : null,
45821
45926
  kindQueryOverlap > 0 ? `kind_query=${kindQueryOverlap.toFixed(2)}` : null,
45927
+ roleBoost > 0 ? "role_profile" : null,
45822
45928
  `scope=${record3.scope.type}`,
45823
45929
  `confidence=${record3.confidence.toFixed(2)}`
45824
45930
  ].filter(Boolean);
@@ -45840,6 +45946,7 @@ function scoreMemoryRecordDetailed(record3, request) {
45840
45946
  }
45841
45947
  function scoreMemoryRecordsWithDiagnostics(records, request) {
45842
45948
  const minScore = request.minScore ?? 0;
45949
+ const context = createScoringContext(request);
45843
45950
  const diagnostics = {
45844
45951
  candidateCount: records.length,
45845
45952
  preScoredFilteredCount: 0,
@@ -45850,7 +45957,7 @@ function scoreMemoryRecordsWithDiagnostics(records, request) {
45850
45957
  };
45851
45958
  const items = [];
45852
45959
  for (const record3 of records) {
45853
- const result = scoreMemoryRecordDetailed(record3, request);
45960
+ const result = scoreMemoryRecordDetailed(record3, request, context);
45854
45961
  if (!result.item) {
45855
45962
  if (result.skipReason === "filtered")
45856
45963
  diagnostics.preScoredFilteredCount++;
@@ -45869,7 +45976,24 @@ function scoreMemoryRecordsWithDiagnostics(records, request) {
45869
45976
  diagnostics.returnedCount = items.length;
45870
45977
  return { items, diagnostics };
45871
45978
  }
45979
+ function createScoringContext(request) {
45980
+ const taskTokens = request.task ? tokenize(request.task) : undefined;
45981
+ return {
45982
+ taskTokens,
45983
+ queryTokens: tokenize(request.query),
45984
+ roleProfileKinds: request.agentRole ? new Set(resolveMemoryRecallProfile(request.agentRole).kinds) : undefined
45985
+ };
45986
+ }
45987
+ function unionTokens(...sets) {
45988
+ const union3 = new Set;
45989
+ for (const set3 of sets) {
45990
+ for (const token of set3)
45991
+ union3.add(token);
45992
+ }
45993
+ return union3;
45994
+ }
45872
45995
  var init_scoring = __esm(() => {
45996
+ init_role_profiles();
45873
45997
  init_schema2();
45874
45998
  });
45875
45999
 
@@ -46444,6 +46568,7 @@ class SQLiteMemoryProvider {
46444
46568
  config;
46445
46569
  initialized = false;
46446
46570
  db = null;
46571
+ ftsAvailable = false;
46447
46572
  memories = new Map;
46448
46573
  proposals = new Map;
46449
46574
  lastAutomaticJsonlMigration = null;
@@ -46491,6 +46616,7 @@ class SQLiteMemoryProvider {
46491
46616
  this.db.run(`PRAGMA busy_timeout = ${busyTimeoutMs};`);
46492
46617
  this.db.run("PRAGMA foreign_keys = ON;");
46493
46618
  this.runMigrations();
46619
+ this.ftsAvailable = this.initializeFtsIndex();
46494
46620
  this.lastAutomaticJsonlMigration = null;
46495
46621
  await this.migrateLegacyJsonlIfNeeded();
46496
46622
  const memoryLoad = this.loadMemories();
@@ -46532,6 +46658,7 @@ class SQLiteMemoryProvider {
46532
46658
  if (this.config.hardDelete) {
46533
46659
  this.memories.delete(id);
46534
46660
  this.requireDb().run("DELETE FROM memory_items WHERE id = ?", [id]);
46661
+ this.deleteMemoryFts(id);
46535
46662
  } else {
46536
46663
  const tombstone = {
46537
46664
  ...existing,
@@ -46548,17 +46675,19 @@ class SQLiteMemoryProvider {
46548
46675
  }
46549
46676
  async recallWithDiagnostics(request) {
46550
46677
  await this.initialize();
46551
- const records = await this.list({
46678
+ const scopedRecords = await this.list({
46552
46679
  scopes: request.scopes,
46553
46680
  kinds: request.kinds,
46554
46681
  includeExpired: request.includeExpired
46555
46682
  });
46556
- const result = scoreMemoryRecordsWithDiagnostics(records, request);
46683
+ const candidates = this.selectRecallCandidates(request, scopedRecords);
46684
+ const result = scoreMemoryRecordsWithDiagnostics(candidates.records, request);
46685
+ const reranked = candidates.ftsOrder ? rerankWithFts(result.items, candidates.ftsOrder) : result.items;
46557
46686
  return {
46558
- items: result.items.slice(0, request.maxItems),
46687
+ items: reranked.slice(0, request.maxItems),
46559
46688
  diagnostics: {
46560
46689
  ...result.diagnostics,
46561
- returnedCount: Math.min(result.diagnostics.returnedCount, request.maxItems)
46690
+ returnedCount: Math.min(reranked.length, request.maxItems)
46562
46691
  }
46563
46692
  };
46564
46693
  }
@@ -46648,6 +46777,7 @@ class SQLiteMemoryProvider {
46648
46777
  return;
46649
46778
  this.db.close();
46650
46779
  this.db = null;
46780
+ this.ftsAvailable = false;
46651
46781
  this.initialized = false;
46652
46782
  this.lastAutomaticJsonlMigration = null;
46653
46783
  }
@@ -46677,6 +46807,38 @@ class SQLiteMemoryProvider {
46677
46807
  markMigration(version4, name) {
46678
46808
  this.requireDb().run("INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (?, ?)", [version4, name]);
46679
46809
  }
46810
+ selectRecallCandidates(request, scopedRecords) {
46811
+ const ftsQuery = buildFtsQuery(request);
46812
+ if (!this.ftsAvailable || !ftsQuery) {
46813
+ return { records: scopedRecords, usedFts: false };
46814
+ }
46815
+ const scopedIds = new Set(scopedRecords.map((record3) => record3.id));
46816
+ if (scopedIds.size === 0) {
46817
+ return { records: [], usedFts: true, ftsOrder: new Map };
46818
+ }
46819
+ try {
46820
+ const rows = this.requireDb().query(`SELECT id, bm25(${FTS_TABLE_NAME}) AS rank
46821
+ FROM ${FTS_TABLE_NAME}
46822
+ WHERE ${FTS_TABLE_NAME} MATCH ?
46823
+ AND id IN (SELECT value FROM json_each(?))
46824
+ ORDER BY rank ASC
46825
+ LIMIT ?`).all(ftsQuery, JSON.stringify(Array.from(scopedIds)), Math.max(100, request.maxItems * 20));
46826
+ const ftsOrder = new Map;
46827
+ for (const row of rows) {
46828
+ if (!scopedIds.has(row.id))
46829
+ continue;
46830
+ ftsOrder.set(row.id, ftsOrder.size);
46831
+ }
46832
+ if (ftsOrder.size === 0 && (request.mode ?? "manual") === "manual") {
46833
+ return { records: scopedRecords, usedFts: false };
46834
+ }
46835
+ const records = scopedRecords.filter((record3) => ftsOrder.has(record3.id));
46836
+ return { records, usedFts: true, ftsOrder };
46837
+ } catch {
46838
+ this.ftsAvailable = false;
46839
+ return { records: scopedRecords, usedFts: false };
46840
+ }
46841
+ }
46680
46842
  runMigrations() {
46681
46843
  const db = this.requireDb();
46682
46844
  db.run(`CREATE TABLE IF NOT EXISTS schema_migrations (
@@ -46702,16 +46864,82 @@ class SQLiteMemoryProvider {
46702
46864
  apply();
46703
46865
  }
46704
46866
  }
46867
+ initializeFtsIndex() {
46868
+ const db = this.requireDb();
46869
+ try {
46870
+ if (!this.hasMigration(FTS_SCHEMA_MIGRATION_NAME)) {
46871
+ this.recreateFtsIndex();
46872
+ this.markMigration(FTS_SCHEMA_MIGRATION_VERSION, FTS_SCHEMA_MIGRATION_NAME);
46873
+ this.insertEvent("migration", String(FTS_SCHEMA_MIGRATION_VERSION), FTS_SCHEMA_MIGRATION_NAME);
46874
+ } else {
46875
+ db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS ${FTS_TABLE_NAME} USING fts5(
46876
+ ${ftsCreateColumnsSql()}
46877
+ )`);
46878
+ }
46879
+ this.ftsAvailable = true;
46880
+ const validMemoryCount = this.countValidMemoryRows();
46881
+ const ftsCount = db.query(`SELECT COUNT(*) AS count FROM ${FTS_TABLE_NAME}`).get()?.count ?? 0;
46882
+ if (validMemoryCount !== ftsCount) {
46883
+ this.rebuildFtsIndex();
46884
+ }
46885
+ return true;
46886
+ } catch {
46887
+ this.ftsAvailable = false;
46888
+ return false;
46889
+ }
46890
+ }
46891
+ recreateFtsIndex() {
46892
+ const db = this.requireDb();
46893
+ const recreate = db.transaction(() => {
46894
+ db.run(`DROP TABLE IF EXISTS ${FTS_TABLE_NAME}`);
46895
+ db.run(`CREATE VIRTUAL TABLE ${FTS_TABLE_NAME} USING fts5(
46896
+ ${ftsCreateColumnsSql()}
46897
+ )`);
46898
+ });
46899
+ recreate();
46900
+ }
46901
+ rebuildFtsIndex() {
46902
+ const db = this.requireDb();
46903
+ const rebuild = db.transaction(() => {
46904
+ db.run(`DELETE FROM ${FTS_TABLE_NAME}`);
46905
+ for (const row of this.iterateMemoryRows()) {
46906
+ const record3 = this.parseMemoryRow(row);
46907
+ if (record3) {
46908
+ this.writeMemoryFts(record3);
46909
+ }
46910
+ }
46911
+ });
46912
+ rebuild();
46913
+ }
46914
+ countValidMemoryRows() {
46915
+ let count = 0;
46916
+ for (const row of this.iterateMemoryRows()) {
46917
+ if (this.parseMemoryRow(row))
46918
+ count++;
46919
+ }
46920
+ return count;
46921
+ }
46922
+ *iterateMemoryRows() {
46923
+ yield* this.requireDb().query("SELECT id, record_json FROM memory_items").iterate();
46924
+ }
46925
+ parseMemoryRow(row) {
46926
+ try {
46927
+ return validateMemoryRecordRules(JSON.parse(row.record_json), {
46928
+ rejectDurableSecrets: this.config.redaction.rejectDurableSecrets
46929
+ });
46930
+ } catch {
46931
+ return null;
46932
+ }
46933
+ }
46705
46934
  loadMemories() {
46706
46935
  const rows = this.requireDb().query("SELECT id, record_json FROM memory_items ORDER BY updated_at ASC").all();
46707
46936
  const records = [];
46708
46937
  let invalidCount = 0;
46709
46938
  for (const row of rows) {
46710
- try {
46711
- records.push(validateMemoryRecordRules(JSON.parse(row.record_json), {
46712
- rejectDurableSecrets: this.config.redaction.rejectDurableSecrets
46713
- }));
46714
- } catch {
46939
+ const record3 = this.parseMemoryRow(row);
46940
+ if (record3) {
46941
+ records.push(record3);
46942
+ } else {
46715
46943
  invalidCount++;
46716
46944
  }
46717
46945
  }
@@ -46756,6 +46984,29 @@ class SQLiteMemoryProvider {
46756
46984
  record3.metadata.deleted === true ? 1 : 0,
46757
46985
  JSON.stringify(record3)
46758
46986
  ]);
46987
+ this.writeMemoryFts(record3);
46988
+ }
46989
+ writeMemoryFts(record3) {
46990
+ if (!this.ftsAvailable)
46991
+ return;
46992
+ try {
46993
+ const db = this.requireDb();
46994
+ db.run(`DELETE FROM ${FTS_TABLE_NAME} WHERE id = ?`, [record3.id]);
46995
+ db.run(`INSERT INTO ${FTS_TABLE_NAME} (
46996
+ ${FTS_INSERT_COLUMNS.join(", ")}
46997
+ ) VALUES (${FTS_INSERT_COLUMNS.map(() => "?").join(", ")})`, [record3.id, ...ftsColumnValues(record3)]);
46998
+ } catch {
46999
+ this.ftsAvailable = false;
47000
+ }
47001
+ }
47002
+ deleteMemoryFts(id) {
47003
+ if (!this.ftsAvailable)
47004
+ return;
47005
+ try {
47006
+ this.requireDb().run(`DELETE FROM ${FTS_TABLE_NAME} WHERE id = ?`, [id]);
47007
+ } catch {
47008
+ this.ftsAvailable = false;
47009
+ }
46759
47010
  }
46760
47011
  writeProposal(proposal) {
46761
47012
  this.requireDb().run(`INSERT OR REPLACE INTO memory_proposals (
@@ -46956,7 +47207,69 @@ class SQLiteMemoryProvider {
46956
47207
  function splitSql(sql) {
46957
47208
  return sql.split(";").map((statement) => statement.trim()).filter(Boolean);
46958
47209
  }
46959
- var _DatabaseCtor2 = null, MIGRATIONS2;
47210
+ function buildFtsQuery(request) {
47211
+ const text = request.mode === "injection" && request.task ? `${request.task}
47212
+ ${request.query}` : `${request.query}
47213
+ ${request.task ?? ""}`;
47214
+ const terms = Array.from(extractFtsTerms(text)).slice(0, 40);
47215
+ if (terms.length === 0)
47216
+ return null;
47217
+ return terms.map((term) => `"${term}"`).join(" OR ");
47218
+ }
47219
+ function extractFtsTerms(text) {
47220
+ const terms = new Set;
47221
+ for (const match of text.toLowerCase().matchAll(/[a-z0-9_]{2,}/g)) {
47222
+ const term = match[0];
47223
+ if (FTS_STOP_WORDS.has(term))
47224
+ continue;
47225
+ if (term.length < 3 && !/^\d+$/.test(term))
47226
+ continue;
47227
+ terms.add(term);
47228
+ }
47229
+ return terms;
47230
+ }
47231
+ function ftsCreateColumnsSql() {
47232
+ return [
47233
+ "id UNINDEXED",
47234
+ ...FTS_INDEX_COLUMNS.map((column) => column.name)
47235
+ ].join(`,
47236
+ `);
47237
+ }
47238
+ function ftsColumnValues(record3) {
47239
+ return FTS_INDEX_COLUMNS.map((column) => column.value(record3));
47240
+ }
47241
+ function collectMetadataSearchStrings(metadata, keys) {
47242
+ const values = [];
47243
+ for (const key of keys) {
47244
+ const value = metadata[key];
47245
+ if (typeof value === "string") {
47246
+ values.push(value);
47247
+ continue;
47248
+ }
47249
+ if (!Array.isArray(value))
47250
+ continue;
47251
+ for (const item of value) {
47252
+ if (typeof item === "string")
47253
+ values.push(item);
47254
+ }
47255
+ }
47256
+ return values;
47257
+ }
47258
+ function rerankWithFts(items, ftsOrder) {
47259
+ const denominator = Math.max(ftsOrder.size, 1);
47260
+ return items.map((item) => {
47261
+ const order = ftsOrder.get(item.record.id);
47262
+ if (order === undefined)
47263
+ return item;
47264
+ const ftsBoost = (denominator - order) / denominator * 0.08;
47265
+ return {
47266
+ ...item,
47267
+ score: item.score + ftsBoost,
47268
+ reason: `${item.reason}, fts_rank=${order + 1}`
47269
+ };
47270
+ }).sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
47271
+ }
47272
+ var _DatabaseCtor2 = null, FTS_SCHEMA_MIGRATION_VERSION = 3, FTS_SCHEMA_MIGRATION_NAME = "create_memory_fts5_shadow_index", FTS_TABLE_NAME = "memory_items_fts", FTS_INDEX_COLUMNS, FTS_INSERT_COLUMNS, MIGRATIONS2, FTS_STOP_WORDS;
46960
47273
  var init_sqlite_provider = __esm(() => {
46961
47274
  init_utils2();
46962
47275
  init_config3();
@@ -46965,6 +47278,45 @@ var init_sqlite_provider = __esm(() => {
46965
47278
  init_jsonl_migration();
46966
47279
  init_schema2();
46967
47280
  init_scoring();
47281
+ FTS_INDEX_COLUMNS = [
47282
+ {
47283
+ name: "text",
47284
+ value: (record3) => record3.text
47285
+ },
47286
+ {
47287
+ name: "tags",
47288
+ value: (record3) => record3.tags.join(" ")
47289
+ },
47290
+ {
47291
+ name: "kind",
47292
+ value: (record3) => record3.kind.replace(/_/g, " ")
47293
+ },
47294
+ {
47295
+ name: "source_file_path",
47296
+ value: (record3) => record3.source.filePath ?? ""
47297
+ },
47298
+ {
47299
+ name: "source_ref",
47300
+ value: (record3) => record3.source.ref ?? ""
47301
+ },
47302
+ {
47303
+ name: "metadata_symbols",
47304
+ value: (record3) => collectMetadataSearchStrings(record3.metadata, ["symbol", "symbols"]).join(" ")
47305
+ },
47306
+ {
47307
+ name: "metadata_files",
47308
+ value: (record3) => collectMetadataSearchStrings(record3.metadata, [
47309
+ "file",
47310
+ "filePath",
47311
+ "files",
47312
+ "touchedFiles"
47313
+ ]).join(" ")
47314
+ }
47315
+ ];
47316
+ FTS_INSERT_COLUMNS = [
47317
+ "id",
47318
+ ...FTS_INDEX_COLUMNS.map((column) => column.name)
47319
+ ];
46968
47320
  MIGRATIONS2 = [
46969
47321
  {
46970
47322
  version: 1,
@@ -47014,6 +47366,37 @@ var init_sqlite_provider = __esm(() => {
47014
47366
  `
47015
47367
  }
47016
47368
  ];
47369
+ FTS_STOP_WORDS = new Set([
47370
+ "a",
47371
+ "an",
47372
+ "and",
47373
+ "are",
47374
+ "as",
47375
+ "at",
47376
+ "be",
47377
+ "by",
47378
+ "for",
47379
+ "from",
47380
+ "goal",
47381
+ "how",
47382
+ "in",
47383
+ "into",
47384
+ "is",
47385
+ "it",
47386
+ "of",
47387
+ "on",
47388
+ "or",
47389
+ "role",
47390
+ "task",
47391
+ "that",
47392
+ "the",
47393
+ "this",
47394
+ "to",
47395
+ "user",
47396
+ "what",
47397
+ "when",
47398
+ "with"
47399
+ ]);
47017
47400
  });
47018
47401
 
47019
47402
  // src/memory/gateway.ts
@@ -47064,11 +47447,6 @@ var init_agent_output_schema = __esm(() => {
47064
47447
  }).passthrough();
47065
47448
  });
47066
47449
 
47067
- // src/memory/role-profiles.ts
47068
- var init_role_profiles = __esm(() => {
47069
- init_schema();
47070
- });
47071
-
47072
47450
  // src/memory/recall-planner.ts
47073
47451
  var init_recall_planner = __esm(() => {
47074
47452
  init_role_profiles();
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ var package_default;
48
48
  var init_package = __esm(() => {
49
49
  package_default = {
50
50
  name: "opencode-swarm",
51
- version: "7.36.0",
51
+ version: "7.37.0",
52
52
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
53
53
  main: "dist/index.js",
54
54
  types: "dist/index.d.ts",
@@ -67230,6 +67230,102 @@ var init_curator_decision_helpers = __esm(() => {
67230
67230
  init_schema2();
67231
67231
  });
67232
67232
 
67233
+ // src/memory/role-profiles.ts
67234
+ function resolveMemoryRecallProfile(agentRole) {
67235
+ const role = normalizeMemoryAgentRole(agentRole);
67236
+ return MEMORY_RECALL_PROFILES[role] ?? MEMORY_RECALL_PROFILES.coder;
67237
+ }
67238
+ function normalizeMemoryAgentRole(agentRole) {
67239
+ const base = stripKnownSwarmPrefix(agentRole ?? "architect");
67240
+ if (base === "reviewer" || base === "test_engineer")
67241
+ return "qa";
67242
+ if (base === "critic" || base === "critic_sounding_board" || base === "critic_drift_verifier" || base === "critic_hallucination_verifier") {
67243
+ return "security";
67244
+ }
67245
+ if (base === "curator_init" || base === "curator_phase")
67246
+ return "curator";
67247
+ if (base === "docs")
67248
+ return "sme";
67249
+ if (base === "architect" || base === "sme" || base === "coder" || base === "security" || base === "curator") {
67250
+ return base;
67251
+ }
67252
+ return "coder";
67253
+ }
67254
+ var MEMORY_RECALL_PROFILES;
67255
+ var init_role_profiles = __esm(() => {
67256
+ init_schema();
67257
+ MEMORY_RECALL_PROFILES = {
67258
+ architect: {
67259
+ kinds: [
67260
+ "project_fact",
67261
+ "architecture_decision",
67262
+ "repo_convention",
67263
+ "failure_pattern",
67264
+ "security_note"
67265
+ ],
67266
+ maxItems: 10,
67267
+ tokenBudget: 1600
67268
+ },
67269
+ sme: {
67270
+ kinds: [
67271
+ "api_finding",
67272
+ "code_pattern",
67273
+ "repo_convention",
67274
+ "failure_pattern",
67275
+ "evidence"
67276
+ ],
67277
+ maxItems: 8,
67278
+ tokenBudget: 1200
67279
+ },
67280
+ coder: {
67281
+ kinds: [
67282
+ "architecture_decision",
67283
+ "repo_convention",
67284
+ "code_pattern",
67285
+ "test_pattern",
67286
+ "failure_pattern"
67287
+ ],
67288
+ maxItems: 8,
67289
+ tokenBudget: 1200
67290
+ },
67291
+ qa: {
67292
+ kinds: [
67293
+ "test_pattern",
67294
+ "failure_pattern",
67295
+ "repo_convention",
67296
+ "security_note"
67297
+ ],
67298
+ maxItems: 8,
67299
+ tokenBudget: 1200
67300
+ },
67301
+ security: {
67302
+ kinds: [
67303
+ "security_note",
67304
+ "architecture_decision",
67305
+ "repo_convention",
67306
+ "evidence"
67307
+ ],
67308
+ maxItems: 8,
67309
+ tokenBudget: 1200
67310
+ },
67311
+ curator: {
67312
+ kinds: [
67313
+ "project_fact",
67314
+ "architecture_decision",
67315
+ "repo_convention",
67316
+ "api_finding",
67317
+ "code_pattern",
67318
+ "test_pattern",
67319
+ "failure_pattern",
67320
+ "security_note",
67321
+ "evidence"
67322
+ ],
67323
+ maxItems: 20,
67324
+ tokenBudget: 3000
67325
+ }
67326
+ };
67327
+ });
67328
+
67233
67329
  // src/memory/scoring.ts
67234
67330
  function tokenize(text) {
67235
67331
  return new Set(text.toLowerCase().replace(/[^\w\s-]/g, " ").split(/\s+/).map((token) => token.trim()).filter(Boolean));
@@ -67283,13 +67379,16 @@ function kindProfileBoost(kind, request) {
67283
67379
  return 0.5;
67284
67380
  return request.kinds.includes(kind) ? 1 : 0;
67285
67381
  }
67382
+ function roleProfileBoost(kind, context) {
67383
+ return context.roleProfileKinds?.has(kind) ? 1 : 0;
67384
+ }
67286
67385
  function sameScope(a, b) {
67287
67386
  return stableScopeKey(a) === stableScopeKey(b);
67288
67387
  }
67289
67388
  function scopeAllowed(recordScope, allowedScopes) {
67290
67389
  return allowedScopes.some((scope) => sameScope(recordScope, scope));
67291
67390
  }
67292
- function scoreMemoryRecordDetailed(record3, request) {
67391
+ function scoreMemoryRecordDetailed(record3, request, context) {
67293
67392
  if (!request.includeExpired && isExpired(record3)) {
67294
67393
  return { item: null, skipReason: "filtered" };
67295
67394
  }
@@ -67304,7 +67403,7 @@ function scoreMemoryRecordDetailed(record3, request) {
67304
67403
  if (request.kinds && !request.kinds.includes(record3.kind)) {
67305
67404
  return { item: null, skipReason: "filtered" };
67306
67405
  }
67307
- const queryTokens = request.mode === "injection" && request.task ? tokenize(request.task) : tokenize(request.query);
67406
+ const queryTokens = request.mode === "injection" && context.taskTokens ? context.taskTokens : context.queryTokens;
67308
67407
  const textTokens = tokenize(record3.text);
67309
67408
  const tagTokens = tokenize(record3.tags.join(" "));
67310
67409
  const fileTokens = tokenize([
@@ -67317,24 +67416,31 @@ function scoreMemoryRecordDetailed(record3, request) {
67317
67416
  ])
67318
67417
  ].filter((value) => typeof value === "string").join(" "));
67319
67418
  const symbolTokens = tokenize(collectMetadataStrings(record3.metadata, ["symbol", "symbols"]).join(" "));
67320
- const kindQueryOverlap = overlap(queryTokens, tokenize(normalizeKindText(record3.kind)));
67419
+ const kindTokens = tokenize(normalizeKindText(record3.kind));
67420
+ const sourceRefTokens = tokenize(record3.source.ref ?? "");
67421
+ const taskSearchTokens = unionTokens(textTokens, tagTokens, fileTokens, symbolTokens, kindTokens, sourceRefTokens);
67422
+ const taskTermOverlap = context.taskTokens ? overlap(context.taskTokens, taskSearchTokens) : 0;
67423
+ const kindQueryOverlap = overlap(queryTokens, kindTokens);
67321
67424
  const textOverlap = overlap(queryTokens, textTokens);
67322
67425
  const tagOverlap = overlap(queryTokens, tagTokens);
67323
67426
  const fileOverlap = overlap(queryTokens, fileTokens);
67324
67427
  const symbolOverlap = overlap(queryTokens, symbolTokens);
67325
67428
  const kindMatch = request.kinds?.includes(record3.kind) ?? false;
67326
67429
  const scopeMatch = scopeAllowed(record3.scope, request.scopes);
67430
+ const roleBoost = roleProfileBoost(record3.kind, context);
67327
67431
  const hasQuerySignal = textOverlap > 0 || tagOverlap > 0 || fileOverlap > 0 || symbolOverlap > 0 || kindQueryOverlap > 0;
67328
67432
  if (request.mode === "injection" && request.requireQuerySignal !== false && !hasQuerySignal) {
67329
67433
  return { item: null, skipReason: "no_signal" };
67330
67434
  }
67331
- const score = textOverlap * 0.45 + tagOverlap * 0.2 + fileOverlap * 0.05 + symbolOverlap * 0.05 + scopeSpecificityBoost(record3.scope) * 0.15 + kindProfileBoost(record3.kind, request) * 0.1 + record3.confidence * 0.1;
67435
+ const score = textOverlap * 0.38 + tagOverlap * 0.16 + fileOverlap * 0.12 + symbolOverlap * 0.08 + taskTermOverlap * 0.08 + scopeSpecificityBoost(record3.scope) * 0.12 + kindProfileBoost(record3.kind, request) * 0.06 + roleBoost * 0.05 + record3.confidence * 0.08;
67332
67436
  const reasonParts = [
67333
67437
  textOverlap > 0 ? `text_overlap=${textOverlap.toFixed(2)}` : null,
67334
67438
  tagOverlap > 0 ? `tag_overlap=${tagOverlap.toFixed(2)}` : null,
67335
67439
  fileOverlap > 0 ? `file_overlap=${fileOverlap.toFixed(2)}` : null,
67336
67440
  symbolOverlap > 0 ? `symbol_overlap=${symbolOverlap.toFixed(2)}` : null,
67441
+ taskTermOverlap > 0 ? `task_terms=${taskTermOverlap.toFixed(2)}` : null,
67337
67442
  kindQueryOverlap > 0 ? `kind_query=${kindQueryOverlap.toFixed(2)}` : null,
67443
+ roleBoost > 0 ? "role_profile" : null,
67338
67444
  `scope=${record3.scope.type}`,
67339
67445
  `confidence=${record3.confidence.toFixed(2)}`
67340
67446
  ].filter(Boolean);
@@ -67356,6 +67462,7 @@ function scoreMemoryRecordDetailed(record3, request) {
67356
67462
  }
67357
67463
  function scoreMemoryRecordsWithDiagnostics(records, request) {
67358
67464
  const minScore = request.minScore ?? 0;
67465
+ const context = createScoringContext(request);
67359
67466
  const diagnostics = {
67360
67467
  candidateCount: records.length,
67361
67468
  preScoredFilteredCount: 0,
@@ -67366,7 +67473,7 @@ function scoreMemoryRecordsWithDiagnostics(records, request) {
67366
67473
  };
67367
67474
  const items = [];
67368
67475
  for (const record3 of records) {
67369
- const result = scoreMemoryRecordDetailed(record3, request);
67476
+ const result = scoreMemoryRecordDetailed(record3, request, context);
67370
67477
  if (!result.item) {
67371
67478
  if (result.skipReason === "filtered")
67372
67479
  diagnostics.preScoredFilteredCount++;
@@ -67385,7 +67492,24 @@ function scoreMemoryRecordsWithDiagnostics(records, request) {
67385
67492
  diagnostics.returnedCount = items.length;
67386
67493
  return { items, diagnostics };
67387
67494
  }
67495
+ function createScoringContext(request) {
67496
+ const taskTokens = request.task ? tokenize(request.task) : undefined;
67497
+ return {
67498
+ taskTokens,
67499
+ queryTokens: tokenize(request.query),
67500
+ roleProfileKinds: request.agentRole ? new Set(resolveMemoryRecallProfile(request.agentRole).kinds) : undefined
67501
+ };
67502
+ }
67503
+ function unionTokens(...sets) {
67504
+ const union3 = new Set;
67505
+ for (const set3 of sets) {
67506
+ for (const token of set3)
67507
+ union3.add(token);
67508
+ }
67509
+ return union3;
67510
+ }
67388
67511
  var init_scoring = __esm(() => {
67512
+ init_role_profiles();
67389
67513
  init_schema2();
67390
67514
  });
67391
67515
 
@@ -68034,6 +68158,7 @@ class SQLiteMemoryProvider {
68034
68158
  config;
68035
68159
  initialized = false;
68036
68160
  db = null;
68161
+ ftsAvailable = false;
68037
68162
  memories = new Map;
68038
68163
  proposals = new Map;
68039
68164
  lastAutomaticJsonlMigration = null;
@@ -68081,6 +68206,7 @@ class SQLiteMemoryProvider {
68081
68206
  this.db.run(`PRAGMA busy_timeout = ${busyTimeoutMs};`);
68082
68207
  this.db.run("PRAGMA foreign_keys = ON;");
68083
68208
  this.runMigrations();
68209
+ this.ftsAvailable = this.initializeFtsIndex();
68084
68210
  this.lastAutomaticJsonlMigration = null;
68085
68211
  await this.migrateLegacyJsonlIfNeeded();
68086
68212
  const memoryLoad = this.loadMemories();
@@ -68122,6 +68248,7 @@ class SQLiteMemoryProvider {
68122
68248
  if (this.config.hardDelete) {
68123
68249
  this.memories.delete(id);
68124
68250
  this.requireDb().run("DELETE FROM memory_items WHERE id = ?", [id]);
68251
+ this.deleteMemoryFts(id);
68125
68252
  } else {
68126
68253
  const tombstone = {
68127
68254
  ...existing,
@@ -68138,17 +68265,19 @@ class SQLiteMemoryProvider {
68138
68265
  }
68139
68266
  async recallWithDiagnostics(request) {
68140
68267
  await this.initialize();
68141
- const records = await this.list({
68268
+ const scopedRecords = await this.list({
68142
68269
  scopes: request.scopes,
68143
68270
  kinds: request.kinds,
68144
68271
  includeExpired: request.includeExpired
68145
68272
  });
68146
- const result = scoreMemoryRecordsWithDiagnostics(records, request);
68273
+ const candidates = this.selectRecallCandidates(request, scopedRecords);
68274
+ const result = scoreMemoryRecordsWithDiagnostics(candidates.records, request);
68275
+ const reranked = candidates.ftsOrder ? rerankWithFts(result.items, candidates.ftsOrder) : result.items;
68147
68276
  return {
68148
- items: result.items.slice(0, request.maxItems),
68277
+ items: reranked.slice(0, request.maxItems),
68149
68278
  diagnostics: {
68150
68279
  ...result.diagnostics,
68151
- returnedCount: Math.min(result.diagnostics.returnedCount, request.maxItems)
68280
+ returnedCount: Math.min(reranked.length, request.maxItems)
68152
68281
  }
68153
68282
  };
68154
68283
  }
@@ -68238,6 +68367,7 @@ class SQLiteMemoryProvider {
68238
68367
  return;
68239
68368
  this.db.close();
68240
68369
  this.db = null;
68370
+ this.ftsAvailable = false;
68241
68371
  this.initialized = false;
68242
68372
  this.lastAutomaticJsonlMigration = null;
68243
68373
  }
@@ -68267,6 +68397,38 @@ class SQLiteMemoryProvider {
68267
68397
  markMigration(version4, name2) {
68268
68398
  this.requireDb().run("INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (?, ?)", [version4, name2]);
68269
68399
  }
68400
+ selectRecallCandidates(request, scopedRecords) {
68401
+ const ftsQuery = buildFtsQuery(request);
68402
+ if (!this.ftsAvailable || !ftsQuery) {
68403
+ return { records: scopedRecords, usedFts: false };
68404
+ }
68405
+ const scopedIds = new Set(scopedRecords.map((record3) => record3.id));
68406
+ if (scopedIds.size === 0) {
68407
+ return { records: [], usedFts: true, ftsOrder: new Map };
68408
+ }
68409
+ try {
68410
+ const rows = this.requireDb().query(`SELECT id, bm25(${FTS_TABLE_NAME}) AS rank
68411
+ FROM ${FTS_TABLE_NAME}
68412
+ WHERE ${FTS_TABLE_NAME} MATCH ?
68413
+ AND id IN (SELECT value FROM json_each(?))
68414
+ ORDER BY rank ASC
68415
+ LIMIT ?`).all(ftsQuery, JSON.stringify(Array.from(scopedIds)), Math.max(100, request.maxItems * 20));
68416
+ const ftsOrder = new Map;
68417
+ for (const row of rows) {
68418
+ if (!scopedIds.has(row.id))
68419
+ continue;
68420
+ ftsOrder.set(row.id, ftsOrder.size);
68421
+ }
68422
+ if (ftsOrder.size === 0 && (request.mode ?? "manual") === "manual") {
68423
+ return { records: scopedRecords, usedFts: false };
68424
+ }
68425
+ const records = scopedRecords.filter((record3) => ftsOrder.has(record3.id));
68426
+ return { records, usedFts: true, ftsOrder };
68427
+ } catch {
68428
+ this.ftsAvailable = false;
68429
+ return { records: scopedRecords, usedFts: false };
68430
+ }
68431
+ }
68270
68432
  runMigrations() {
68271
68433
  const db = this.requireDb();
68272
68434
  db.run(`CREATE TABLE IF NOT EXISTS schema_migrations (
@@ -68292,16 +68454,82 @@ class SQLiteMemoryProvider {
68292
68454
  apply();
68293
68455
  }
68294
68456
  }
68457
+ initializeFtsIndex() {
68458
+ const db = this.requireDb();
68459
+ try {
68460
+ if (!this.hasMigration(FTS_SCHEMA_MIGRATION_NAME)) {
68461
+ this.recreateFtsIndex();
68462
+ this.markMigration(FTS_SCHEMA_MIGRATION_VERSION, FTS_SCHEMA_MIGRATION_NAME);
68463
+ this.insertEvent("migration", String(FTS_SCHEMA_MIGRATION_VERSION), FTS_SCHEMA_MIGRATION_NAME);
68464
+ } else {
68465
+ db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS ${FTS_TABLE_NAME} USING fts5(
68466
+ ${ftsCreateColumnsSql()}
68467
+ )`);
68468
+ }
68469
+ this.ftsAvailable = true;
68470
+ const validMemoryCount = this.countValidMemoryRows();
68471
+ const ftsCount = db.query(`SELECT COUNT(*) AS count FROM ${FTS_TABLE_NAME}`).get()?.count ?? 0;
68472
+ if (validMemoryCount !== ftsCount) {
68473
+ this.rebuildFtsIndex();
68474
+ }
68475
+ return true;
68476
+ } catch {
68477
+ this.ftsAvailable = false;
68478
+ return false;
68479
+ }
68480
+ }
68481
+ recreateFtsIndex() {
68482
+ const db = this.requireDb();
68483
+ const recreate = db.transaction(() => {
68484
+ db.run(`DROP TABLE IF EXISTS ${FTS_TABLE_NAME}`);
68485
+ db.run(`CREATE VIRTUAL TABLE ${FTS_TABLE_NAME} USING fts5(
68486
+ ${ftsCreateColumnsSql()}
68487
+ )`);
68488
+ });
68489
+ recreate();
68490
+ }
68491
+ rebuildFtsIndex() {
68492
+ const db = this.requireDb();
68493
+ const rebuild = db.transaction(() => {
68494
+ db.run(`DELETE FROM ${FTS_TABLE_NAME}`);
68495
+ for (const row of this.iterateMemoryRows()) {
68496
+ const record3 = this.parseMemoryRow(row);
68497
+ if (record3) {
68498
+ this.writeMemoryFts(record3);
68499
+ }
68500
+ }
68501
+ });
68502
+ rebuild();
68503
+ }
68504
+ countValidMemoryRows() {
68505
+ let count = 0;
68506
+ for (const row of this.iterateMemoryRows()) {
68507
+ if (this.parseMemoryRow(row))
68508
+ count++;
68509
+ }
68510
+ return count;
68511
+ }
68512
+ *iterateMemoryRows() {
68513
+ yield* this.requireDb().query("SELECT id, record_json FROM memory_items").iterate();
68514
+ }
68515
+ parseMemoryRow(row) {
68516
+ try {
68517
+ return validateMemoryRecordRules(JSON.parse(row.record_json), {
68518
+ rejectDurableSecrets: this.config.redaction.rejectDurableSecrets
68519
+ });
68520
+ } catch {
68521
+ return null;
68522
+ }
68523
+ }
68295
68524
  loadMemories() {
68296
68525
  const rows = this.requireDb().query("SELECT id, record_json FROM memory_items ORDER BY updated_at ASC").all();
68297
68526
  const records = [];
68298
68527
  let invalidCount = 0;
68299
68528
  for (const row of rows) {
68300
- try {
68301
- records.push(validateMemoryRecordRules(JSON.parse(row.record_json), {
68302
- rejectDurableSecrets: this.config.redaction.rejectDurableSecrets
68303
- }));
68304
- } catch {
68529
+ const record3 = this.parseMemoryRow(row);
68530
+ if (record3) {
68531
+ records.push(record3);
68532
+ } else {
68305
68533
  invalidCount++;
68306
68534
  }
68307
68535
  }
@@ -68346,6 +68574,29 @@ class SQLiteMemoryProvider {
68346
68574
  record3.metadata.deleted === true ? 1 : 0,
68347
68575
  JSON.stringify(record3)
68348
68576
  ]);
68577
+ this.writeMemoryFts(record3);
68578
+ }
68579
+ writeMemoryFts(record3) {
68580
+ if (!this.ftsAvailable)
68581
+ return;
68582
+ try {
68583
+ const db = this.requireDb();
68584
+ db.run(`DELETE FROM ${FTS_TABLE_NAME} WHERE id = ?`, [record3.id]);
68585
+ db.run(`INSERT INTO ${FTS_TABLE_NAME} (
68586
+ ${FTS_INSERT_COLUMNS.join(", ")}
68587
+ ) VALUES (${FTS_INSERT_COLUMNS.map(() => "?").join(", ")})`, [record3.id, ...ftsColumnValues(record3)]);
68588
+ } catch {
68589
+ this.ftsAvailable = false;
68590
+ }
68591
+ }
68592
+ deleteMemoryFts(id) {
68593
+ if (!this.ftsAvailable)
68594
+ return;
68595
+ try {
68596
+ this.requireDb().run(`DELETE FROM ${FTS_TABLE_NAME} WHERE id = ?`, [id]);
68597
+ } catch {
68598
+ this.ftsAvailable = false;
68599
+ }
68349
68600
  }
68350
68601
  writeProposal(proposal) {
68351
68602
  this.requireDb().run(`INSERT OR REPLACE INTO memory_proposals (
@@ -68546,7 +68797,69 @@ class SQLiteMemoryProvider {
68546
68797
  function splitSql(sql) {
68547
68798
  return sql.split(";").map((statement) => statement.trim()).filter(Boolean);
68548
68799
  }
68549
- var _DatabaseCtor2 = null, MIGRATIONS2;
68800
+ function buildFtsQuery(request) {
68801
+ const text = request.mode === "injection" && request.task ? `${request.task}
68802
+ ${request.query}` : `${request.query}
68803
+ ${request.task ?? ""}`;
68804
+ const terms = Array.from(extractFtsTerms(text)).slice(0, 40);
68805
+ if (terms.length === 0)
68806
+ return null;
68807
+ return terms.map((term) => `"${term}"`).join(" OR ");
68808
+ }
68809
+ function extractFtsTerms(text) {
68810
+ const terms = new Set;
68811
+ for (const match of text.toLowerCase().matchAll(/[a-z0-9_]{2,}/g)) {
68812
+ const term = match[0];
68813
+ if (FTS_STOP_WORDS.has(term))
68814
+ continue;
68815
+ if (term.length < 3 && !/^\d+$/.test(term))
68816
+ continue;
68817
+ terms.add(term);
68818
+ }
68819
+ return terms;
68820
+ }
68821
+ function ftsCreateColumnsSql() {
68822
+ return [
68823
+ "id UNINDEXED",
68824
+ ...FTS_INDEX_COLUMNS.map((column) => column.name)
68825
+ ].join(`,
68826
+ `);
68827
+ }
68828
+ function ftsColumnValues(record3) {
68829
+ return FTS_INDEX_COLUMNS.map((column) => column.value(record3));
68830
+ }
68831
+ function collectMetadataSearchStrings(metadata2, keys) {
68832
+ const values = [];
68833
+ for (const key of keys) {
68834
+ const value = metadata2[key];
68835
+ if (typeof value === "string") {
68836
+ values.push(value);
68837
+ continue;
68838
+ }
68839
+ if (!Array.isArray(value))
68840
+ continue;
68841
+ for (const item of value) {
68842
+ if (typeof item === "string")
68843
+ values.push(item);
68844
+ }
68845
+ }
68846
+ return values;
68847
+ }
68848
+ function rerankWithFts(items, ftsOrder) {
68849
+ const denominator = Math.max(ftsOrder.size, 1);
68850
+ return items.map((item) => {
68851
+ const order = ftsOrder.get(item.record.id);
68852
+ if (order === undefined)
68853
+ return item;
68854
+ const ftsBoost = (denominator - order) / denominator * 0.08;
68855
+ return {
68856
+ ...item,
68857
+ score: item.score + ftsBoost,
68858
+ reason: `${item.reason}, fts_rank=${order + 1}`
68859
+ };
68860
+ }).sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
68861
+ }
68862
+ var _DatabaseCtor2 = null, FTS_SCHEMA_MIGRATION_VERSION = 3, FTS_SCHEMA_MIGRATION_NAME = "create_memory_fts5_shadow_index", FTS_TABLE_NAME = "memory_items_fts", FTS_INDEX_COLUMNS, FTS_INSERT_COLUMNS, MIGRATIONS2, FTS_STOP_WORDS;
68550
68863
  var init_sqlite_provider = __esm(() => {
68551
68864
  init_utils2();
68552
68865
  init_config3();
@@ -68555,6 +68868,45 @@ var init_sqlite_provider = __esm(() => {
68555
68868
  init_jsonl_migration();
68556
68869
  init_schema2();
68557
68870
  init_scoring();
68871
+ FTS_INDEX_COLUMNS = [
68872
+ {
68873
+ name: "text",
68874
+ value: (record3) => record3.text
68875
+ },
68876
+ {
68877
+ name: "tags",
68878
+ value: (record3) => record3.tags.join(" ")
68879
+ },
68880
+ {
68881
+ name: "kind",
68882
+ value: (record3) => record3.kind.replace(/_/g, " ")
68883
+ },
68884
+ {
68885
+ name: "source_file_path",
68886
+ value: (record3) => record3.source.filePath ?? ""
68887
+ },
68888
+ {
68889
+ name: "source_ref",
68890
+ value: (record3) => record3.source.ref ?? ""
68891
+ },
68892
+ {
68893
+ name: "metadata_symbols",
68894
+ value: (record3) => collectMetadataSearchStrings(record3.metadata, ["symbol", "symbols"]).join(" ")
68895
+ },
68896
+ {
68897
+ name: "metadata_files",
68898
+ value: (record3) => collectMetadataSearchStrings(record3.metadata, [
68899
+ "file",
68900
+ "filePath",
68901
+ "files",
68902
+ "touchedFiles"
68903
+ ]).join(" ")
68904
+ }
68905
+ ];
68906
+ FTS_INSERT_COLUMNS = [
68907
+ "id",
68908
+ ...FTS_INDEX_COLUMNS.map((column) => column.name)
68909
+ ];
68558
68910
  MIGRATIONS2 = [
68559
68911
  {
68560
68912
  version: 1,
@@ -68604,6 +68956,37 @@ var init_sqlite_provider = __esm(() => {
68604
68956
  `
68605
68957
  }
68606
68958
  ];
68959
+ FTS_STOP_WORDS = new Set([
68960
+ "a",
68961
+ "an",
68962
+ "and",
68963
+ "are",
68964
+ "as",
68965
+ "at",
68966
+ "be",
68967
+ "by",
68968
+ "for",
68969
+ "from",
68970
+ "goal",
68971
+ "how",
68972
+ "in",
68973
+ "into",
68974
+ "is",
68975
+ "it",
68976
+ "of",
68977
+ "on",
68978
+ "or",
68979
+ "role",
68980
+ "task",
68981
+ "that",
68982
+ "the",
68983
+ "this",
68984
+ "to",
68985
+ "user",
68986
+ "what",
68987
+ "when",
68988
+ "with"
68989
+ ]);
68607
68990
  });
68608
68991
 
68609
68992
  // src/memory/gateway.ts
@@ -69119,102 +69502,6 @@ var init_agent_output_schema = __esm(() => {
69119
69502
  }).passthrough();
69120
69503
  });
69121
69504
 
69122
- // src/memory/role-profiles.ts
69123
- function resolveMemoryRecallProfile(agentRole) {
69124
- const role = normalizeMemoryAgentRole(agentRole);
69125
- return MEMORY_RECALL_PROFILES[role] ?? MEMORY_RECALL_PROFILES.coder;
69126
- }
69127
- function normalizeMemoryAgentRole(agentRole) {
69128
- const base = stripKnownSwarmPrefix(agentRole ?? "architect");
69129
- if (base === "reviewer" || base === "test_engineer")
69130
- return "qa";
69131
- if (base === "critic" || base === "critic_sounding_board" || base === "critic_drift_verifier" || base === "critic_hallucination_verifier") {
69132
- return "security";
69133
- }
69134
- if (base === "curator_init" || base === "curator_phase")
69135
- return "curator";
69136
- if (base === "docs")
69137
- return "sme";
69138
- if (base === "architect" || base === "sme" || base === "coder" || base === "security" || base === "curator") {
69139
- return base;
69140
- }
69141
- return "coder";
69142
- }
69143
- var MEMORY_RECALL_PROFILES;
69144
- var init_role_profiles = __esm(() => {
69145
- init_schema();
69146
- MEMORY_RECALL_PROFILES = {
69147
- architect: {
69148
- kinds: [
69149
- "project_fact",
69150
- "architecture_decision",
69151
- "repo_convention",
69152
- "failure_pattern",
69153
- "security_note"
69154
- ],
69155
- maxItems: 10,
69156
- tokenBudget: 1600
69157
- },
69158
- sme: {
69159
- kinds: [
69160
- "api_finding",
69161
- "code_pattern",
69162
- "repo_convention",
69163
- "failure_pattern",
69164
- "evidence"
69165
- ],
69166
- maxItems: 8,
69167
- tokenBudget: 1200
69168
- },
69169
- coder: {
69170
- kinds: [
69171
- "architecture_decision",
69172
- "repo_convention",
69173
- "code_pattern",
69174
- "test_pattern",
69175
- "failure_pattern"
69176
- ],
69177
- maxItems: 8,
69178
- tokenBudget: 1200
69179
- },
69180
- qa: {
69181
- kinds: [
69182
- "test_pattern",
69183
- "failure_pattern",
69184
- "repo_convention",
69185
- "security_note"
69186
- ],
69187
- maxItems: 8,
69188
- tokenBudget: 1200
69189
- },
69190
- security: {
69191
- kinds: [
69192
- "security_note",
69193
- "architecture_decision",
69194
- "repo_convention",
69195
- "evidence"
69196
- ],
69197
- maxItems: 8,
69198
- tokenBudget: 1200
69199
- },
69200
- curator: {
69201
- kinds: [
69202
- "project_fact",
69203
- "architecture_decision",
69204
- "repo_convention",
69205
- "api_finding",
69206
- "code_pattern",
69207
- "test_pattern",
69208
- "failure_pattern",
69209
- "security_note",
69210
- "evidence"
69211
- ],
69212
- maxItems: 20,
69213
- tokenBudget: 3000
69214
- }
69215
- };
69216
- });
69217
-
69218
69505
  // src/memory/recall-planner.ts
69219
69506
  function buildMemoryRecallPlan(input, options = {}) {
69220
69507
  const profile = options.profile ?? resolveMemoryRecallProfile(input.agentRole);
@@ -15,6 +15,7 @@ export declare class SQLiteMemoryProvider implements MemoryProvider, MemoryPropo
15
15
  private readonly config;
16
16
  private initialized;
17
17
  private db;
18
+ private ftsAvailable;
18
19
  private memories;
19
20
  private proposals;
20
21
  private lastAutomaticJsonlMigration;
@@ -48,10 +49,19 @@ export declare class SQLiteMemoryProvider implements MemoryProvider, MemoryPropo
48
49
  }>;
49
50
  hasMigration(name: string): boolean;
50
51
  markMigration(version: number, name: string): void;
52
+ private selectRecallCandidates;
51
53
  private runMigrations;
54
+ private initializeFtsIndex;
55
+ private recreateFtsIndex;
56
+ private rebuildFtsIndex;
57
+ private countValidMemoryRows;
58
+ private iterateMemoryRows;
59
+ private parseMemoryRow;
52
60
  private loadMemories;
53
61
  private loadProposals;
54
62
  private writeMemory;
63
+ private writeMemoryFts;
64
+ private deleteMemoryFts;
55
65
  private writeProposal;
56
66
  private applyDecisionToStorage;
57
67
  private readPendingProposal;
@@ -63,3 +73,12 @@ export declare class SQLiteMemoryProvider implements MemoryProvider, MemoryPropo
63
73
  private insertEvent;
64
74
  private requireDb;
65
75
  }
76
+ declare function buildFtsQuery(request: RecallRequest): string | null;
77
+ declare function extractFtsTerms(text: string): Set<string>;
78
+ export declare const _test_exports: {
79
+ buildFtsQuery: typeof buildFtsQuery;
80
+ extractFtsTerms: typeof extractFtsTerms;
81
+ FTS_SCHEMA_MIGRATION_NAME: string;
82
+ FTS_SCHEMA_MIGRATION_VERSION: number;
83
+ };
84
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.36.0",
3
+ "version": "7.37.0",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",