@try-works/dsh-memory 0.1.29 → 0.1.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/package.json +1 -1
- package/src/inject.js +28 -3
- package/src/ledger.js +47 -19
- package/src/search.js +4 -2
package/package.json
CHANGED
package/src/inject.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Session-start injection text: goal + working head + recent learnings + footer,
|
|
2
2
|
// char-capped and scope-aware.
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { layerDir } from './store.js'
|
|
6
|
-
import {
|
|
6
|
+
import { buildLedgerFor } from './ledger.js'
|
|
7
7
|
import { readLayerTopics } from './layers.js'
|
|
8
8
|
import { freshnessMarker, readState } from './state.js'
|
|
9
9
|
import { readWorking, workingMarker } from './working.js'
|
|
@@ -12,7 +12,11 @@ import { relevanceQuery, selectEntries } from './select.js'
|
|
|
12
12
|
export function buildInjectionText(store, { scope = 'global', key, goal, maxChars = 3600, subagent = false } = {}) {
|
|
13
13
|
const dir = layerDir(store.root, scope, key)
|
|
14
14
|
// Working memory is the AGENT's intent (task, recent activity, next steps); repository
|
|
15
|
-
|
|
15
|
+
//
|
|
16
|
+
// Only the two layers this card actually quotes are read. Building the whole ledger here meant
|
|
17
|
+
// walking every indexed file - ~8,000 of them, history shards included - on every injection,
|
|
18
|
+
// which measured ~17 s and dwarfed the text it produced.
|
|
19
|
+
const ledger = buildLedgerFor(store.root, layerFilesFor(dir, ['learnings', 'decisions']))
|
|
16
20
|
const prefix = scope === 'global' ? 'global/' : 'workspaces/' + key + '/'
|
|
17
21
|
// The pools are LISTED here and CHOSEN later, because choosing needs the question, and the
|
|
18
22
|
// question comes from working memory - which is read below.
|
|
@@ -106,3 +110,24 @@ export function buildInjectionText(store, { scope = 'global', key, goal, maxChar
|
|
|
106
110
|
const body = parts.join('\n\n')
|
|
107
111
|
return (body.length <= maxChars ? body : body.slice(0, maxChars)) + '\n\n' + footer
|
|
108
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The absolute file paths of the named layer directories in one scope.
|
|
116
|
+
*
|
|
117
|
+
* Layer directories are flat (one topic file per entry), so listing them is cheap. This exists
|
|
118
|
+
* because the injection card quotes only learnings and decisions: reading every OTHER layer -
|
|
119
|
+
* semantic, episodic, procedural, codebase, history - was the expensive part of the old
|
|
120
|
+
* whole-store ledger, and none of it could ever appear in the card.
|
|
121
|
+
* @param dir - the scope directory (a layer dir already), from {@link layerDir}.
|
|
122
|
+
* @param layers - directory names under it, e.g. 'learnings' and 'decisions'.
|
|
123
|
+
* @returns absolute paths, in directory order.
|
|
124
|
+
*/
|
|
125
|
+
function layerFilesFor(dir, layers) {
|
|
126
|
+
const files = []
|
|
127
|
+
for (const name of layers) {
|
|
128
|
+
const layerPath = join(dir, name)
|
|
129
|
+
if (!existsSync(layerPath)) continue
|
|
130
|
+
for (const file of readdirSync(layerPath)) if (file.endsWith('.md')) files.push(join(layerPath, file))
|
|
131
|
+
}
|
|
132
|
+
return files
|
|
133
|
+
}
|
package/src/ledger.js
CHANGED
|
@@ -1,19 +1,47 @@
|
|
|
1
|
-
// Derived entry ledger: per-entry metadata (file, layer, line span, importance,
|
|
2
|
-
// dates) rebuilt from the markdown in one step. Feeds ranking + freshness.
|
|
3
|
-
import { readFileSync } from 'node:fs'
|
|
4
|
-
import { relative } from 'node:path'
|
|
5
|
-
import { indexedFiles, layerOfFile } from './files.js'
|
|
6
|
-
import { parseEntries } from './entry.js'
|
|
7
|
-
|
|
8
|
-
export function buildLedger(root) {
|
|
9
|
-
const entries = []
|
|
10
|
-
for (const file of indexedFiles(root)) {
|
|
11
|
-
const rel = relative(root, file).replace(/\\/g, '/')
|
|
12
|
-
const layer = layerOfFile(root, file)
|
|
13
|
-
if (!layer) continue
|
|
14
|
-
for (const e of parseEntries(readFileSync(file, 'utf8'))) {
|
|
15
|
-
entries.push({ ...e, file: rel, layer })
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return entries
|
|
19
|
-
}
|
|
1
|
+
// Derived entry ledger: per-entry metadata (file, layer, line span, importance,
|
|
2
|
+
// dates) rebuilt from the markdown in one step. Feeds ranking + freshness.
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { isAbsolute, join, relative, sep } from 'node:path'
|
|
5
|
+
import { indexedFiles, layerOfFile } from './files.js'
|
|
6
|
+
import { parseEntries } from './entry.js'
|
|
7
|
+
|
|
8
|
+
export function buildLedger(root) {
|
|
9
|
+
const entries = []
|
|
10
|
+
for (const file of indexedFiles(root)) {
|
|
11
|
+
const rel = relative(root, file).replace(/\\/g, '/')
|
|
12
|
+
const layer = layerOfFile(root, file)
|
|
13
|
+
if (!layer) continue
|
|
14
|
+
for (const e of parseEntries(readFileSync(file, 'utf8'))) {
|
|
15
|
+
entries.push({ ...e, file: rel, layer })
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return entries
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build entries for a KNOWN SET of files instead of the whole store.
|
|
23
|
+
*
|
|
24
|
+
* The full ledger reads every indexed file, and on this store that is ~8,000 files and ~17 s —
|
|
25
|
+
* far too expensive for a call that only needs a handful. Both callers below know which files
|
|
26
|
+
* can possibly matter: ranking needs the files that produced a hit, and the injection card needs
|
|
27
|
+
* only the two layers it quotes. Reading the rest was pure I/O.
|
|
28
|
+
* @param files - paths, absolute or root-relative with forward slashes; duplicates are ignored.
|
|
29
|
+
* @returns the same entry shape as {@link buildLedger}, for the files that resolved to a layer.
|
|
30
|
+
*/
|
|
31
|
+
export function buildLedgerFor(root, files) {
|
|
32
|
+
const entries = []
|
|
33
|
+
const seen = new Set()
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
if (!file) continue
|
|
36
|
+
const abs = isAbsolute(file) ? file : join(root, String(file).split('/').join(sep))
|
|
37
|
+
const rel = relative(root, abs).replace(/\\/g, '/')
|
|
38
|
+
if (seen.has(rel)) continue
|
|
39
|
+
seen.add(rel)
|
|
40
|
+
const layer = layerOfFile(root, abs)
|
|
41
|
+
if (!layer) continue
|
|
42
|
+
let text
|
|
43
|
+
try { text = readFileSync(abs, 'utf8') } catch { continue }
|
|
44
|
+
for (const e of parseEntries(text)) entries.push({ ...e, file: rel, layer })
|
|
45
|
+
}
|
|
46
|
+
return entries
|
|
47
|
+
}
|
package/src/search.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
8
|
import { DatabaseSync } from 'node:sqlite'
|
|
9
9
|
import { buildFtsQuery, buildIndex, buildLikeQuery, buildTrigramQuery, indexPath, needsSubstring, SUBSTRING_TABLE } from './fts.js'
|
|
10
|
-
import {
|
|
10
|
+
import { buildLedgerFor } from './ledger.js'
|
|
11
11
|
import { rankBlend } from './rank.js'
|
|
12
12
|
|
|
13
13
|
/** Rows pulled from the word index before ranking. */
|
|
@@ -67,7 +67,9 @@ export function search(store, query, { limit = 10, scope, key } = {}) {
|
|
|
67
67
|
if (scope === 'global') filtered = rows.filter((r) => r.file.startsWith('global/'))
|
|
68
68
|
else if (scope === 'workspace') filtered = rows.filter((r) => r.file.startsWith('workspaces/' + (key ?? '') + '/'))
|
|
69
69
|
const hits = filtered.map((r) => ({ file: r.file, line: r.line, section: r.section, snippet: String(r.content).trim() }))
|
|
70
|
-
|
|
70
|
+
// Ranking needs importance and recency only for the files that matched. Building the whole
|
|
71
|
+
// ledger here read ~8,000 files for a handful of hits and made one query cost ~15 s.
|
|
72
|
+
return rankBlend(hits, buildLedgerFor(store.root, hits.map((h) => h.file))).slice(0, limit)
|
|
71
73
|
.map((h) => ({ file: h.file, line: h.line, section: h.section ?? '', snippet: h.snippet, id: h.id ?? '', score: Math.round(h.score * 1000) / 1000 }))
|
|
72
74
|
} finally {
|
|
73
75
|
db.close()
|