@try-works/dsh-memory 0.1.19 → 0.1.20
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/LICENSE +21 -21
- package/README.md +585 -585
- package/STATUS.md +287 -287
- package/cordis.patch.yml +69 -69
- package/package.json +1 -1
- package/scripts/check-package.mjs +76 -76
- package/src/SYSTEM.template.md +311 -311
- package/src/capture.js +242 -242
- package/src/client/index.js +886 -886
- package/src/config.js +57 -57
- package/src/consolidate-run.js +132 -132
- package/src/consolidate.js +330 -330
- package/src/context.js +88 -88
- package/src/decisions.js +106 -106
- package/src/defaults.js +93 -93
- package/src/digest.js +255 -255
- package/src/entry.js +179 -179
- package/src/eval.js +72 -72
- package/src/files.js +60 -60
- package/src/fixture.js +56 -56
- package/src/forget.js +71 -71
- package/src/freshness.js +65 -65
- package/src/fts.js +166 -166
- package/src/git.js +53 -53
- package/src/history.js +226 -226
- package/src/host.js +866 -866
- package/src/index.js +115 -94
- package/src/inject.js +108 -108
- package/src/layers.js +178 -178
- package/src/ledger.js +19 -19
- package/src/map.js +184 -184
- package/src/models.js +136 -136
- package/src/queue.js +602 -602
- package/src/rank.js +31 -31
- package/src/recover.js +91 -91
- package/src/refresh.js +80 -80
- package/src/remember.js +58 -58
- package/src/rerank.js +90 -90
- package/src/search.js +75 -75
- package/src/select.js +57 -57
- package/src/session.js +60 -60
- package/src/state.js +240 -240
- package/src/store.js +128 -128
- package/src/summarize.js +195 -195
- package/src/system-doc.js +171 -171
- package/src/tasks.js +617 -617
- package/src/telemetry.js +122 -122
- package/src/tools.js +828 -818
- package/src/working.js +288 -288
- package/src/workspace-key.js +68 -68
- package/src/workspace.js +87 -87
package/src/index.js
CHANGED
|
@@ -1,94 +1,115 @@
|
|
|
1
|
-
// dsh-memory plugin entry (Cordis). Config/settings, store lifecycle, the ctx.memory
|
|
2
|
-
// service, memory_* tools, and the deep host hooks (capture/consolidate/inject/sync).
|
|
3
|
-
import { Config } from './config.js'
|
|
4
|
-
import { resolveConfig } from './defaults.js'
|
|
5
|
-
import { MemoryStore } from './store.js'
|
|
6
|
-
import { search } from './search.js'
|
|
7
|
-
import { remember } from './remember.js'
|
|
8
|
-
import { associate, listWorkspaces, resolveKeyFor } from './workspace.js'
|
|
9
|
-
import { pendingDigests } from './capture.js'
|
|
10
|
-
import { migrateToLayered } from './layers.js'
|
|
11
|
-
import { writeSystemDoc } from './system-doc.js'
|
|
12
|
-
import { readFileSync } from 'node:fs'
|
|
13
|
-
import { dirname, join } from 'node:path'
|
|
14
|
-
import { fileURLToPath } from 'node:url'
|
|
15
|
-
|
|
16
|
-
/** The plugin version, read from its own manifest; never fatal if it cannot be read. */
|
|
17
|
-
function readPluginVersion() {
|
|
18
|
-
try {
|
|
19
|
-
const manifest = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
|
|
20
|
-
return JSON.parse(readFileSync(manifest, 'utf8')).version ?? 'unknown'
|
|
21
|
-
} catch { return 'unknown' }
|
|
22
|
-
}
|
|
23
|
-
const PLUGIN_VERSION = readPluginVersion()
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The capabilities registerTools needs, taken from the host in ONE place.
|
|
27
|
-
*
|
|
28
|
-
* This used to be an inline object literal, and the wiring test wrote its own COPY of it. A copy
|
|
29
|
-
* drifts: the test's version was missing `recover`, so a tool wired to nothing passed every test
|
|
30
|
-
* and failed the moment a real session called it. Deriving both from the host object means the
|
|
31
|
-
* test cannot describe a wiring the plugin does not have.
|
|
32
|
-
*/
|
|
33
|
-
export function hostExtras(host) {
|
|
34
|
-
return {
|
|
35
|
-
syncNow: host.syncNow,
|
|
36
|
-
consolidate: host.consolidate,
|
|
37
|
-
refresh: host.refresh,
|
|
38
|
-
status: host.status,
|
|
39
|
-
recover: host.recover,
|
|
40
|
-
rerankSearch: host.rerankSearch,
|
|
41
|
-
workspaceOf: host.workspaceOf,
|
|
42
|
-
historyOf: host.historyOf,
|
|
43
|
-
injectionMaxChars: host.injectionMaxChars,
|
|
44
|
-
tasksNow: host.tasksNow,
|
|
45
|
-
tasksRestore: host.tasksRestore,
|
|
46
|
-
schedulePath: host.schedulePath,
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
export const name = 'dsh-memory'
|
|
50
|
-
import { registerTools } from './tools.js'
|
|
51
|
-
import { wireHost } from './host.js'
|
|
52
|
-
|
|
53
|
-
export const inject = ['tools']
|
|
54
|
-
export { Config }
|
|
55
|
-
|
|
56
|
-
export function apply(ctx, config = {}) {
|
|
57
|
-
const cfg = resolveConfig(config)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
ctx.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
1
|
+
// dsh-memory plugin entry (Cordis). Config/settings, store lifecycle, the ctx.memory
|
|
2
|
+
// service, memory_* tools, and the deep host hooks (capture/consolidate/inject/sync).
|
|
3
|
+
import { Config } from './config.js'
|
|
4
|
+
import { resolveConfig } from './defaults.js'
|
|
5
|
+
import { MemoryStore } from './store.js'
|
|
6
|
+
import { search } from './search.js'
|
|
7
|
+
import { remember } from './remember.js'
|
|
8
|
+
import { associate, listWorkspaces, resolveKeyFor } from './workspace.js'
|
|
9
|
+
import { pendingDigests } from './capture.js'
|
|
10
|
+
import { migrateToLayered } from './layers.js'
|
|
11
|
+
import { writeSystemDoc } from './system-doc.js'
|
|
12
|
+
import { readFileSync } from 'node:fs'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
/** The plugin version, read from its own manifest; never fatal if it cannot be read. */
|
|
17
|
+
function readPluginVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const manifest = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
|
|
20
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).version ?? 'unknown'
|
|
21
|
+
} catch { return 'unknown' }
|
|
22
|
+
}
|
|
23
|
+
const PLUGIN_VERSION = readPluginVersion()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The capabilities registerTools needs, taken from the host in ONE place.
|
|
27
|
+
*
|
|
28
|
+
* This used to be an inline object literal, and the wiring test wrote its own COPY of it. A copy
|
|
29
|
+
* drifts: the test's version was missing `recover`, so a tool wired to nothing passed every test
|
|
30
|
+
* and failed the moment a real session called it. Deriving both from the host object means the
|
|
31
|
+
* test cannot describe a wiring the plugin does not have.
|
|
32
|
+
*/
|
|
33
|
+
export function hostExtras(host) {
|
|
34
|
+
return {
|
|
35
|
+
syncNow: host.syncNow,
|
|
36
|
+
consolidate: host.consolidate,
|
|
37
|
+
refresh: host.refresh,
|
|
38
|
+
status: host.status,
|
|
39
|
+
recover: host.recover,
|
|
40
|
+
rerankSearch: host.rerankSearch,
|
|
41
|
+
workspaceOf: host.workspaceOf,
|
|
42
|
+
historyOf: host.historyOf,
|
|
43
|
+
injectionMaxChars: host.injectionMaxChars,
|
|
44
|
+
tasksNow: host.tasksNow,
|
|
45
|
+
tasksRestore: host.tasksRestore,
|
|
46
|
+
schedulePath: host.schedulePath,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const name = 'dsh-memory'
|
|
50
|
+
import { registerTools } from './tools.js'
|
|
51
|
+
import { wireHost } from './host.js'
|
|
52
|
+
|
|
53
|
+
export const inject = ['tools']
|
|
54
|
+
export { Config }
|
|
55
|
+
|
|
56
|
+
export function apply(ctx, config = {}) {
|
|
57
|
+
const cfg = resolveConfig(config)
|
|
58
|
+
/**
|
|
59
|
+
* Re-read volatile fields when the loader commits a configuration change.
|
|
60
|
+
*
|
|
61
|
+
* resolveConfig UNWRAPS a volatile reference into a plain value and is called once, here, at
|
|
62
|
+
* load. That snapshot never moves - so a git origin typed into Settings was written to the
|
|
63
|
+
* profile file, committed into the running fiber by the loader, and still never reached this
|
|
64
|
+
* instance: memory_sync answered synced:false with no remote added and no error to explain it.
|
|
65
|
+
*
|
|
66
|
+
* The contract is documented and this code already half-knew it: volatile fields parse to a
|
|
67
|
+
* reference rather than a value, and the cookbook says to read it when STARTING an operation
|
|
68
|
+
* (docs/cookbook/adding-a-settings-card.md). Mutating cfg IN PLACE is what makes that true here,
|
|
69
|
+
* because every consumer reads cfg.<field> at call time instead of capturing it.
|
|
70
|
+
*/
|
|
71
|
+
ctx.on('loader/volatile-update', () => {
|
|
72
|
+
try {
|
|
73
|
+
const next = resolveConfig(config)
|
|
74
|
+
for (const key of Object.keys(next)) cfg[key] = next[key]
|
|
75
|
+
} catch (error) {
|
|
76
|
+
ctx.logger.warn('dsh-memory: could not re-read configuration: ' + (error && error.message))
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
const store = new MemoryStore(cfg)
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
store.ensureLayout()
|
|
83
|
+
// Layered storage landed after the first stores existed: move any entries still sitting
|
|
84
|
+
// in a single layer file into per-topic files and rebuild its index. Idempotent.
|
|
85
|
+
store.syncGitBlock()
|
|
86
|
+
const moved = migrateToLayered(store.root)
|
|
87
|
+
if (moved > 0) ctx.logger.info('dsh-memory: moved ' + moved + ' entr(ies) into per-topic files')
|
|
88
|
+
// Regenerate the manual for the machinery on every boot, so it can never describe a
|
|
89
|
+
// version of the plugin that no longer exists.
|
|
90
|
+
writeSystemDoc(store.root, { cfg, version: PLUGIN_VERSION })
|
|
91
|
+
} catch (error) { ctx.logger.warn('dsh-memory: could not prepare memory store at ' + store.root + ': ' + (error && error.message)) }
|
|
92
|
+
|
|
93
|
+
ctx.provide('memory', {
|
|
94
|
+
get root() { return store.root },
|
|
95
|
+
status: () => store.status(),
|
|
96
|
+
search: (query, opts) => search(store, query, opts),
|
|
97
|
+
remember: (opts) => remember(store, opts),
|
|
98
|
+
workspaces: () => listWorkspaces(store),
|
|
99
|
+
associate: (opts) => associate(store, opts),
|
|
100
|
+
resolveKey: (input) => resolveKeyFor(store.root, input),
|
|
101
|
+
pending: () => pendingDigests(store).length,
|
|
102
|
+
consolidate: (agent, opts) => host.consolidate(agent, opts),
|
|
103
|
+
workspaceOf: (agent) => host.workspaceOf(agent),
|
|
104
|
+
historyOf: (agent, opts) => host.historyOf(agent, opts),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const host = wireHost(ctx, store, cfg)
|
|
108
|
+
registerTools(ctx, store, hostExtras(host))
|
|
109
|
+
|
|
110
|
+
ctx.on('settings/document-updated', () => { ctx.logger.debug('dsh-memory: settings changed') })
|
|
111
|
+
|
|
112
|
+
ctx.effect(() => { store.dispose() })
|
|
113
|
+
|
|
114
|
+
return host
|
|
115
|
+
}
|
package/src/inject.js
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
// Session-start injection text: goal + working head + recent learnings + footer,
|
|
2
|
-
// char-capped and scope-aware.
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { layerDir } from './store.js'
|
|
6
|
-
import { buildLedger } from './ledger.js'
|
|
7
|
-
import { readLayerTopics } from './layers.js'
|
|
8
|
-
import { freshnessMarker, readState } from './state.js'
|
|
9
|
-
import { readWorking, workingMarker } from './working.js'
|
|
10
|
-
import { relevanceQuery, selectEntries } from './select.js'
|
|
11
|
-
|
|
12
|
-
export function buildInjectionText(store, { scope = 'global', key, goal, maxChars = 3600, subagent = false } = {}) {
|
|
13
|
-
const dir = layerDir(store.root, scope, key)
|
|
14
|
-
// Working memory is the AGENT's intent (task, recent activity, next steps); repository
|
|
15
|
-
const ledger = buildLedger(store.root)
|
|
16
|
-
const prefix = scope === 'global' ? 'global/' : 'workspaces/' + key + '/'
|
|
17
|
-
// The pools are LISTED here and CHOSEN later, because choosing needs the question, and the
|
|
18
|
-
// question comes from working memory - which is read below.
|
|
19
|
-
const learningPool = ledger.filter((e) => e.file.startsWith(prefix) && e.layer === 'learning')
|
|
20
|
-
const decisionPool = ledger.filter((e) => e.file.startsWith(prefix) && e.layer === 'decision')
|
|
21
|
-
// state (state.md) is the repository's. Different owners, different lifetimes: a repository
|
|
22
|
-
const workingMemory = scope === 'workspace' ? readWorking(store.root, scope, key) : undefined
|
|
23
|
-
// What this turn is about, from the agent's own live task - see relevanceQuery for why the
|
|
24
|
-
// task outranks the goal.
|
|
25
|
-
const query = relevanceQuery(workingMemory, goal)
|
|
26
|
-
const learned = selectEntries(store, learningPool, query, 5, { scope, key })
|
|
27
|
-
const learnings = learned.entries
|
|
28
|
-
const learningsRelevant = learned.relevant
|
|
29
|
-
const decisions = selectEntries(store, decisionPool, query, 5, { scope, key }).entries
|
|
30
|
-
const parts = ['## DSH memory context']
|
|
31
|
-
if (goal) parts.push('### Goal\n' + goal)
|
|
32
|
-
// Workspace state goes FIRST and is never buried: it is the overview, and its value
|
|
33
|
-
// depends on the reader knowing how current it is. A stale section is shown WITH its age
|
|
34
|
-
// rather than hidden, so it reads as something to check instead of something to trust.
|
|
35
|
-
if (scope === 'workspace') {
|
|
36
|
-
const state = readState(store.root, key)
|
|
37
|
-
if (state && state.sections.length > 0) {
|
|
38
|
-
const lines = state.sections.slice(0, 8).map((section) => {
|
|
39
|
-
const marker = freshnessMarker(section.verdict, section.ageMs)
|
|
40
|
-
const body = section.content.split(String.fromCharCode(10)).slice(0, 3).join(' ')
|
|
41
|
-
return '- **' + section.name + '**' + marker + (body ? ': ' + body : '')
|
|
42
|
-
})
|
|
43
|
-
parts.push('### Repository state' + String.fromCharCode(10) + lines.join(String.fromCharCode(10)))
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// The agent must know this file exists, that it is THEIRS, and what it is for. So the card
|
|
47
|
-
// names the file and its purpose, and an unrecorded file is called out rather than hidden:
|
|
48
|
-
// silence would read as 'no working memory' when it actually means 'nothing written yet'.
|
|
49
|
-
if (scope === 'workspace') {
|
|
50
|
-
const relative = 'workspaces/' + key + '/working.md'
|
|
51
|
-
const marker = workingMemory ? workingMarker(workingMemory.verdict, workingMemory.ageMs) : ''
|
|
52
|
-
const hasContent = workingMemory && (workingMemory.sections.length > 0 || workingMemory.activity.length > 0)
|
|
53
|
-
const lines = ['### Working memory — yours' + marker]
|
|
54
|
-
lines.push('Your own file at ' + relative + ': the task in hand, recent activity, next steps.')
|
|
55
|
-
lines.push('You own it — read and edit it freely; keep it current. Repository state lives separately in state.md.')
|
|
56
|
-
if (hasContent) {
|
|
57
|
-
for (const section of workingMemory.sections.slice(0, 4)) {
|
|
58
|
-
lines.push('- **' + section.name + '**: ' + section.content.split(String.fromCharCode(10)).join(' ').slice(0, 300))
|
|
59
|
-
}
|
|
60
|
-
const recent = workingMemory.activity.slice(-4)
|
|
61
|
-
if (recent.length) lines.push('- **Recently**: ' + recent.map((a) => a.text).join('; ').slice(0, 400))
|
|
62
|
-
} else {
|
|
63
|
-
lines.push('- (nothing recorded yet. Record your task with memory_working if it helps you — this is optional.)')
|
|
64
|
-
}
|
|
65
|
-
parts.push(lines.join(String.fromCharCode(10)))
|
|
66
|
-
}
|
|
67
|
-
// The heading says WHICH rule chose these. 'Recent' and 'relevant to this task' are different
|
|
68
|
-
// claims, and a reader deciding how much to trust a line needs to know which one was made.
|
|
69
|
-
if (learnings.length) {
|
|
70
|
-
const heading = learningsRelevant ? '### Relevant learnings' : '### Recent learnings'
|
|
71
|
-
parts.push(heading + '\n' + learnings.map((e) => '- ' + e.heading + ': ' + e.content).join('\n'))
|
|
72
|
-
}
|
|
73
|
-
// Settled decisions belong in front of the agent: they are the constraints it must respect.
|
|
74
|
-
if (decisions.length) parts.push('### Decisions in force\n' + decisions.map((e) => '- ' + e.heading).join('\n'))
|
|
75
|
-
// The codebase map is per workspace: show what has been mapped so far, so the agent
|
|
76
|
-
// knows what it can already rely on and what is still unexplored.
|
|
77
|
-
const mapped = scope === 'workspace' ? readLayerTopics(store.root, scope, key, 'codebase') : []
|
|
78
|
-
if (mapped.length) {
|
|
79
|
-
const shown = mapped.slice(0, 30).map((topic) => {
|
|
80
|
-
const entry = topic.entries[0]
|
|
81
|
-
return '- ' + (entry ? entry.heading : topic.slug)
|
|
82
|
-
})
|
|
83
|
-
if (mapped.length > 30) shown.push('- (+' + (mapped.length - 30) + ' more)')
|
|
84
|
-
parts.push('### Codebase map\n' + shown.join('\n'))
|
|
85
|
-
}
|
|
86
|
-
// The footer is the one line the agent sees on every turn, so it carries the standing rule
|
|
87
|
-
// about RESTRAINT, not just the tools. An agent that feels obliged to write every few
|
|
88
|
-
// minutes turns memory into a junk drawer; declining is the correct outcome when nothing
|
|
89
|
-
// durable was learned.
|
|
90
|
-
// A subagent READS but does not write: memory is the main agent's account of the work, and the
|
|
91
|
-
// parent already owns the delegation and its result. Telling a child to record would invite it
|
|
92
|
-
// to write a partial account competing with its parent's, so the footer states the actual rule.
|
|
93
|
-
const WRITE_FOOTER = [
|
|
94
|
-
'Use memory_search to recall more.',
|
|
95
|
-
'Write ONLY when something durable was actually learned: a decision, a constraint, a',
|
|
96
|
-
'preference, an environment fact, a lesson, or how a module works.',
|
|
97
|
-
'Writing NOTHING is a correct and expected outcome - most turns add nothing, and a fact that',
|
|
98
|
-
'only mattered for one turn is not memory. Never invent an entry to fill a gap.',
|
|
99
|
-
].join(String.fromCharCode(10))
|
|
100
|
-
const READ_FOOTER = [
|
|
101
|
-
'Use memory_search to recall more.',
|
|
102
|
-
'Memory is written by the main agent that delegated this work.',
|
|
103
|
-
'Report what you find back to it, and do not record memory yourself.',
|
|
104
|
-
].join(String.fromCharCode(10))
|
|
105
|
-
const footer = subagent ? READ_FOOTER : WRITE_FOOTER
|
|
106
|
-
const body = parts.join('\n\n')
|
|
107
|
-
return (body.length <= maxChars ? body : body.slice(0, maxChars)) + '\n\n' + footer
|
|
108
|
-
}
|
|
1
|
+
// Session-start injection text: goal + working head + recent learnings + footer,
|
|
2
|
+
// char-capped and scope-aware.
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { layerDir } from './store.js'
|
|
6
|
+
import { buildLedger } from './ledger.js'
|
|
7
|
+
import { readLayerTopics } from './layers.js'
|
|
8
|
+
import { freshnessMarker, readState } from './state.js'
|
|
9
|
+
import { readWorking, workingMarker } from './working.js'
|
|
10
|
+
import { relevanceQuery, selectEntries } from './select.js'
|
|
11
|
+
|
|
12
|
+
export function buildInjectionText(store, { scope = 'global', key, goal, maxChars = 3600, subagent = false } = {}) {
|
|
13
|
+
const dir = layerDir(store.root, scope, key)
|
|
14
|
+
// Working memory is the AGENT's intent (task, recent activity, next steps); repository
|
|
15
|
+
const ledger = buildLedger(store.root)
|
|
16
|
+
const prefix = scope === 'global' ? 'global/' : 'workspaces/' + key + '/'
|
|
17
|
+
// The pools are LISTED here and CHOSEN later, because choosing needs the question, and the
|
|
18
|
+
// question comes from working memory - which is read below.
|
|
19
|
+
const learningPool = ledger.filter((e) => e.file.startsWith(prefix) && e.layer === 'learning')
|
|
20
|
+
const decisionPool = ledger.filter((e) => e.file.startsWith(prefix) && e.layer === 'decision')
|
|
21
|
+
// state (state.md) is the repository's. Different owners, different lifetimes: a repository
|
|
22
|
+
const workingMemory = scope === 'workspace' ? readWorking(store.root, scope, key) : undefined
|
|
23
|
+
// What this turn is about, from the agent's own live task - see relevanceQuery for why the
|
|
24
|
+
// task outranks the goal.
|
|
25
|
+
const query = relevanceQuery(workingMemory, goal)
|
|
26
|
+
const learned = selectEntries(store, learningPool, query, 5, { scope, key })
|
|
27
|
+
const learnings = learned.entries
|
|
28
|
+
const learningsRelevant = learned.relevant
|
|
29
|
+
const decisions = selectEntries(store, decisionPool, query, 5, { scope, key }).entries
|
|
30
|
+
const parts = ['## DSH memory context']
|
|
31
|
+
if (goal) parts.push('### Goal\n' + goal)
|
|
32
|
+
// Workspace state goes FIRST and is never buried: it is the overview, and its value
|
|
33
|
+
// depends on the reader knowing how current it is. A stale section is shown WITH its age
|
|
34
|
+
// rather than hidden, so it reads as something to check instead of something to trust.
|
|
35
|
+
if (scope === 'workspace') {
|
|
36
|
+
const state = readState(store.root, key)
|
|
37
|
+
if (state && state.sections.length > 0) {
|
|
38
|
+
const lines = state.sections.slice(0, 8).map((section) => {
|
|
39
|
+
const marker = freshnessMarker(section.verdict, section.ageMs)
|
|
40
|
+
const body = section.content.split(String.fromCharCode(10)).slice(0, 3).join(' ')
|
|
41
|
+
return '- **' + section.name + '**' + marker + (body ? ': ' + body : '')
|
|
42
|
+
})
|
|
43
|
+
parts.push('### Repository state' + String.fromCharCode(10) + lines.join(String.fromCharCode(10)))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// The agent must know this file exists, that it is THEIRS, and what it is for. So the card
|
|
47
|
+
// names the file and its purpose, and an unrecorded file is called out rather than hidden:
|
|
48
|
+
// silence would read as 'no working memory' when it actually means 'nothing written yet'.
|
|
49
|
+
if (scope === 'workspace') {
|
|
50
|
+
const relative = 'workspaces/' + key + '/working.md'
|
|
51
|
+
const marker = workingMemory ? workingMarker(workingMemory.verdict, workingMemory.ageMs) : ''
|
|
52
|
+
const hasContent = workingMemory && (workingMemory.sections.length > 0 || workingMemory.activity.length > 0)
|
|
53
|
+
const lines = ['### Working memory — yours' + marker]
|
|
54
|
+
lines.push('Your own file at ' + relative + ': the task in hand, recent activity, next steps.')
|
|
55
|
+
lines.push('You own it — read and edit it freely; keep it current. Repository state lives separately in state.md.')
|
|
56
|
+
if (hasContent) {
|
|
57
|
+
for (const section of workingMemory.sections.slice(0, 4)) {
|
|
58
|
+
lines.push('- **' + section.name + '**: ' + section.content.split(String.fromCharCode(10)).join(' ').slice(0, 300))
|
|
59
|
+
}
|
|
60
|
+
const recent = workingMemory.activity.slice(-4)
|
|
61
|
+
if (recent.length) lines.push('- **Recently**: ' + recent.map((a) => a.text).join('; ').slice(0, 400))
|
|
62
|
+
} else {
|
|
63
|
+
lines.push('- (nothing recorded yet. Record your task with memory_working if it helps you — this is optional.)')
|
|
64
|
+
}
|
|
65
|
+
parts.push(lines.join(String.fromCharCode(10)))
|
|
66
|
+
}
|
|
67
|
+
// The heading says WHICH rule chose these. 'Recent' and 'relevant to this task' are different
|
|
68
|
+
// claims, and a reader deciding how much to trust a line needs to know which one was made.
|
|
69
|
+
if (learnings.length) {
|
|
70
|
+
const heading = learningsRelevant ? '### Relevant learnings' : '### Recent learnings'
|
|
71
|
+
parts.push(heading + '\n' + learnings.map((e) => '- ' + e.heading + ': ' + e.content).join('\n'))
|
|
72
|
+
}
|
|
73
|
+
// Settled decisions belong in front of the agent: they are the constraints it must respect.
|
|
74
|
+
if (decisions.length) parts.push('### Decisions in force\n' + decisions.map((e) => '- ' + e.heading).join('\n'))
|
|
75
|
+
// The codebase map is per workspace: show what has been mapped so far, so the agent
|
|
76
|
+
// knows what it can already rely on and what is still unexplored.
|
|
77
|
+
const mapped = scope === 'workspace' ? readLayerTopics(store.root, scope, key, 'codebase') : []
|
|
78
|
+
if (mapped.length) {
|
|
79
|
+
const shown = mapped.slice(0, 30).map((topic) => {
|
|
80
|
+
const entry = topic.entries[0]
|
|
81
|
+
return '- ' + (entry ? entry.heading : topic.slug)
|
|
82
|
+
})
|
|
83
|
+
if (mapped.length > 30) shown.push('- (+' + (mapped.length - 30) + ' more)')
|
|
84
|
+
parts.push('### Codebase map\n' + shown.join('\n'))
|
|
85
|
+
}
|
|
86
|
+
// The footer is the one line the agent sees on every turn, so it carries the standing rule
|
|
87
|
+
// about RESTRAINT, not just the tools. An agent that feels obliged to write every few
|
|
88
|
+
// minutes turns memory into a junk drawer; declining is the correct outcome when nothing
|
|
89
|
+
// durable was learned.
|
|
90
|
+
// A subagent READS but does not write: memory is the main agent's account of the work, and the
|
|
91
|
+
// parent already owns the delegation and its result. Telling a child to record would invite it
|
|
92
|
+
// to write a partial account competing with its parent's, so the footer states the actual rule.
|
|
93
|
+
const WRITE_FOOTER = [
|
|
94
|
+
'Use memory_search to recall more.',
|
|
95
|
+
'Write ONLY when something durable was actually learned: a decision, a constraint, a',
|
|
96
|
+
'preference, an environment fact, a lesson, or how a module works.',
|
|
97
|
+
'Writing NOTHING is a correct and expected outcome - most turns add nothing, and a fact that',
|
|
98
|
+
'only mattered for one turn is not memory. Never invent an entry to fill a gap.',
|
|
99
|
+
].join(String.fromCharCode(10))
|
|
100
|
+
const READ_FOOTER = [
|
|
101
|
+
'Use memory_search to recall more.',
|
|
102
|
+
'Memory is written by the main agent that delegated this work.',
|
|
103
|
+
'Report what you find back to it, and do not record memory yourself.',
|
|
104
|
+
].join(String.fromCharCode(10))
|
|
105
|
+
const footer = subagent ? READ_FOOTER : WRITE_FOOTER
|
|
106
|
+
const body = parts.join('\n\n')
|
|
107
|
+
return (body.length <= maxChars ? body : body.slice(0, maxChars)) + '\n\n' + footer
|
|
108
|
+
}
|