@monoes/monomindcli 1.10.29 → 1.10.30
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/.claude/helpers/auto-memory-hook.mjs +39 -4
- package/.claude/helpers/handlers/edit-handler.cjs +145 -0
- package/.claude/helpers/handlers/route-handler.cjs +393 -0
- package/.claude/helpers/handlers/session-handler.cjs +167 -0
- package/.claude/helpers/handlers/session-restore-handler.cjs +343 -0
- package/.claude/helpers/handlers/task-handler.cjs +329 -0
- package/.claude/helpers/hook-handler.cjs +114 -2273
- package/.claude/helpers/intelligence.cjs +21 -2
- package/.claude/helpers/learning-service.mjs +166 -8
- package/.claude/helpers/memory-palace.cjs +72 -12
- package/.claude/helpers/router.cjs +79 -5
- package/.claude/helpers/statusline.cjs +193 -399
- package/.claude/helpers/utils/micro-agents.cjs +338 -0
- package/.claude/helpers/utils/monograph.cjs +349 -0
- package/.claude/helpers/utils/telemetry.cjs +144 -0
- package/.claude/skills/agent-browser-testing/SKILL.md +3 -2
- package/.claude/skills/monomind/browse-agentcore.md +116 -0
- package/.claude/skills/monomind/browse-electron.md +189 -0
- package/.claude/skills/monomind/browse-qa.md +229 -0
- package/.claude/skills/monomind/browse-references/authentication.md +162 -0
- package/.claude/skills/monomind/browse-references/trust-boundaries.md +41 -0
- package/.claude/skills/monomind/browse-references/video-recording.md +84 -0
- package/.claude/skills/monomind/browse-slack.md +189 -0
- package/.claude/skills/monomind/browse-vercel.md +240 -0
- package/.claude/skills/monomind/browse.md +724 -0
- package/dist/src/browser/actions.d.ts +13 -0
- package/dist/src/browser/actions.d.ts.map +1 -0
- package/dist/src/browser/actions.js +201 -0
- package/dist/src/browser/actions.js.map +1 -0
- package/dist/src/browser/browser.d.ts +14 -0
- package/dist/src/browser/browser.d.ts.map +1 -0
- package/dist/src/browser/browser.js +198 -0
- package/dist/src/browser/browser.js.map +1 -0
- package/dist/src/browser/cdp.d.ts +17 -0
- package/dist/src/browser/cdp.d.ts.map +1 -0
- package/dist/src/browser/cdp.js +106 -0
- package/dist/src/browser/cdp.js.map +1 -0
- package/dist/src/browser/index.d.ts +11 -0
- package/dist/src/browser/index.d.ts.map +1 -0
- package/dist/src/browser/index.js +11 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/network.d.ts +11 -0
- package/dist/src/browser/network.d.ts.map +1 -0
- package/dist/src/browser/network.js +81 -0
- package/dist/src/browser/network.js.map +1 -0
- package/dist/src/browser/screenshot.d.ts +15 -0
- package/dist/src/browser/screenshot.d.ts.map +1 -0
- package/dist/src/browser/screenshot.js +36 -0
- package/dist/src/browser/screenshot.js.map +1 -0
- package/dist/src/browser/session.d.ts +8 -0
- package/dist/src/browser/session.d.ts.map +1 -0
- package/dist/src/browser/session.js +50 -0
- package/dist/src/browser/session.js.map +1 -0
- package/dist/src/browser/snapshot.d.ts +12 -0
- package/dist/src/browser/snapshot.d.ts.map +1 -0
- package/dist/src/browser/snapshot.js +147 -0
- package/dist/src/browser/snapshot.js.map +1 -0
- package/dist/src/browser/tabs.d.ts +8 -0
- package/dist/src/browser/tabs.d.ts.map +1 -0
- package/dist/src/browser/tabs.js +25 -0
- package/dist/src/browser/tabs.js.map +1 -0
- package/dist/src/browser/types.d.ts +109 -0
- package/dist/src/browser/types.d.ts.map +1 -0
- package/dist/src/browser/types.js +16 -0
- package/dist/src/browser/types.js.map +1 -0
- package/dist/src/browser/wait.d.ts +4 -0
- package/dist/src/browser/wait.d.ts.map +1 -0
- package/dist/src/browser/wait.js +122 -0
- package/dist/src/browser/wait.js.map +1 -0
- package/dist/src/commands/browse.d.ts +8 -0
- package/dist/src/commands/browse.d.ts.map +1 -0
- package/dist/src/commands/browse.js +573 -0
- package/dist/src/commands/browse.js.map +1 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/ui/dashboard-v2.html +1692 -0
- package/dist/src/ui/server.mjs +15 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
|
@@ -158,6 +158,9 @@ function matchScore(promptWords, entryWords) {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
var cachedEntries = null;
|
|
161
|
+
// Module-level state for feedback correlation
|
|
162
|
+
var _lastContext = null; // stores last prompt string from getContext() calls
|
|
163
|
+
var _recentEdits = []; // ring buffer of recent file edits (max 50)
|
|
161
164
|
|
|
162
165
|
module.exports = {
|
|
163
166
|
init: function() {
|
|
@@ -192,15 +195,31 @@ module.exports = {
|
|
|
192
195
|
var summary = (e.entry.summary || e.entry.content || "").substring(0, 80);
|
|
193
196
|
lines.push(" * (" + conf.toFixed(2) + ") " + summary);
|
|
194
197
|
}
|
|
198
|
+
_lastContext = prompt; // capture for feedback() correlation
|
|
195
199
|
return lines.join("\n");
|
|
196
200
|
},
|
|
197
201
|
|
|
198
202
|
recordEdit: function(file) {
|
|
199
|
-
//
|
|
203
|
+
// Track recent edits for context relevance adjustment (ring buffer, max 50)
|
|
204
|
+
_recentEdits.push({ filePath: file, ts: Date.now() });
|
|
205
|
+
if (_recentEdits.length > 50) _recentEdits.shift();
|
|
200
206
|
},
|
|
201
207
|
|
|
202
208
|
feedback: function(success) {
|
|
203
|
-
//
|
|
209
|
+
// Append outcome to intelligence-outcomes.jsonl. At session-end,
|
|
210
|
+
// session-handler.cjs reads this file (30-minute window, majority-vote)
|
|
211
|
+
// to derive intelligenceFeedback for routing-feedback.jsonl.
|
|
212
|
+
try {
|
|
213
|
+
var outPath = path.join(DATA_DIR, 'intelligence-outcomes.jsonl');
|
|
214
|
+
ensureDir(DATA_DIR);
|
|
215
|
+
var entry = JSON.stringify({
|
|
216
|
+
ts: Date.now(),
|
|
217
|
+
success: success,
|
|
218
|
+
context: _lastContext ? _lastContext.substring(0, 200) : null,
|
|
219
|
+
recentEdits: _recentEdits.slice(-5).map(function(e) { return e.filePath; }),
|
|
220
|
+
});
|
|
221
|
+
fs.appendFileSync(outPath, entry + '\n', 'utf-8');
|
|
222
|
+
} catch (e) { /* non-critical — must not break session-end hook */ }
|
|
204
223
|
},
|
|
205
224
|
|
|
206
225
|
consolidate: function() {
|
|
@@ -113,7 +113,9 @@ function initializeDatabase(db) {
|
|
|
113
113
|
metadata TEXT
|
|
114
114
|
);
|
|
115
115
|
|
|
116
|
-
-- HNSW index
|
|
116
|
+
-- HNSW index per-pattern mapping (unused: full index snapshots are stored in
|
|
117
|
+
-- session_state under keys hnsw_short_term/hnsw_long_term as JSON blobs.
|
|
118
|
+
-- This table exists for future per-vector indexing if needed.)
|
|
117
119
|
CREATE TABLE IF NOT EXISTS hnsw_index (
|
|
118
120
|
id INTEGER PRIMARY KEY,
|
|
119
121
|
pattern_type TEXT NOT NULL, -- 'short_term' or 'long_term'
|
|
@@ -168,6 +170,20 @@ function initializeDatabase(db) {
|
|
|
168
170
|
// HNSW Index (In-Memory with SQLite persistence)
|
|
169
171
|
// =============================================================================
|
|
170
172
|
|
|
173
|
+
// TODO: Replace HNSWIndex with shared HnswLite from @monomind/memory once
|
|
174
|
+
// API differences are resolved. Migration notes:
|
|
175
|
+
// - HnswLite constructor: (dimensions: number, m: number, efConstruction: number, metric: string)
|
|
176
|
+
// vs HNSWIndex constructor: (config: { embedding: { dimension }, hnsw: { m, efConstruction } })
|
|
177
|
+
// - HnswLite.add(id: string, vector: Float32Array): void
|
|
178
|
+
// vs HNSWIndex.add(patternId, embedding): vectorId (numeric)
|
|
179
|
+
// - HnswLite.search(query: Float32Array, k: number, threshold?): { id, score }[]
|
|
180
|
+
// vs HNSWIndex.search(queryEmbedding, k): { results: { patternId, similarity, vectorId }[], searchTimeMs }
|
|
181
|
+
// - HnswLite.remove(id: string): void (no return)
|
|
182
|
+
// vs HNSWIndex.remove(patternId): boolean
|
|
183
|
+
// - dist/hnsw-lite.js exists at packages/@monomind/memory/dist/hnsw-lite.js
|
|
184
|
+
// - Import: import { HnswLite } from '../../packages/@monomind/memory/dist/hnsw-lite.js'
|
|
185
|
+
// Distance metric: HNSWIndex uses cosine similarity (1 - distance) in _searchGraph;
|
|
186
|
+
// HnswLite defaults to cosineSimilarity — compatible. See D3 comment in hnsw-lite.ts.
|
|
171
187
|
class HNSWIndex {
|
|
172
188
|
constructor(config) {
|
|
173
189
|
this.config = config;
|
|
@@ -565,6 +581,15 @@ class EmbeddingService {
|
|
|
565
581
|
|
|
566
582
|
// =============================================================================
|
|
567
583
|
// Learning Service
|
|
584
|
+
//
|
|
585
|
+
// Singleton contract (enforced by hook-handler.cjs::getLearningService()):
|
|
586
|
+
// - One LearningService instance is created per hook-handler process.
|
|
587
|
+
// - initialize() opens the SQLite DB at DB_PATH and loads HNSW indexes;
|
|
588
|
+
// it must be called exactly once after construction.
|
|
589
|
+
// - consolidate() is called at session-end to prune + promote patterns;
|
|
590
|
+
// it operates on the already-open DB — no re-initialization needed.
|
|
591
|
+
// - The constructor intentionally leaves all fields null until initialize()
|
|
592
|
+
// is called; callers must not invoke other methods before initialize().
|
|
568
593
|
// =============================================================================
|
|
569
594
|
|
|
570
595
|
class LearningService {
|
|
@@ -658,6 +683,9 @@ class LearningService {
|
|
|
658
683
|
// Check if we need to prune
|
|
659
684
|
this._pruneShortTerm();
|
|
660
685
|
|
|
686
|
+
// Persist index snapshot non-blocking
|
|
687
|
+
this._saveIndexes().catch(e => console.error('[LearningService] saveIndexes error:', e));
|
|
688
|
+
|
|
661
689
|
return { id, action: 'created', embedding: Array.from(embedding).slice(0, 5) };
|
|
662
690
|
}
|
|
663
691
|
|
|
@@ -817,21 +845,32 @@ class LearningService {
|
|
|
817
845
|
// 2. Rebuild indexes
|
|
818
846
|
await this._loadIndexes();
|
|
819
847
|
|
|
820
|
-
// 3. Remove duplicates in long-term
|
|
848
|
+
// 3. Remove duplicates in long-term (simhash-bucketed to avoid O(n²) cosine)
|
|
821
849
|
const longTermPatterns = this.db.prepare('SELECT * FROM long_term_patterns').all();
|
|
850
|
+
|
|
851
|
+
// Build simhash fingerprints for all patterns
|
|
852
|
+
const fingerprints = new Map();
|
|
853
|
+
for (const p of longTermPatterns) {
|
|
854
|
+
fingerprints.set(p.id, this._simhash(p.strategy || ''));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Near-dup detection: only compare pairs with Hamming distance < 4
|
|
822
858
|
for (let i = 0; i < longTermPatterns.length; i++) {
|
|
823
859
|
for (let j = i + 1; j < longTermPatterns.length; j++) {
|
|
860
|
+
const hammingDist = this._hammingDistance(
|
|
861
|
+
fingerprints.get(longTermPatterns[i].id),
|
|
862
|
+
fingerprints.get(longTermPatterns[j].id)
|
|
863
|
+
);
|
|
864
|
+
if (hammingDist >= 4) continue; // Skip dissimilar pairs (fast path)
|
|
824
865
|
const sim = this._cosineSimilarity(
|
|
825
866
|
this._bufferToFloat32Array(longTermPatterns[i].embedding),
|
|
826
867
|
this._bufferToFloat32Array(longTermPatterns[j].embedding)
|
|
827
868
|
);
|
|
828
|
-
|
|
829
869
|
if (sim > CONFIG.patterns.dedupThreshold) {
|
|
830
870
|
// Keep the higher quality one
|
|
831
871
|
const toRemove = longTermPatterns[i].quality >= longTermPatterns[j].quality
|
|
832
872
|
? longTermPatterns[j].id
|
|
833
873
|
: longTermPatterns[i].id;
|
|
834
|
-
|
|
835
874
|
this.db.prepare('DELETE FROM long_term_patterns WHERE id = ?').run(toRemove);
|
|
836
875
|
stats.duplicatesRemoved++;
|
|
837
876
|
}
|
|
@@ -851,10 +890,74 @@ class LearningService {
|
|
|
851
890
|
|
|
852
891
|
this.metrics.consolidations++;
|
|
853
892
|
|
|
893
|
+
// 5. Promote eligible episodic patterns to semantic long-term memory
|
|
894
|
+
const promResult = await this.promoteEpisodic();
|
|
895
|
+
if (promResult.promoted > 0) {
|
|
896
|
+
console.log(`[Learning] Promoted ${promResult.promoted} episodic pattern(s) to semantic memory`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Persist updated index snapshot non-blocking
|
|
900
|
+
this._saveIndexes().catch(e => console.error('[LearningService] saveIndexes error:', e));
|
|
901
|
+
|
|
854
902
|
const duration = Date.now() - startTime;
|
|
855
903
|
console.log(`[Learning] Consolidation complete in ${duration}ms:`, stats);
|
|
856
904
|
|
|
857
|
-
return { ...stats, durationMs: duration };
|
|
905
|
+
return { ...stats, promoted: promResult.promoted, durationMs: duration };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Promote short-term episodic patterns to long-term semantic memory
|
|
909
|
+
async promoteEpisodic() {
|
|
910
|
+
const threshold = CONFIG.patterns.promotionThreshold;
|
|
911
|
+
const candidates = this.db.prepare(`
|
|
912
|
+
SELECT * FROM short_term_patterns
|
|
913
|
+
WHERE usage_count >= ?
|
|
914
|
+
ORDER BY quality DESC
|
|
915
|
+
`).all(threshold);
|
|
916
|
+
|
|
917
|
+
let promoted = 0;
|
|
918
|
+
for (const pattern of candidates) {
|
|
919
|
+
try {
|
|
920
|
+
// Check if already in long-term (using source_pattern_id link)
|
|
921
|
+
const exists = this.db.prepare('SELECT id FROM long_term_patterns WHERE source_pattern_id = ? OR id = ?').get(pattern.id, `lt_${pattern.id}`);
|
|
922
|
+
if (exists) {
|
|
923
|
+
// Remove from short-term since it was already promoted
|
|
924
|
+
this.db.prepare('DELETE FROM short_term_patterns WHERE id = ?').run(pattern.id);
|
|
925
|
+
this.shortTermIndex.remove(pattern.id);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const now = Date.now();
|
|
930
|
+
const ltId = `lt_${pattern.id}`;
|
|
931
|
+
|
|
932
|
+
// Promote to long-term
|
|
933
|
+
this.db.prepare(`
|
|
934
|
+
INSERT OR IGNORE INTO long_term_patterns
|
|
935
|
+
(id, strategy, domain, embedding, quality, usage_count, success_count,
|
|
936
|
+
created_at, updated_at, promoted_at, source_pattern_id, metadata)
|
|
937
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
938
|
+
`).run(
|
|
939
|
+
ltId, pattern.strategy, pattern.domain, pattern.embedding,
|
|
940
|
+
pattern.quality, pattern.usage_count, pattern.success_count || 0,
|
|
941
|
+
pattern.created_at, now, now, pattern.id, pattern.metadata
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
// Add to long-term HNSW index
|
|
945
|
+
const embedding = this._bufferToFloat32Array(pattern.embedding);
|
|
946
|
+
if (embedding) this.longTermIndex.add(ltId, embedding);
|
|
947
|
+
|
|
948
|
+
// Remove from short-term
|
|
949
|
+
this.db.prepare('DELETE FROM short_term_patterns WHERE id = ?').run(pattern.id);
|
|
950
|
+
this.shortTermIndex.remove(pattern.id);
|
|
951
|
+
promoted++;
|
|
952
|
+
} catch (e) { /* skip individual failures */ }
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (promoted > 0) {
|
|
956
|
+
this.metrics.promotions += promoted;
|
|
957
|
+
this._saveIndexes().catch(e => console.error('[LearningService] saveIndexes error:', e));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return { promoted };
|
|
858
961
|
}
|
|
859
962
|
|
|
860
963
|
// Export learning data for session end
|
|
@@ -903,9 +1006,41 @@ class LearningService {
|
|
|
903
1006
|
};
|
|
904
1007
|
}
|
|
905
1008
|
|
|
906
|
-
//
|
|
1009
|
+
// Save full HNSW index snapshots to session_state (key-value TEXT store).
|
|
1010
|
+
// Using session_state (not hnsw_index) because session_state stores arbitrary blobs;
|
|
1011
|
+
// hnsw_index stores per-pattern mappings and is not designed for full snapshots.
|
|
1012
|
+
async _saveIndexes() {
|
|
1013
|
+
try {
|
|
1014
|
+
const shortTermData = JSON.stringify(this.shortTermIndex.serialize());
|
|
1015
|
+
const longTermData = JSON.stringify(this.longTermIndex.serialize());
|
|
1016
|
+
this.db.prepare(`
|
|
1017
|
+
INSERT OR REPLACE INTO session_state (key, value, updated_at)
|
|
1018
|
+
VALUES ('hnsw_short_term', ?, ?)
|
|
1019
|
+
`).run(shortTermData, Date.now());
|
|
1020
|
+
this.db.prepare(`
|
|
1021
|
+
INSERT OR REPLACE INTO session_state (key, value, updated_at)
|
|
1022
|
+
VALUES ('hnsw_long_term', ?, ?)
|
|
1023
|
+
`).run(longTermData, Date.now());
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
console.error('[LearningService] Failed to save HNSW indexes:', e.message);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Load indexes from database — try serialized snapshot first, fall back to rebuild
|
|
907
1030
|
async _loadIndexes() {
|
|
908
|
-
|
|
1031
|
+
try {
|
|
1032
|
+
const shortRow = this.db.prepare("SELECT value FROM session_state WHERE key = 'hnsw_short_term'").get();
|
|
1033
|
+
const longRow = this.db.prepare("SELECT value FROM session_state WHERE key = 'hnsw_long_term'").get();
|
|
1034
|
+
if (shortRow && longRow) {
|
|
1035
|
+
this.shortTermIndex = HNSWIndex.deserialize(JSON.parse(shortRow.value), CONFIG);
|
|
1036
|
+
this.longTermIndex = HNSWIndex.deserialize(JSON.parse(longRow.value), CONFIG);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
} catch (e) {
|
|
1040
|
+
console.error('[LearningService] Failed to restore HNSW snapshots, rebuilding:', e.message);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Rebuild from patterns tables (fallback)
|
|
909
1044
|
this.shortTermIndex = new HNSWIndex(CONFIG);
|
|
910
1045
|
const shortTermPatterns = this.db.prepare('SELECT id, embedding FROM short_term_patterns').all();
|
|
911
1046
|
for (const row of shortTermPatterns) {
|
|
@@ -915,7 +1050,6 @@ class LearningService {
|
|
|
915
1050
|
}
|
|
916
1051
|
}
|
|
917
1052
|
|
|
918
|
-
// Load long-term patterns
|
|
919
1053
|
this.longTermIndex = new HNSWIndex(CONFIG);
|
|
920
1054
|
const longTermPatterns = this.db.prepare('SELECT id, embedding FROM long_term_patterns').all();
|
|
921
1055
|
for (const row of longTermPatterns) {
|
|
@@ -959,6 +1093,30 @@ class LearningService {
|
|
|
959
1093
|
`).run(key, value, Date.now());
|
|
960
1094
|
}
|
|
961
1095
|
|
|
1096
|
+
// 32-bit simhash via bit-weighting of character bigrams
|
|
1097
|
+
_simhash(text) {
|
|
1098
|
+
const bits = new Int32Array(32);
|
|
1099
|
+
const chars = text.toLowerCase().replace(/\s+/g, ' ');
|
|
1100
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
1101
|
+
const bigram = chars.charCodeAt(i) * 31 + chars.charCodeAt(i + 1);
|
|
1102
|
+
for (let b = 0; b < 32; b++) {
|
|
1103
|
+
bits[b] += (bigram >> b) & 1 ? 1 : -1;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
let hash = 0;
|
|
1107
|
+
for (let b = 0; b < 32; b++) {
|
|
1108
|
+
if (bits[b] > 0) hash |= (1 << b);
|
|
1109
|
+
}
|
|
1110
|
+
return hash >>> 0; // unsigned
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
_hammingDistance(a, b) {
|
|
1114
|
+
let x = a ^ b;
|
|
1115
|
+
let count = 0;
|
|
1116
|
+
while (x) { count += x & 1; x >>>= 1; }
|
|
1117
|
+
return count;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
962
1120
|
// Cosine similarity helper
|
|
963
1121
|
_cosineSimilarity(a, b) {
|
|
964
1122
|
let dot = 0, normA = 0, normB = 0;
|
|
@@ -199,23 +199,83 @@ function storeVerbatim(cwd, content, meta) {
|
|
|
199
199
|
// ── score persistence ─────────────────────────────────────────────────────────
|
|
200
200
|
/**
|
|
201
201
|
* _bumpScores(drawersFile, ids)
|
|
202
|
-
*
|
|
202
|
+
* Appends score bumps to a sidecar .score-diffs.jsonl instead of rewriting
|
|
203
|
+
* the full drawers.jsonl on every search. Compacts when sidecar reaches 100 entries.
|
|
203
204
|
* Called after search/recall so frequently-retrieved drawers rise to L1.
|
|
204
205
|
*/
|
|
205
206
|
function _bumpScores(drawersFile, ids) {
|
|
206
207
|
if (!ids || ids.length === 0) return;
|
|
207
|
-
var
|
|
208
|
-
|
|
208
|
+
var diffPath = drawersFile.replace('.jsonl', '-score-diffs.jsonl');
|
|
209
|
+
try {
|
|
210
|
+
var entry = JSON.stringify({ ts: Date.now(), bumpIds: ids, delta: 1 });
|
|
211
|
+
fs.appendFileSync(diffPath, entry + '\n', 'utf-8');
|
|
212
|
+
_maybeCompactScoreDiffs(drawersFile, diffPath);
|
|
213
|
+
} catch (e) { /* non-fatal */ }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* _maybeCompactScoreDiffs(drawersFile, diffPath)
|
|
218
|
+
* When the sidecar accumulates >= 100 entries, apply all diffs to the drawers
|
|
219
|
+
* file atomically and delete the sidecar.
|
|
220
|
+
*/
|
|
221
|
+
function _maybeCompactScoreDiffs(drawersFile, diffPath) {
|
|
222
|
+
var lines;
|
|
223
|
+
try {
|
|
224
|
+
lines = fs.readFileSync(diffPath, 'utf-8').split('\n').filter(Boolean);
|
|
225
|
+
} catch { return; }
|
|
226
|
+
if (lines.length < 100) return;
|
|
227
|
+
_applyScoreDiffs(drawersFile, diffPath, lines);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* _applyScoreDiffs(drawersFile, diffPath, lines)
|
|
232
|
+
* Apply accumulated score diffs to drawers.jsonl atomically, then remove sidecar.
|
|
233
|
+
*/
|
|
234
|
+
function _applyScoreDiffs(drawersFile, diffPath, lines) {
|
|
209
235
|
try {
|
|
210
236
|
var drawers = readJsonl(drawersFile);
|
|
211
|
-
var
|
|
237
|
+
var diffs = (lines || fs.readFileSync(diffPath, 'utf-8').split('\n').filter(Boolean))
|
|
238
|
+
.map(function(l) { try { return JSON.parse(l); } catch { return null; } })
|
|
239
|
+
.filter(Boolean);
|
|
240
|
+
for (var i = 0; i < diffs.length; i++) {
|
|
241
|
+
var diff = diffs[i];
|
|
242
|
+
var delta = diff.delta || 1;
|
|
243
|
+
(diff.bumpIds || []).forEach(function(id) {
|
|
244
|
+
var d = drawers.find(function(x) { return x.id === id; });
|
|
245
|
+
if (d) d.score = (d.score || 1.0) + delta;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
var tmpPath = drawersFile + '.tmp';
|
|
249
|
+
fs.writeFileSync(tmpPath, drawers.map(function(d) { return JSON.stringify(d); }).join('\n') + '\n', 'utf-8');
|
|
250
|
+
fs.renameSync(tmpPath, drawersFile);
|
|
251
|
+
try { fs.unlinkSync(diffPath); } catch {}
|
|
252
|
+
} catch (e) { /* non-fatal — leave sidecar intact for next attempt */ }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* _getEffectiveDrawers(drawersFile)
|
|
257
|
+
* Read drawers.jsonl and apply any pending score diffs from the sidecar,
|
|
258
|
+
* returning the merged result without compacting. Used by wakeUp/recall/search
|
|
259
|
+
* to get accurate scores without triggering a full compact.
|
|
260
|
+
*/
|
|
261
|
+
function _getEffectiveDrawers(drawersFile) {
|
|
262
|
+
var drawers = readJsonl(drawersFile);
|
|
263
|
+
var diffPath = drawersFile.replace('.jsonl', '-score-diffs.jsonl');
|
|
264
|
+
try {
|
|
265
|
+
var lines = fs.readFileSync(diffPath, 'utf-8').split('\n').filter(Boolean);
|
|
266
|
+
var diffs = lines.map(function(l) { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
267
|
+
// Build score bump totals per id
|
|
268
|
+
var bumps = {};
|
|
269
|
+
diffs.forEach(function(diff) {
|
|
270
|
+
(diff.bumpIds || []).forEach(function(id) {
|
|
271
|
+
bumps[id] = (bumps[id] || 0) + (diff.delta || 1);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
212
274
|
drawers.forEach(function(d) {
|
|
213
|
-
if (
|
|
275
|
+
if (bumps[d.id]) d.score = (d.score || 1.0) + bumps[d.id];
|
|
214
276
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
} catch (e) { /* non-fatal */ }
|
|
277
|
+
} catch { /* sidecar absent — use raw drawers */ }
|
|
278
|
+
return drawers;
|
|
219
279
|
}
|
|
220
280
|
|
|
221
281
|
// ── search (L3 deep) ──────────────────────────────────────────────────────────
|
|
@@ -229,7 +289,7 @@ function search(cwd, query, opts) {
|
|
|
229
289
|
var limit = opts.limit || 5;
|
|
230
290
|
var pDir = palaceDir(cwd);
|
|
231
291
|
var drawersFile = path.join(pDir, 'drawers.jsonl');
|
|
232
|
-
var drawers =
|
|
292
|
+
var drawers = _getEffectiveDrawers(drawersFile);
|
|
233
293
|
|
|
234
294
|
if (opts.wing) drawers = drawers.filter(function(d) { return d.wing === opts.wing; });
|
|
235
295
|
if (opts.room) drawers = drawers.filter(function(d) { return d.room === opts.room; });
|
|
@@ -273,7 +333,7 @@ function recall(cwd, opts) {
|
|
|
273
333
|
opts = opts || {};
|
|
274
334
|
var limit = opts.limit || 10;
|
|
275
335
|
var drawersFile = path.join(palaceDir(cwd), 'drawers.jsonl');
|
|
276
|
-
var drawers =
|
|
336
|
+
var drawers = _getEffectiveDrawers(drawersFile);
|
|
277
337
|
if (opts.wing) drawers = drawers.filter(function(d) { return d.wing === opts.wing; });
|
|
278
338
|
if (opts.room) drawers = drawers.filter(function(d) { return d.room === opts.room; });
|
|
279
339
|
var top = drawers
|
|
@@ -363,7 +423,7 @@ function wakeUp(cwd) {
|
|
|
363
423
|
var drawersFile = path.join(pDir, 'drawers.jsonl');
|
|
364
424
|
if (fs.existsSync(drawersFile)) {
|
|
365
425
|
try {
|
|
366
|
-
var drawers =
|
|
426
|
+
var drawers = _getEffectiveDrawers(drawersFile);
|
|
367
427
|
var cutoff = Date.now() - L1_DAYS * 24 * 60 * 60 * 1000;
|
|
368
428
|
var recent = drawers.filter(function(d) {
|
|
369
429
|
return d.ts && new Date(d.ts).getTime() > cutoff;
|
|
@@ -395,6 +395,73 @@ async function routeTaskSemantic(task) {
|
|
|
395
395
|
return keywordResult;
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
// ─── Feedback weight loader ───────────────────────────────────────────────────
|
|
399
|
+
// Reads .monomind/routing-feedback.jsonl to compute per-agent success-rate
|
|
400
|
+
// weights. Weights are memoized with a 60-second TTL.
|
|
401
|
+
//
|
|
402
|
+
// Signal source: routing-feedback.jsonl entries with a non-null intelligenceFeedback
|
|
403
|
+
// boolean. Derived at session-end from intelligence-outcomes.jsonl (30-minute window,
|
|
404
|
+
// majority-vote). Requires MIN_FEEDBACK_SAMPLES entries per agent to activate.
|
|
405
|
+
//
|
|
406
|
+
// To verify: copy .monomind/test-fixtures/routing-feedback-seeded.jsonl to
|
|
407
|
+
// .monomind/routing-feedback.jsonl, then call loadFeedbackWeights() in a REPL.
|
|
408
|
+
|
|
409
|
+
var _feedbackWeightsCache = null;
|
|
410
|
+
var _feedbackWeightsCacheTime = 0;
|
|
411
|
+
var FEEDBACK_CACHE_TTL_MS = 60000;
|
|
412
|
+
var MIN_FEEDBACK_SAMPLES = 10;
|
|
413
|
+
|
|
414
|
+
function loadFeedbackWeights() {
|
|
415
|
+
var now = Date.now();
|
|
416
|
+
if (_feedbackWeightsCache && now - _feedbackWeightsCacheTime < FEEDBACK_CACHE_TTL_MS) {
|
|
417
|
+
return _feedbackWeightsCache;
|
|
418
|
+
}
|
|
419
|
+
var feedbackPath = path.join(process.cwd(), '.monomind', 'routing-feedback.jsonl');
|
|
420
|
+
if (!fs.existsSync(feedbackPath)) {
|
|
421
|
+
_feedbackWeightsCache = new Map();
|
|
422
|
+
_feedbackWeightsCacheTime = now;
|
|
423
|
+
return _feedbackWeightsCache;
|
|
424
|
+
}
|
|
425
|
+
var lines;
|
|
426
|
+
try {
|
|
427
|
+
lines = fs.readFileSync(feedbackPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
428
|
+
} catch {
|
|
429
|
+
_feedbackWeightsCache = new Map();
|
|
430
|
+
_feedbackWeightsCacheTime = now;
|
|
431
|
+
return _feedbackWeightsCache;
|
|
432
|
+
}
|
|
433
|
+
var agentStats = {};
|
|
434
|
+
for (var i = 0; i < lines.length; i++) {
|
|
435
|
+
try {
|
|
436
|
+
var entry = JSON.parse(lines[i]);
|
|
437
|
+
var agent = entry.suggestedAgent || entry.agent;
|
|
438
|
+
if (!agent) continue;
|
|
439
|
+
if (!agentStats[agent]) agentStats[agent] = { total: 0, successTotal: 0, withSignal: 0 };
|
|
440
|
+
agentStats[agent].total++;
|
|
441
|
+
// Only count entries where intelligenceFeedback is non-null as having signal
|
|
442
|
+
var fb = entry.intelligenceFeedback;
|
|
443
|
+
if (fb !== null && fb !== undefined) {
|
|
444
|
+
agentStats[agent].withSignal++;
|
|
445
|
+
var success = fb === true || (fb && fb.success === true);
|
|
446
|
+
if (success) agentStats[agent].successTotal++;
|
|
447
|
+
}
|
|
448
|
+
} catch { /* skip malformed lines */ }
|
|
449
|
+
}
|
|
450
|
+
var weights = new Map();
|
|
451
|
+
for (var agentName in agentStats) {
|
|
452
|
+
var stats = agentStats[agentName];
|
|
453
|
+
// Only activate weight when we have enough non-null signal entries
|
|
454
|
+
if (stats.withSignal >= MIN_FEEDBACK_SAMPLES) {
|
|
455
|
+
var rate = stats.successTotal / stats.withSignal;
|
|
456
|
+
// Clamp to [0.5, 1.5]: bad agents get penalized, good agents boosted slightly
|
|
457
|
+
weights.set(agentName, Math.max(0.5, Math.min(1.5, rate * 2)));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
_feedbackWeightsCache = weights;
|
|
461
|
+
_feedbackWeightsCacheTime = now;
|
|
462
|
+
return weights;
|
|
463
|
+
}
|
|
464
|
+
|
|
398
465
|
// ─── Main routing ─────────────────────────────────────────────────────────────
|
|
399
466
|
function routeTask(task) {
|
|
400
467
|
if (typeof task !== 'string' || !task) {
|
|
@@ -419,14 +486,19 @@ function routeTask(task) {
|
|
|
419
486
|
};
|
|
420
487
|
}
|
|
421
488
|
|
|
489
|
+
var feedbackWeights = loadFeedbackWeights();
|
|
490
|
+
|
|
422
491
|
// Dev task pattern matching
|
|
423
492
|
for (const [pattern, agent] of Object.entries(TASK_PATTERNS)) {
|
|
424
493
|
const regex = new RegExp(pattern, 'i');
|
|
425
494
|
if (regex.test(taskLower)) {
|
|
495
|
+
// Apply feedback weight to confidence (defaults to 1.0 = no change)
|
|
496
|
+
var baseConfidence = 0.8;
|
|
497
|
+
var weight = feedbackWeights.get(agent) || 1.0;
|
|
426
498
|
return {
|
|
427
499
|
agent,
|
|
428
500
|
agentSlug: agent,
|
|
429
|
-
confidence: 0
|
|
501
|
+
confidence: Math.min(1.0, baseConfidence * weight),
|
|
430
502
|
reason: `Matched pattern: ${pattern}`,
|
|
431
503
|
skillMatches: matchSkills(task),
|
|
432
504
|
extrasMatches: [],
|
|
@@ -436,14 +508,16 @@ function routeTask(task) {
|
|
|
436
508
|
}
|
|
437
509
|
|
|
438
510
|
// Default — low confidence, show both skill and extras suggestions
|
|
511
|
+
var defaultAgent = 'coder';
|
|
512
|
+
var defaultWeight = feedbackWeights.get(defaultAgent) || 1.0;
|
|
439
513
|
return {
|
|
440
|
-
agent:
|
|
441
|
-
agentSlug:
|
|
442
|
-
confidence: 0.5,
|
|
514
|
+
agent: defaultAgent,
|
|
515
|
+
agentSlug: defaultAgent,
|
|
516
|
+
confidence: Math.min(1.0, 0.5 * defaultWeight),
|
|
443
517
|
reason: 'Default routing - no specific pattern matched',
|
|
444
518
|
skillMatches: matchSkills(task),
|
|
445
519
|
extrasMatches: matchExtras(task),
|
|
446
|
-
specificAgents: SPECIFIC_AGENTS_MAP[
|
|
520
|
+
specificAgents: SPECIFIC_AGENTS_MAP[defaultAgent] || [],
|
|
447
521
|
};
|
|
448
522
|
}
|
|
449
523
|
|