opencode-codex-memory 0.1.3 → 0.1.6
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/opencode.json +37 -0
- package/dist/src/capture.d.ts +19 -0
- package/dist/src/capture.js +120 -0
- package/dist/src/citation.d.ts +14 -0
- package/dist/src/citation.js +81 -0
- package/dist/src/db.d.ts +3 -0
- package/dist/src/db.js +78 -0
- package/dist/src/git-baseline.d.ts +24 -0
- package/dist/src/git-baseline.js +150 -0
- package/dist/src/index.d.ts +163 -0
- package/dist/src/index.js +365 -0
- package/dist/src/llm.d.ts +19 -0
- package/dist/src/llm.js +251 -0
- package/dist/src/path-guard.d.ts +10 -0
- package/dist/src/path-guard.js +44 -0
- package/dist/src/paths.d.ts +4 -0
- package/dist/src/paths.js +23 -0
- package/dist/src/phase1.d.ts +11 -0
- package/dist/src/phase1.js +104 -0
- package/dist/src/phase2.d.ts +11 -0
- package/dist/src/phase2.js +83 -0
- package/dist/src/ratelimit.d.ts +5 -0
- package/dist/src/ratelimit.js +20 -0
- package/dist/src/redact.d.ts +8 -0
- package/dist/src/redact.js +37 -0
- package/dist/src/source.d.ts +3 -0
- package/dist/src/source.js +46 -0
- package/dist/src/store.d.ts +96 -0
- package/dist/src/store.js +346 -0
- package/dist/src/token.d.ts +8 -0
- package/dist/src/token.js +19 -0
- package/dist/src/workspace.d.ts +8 -0
- package/dist/src/workspace.js +194 -0
- package/dist/tools/control.d.ts +29 -0
- package/dist/tools/control.js +153 -0
- package/dist/tools/memory.d.ts +52 -0
- package/dist/tools/memory.js +322 -0
- package/package.json +24 -6
- package/src/capture.ts +0 -135
- package/src/citation.ts +0 -94
- package/src/db.ts +0 -80
- package/src/git-baseline.ts +0 -162
- package/src/index.ts +0 -366
- package/src/llm.ts +0 -267
- package/src/path-guard.ts +0 -44
- package/src/paths.ts +0 -29
- package/src/phase1.ts +0 -116
- package/src/phase2.ts +0 -99
- package/src/ratelimit.ts +0 -26
- package/src/redact.ts +0 -44
- package/src/source.ts +0 -59
- package/src/store.ts +0 -430
- package/src/token.ts +0 -21
- package/src/workspace.ts +0 -181
- package/tools/control.ts +0 -145
- package/tools/memory.ts +0 -318
- /package/{src → dist/src}/templates/consolidation.md +0 -0
- /package/{src → dist/src}/templates/read_path.md +0 -0
- /package/{src → dist/src}/templates/stage_one_input.md +0 -0
- /package/{src → dist/src}/templates/stage_one_system.md +0 -0
package/src/paths.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import path from "path"
|
|
2
|
-
import os from "os"
|
|
3
|
-
|
|
4
|
-
const MEMORY_DIR_NAME = "memories"
|
|
5
|
-
const MEMORY_DB_NAME = "memory.db"
|
|
6
|
-
|
|
7
|
-
const OVERRIDE_ENV = "OPENCODE_CODEX_MEMORY_TEST_ROOT"
|
|
8
|
-
|
|
9
|
-
function dataRoot(): string {
|
|
10
|
-
const override = process.env[OVERRIDE_ENV]
|
|
11
|
-
if (override) return override
|
|
12
|
-
return path.join(os.homedir(), ".local", "share", "opencode")
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function memoryRoot(): string {
|
|
16
|
-
return path.join(dataRoot(), MEMORY_DIR_NAME)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function memoryDbPath(): string {
|
|
20
|
-
return path.join(dataRoot(), MEMORY_DB_NAME)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function opencodeDbPath(): string {
|
|
24
|
-
return path.join(dataRoot(), "opencode.db")
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function memorySummaryPath(): string {
|
|
28
|
-
return path.join(memoryRoot(), "memory_summary.md")
|
|
29
|
-
}
|
package/src/phase1.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { MemoryStore, STAGE1_CONCURRENCY } from "./store.js"
|
|
2
|
-
import { loadTranscript, selectEligibleSessions } from "./capture.js"
|
|
3
|
-
import { redact, isMemoryExcludedFragment } from "./redact.js"
|
|
4
|
-
import { stripCitations } from "./citation.js"
|
|
5
|
-
import { extractViaSubagent } from "./llm.js"
|
|
6
|
-
import { checkRateLimit } from "./ratelimit.js"
|
|
7
|
-
|
|
8
|
-
export interface Phase1Options {
|
|
9
|
-
maxAgeDays: number
|
|
10
|
-
minIdleHours: number
|
|
11
|
-
// Max rollouts claimed per pass (codex max_rollouts_per_startup / max_claimed).
|
|
12
|
-
maxClaimed?: number
|
|
13
|
-
// Retention for stale stage-1 outputs (codex max_unused_days); pruning runs
|
|
14
|
-
// before the rate gate because it costs no tokens (codex start.rs).
|
|
15
|
-
maxUnusedDays?: number
|
|
16
|
-
excludeSession?: string
|
|
17
|
-
extractModel?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const DEFAULT_PHASE1_OPTIONS: Phase1Options = {
|
|
21
|
-
maxAgeDays: 10,
|
|
22
|
-
minIdleHours: 6,
|
|
23
|
-
maxClaimed: 2,
|
|
24
|
-
maxUnusedDays: 30,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// codex serializes the full transcript and truncates at 70% of the model
|
|
28
|
-
// context window, falling back to 150k tokens; ~4 chars/token puts the
|
|
29
|
-
// char-estimate equivalent at 600k.
|
|
30
|
-
const TRANSCRIPT_MAX_CHARS = 600_000
|
|
31
|
-
// When truncating, keep the head and the tail: the start carries the user's
|
|
32
|
-
// framing, the end carries the final outcome and feedback.
|
|
33
|
-
const TRANSCRIPT_HEAD_CHARS = 360_000
|
|
34
|
-
const TRANSCRIPT_TAIL_CHARS = 240_000
|
|
35
|
-
|
|
36
|
-
export async function runPhase1(store: MemoryStore, opts: Phase1Options = DEFAULT_PHASE1_OPTIONS): Promise<void> {
|
|
37
|
-
store.pruneStage1Outputs(opts.maxUnusedDays ?? 30)
|
|
38
|
-
const rl = await checkRateLimit("phase1")
|
|
39
|
-
if (!rl.ok) {
|
|
40
|
-
console.warn("[opencode-codex-memory] skipping phase1 due to rate limit:", rl.reason)
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
const eligible = selectEligibleSessions(store, opts)
|
|
44
|
-
if (eligible.length === 0) return
|
|
45
|
-
const claimed = store.claimStage1Jobs(eligible, opts.excludeSession, opts.maxClaimed)
|
|
46
|
-
if (claimed.length === 0) return
|
|
47
|
-
const sessionById = new Map(eligible.map((s) => [s.id, s]))
|
|
48
|
-
|
|
49
|
-
await runPool(claimed, STAGE1_CONCURRENCY, async (claim) => {
|
|
50
|
-
const sid = claim.sessionId
|
|
51
|
-
try {
|
|
52
|
-
const session = sessionById.get(sid)
|
|
53
|
-
const sourceUpdatedAt = session?.updated_at ?? Date.now()
|
|
54
|
-
const transcript = buildTranscript(sid)
|
|
55
|
-
if (!transcript.trim()) {
|
|
56
|
-
store.markStage1SucceededNoOutput(sid, claim.ownershipToken, sourceUpdatedAt)
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
const result = await extractViaSubagent(sid, transcript, {
|
|
60
|
-
cwd: session?.directory ?? undefined,
|
|
61
|
-
model: opts.extractModel,
|
|
62
|
-
})
|
|
63
|
-
if (!result) {
|
|
64
|
-
// Extractor judged the session not worth remembering.
|
|
65
|
-
store.markStage1SucceededNoOutput(sid, claim.ownershipToken, sourceUpdatedAt)
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
store.markStage1Succeeded(sid, claim.ownershipToken, {
|
|
69
|
-
session_id: sid,
|
|
70
|
-
source_updated_at: sourceUpdatedAt,
|
|
71
|
-
raw_memory: redact(result.raw_memory),
|
|
72
|
-
rollout_summary: redact(result.rollout_summary),
|
|
73
|
-
// codex redacts the slug too — it becomes a filename.
|
|
74
|
-
rollout_slug: result.rollout_slug ? redact(result.rollout_slug) : result.rollout_slug,
|
|
75
|
-
cwd: session?.directory ?? null,
|
|
76
|
-
generated_at: Date.now(),
|
|
77
|
-
})
|
|
78
|
-
} catch (err) {
|
|
79
|
-
store.markStage1Failed(sid, claim.ownershipToken, (err as Error).message)
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function buildTranscript(sessionId: string): string {
|
|
85
|
-
const msgs = loadTranscript(sessionId)
|
|
86
|
-
if (msgs.length === 0) return ""
|
|
87
|
-
const lines: string[] = []
|
|
88
|
-
for (const m of msgs) {
|
|
89
|
-
if (m.type === "system") continue
|
|
90
|
-
const role = m.role ?? m.type
|
|
91
|
-
const text = m.text ?? ""
|
|
92
|
-
if (!text.trim()) continue
|
|
93
|
-
// Injected AGENTS.md/<skill> blocks in user content are excluded from
|
|
94
|
-
// extraction (codex is_memory_excluded_contextual_user_fragment).
|
|
95
|
-
if (role === "user" && isMemoryExcludedFragment(text)) continue
|
|
96
|
-
lines.push(`### ${role}\n${redact(stripCitations(text))}`)
|
|
97
|
-
}
|
|
98
|
-
const full = lines.join("\n\n")
|
|
99
|
-
if (full.length <= TRANSCRIPT_MAX_CHARS) return full
|
|
100
|
-
return (
|
|
101
|
-
full.slice(0, TRANSCRIPT_HEAD_CHARS) +
|
|
102
|
-
"\n\n[... transcript truncated ...]\n\n" +
|
|
103
|
-
full.slice(full.length - TRANSCRIPT_TAIL_CHARS)
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function runPool<T>(items: T[], concurrency: number, fn: (item: T) => Promise<void>): Promise<void> {
|
|
108
|
-
let cursor = 0
|
|
109
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
110
|
-
while (cursor < items.length) {
|
|
111
|
-
const i = cursor++
|
|
112
|
-
await fn(items[i])
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
await Promise.all(workers)
|
|
116
|
-
}
|
package/src/phase2.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { MemoryStore } from "./store.js"
|
|
2
|
-
import { ensureLayout, rebuildRawMemories, writeRolloutSummaries, pruneExtensionResources, writeWorkspaceDiff } from "./workspace.js"
|
|
3
|
-
import { ensureBaseline, captureWorkspaceDiff, resetBaseline, DIFF_ARTIFACT } from "./git-baseline.js"
|
|
4
|
-
import { consolidateViaSubagent } from "./llm.js"
|
|
5
|
-
import { invalidateCache } from "./source.js"
|
|
6
|
-
import { memoryRoot } from "./paths.js"
|
|
7
|
-
import { checkRateLimit } from "./ratelimit.js"
|
|
8
|
-
|
|
9
|
-
export interface Phase2Options {
|
|
10
|
-
maxRaw: number
|
|
11
|
-
maxUnusedDays: number
|
|
12
|
-
extensionRetentionDays: number
|
|
13
|
-
consolidationModel?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const DEFAULT_PHASE2_OPTIONS: Phase2Options = {
|
|
17
|
-
maxRaw: 256,
|
|
18
|
-
maxUnusedDays: 30,
|
|
19
|
-
extensionRetentionDays: 7,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let phase2InFlight = false
|
|
23
|
-
|
|
24
|
-
export async function runPhase2(store: MemoryStore, opts: Phase2Options = DEFAULT_PHASE2_OPTIONS): Promise<{ status: string }> {
|
|
25
|
-
if (phase2InFlight) return { status: "already_running" }
|
|
26
|
-
phase2InFlight = true
|
|
27
|
-
try {
|
|
28
|
-
const rl = await checkRateLimit("phase2")
|
|
29
|
-
if (!rl.ok) return { status: "skipped_rate_limit" }
|
|
30
|
-
|
|
31
|
-
const claim = store.claimGlobalPhase2Job()
|
|
32
|
-
if (claim.type !== "claimed") return { status: claim.type }
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
ensureLayout()
|
|
36
|
-
|
|
37
|
-
// Preserves an existing baseline (only initializes a missing one): the
|
|
38
|
-
// diff below must span last-successful-run -> now so user edits and
|
|
39
|
-
// ad-hoc notes added since then reach consolidation. Stale stage-1
|
|
40
|
-
// output pruning happens in phase 1, before the rate gate (codex
|
|
41
|
-
// start.rs ordering).
|
|
42
|
-
if (!await ensureBaseline()) {
|
|
43
|
-
store.markPhase2Failed(claim.ownershipToken, "git baseline failed")
|
|
44
|
-
return { status: "baseline_failed" }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const outputs = store.getPhase2InputSelection(opts.maxRaw, opts.maxUnusedDays)
|
|
48
|
-
rebuildRawMemories(outputs)
|
|
49
|
-
writeRolloutSummaries(outputs)
|
|
50
|
-
pruneExtensionResources(opts.extensionRetentionDays)
|
|
51
|
-
|
|
52
|
-
const diff = await captureWorkspaceDiff()
|
|
53
|
-
if (diff.changes.length === 0) {
|
|
54
|
-
store.markPhase2Succeeded(claim.ownershipToken, outputs)
|
|
55
|
-
return { status: "no_workspace_changes" }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
writeWorkspaceDiff(diff)
|
|
59
|
-
|
|
60
|
-
let heartbeatLost = false
|
|
61
|
-
const heartbeat = setInterval(() => {
|
|
62
|
-
if (!store.heartbeatPhase2Job(claim.ownershipToken)) {
|
|
63
|
-
heartbeatLost = true
|
|
64
|
-
}
|
|
65
|
-
}, 90_000)
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
await consolidateViaSubagent(memoryRoot(), DIFF_ARTIFACT, opts.consolidationModel)
|
|
69
|
-
} finally {
|
|
70
|
-
clearInterval(heartbeat)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Final synchronous ownership confirmation before the destructive
|
|
74
|
-
// baseline reset (codex phase2.rs does the same): the periodic flag can
|
|
75
|
-
// be up to 90s stale, and a stale worker resetting the baseline would
|
|
76
|
-
// swallow the diff a re-claiming worker is about to consume. The
|
|
77
|
-
// heartbeat is token+status guarded, so it fails once ownership is lost;
|
|
78
|
-
// markPhase2Failed is equally guarded and becomes a no-op then.
|
|
79
|
-
if (heartbeatLost || !store.heartbeatPhase2Job(claim.ownershipToken)) {
|
|
80
|
-
store.markPhase2Failed(claim.ownershipToken, "ownership lost")
|
|
81
|
-
return { status: "heartbeat_lost" }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (!await resetBaseline()) {
|
|
85
|
-
store.markPhase2Failed(claim.ownershipToken, "baseline reset failed")
|
|
86
|
-
return { status: "baseline_reset_failed" }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
store.markPhase2Succeeded(claim.ownershipToken, outputs)
|
|
90
|
-
invalidateCache()
|
|
91
|
-
return { status: "succeeded" }
|
|
92
|
-
} catch (err) {
|
|
93
|
-
store.markPhase2Failed(claim.ownershipToken, (err as Error).message)
|
|
94
|
-
return { status: "failed" }
|
|
95
|
-
}
|
|
96
|
-
} finally {
|
|
97
|
-
phase2InFlight = false
|
|
98
|
-
}
|
|
99
|
-
}
|
package/src/ratelimit.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export interface RateLimitInfo {
|
|
2
|
-
ok: boolean
|
|
3
|
-
reason?: string
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
let lastPhase1 = 0
|
|
7
|
-
let lastPhase2 = 0
|
|
8
|
-
|
|
9
|
-
const MIN_PHASE1_INTERVAL_MS = 30_000
|
|
10
|
-
const MIN_PHASE2_INTERVAL_MS = 5 * 60 * 1000
|
|
11
|
-
|
|
12
|
-
export async function checkRateLimit(kind: "phase1" | "phase2" = "phase1"): Promise<RateLimitInfo> {
|
|
13
|
-
const now = Date.now()
|
|
14
|
-
if (kind === "phase1") {
|
|
15
|
-
if (now - lastPhase1 < MIN_PHASE1_INTERVAL_MS) {
|
|
16
|
-
return { ok: false, reason: "phase1 rate limit (30s)" }
|
|
17
|
-
}
|
|
18
|
-
lastPhase1 = now
|
|
19
|
-
} else {
|
|
20
|
-
if (now - lastPhase2 < MIN_PHASE2_INTERVAL_MS) {
|
|
21
|
-
return { ok: false, reason: "phase2 rate limit (5min)" }
|
|
22
|
-
}
|
|
23
|
-
lastPhase2 = now
|
|
24
|
-
}
|
|
25
|
-
return { ok: true }
|
|
26
|
-
}
|
package/src/redact.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
const REDACTIONS: { re: RegExp; replacement: string }[] = [
|
|
2
|
-
{ re: /sk-ant-[A-Za-z0-9_\-]{20,}/g, replacement: "[REDACTED:anthropic-key]" },
|
|
3
|
-
{ re: /sk-[A-Za-z0-9]{20,}/g, replacement: "[REDACTED:openai-key]" },
|
|
4
|
-
{ re: /AKIA[0-9A-Z]{16}/g, replacement: "[REDACTED:aws-key]" },
|
|
5
|
-
{ re: /gh[pousr]_[A-Za-z0-9]{36,}/g, replacement: "[REDACTED:github-token]" },
|
|
6
|
-
{ re: /xox[baprs]-[A-Za-z0-9\-]{10,}/g, replacement: "[REDACTED:slack-token]" },
|
|
7
|
-
// Case-insensitive with a 16-char floor, matching codex's sanitizer.
|
|
8
|
-
{ re: /bearer\s+[A-Za-z0-9\-\._~+\/=]{16,}/gi, replacement: "Bearer [REDACTED]" },
|
|
9
|
-
{
|
|
10
|
-
re: /-----BEGIN [A-Z]+ PRIVATE KEY-----[\s\S]*?-----END [A-Z]+ PRIVATE KEY-----/g,
|
|
11
|
-
replacement: "[REDACTED:private-key]",
|
|
12
|
-
},
|
|
13
|
-
{ re: /(password|passwd|pwd|secret|api[_-]?key|token|access[_-]?token)\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, replacement: "$1=[REDACTED]" },
|
|
14
|
-
{ re: /(?:aws_secret_access_key|aws_access_key_id)\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, replacement: "$1=[REDACTED]" },
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
export function redact(text: string): string {
|
|
18
|
-
let out = text
|
|
19
|
-
for (const { re, replacement } of REDACTIONS) {
|
|
20
|
-
out = out.replace(re, replacement)
|
|
21
|
-
}
|
|
22
|
-
return out
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function matchesMarkedFragment(text: string, startMarker: string, endMarker: string): boolean {
|
|
26
|
-
const trimmed = text.trim()
|
|
27
|
-
return (
|
|
28
|
-
trimmed.slice(0, startMarker.length).toLowerCase() === startMarker.toLowerCase() &&
|
|
29
|
-
trimmed.slice(-endMarker.length).toLowerCase() === endMarker.toLowerCase()
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Mirrors codex is_memory_excluded_contextual_user_fragment (phase1.rs):
|
|
35
|
-
* injected AGENTS.md instruction blocks and <skill> payloads inside user
|
|
36
|
-
* content are contextual boilerplate, not conversation — they must not be
|
|
37
|
-
* mined for memories.
|
|
38
|
-
*/
|
|
39
|
-
export function isMemoryExcludedFragment(text: string): boolean {
|
|
40
|
-
return (
|
|
41
|
-
matchesMarkedFragment(text, "# AGENTS.md instructions", "</INSTRUCTIONS>") ||
|
|
42
|
-
matchesMarkedFragment(text, "<skill>", "</skill>")
|
|
43
|
-
)
|
|
44
|
-
}
|
package/src/source.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import fs from "fs"
|
|
2
|
-
import path from "path"
|
|
3
|
-
import { memorySummaryPath, memoryRoot } from "./paths.js"
|
|
4
|
-
import { truncateToTokens } from "./token.js"
|
|
5
|
-
import { fillTemplate } from "./llm.js"
|
|
6
|
-
|
|
7
|
-
const MEMORY_SUMMARY_TOKEN_LIMIT = 2500
|
|
8
|
-
const READ_PATH_TEMPLATE = "read_path.md"
|
|
9
|
-
|
|
10
|
-
interface CachedSummary {
|
|
11
|
-
content: string
|
|
12
|
-
mtime: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let cached: CachedSummary | null = null
|
|
16
|
-
|
|
17
|
-
function readTemplate(): string {
|
|
18
|
-
const templatePath = path.join(import.meta.dirname, "templates", READ_PATH_TEMPLATE)
|
|
19
|
-
return fs.readFileSync(templatePath, "utf8")
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function readMemorySummary(): string | null {
|
|
23
|
-
const summaryPath = memorySummaryPath()
|
|
24
|
-
if (!fs.existsSync(summaryPath)) return null
|
|
25
|
-
|
|
26
|
-
const stat = fs.statSync(summaryPath)
|
|
27
|
-
if (cached && cached.mtime === stat.mtimeMs) {
|
|
28
|
-
return cached.content
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const raw = fs.readFileSync(summaryPath, "utf8").trim()
|
|
32
|
-
if (!raw) return null
|
|
33
|
-
|
|
34
|
-
const truncated = truncateToTokens(raw, MEMORY_SUMMARY_TOKEN_LIMIT)
|
|
35
|
-
cached = {
|
|
36
|
-
content: truncated,
|
|
37
|
-
mtime: stat.mtimeMs,
|
|
38
|
-
}
|
|
39
|
-
return truncated
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function invalidateCache(): void {
|
|
43
|
-
cached = null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function buildMemorySystemPrompt(): string | null {
|
|
47
|
-
const summary = readMemorySummary()
|
|
48
|
-
if (!summary) return null
|
|
49
|
-
|
|
50
|
-
const template = readTemplate()
|
|
51
|
-
return fillTemplate(template, {
|
|
52
|
-
base_path: memoryRoot(),
|
|
53
|
-
memory_summary: summary,
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function ensureMemoryLayout(): void {
|
|
58
|
-
fs.mkdirSync(memoryRoot(), { recursive: true })
|
|
59
|
-
}
|