omegon 0.7.7 → 0.8.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.
@@ -555,10 +555,13 @@ export class FactStore {
555
555
  const source = opts.source ?? "manual";
556
556
  const content = opts.content.replace(/^-\s*/, "").trim();
557
557
 
558
- // Dedup check — same mind, same hash, still active
558
+ // Dedup check — same mind (or parent chain), same hash, still active.
559
+ // Check the full chain so directive minds don't duplicate parent facts.
560
+ const chain = this.resolveMindChain(mind);
561
+ const dedupPlaceholders = chain.map(() => "?").join(", ");
559
562
  const existing = this.db.prepare(
560
- `SELECT id FROM facts WHERE mind = ? AND content_hash = ? AND status = 'active'`
561
- ).get(mind, hash);
563
+ `SELECT id FROM facts WHERE mind IN (${dedupPlaceholders}) AND content_hash = ? AND status = 'active'`
564
+ ).get(...chain, hash);
562
565
 
563
566
  if (existing) {
564
567
  // Reinforce the existing fact instead of duplicating
@@ -646,15 +649,19 @@ export class FactStore {
646
649
  let added = 0;
647
650
  const newFactIds: string[] = [];
648
651
 
652
+ // Resolve chain once for all observe dedup checks (cached, but avoid repeated map/join)
653
+ const observeChain = this.resolveMindChain(mind);
654
+ const observePlaceholders = observeChain.map(() => "?").join(", ");
655
+
649
656
  const tx = this.db.transaction(() => {
650
657
  for (const action of actions) {
651
658
  switch (action.type) {
652
659
  case "observe": {
653
- // Fact observed in session — reinforce if exists, add if new
660
+ // Fact observed in session — reinforce if exists (in mind or parent chain), add if new
654
661
  const hash = contentHash(action.content ?? "");
655
662
  const existing = this.db.prepare(
656
- `SELECT id FROM facts WHERE mind = ? AND content_hash = ? AND status = 'active'`
657
- ).get(mind, hash);
663
+ `SELECT id FROM facts WHERE mind IN (${observePlaceholders}) AND content_hash = ? AND status = 'active'`
664
+ ).get(...observeChain, hash);
658
665
 
659
666
  if (existing) {
660
667
  this.reinforceFact((existing as { id: string }).id);
@@ -743,7 +750,24 @@ export class FactStore {
743
750
  * the minimumConfidence threshold is consistent with the confidence computation.
744
751
  */
745
752
  sweepDecayedFacts(mind: string): number {
746
- const facts = this.getActiveFacts(mind);
753
+ // Only sweep facts directly in this mind, not inherited from parents.
754
+ // Parent facts are managed by their own mind's sweep cycle.
755
+ const facts = this.db.prepare(
756
+ `SELECT * FROM facts WHERE mind = ? AND status = 'active' ORDER BY section, created_at`
757
+ ).all(mind) as Fact[];
758
+ // Apply decay (same logic as getActiveFacts)
759
+ const NO_DECAY_SECTIONS: readonly string[] = ["Specs"];
760
+ const now = Date.now();
761
+ for (const fact of facts) {
762
+ if (NO_DECAY_SECTIONS.includes(fact.section)) {
763
+ fact.confidence = 1.0;
764
+ } else {
765
+ const lastReinforced = new Date(fact.last_reinforced).getTime();
766
+ const daysSince = (now - lastReinforced) / (1000 * 60 * 60 * 24);
767
+ const profile = SECTION_DECAY_OVERRIDES[fact.section] ?? this.decayProfile;
768
+ fact.confidence = computeConfidence(daysSince, fact.reinforcement_count, profile);
769
+ }
770
+ }
747
771
  let swept = 0;
748
772
 
749
773
  for (const fact of facts) {
@@ -848,10 +872,12 @@ export class FactStore {
848
872
  getActiveEdges(mind?: string): Edge[] {
849
873
  let edges: Edge[];
850
874
  if (mind) {
875
+ const chain = this.resolveMindChain(mind);
876
+ const placeholders = chain.map(() => "?").join(", ");
851
877
  edges = this.db.prepare(`
852
878
  SELECT * FROM edges
853
- WHERE (source_mind = ? OR target_mind = ?) AND status = 'active'
854
- `).all(mind, mind) as Edge[];
879
+ WHERE (source_mind IN (${placeholders}) OR target_mind IN (${placeholders})) AND status = 'active'
880
+ `).all(...chain, ...chain) as Edge[];
855
881
  } else {
856
882
  edges = this.db.prepare(
857
883
  `SELECT * FROM edges WHERE status = 'active'`
@@ -947,15 +973,65 @@ export class FactStore {
947
973
  // Queries
948
974
  // ---------------------------------------------------------------------------
949
975
 
976
+ private mindChainCache = new Map<string, string[]>();
977
+
978
+ /**
979
+ * Resolve the mind chain: [mind, parent, grandparent, ...].
980
+ * Used to include inherited facts from parent minds.
981
+ * Stops at 'default' or when no parent exists. Max depth 5 to prevent cycles.
982
+ * Cached per-mind since the parent chain is immutable within a session.
983
+ */
984
+ private resolveMindChain(mind: string): string[] {
985
+ const cached = this.mindChainCache.get(mind);
986
+ if (cached) return cached;
987
+
988
+ const chain: string[] = [mind];
989
+ let current = mind;
990
+ for (let i = 0; i < 5; i++) {
991
+ const rec = this.getMind(current);
992
+ if (!rec?.parent || rec.parent === current) break;
993
+ chain.push(rec.parent);
994
+ current = rec.parent;
995
+ }
996
+ this.mindChainCache.set(mind, chain);
997
+ return chain;
998
+ }
999
+
1000
+ /** Clear the mind chain cache (call after mind creation/deletion). */
1001
+ private invalidateMindChainCache(): void {
1002
+ this.mindChainCache.clear();
1003
+ }
1004
+
950
1005
  /**
951
1006
  * Get active facts for a mind, with confidence decay applied.
1007
+ * If the mind has a parent, includes inherited parent facts (deduped
1008
+ * by content_hash — child facts shadow parent facts with the same content).
952
1009
  * Optionally limit to top N by confidence.
953
1010
  */
954
1011
  getActiveFacts(mind: string, limit?: number): Fact[] {
955
- const facts = this.db.prepare(
956
- `SELECT * FROM facts WHERE mind = ? AND status = 'active'
1012
+ const chain = this.resolveMindChain(mind);
1013
+ const placeholders = chain.map(() => "?").join(", ");
1014
+ const allFacts = this.db.prepare(
1015
+ `SELECT * FROM facts WHERE mind IN (${placeholders}) AND status = 'active'
957
1016
  ORDER BY section, created_at`
958
- ).all(mind) as Fact[];
1017
+ ).all(...chain) as Fact[];
1018
+
1019
+ // Deduplicate: child facts shadow parent facts with the same content_hash.
1020
+ // Keep the fact from the earliest mind in the chain (= the child).
1021
+ const seen = new Map<string, number>();
1022
+ const facts: Fact[] = [];
1023
+ for (const fact of allFacts) {
1024
+ const chainIdx = chain.indexOf(fact.mind);
1025
+ const existing = seen.get(fact.content_hash);
1026
+ if (existing !== undefined && existing <= chainIdx) continue; // child already present
1027
+ seen.set(fact.content_hash, chainIdx);
1028
+ // Remove any previously added parent fact with same hash
1029
+ if (existing !== undefined) {
1030
+ const idx = facts.findIndex(f => f.content_hash === fact.content_hash);
1031
+ if (idx !== -1) facts.splice(idx, 1);
1032
+ }
1033
+ facts.push(fact);
1034
+ }
959
1035
 
960
1036
  // Apply time-based confidence decay.
961
1037
  // Specs are exempt (binary exist/not-exist).
@@ -992,9 +1068,25 @@ export class FactStore {
992
1068
 
993
1069
  /** Get active facts for a specific section, sorted by confidence descending. */
994
1070
  getFactsBySection(mind: string, section: string): Fact[] {
995
- const facts = this.db.prepare(
996
- `SELECT * FROM facts WHERE mind = ? AND section = ? AND status = 'active' ORDER BY created_at`
997
- ).all(mind, section) as Fact[];
1071
+ const chain = this.resolveMindChain(mind);
1072
+ const placeholders = chain.map(() => "?").join(", ");
1073
+ const allFacts = this.db.prepare(
1074
+ `SELECT * FROM facts WHERE mind IN (${placeholders}) AND section = ? AND status = 'active' ORDER BY created_at`
1075
+ ).all(...chain, section) as Fact[];
1076
+ // Deduplicate: child facts shadow parent facts with the same content_hash.
1077
+ const seen = new Map<string, number>();
1078
+ const facts: Fact[] = [];
1079
+ for (const f of allFacts) {
1080
+ const chainIdx = chain.indexOf(f.mind);
1081
+ const existing = seen.get(f.content_hash);
1082
+ if (existing !== undefined && existing <= chainIdx) continue;
1083
+ seen.set(f.content_hash, chainIdx);
1084
+ if (existing !== undefined) {
1085
+ const idx = facts.findIndex(ff => ff.content_hash === f.content_hash);
1086
+ if (idx !== -1) facts.splice(idx, 1);
1087
+ }
1088
+ facts.push(f);
1089
+ }
998
1090
 
999
1091
  const NO_DECAY_SECTIONS: readonly string[] = ["Specs"];
1000
1092
  const now = Date.now();
@@ -1015,17 +1107,21 @@ export class FactStore {
1015
1107
 
1016
1108
  /** Get the count of active facts per section for a mind. */
1017
1109
  getSectionCounts(mind: string): Map<string, number> {
1110
+ const chain = this.resolveMindChain(mind);
1111
+ const placeholders = chain.map(() => "?").join(", ");
1018
1112
  const rows = this.db.prepare(
1019
- `SELECT section, COUNT(*) as count FROM facts WHERE mind = ? AND status = 'active' GROUP BY section`
1020
- ).all(mind) as { section: string; count: number }[];
1113
+ `SELECT section, COUNT(DISTINCT content_hash) as count FROM facts WHERE mind IN (${placeholders}) AND status = 'active' GROUP BY section`
1114
+ ).all(...chain) as { section: string; count: number }[];
1021
1115
  return new Map(rows.map(r => [r.section, r.count]));
1022
1116
  }
1023
1117
 
1024
- /** Count active facts for a mind */
1118
+ /** Count active facts for a mind (including inherited parent facts) */
1025
1119
  countActiveFacts(mind: string): number {
1120
+ const chain = this.resolveMindChain(mind);
1121
+ const placeholders = chain.map(() => "?").join(", ");
1026
1122
  const row = this.db.prepare(
1027
- `SELECT COUNT(*) as count FROM facts WHERE mind = ? AND status = 'active'`
1028
- ).get(mind);
1123
+ `SELECT COUNT(DISTINCT content_hash) as count FROM facts WHERE mind IN (${placeholders}) AND status = 'active'`
1124
+ ).get(...chain);
1029
1125
  return row?.count ?? 0;
1030
1126
  }
1031
1127
 
@@ -1036,11 +1132,13 @@ export class FactStore {
1036
1132
  const pattern = `${escaped}%`;
1037
1133
 
1038
1134
  if (mind) {
1135
+ const chain = this.resolveMindChain(mind);
1136
+ const placeholders = chain.map(() => "?").join(", ");
1039
1137
  return this.db.prepare(`
1040
1138
  SELECT * FROM facts
1041
- WHERE content LIKE ? ESCAPE '\\' AND mind = ? AND status = 'active'
1139
+ WHERE content LIKE ? ESCAPE '\\' AND mind IN (${placeholders}) AND status = 'active'
1042
1140
  ORDER BY created_at DESC
1043
- `).all(pattern, mind) as Fact[];
1141
+ `).all(pattern, ...chain) as Fact[];
1044
1142
  }
1045
1143
 
1046
1144
  return this.db.prepare(`
@@ -1057,12 +1155,14 @@ export class FactStore {
1057
1155
 
1058
1156
  try {
1059
1157
  if (mind) {
1158
+ const chain = this.resolveMindChain(mind);
1159
+ const placeholders = chain.map(() => "?").join(", ");
1060
1160
  return this.db.prepare(`
1061
1161
  SELECT f.* FROM facts f
1062
1162
  JOIN facts_fts fts ON f.rowid = fts.rowid
1063
- WHERE facts_fts MATCH ? AND f.mind = ?
1163
+ WHERE facts_fts MATCH ? AND f.mind IN (${placeholders})
1064
1164
  ORDER BY rank
1065
- `).all(ftsQuery, mind) as Fact[];
1165
+ `).all(ftsQuery, ...chain) as Fact[];
1066
1166
  }
1067
1167
 
1068
1168
  return this.db.prepare(`
@@ -1084,12 +1184,14 @@ export class FactStore {
1084
1184
 
1085
1185
  try {
1086
1186
  if (mind) {
1187
+ const chain = this.resolveMindChain(mind);
1188
+ const placeholders = chain.map(() => "?").join(", ");
1087
1189
  return this.db.prepare(`
1088
1190
  SELECT f.* FROM facts f
1089
1191
  JOIN facts_fts fts ON f.rowid = fts.rowid
1090
- WHERE facts_fts MATCH ? AND f.mind = ? AND f.status IN ('archived', 'superseded')
1192
+ WHERE facts_fts MATCH ? AND f.mind IN (${placeholders}) AND f.status IN ('archived', 'superseded')
1091
1193
  ORDER BY f.created_at DESC
1092
- `).all(ftsQuery, mind) as Fact[];
1194
+ `).all(ftsQuery, ...chain) as Fact[];
1093
1195
  }
1094
1196
 
1095
1197
  return this.db.prepare(`
@@ -1326,6 +1428,7 @@ export class FactStore {
1326
1428
  this.db.prepare(`DELETE FROM minds WHERE name = ?`).run(name);
1327
1429
  });
1328
1430
  tx();
1431
+ this.invalidateMindChainCache();
1329
1432
  }
1330
1433
 
1331
1434
  /** Check if a mind exists */
@@ -1339,32 +1442,28 @@ export class FactStore {
1339
1442
  return mind?.readonly === 1;
1340
1443
  }
1341
1444
 
1342
- /** Fork a mind — copy all active facts to a new mind */
1445
+ /**
1446
+ * Fork a mind — create a child scope that inherits the parent's facts.
1447
+ *
1448
+ * Lightweight: creates the mind record with `parent` set but copies zero
1449
+ * facts, edges, or embeddings. Query methods (getActiveFacts, vector search)
1450
+ * automatically include parent facts via the parent chain. Only facts
1451
+ * explicitly stored in the child are scoped to it.
1452
+ *
1453
+ * On archive, `ingestMind` copies child-only facts back to the parent.
1454
+ */
1343
1455
  forkMind(sourceName: string, newName: string, description: string): void {
1344
- const tx = this.db.transaction(() => {
1345
- this.createMind(newName, description, { parent: sourceName });
1346
-
1347
- const facts = this.getActiveFacts(sourceName);
1348
- const now = new Date().toISOString();
1349
-
1350
- for (const fact of facts) {
1351
- this.db.prepare(`
1352
- INSERT INTO facts (id, mind, section, content, status, created_at, created_session,
1353
- source, content_hash, confidence, last_reinforced,
1354
- reinforcement_count, decay_rate)
1355
- VALUES (?, ?, ?, ?, 'active', ?, NULL, 'ingest', ?, 1.0, ?, ?, ?)
1356
- `).run(
1357
- nanoid(), newName, fact.section, fact.content, now,
1358
- fact.content_hash, now, fact.reinforcement_count, fact.decay_rate,
1359
- );
1360
- }
1361
- });
1362
- tx();
1456
+ this.createMind(newName, description, { parent: sourceName });
1457
+ this.invalidateMindChainCache();
1363
1458
  }
1364
1459
 
1365
1460
  /** Ingest facts from one mind into another */
1366
1461
  ingestMind(sourceName: string, targetName: string): { factsIngested: number; duplicatesSkipped: number } {
1367
- const sourceFacts = this.getActiveFacts(sourceName);
1462
+ // Only ingest facts directly stored in the source mind, not inherited from parents.
1463
+ // Inherited facts already exist in the parent (which is typically the target).
1464
+ const sourceFacts = this.db.prepare(
1465
+ `SELECT * FROM facts WHERE mind = ? AND status = 'active' ORDER BY section, created_at`
1466
+ ).all(sourceName) as Fact[];
1368
1467
  let ingested = 0;
1369
1468
  let skipped = 0;
1370
1469
 
@@ -1750,13 +1849,15 @@ export class FactStore {
1750
1849
  return rows.map(r => r.id);
1751
1850
  }
1752
1851
 
1753
- /** Count facts with vectors for a mind */
1852
+ /** Count facts with vectors for a mind (including parent chain) */
1754
1853
  countFactVectors(mind: string): number {
1854
+ const chain = this.resolveMindChain(mind);
1855
+ const placeholders = chain.map(() => "?").join(", ");
1755
1856
  const row = this.db.prepare(`
1756
1857
  SELECT COUNT(*) as count FROM facts_vec v
1757
1858
  JOIN facts f ON v.fact_id = f.id
1758
- WHERE f.mind = ? AND f.status = 'active'
1759
- `).get(mind);
1859
+ WHERE f.mind IN (${placeholders}) AND f.status = 'active'
1860
+ `).get(...chain);
1760
1861
  return row?.count ?? 0;
1761
1862
  }
1762
1863
 
@@ -1776,13 +1877,15 @@ export class FactStore {
1776
1877
  const minSim = opts?.minSimilarity ?? 0.3;
1777
1878
  const queryDims = queryVec.length;
1778
1879
 
1779
- // Get all active facts with vectors for this mind
1880
+ // Get all active facts with vectors for this mind (including parent chain)
1881
+ const chain = this.resolveMindChain(mind);
1882
+ const placeholders = chain.map(() => "?").join(", ");
1780
1883
  let query = `
1781
1884
  SELECT f.*, v.embedding, v.dims FROM facts f
1782
1885
  JOIN facts_vec v ON f.id = v.fact_id
1783
- WHERE f.mind = ? AND f.status = 'active'
1886
+ WHERE f.mind IN (${placeholders}) AND f.status = 'active'
1784
1887
  `;
1785
- const params: any[] = [mind];
1888
+ const params: any[] = [...chain];
1786
1889
 
1787
1890
  if (opts?.section) {
1788
1891
  query += ` AND f.section = ?`;
@@ -1887,12 +1990,14 @@ export class FactStore {
1887
1990
  const ftsQuery = buildSafeFtsQuery(queryText, "OR");
1888
1991
  if (ftsQuery) {
1889
1992
  try {
1993
+ const ftsChain = this.resolveMindChain(mind);
1994
+ const ftsPlaceholders = ftsChain.map(() => "?").join(", ");
1890
1995
  let query = `
1891
1996
  SELECT f.* FROM facts f
1892
1997
  JOIN facts_fts fts ON f.rowid = fts.rowid
1893
- WHERE facts_fts MATCH ? AND f.mind = ? AND f.status = 'active'
1998
+ WHERE facts_fts MATCH ? AND f.mind IN (${ftsPlaceholders}) AND f.status = 'active'
1894
1999
  `;
1895
- const params: any[] = [ftsQuery, mind];
2000
+ const params: any[] = [ftsQuery, ...ftsChain];
1896
2001
  if (opts?.section) {
1897
2002
  query += ` AND f.section = ?`;
1898
2003
  params.push(opts.section);
@@ -1980,12 +2085,14 @@ export class FactStore {
1980
2085
  const queryDims = queryVec.length;
1981
2086
  const contentHashVal = contentHash(factContent);
1982
2087
 
2088
+ const chain = this.resolveMindChain(mind);
2089
+ const placeholders = chain.map(() => "?").join(", ");
1983
2090
  const rows = this.db.prepare(`
1984
2091
  SELECT f.*, v.embedding, v.dims FROM facts f
1985
2092
  JOIN facts_vec v ON f.id = v.fact_id
1986
- WHERE f.mind = ? AND f.section = ? AND f.status = 'active'
2093
+ WHERE f.mind IN (${placeholders}) AND f.section = ? AND f.status = 'active'
1987
2094
  AND f.content_hash != ?
1988
- `).all(mind, section, contentHashVal) as (Fact & { embedding: Buffer; dims: number })[];
2095
+ `).all(...chain, section, contentHashVal) as (Fact & { embedding: Buffer; dims: number })[];
1989
2096
 
1990
2097
  const results: (Fact & { similarity: number })[] = [];
1991
2098
 
@@ -2071,9 +2178,11 @@ export class FactStore {
2071
2178
 
2072
2179
  /** Get episodes for a mind, ordered by date descending */
2073
2180
  getEpisodes(mind: string, limit?: number): Episode[] {
2074
- const sql = `SELECT * FROM episodes WHERE mind = ? ORDER BY date DESC` +
2181
+ const chain = this.resolveMindChain(mind);
2182
+ const placeholders = chain.map(() => "?").join(", ");
2183
+ const sql = `SELECT * FROM episodes WHERE mind IN (${placeholders}) ORDER BY date DESC` +
2075
2184
  (limit ? ` LIMIT ${limit}` : "");
2076
- return this.db.prepare(sql).all(mind) as Episode[];
2185
+ return this.db.prepare(sql).all(...chain) as Episode[];
2077
2186
  }
2078
2187
 
2079
2188
  /** Get a single episode by ID */
@@ -2119,11 +2228,13 @@ export class FactStore {
2119
2228
  const minSim = opts?.minSimilarity ?? 0.3;
2120
2229
  const queryDims = queryVec.length;
2121
2230
 
2231
+ const chain = this.resolveMindChain(mind);
2232
+ const ePlaceholders = chain.map(() => "?").join(", ");
2122
2233
  const rows = this.db.prepare(`
2123
2234
  SELECT e.*, v.embedding, v.dims FROM episodes e
2124
2235
  JOIN episodes_vec v ON e.id = v.episode_id
2125
- WHERE e.mind = ?
2126
- `).all(mind) as (Episode & { embedding: Buffer; dims: number })[];
2236
+ WHERE e.mind IN (${ePlaceholders})
2237
+ `).all(...chain) as (Episode & { embedding: Buffer; dims: number })[];
2127
2238
 
2128
2239
  const results: (Episode & { similarity: number })[] = [];
2129
2240
 
@@ -2145,9 +2256,11 @@ export class FactStore {
2145
2256
 
2146
2257
  /** Count episodes for a mind */
2147
2258
  countEpisodes(mind: string): number {
2259
+ const chain = this.resolveMindChain(mind);
2260
+ const placeholders = chain.map(() => "?").join(", ");
2148
2261
  const row = this.db.prepare(
2149
- `SELECT COUNT(*) as count FROM episodes WHERE mind = ?`
2150
- ).get(mind);
2262
+ `SELECT COUNT(*) as count FROM episodes WHERE mind IN (${placeholders})`
2263
+ ).get(...chain);
2151
2264
  return row?.count ?? 0;
2152
2265
  }
2153
2266
 
@@ -660,6 +660,33 @@ export default function (pi: ExtensionAPI) {
660
660
  // Best effort
661
661
  }
662
662
 
663
+ // Drain mind lifecycle queue after store is initialized — fork/activate/ingest/delete
664
+ // requests from implement/archive flows that were queued before store existed.
665
+ drainMindLifecycleQueue(ctx);
666
+
667
+ // --- Branch↔mind consistency check for directive minds ---
668
+ // If the active mind is a directive mind (starts with 'directive/'),
669
+ // verify the current git branch matches the expected branch.
670
+ // Directive mind "directive/X" expects branch "feature/X".
671
+ const currentMind = activeMind();
672
+ if (currentMind.startsWith("directive/")) {
673
+ const directiveSuffix = currentMind.slice("directive/".length);
674
+ const expectedBranch = `feature/${directiveSuffix}`;
675
+ try {
676
+ const branchResult = await pi.exec("git", ["branch", "--show-current"], { timeout: 3_000, cwd: ctx.cwd });
677
+ const currentBranch = branchResult.stdout.trim();
678
+ if (currentBranch && currentBranch !== expectedBranch) {
679
+ pi.sendMessage({
680
+ customType: "directive-branch-mismatch",
681
+ content: `⚠️ Active directive mind "${currentMind}" expects branch "${expectedBranch}" but you are on "${currentBranch}". Run \`git checkout ${expectedBranch}\` to resume directive work.`,
682
+ display: false,
683
+ });
684
+ }
685
+ } catch {
686
+ // Git not available or not a git repo — skip check silently
687
+ }
688
+ }
689
+
663
690
  triggerState = createTriggerState();
664
691
  postCompaction = false;
665
692
  firstTurn = true;
@@ -1339,6 +1366,7 @@ export default function (pi: ExtensionAPI) {
1339
1366
  // --- Context Injection ---
1340
1367
 
1341
1368
  pi.on("before_agent_start", async (event, ctx) => {
1369
+ drainMindLifecycleQueue(ctx);
1342
1370
  drainLifecycleCandidateQueue(ctx);
1343
1371
  drainFactArchiveQueue();
1344
1372
  if (!store) return;
@@ -1904,6 +1932,66 @@ export default function (pi: ExtensionAPI) {
1904
1932
  }
1905
1933
  }
1906
1934
 
1935
+ function drainMindLifecycleQueue(ctx: ExtensionContext): void {
1936
+ if (!store) return;
1937
+ const queue = sharedState.mindLifecycleQueue ?? [];
1938
+ if (queue.length === 0) return;
1939
+
1940
+ sharedState.mindLifecycleQueue = [];
1941
+
1942
+ for (const req of queue) {
1943
+ try {
1944
+ switch (req.action) {
1945
+ case "fork": {
1946
+ if (store.mindExists(req.mind)) {
1947
+ // Already forked (e.g. resumed directive) — just skip
1948
+ break;
1949
+ }
1950
+ store.forkMind("default", req.mind, req.description ?? `Directive scope: ${req.mind}`);
1951
+ if (ctx.hasUI) {
1952
+ ctx.ui.notify(`[memory] Forked directive mind '${req.mind}'`, "info");
1953
+ }
1954
+ break;
1955
+ }
1956
+ case "activate": {
1957
+ const target = req.mind === "default" ? null : req.mind;
1958
+ if (target && !store.mindExists(target)) {
1959
+ // Mind doesn't exist (e.g. abandoned before archive) — stay on default
1960
+ break;
1961
+ }
1962
+ store.setActiveMind(target);
1963
+ break;
1964
+ }
1965
+ case "ingest": {
1966
+ const targetMind = req.targetMind ?? "default";
1967
+ if (!store.mindExists(req.mind)) {
1968
+ // Source mind doesn't exist — nothing to ingest
1969
+ break;
1970
+ }
1971
+ const result = store.ingestMind(req.mind, targetMind);
1972
+ if (ctx.hasUI) {
1973
+ ctx.ui.notify(
1974
+ `[memory] Merged '${req.mind}' → '${targetMind}': ${result.factsIngested} ingested, ${result.duplicatesSkipped} deduped`,
1975
+ "info",
1976
+ );
1977
+ }
1978
+ break;
1979
+ }
1980
+ case "delete": {
1981
+ if (req.mind === "default") break; // Safety: never delete default
1982
+ if (!store.mindExists(req.mind)) break;
1983
+ store.deleteMind(req.mind);
1984
+ break;
1985
+ }
1986
+ }
1987
+ } catch (error) {
1988
+ console.error(`[project-memory] Mind lifecycle '${req.action}' failed for '${req.mind}':`, error);
1989
+ }
1990
+ }
1991
+
1992
+ updateStatus(ctx);
1993
+ }
1994
+
1907
1995
  // --- Tools ---
1908
1996
 
1909
1997
  pi.registerTool({
@@ -2908,6 +2996,9 @@ export default function (pi: ExtensionAPI) {
2908
2996
 
2909
2997
  const theme = ctx.ui.theme;
2910
2998
  const mind = activeMind();
2999
+
3000
+ // Publish active mind for dashboard directive indicator
3001
+ sharedState.activeMind = mind === "default" ? null : mind;
2911
3002
  const count = store.countActiveFacts(mind);
2912
3003
 
2913
3004
  // Label + fact count as a single unit: "Memory: 2 facts" or "Memory(mind): 2 facts"
@@ -5,55 +5,42 @@ function _wrapAsyncGenerator(e) {
5
5
  };
6
6
  }
7
7
  function AsyncGenerator(e) {
8
- var r, t;
9
- function resume(r, t) {
8
+ var t, n;
9
+ function resume(t, n) {
10
10
  try {
11
- var n = e[r](t),
12
- o = n.value,
11
+ var r = e[t](n),
12
+ o = r.value,
13
13
  u = o instanceof OverloadYield;
14
- Promise.resolve(u ? o.v : o).then(function (t) {
14
+ Promise.resolve(u ? o.v : o).then(function (n) {
15
15
  if (u) {
16
- var i = "return" === r ? "return" : "next";
17
- if (!o.k || t.done) return resume(i, t);
18
- t = e[i](t).value;
16
+ var i = "return" === t && o.k ? t : "next";
17
+ if (!o.k || n.done) return resume(i, n);
18
+ n = e[i](n).value;
19
19
  }
20
- settle(n.done ? "return" : "normal", t);
20
+ settle(!!r.done, n);
21
21
  }, function (e) {
22
22
  resume("throw", e);
23
23
  });
24
24
  } catch (e) {
25
- settle("throw", e);
25
+ settle(2, e);
26
26
  }
27
27
  }
28
- function settle(e, n) {
29
- switch (e) {
30
- case "return":
31
- r.resolve({
32
- value: n,
33
- done: !0
34
- });
35
- break;
36
- case "throw":
37
- r.reject(n);
38
- break;
39
- default:
40
- r.resolve({
41
- value: n,
42
- done: !1
43
- });
44
- }
45
- (r = r.next) ? resume(r.key, r.arg) : t = null;
28
+ function settle(e, r) {
29
+ 2 === e ? t.reject(r) : t.resolve({
30
+ value: r,
31
+ done: e
32
+ }), (t = t.next) ? resume(t.key, t.arg) : n = null;
46
33
  }
47
- this._invoke = function (e, n) {
34
+ this._invoke = function (e, r) {
48
35
  return new Promise(function (o, u) {
49
36
  var i = {
50
37
  key: e,
51
- arg: n,
38
+ arg: r,
52
39
  resolve: o,
53
40
  reject: u,
54
41
  next: null
55
42
  };
56
- t ? t = t.next = i : (r = t = i, resume(e, n));
43
+ n ? n = n.next = i : (t = n = i, resume(e, r));
57
44
  });
58
45
  }, "function" != typeof e["return"] && (this["return"] = void 0);
59
46
  }