opencode-codex-memory 0.1.3 → 0.1.5
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/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 +23 -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/templates/consolidation.md +0 -448
- package/src/templates/read_path.md +0 -104
- package/src/templates/stage_one_input.md +0 -11
- package/src/templates/stage_one_system.md +0 -333
- package/src/token.ts +0 -21
- package/src/workspace.ts +0 -181
- package/tools/control.ts +0 -145
- package/tools/memory.ts +0 -318
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import os from "os";
|
|
3
|
+
const MEMORY_DIR_NAME = "memories";
|
|
4
|
+
const MEMORY_DB_NAME = "memory.db";
|
|
5
|
+
const OVERRIDE_ENV = "OPENCODE_CODEX_MEMORY_TEST_ROOT";
|
|
6
|
+
function dataRoot() {
|
|
7
|
+
const override = process.env[OVERRIDE_ENV];
|
|
8
|
+
if (override)
|
|
9
|
+
return override;
|
|
10
|
+
return path.join(os.homedir(), ".local", "share", "opencode");
|
|
11
|
+
}
|
|
12
|
+
export function memoryRoot() {
|
|
13
|
+
return path.join(dataRoot(), MEMORY_DIR_NAME);
|
|
14
|
+
}
|
|
15
|
+
export function memoryDbPath() {
|
|
16
|
+
return path.join(dataRoot(), MEMORY_DB_NAME);
|
|
17
|
+
}
|
|
18
|
+
export function opencodeDbPath() {
|
|
19
|
+
return path.join(dataRoot(), "opencode.db");
|
|
20
|
+
}
|
|
21
|
+
export function memorySummaryPath() {
|
|
22
|
+
return path.join(memoryRoot(), "memory_summary.md");
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MemoryStore } from "./store.js";
|
|
2
|
+
export interface Phase1Options {
|
|
3
|
+
maxAgeDays: number;
|
|
4
|
+
minIdleHours: number;
|
|
5
|
+
maxClaimed?: number;
|
|
6
|
+
maxUnusedDays?: number;
|
|
7
|
+
excludeSession?: string;
|
|
8
|
+
extractModel?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const DEFAULT_PHASE1_OPTIONS: Phase1Options;
|
|
11
|
+
export declare function runPhase1(store: MemoryStore, opts?: Phase1Options): Promise<void>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { 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
|
+
export const DEFAULT_PHASE1_OPTIONS = {
|
|
8
|
+
maxAgeDays: 10,
|
|
9
|
+
minIdleHours: 6,
|
|
10
|
+
maxClaimed: 2,
|
|
11
|
+
maxUnusedDays: 30,
|
|
12
|
+
};
|
|
13
|
+
// codex serializes the full transcript and truncates at 70% of the model
|
|
14
|
+
// context window, falling back to 150k tokens; ~4 chars/token puts the
|
|
15
|
+
// char-estimate equivalent at 600k.
|
|
16
|
+
const TRANSCRIPT_MAX_CHARS = 600_000;
|
|
17
|
+
// When truncating, keep the head and the tail: the start carries the user's
|
|
18
|
+
// framing, the end carries the final outcome and feedback.
|
|
19
|
+
const TRANSCRIPT_HEAD_CHARS = 360_000;
|
|
20
|
+
const TRANSCRIPT_TAIL_CHARS = 240_000;
|
|
21
|
+
export async function runPhase1(store, opts = DEFAULT_PHASE1_OPTIONS) {
|
|
22
|
+
store.pruneStage1Outputs(opts.maxUnusedDays ?? 30);
|
|
23
|
+
const rl = await checkRateLimit("phase1");
|
|
24
|
+
if (!rl.ok) {
|
|
25
|
+
console.warn("[opencode-codex-memory] skipping phase1 due to rate limit:", rl.reason);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const eligible = selectEligibleSessions(store, opts);
|
|
29
|
+
if (eligible.length === 0)
|
|
30
|
+
return;
|
|
31
|
+
const claimed = store.claimStage1Jobs(eligible, opts.excludeSession, opts.maxClaimed);
|
|
32
|
+
if (claimed.length === 0)
|
|
33
|
+
return;
|
|
34
|
+
const sessionById = new Map(eligible.map((s) => [s.id, s]));
|
|
35
|
+
await runPool(claimed, STAGE1_CONCURRENCY, async (claim) => {
|
|
36
|
+
const sid = claim.sessionId;
|
|
37
|
+
try {
|
|
38
|
+
const session = sessionById.get(sid);
|
|
39
|
+
const sourceUpdatedAt = session?.updated_at ?? Date.now();
|
|
40
|
+
const transcript = buildTranscript(sid);
|
|
41
|
+
if (!transcript.trim()) {
|
|
42
|
+
store.markStage1SucceededNoOutput(sid, claim.ownershipToken, sourceUpdatedAt);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const result = await extractViaSubagent(sid, transcript, {
|
|
46
|
+
cwd: session?.directory ?? undefined,
|
|
47
|
+
model: opts.extractModel,
|
|
48
|
+
});
|
|
49
|
+
if (!result) {
|
|
50
|
+
// Extractor judged the session not worth remembering.
|
|
51
|
+
store.markStage1SucceededNoOutput(sid, claim.ownershipToken, sourceUpdatedAt);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
store.markStage1Succeeded(sid, claim.ownershipToken, {
|
|
55
|
+
session_id: sid,
|
|
56
|
+
source_updated_at: sourceUpdatedAt,
|
|
57
|
+
raw_memory: redact(result.raw_memory),
|
|
58
|
+
rollout_summary: redact(result.rollout_summary),
|
|
59
|
+
// codex redacts the slug too — it becomes a filename.
|
|
60
|
+
rollout_slug: result.rollout_slug ? redact(result.rollout_slug) : result.rollout_slug,
|
|
61
|
+
cwd: session?.directory ?? null,
|
|
62
|
+
generated_at: Date.now(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
store.markStage1Failed(sid, claim.ownershipToken, err.message);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function buildTranscript(sessionId) {
|
|
71
|
+
const msgs = loadTranscript(sessionId);
|
|
72
|
+
if (msgs.length === 0)
|
|
73
|
+
return "";
|
|
74
|
+
const lines = [];
|
|
75
|
+
for (const m of msgs) {
|
|
76
|
+
if (m.type === "system")
|
|
77
|
+
continue;
|
|
78
|
+
const role = m.role ?? m.type;
|
|
79
|
+
const text = m.text ?? "";
|
|
80
|
+
if (!text.trim())
|
|
81
|
+
continue;
|
|
82
|
+
// Injected AGENTS.md/<skill> blocks in user content are excluded from
|
|
83
|
+
// extraction (codex is_memory_excluded_contextual_user_fragment).
|
|
84
|
+
if (role === "user" && isMemoryExcludedFragment(text))
|
|
85
|
+
continue;
|
|
86
|
+
lines.push(`### ${role}\n${redact(stripCitations(text))}`);
|
|
87
|
+
}
|
|
88
|
+
const full = lines.join("\n\n");
|
|
89
|
+
if (full.length <= TRANSCRIPT_MAX_CHARS)
|
|
90
|
+
return full;
|
|
91
|
+
return (full.slice(0, TRANSCRIPT_HEAD_CHARS) +
|
|
92
|
+
"\n\n[... transcript truncated ...]\n\n" +
|
|
93
|
+
full.slice(full.length - TRANSCRIPT_TAIL_CHARS));
|
|
94
|
+
}
|
|
95
|
+
async function runPool(items, concurrency, fn) {
|
|
96
|
+
let cursor = 0;
|
|
97
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
98
|
+
while (cursor < items.length) {
|
|
99
|
+
const i = cursor++;
|
|
100
|
+
await fn(items[i]);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
await Promise.all(workers);
|
|
104
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MemoryStore } from "./store.js";
|
|
2
|
+
export interface Phase2Options {
|
|
3
|
+
maxRaw: number;
|
|
4
|
+
maxUnusedDays: number;
|
|
5
|
+
extensionRetentionDays: number;
|
|
6
|
+
consolidationModel?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_PHASE2_OPTIONS: Phase2Options;
|
|
9
|
+
export declare function runPhase2(store: MemoryStore, opts?: Phase2Options): Promise<{
|
|
10
|
+
status: string;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ensureLayout, rebuildRawMemories, writeRolloutSummaries, pruneExtensionResources, writeWorkspaceDiff } from "./workspace.js";
|
|
2
|
+
import { ensureBaseline, captureWorkspaceDiff, resetBaseline, DIFF_ARTIFACT } from "./git-baseline.js";
|
|
3
|
+
import { consolidateViaSubagent } from "./llm.js";
|
|
4
|
+
import { invalidateCache } from "./source.js";
|
|
5
|
+
import { memoryRoot } from "./paths.js";
|
|
6
|
+
import { checkRateLimit } from "./ratelimit.js";
|
|
7
|
+
export const DEFAULT_PHASE2_OPTIONS = {
|
|
8
|
+
maxRaw: 256,
|
|
9
|
+
maxUnusedDays: 30,
|
|
10
|
+
extensionRetentionDays: 7,
|
|
11
|
+
};
|
|
12
|
+
let phase2InFlight = false;
|
|
13
|
+
export async function runPhase2(store, opts = DEFAULT_PHASE2_OPTIONS) {
|
|
14
|
+
if (phase2InFlight)
|
|
15
|
+
return { status: "already_running" };
|
|
16
|
+
phase2InFlight = true;
|
|
17
|
+
try {
|
|
18
|
+
const rl = await checkRateLimit("phase2");
|
|
19
|
+
if (!rl.ok)
|
|
20
|
+
return { status: "skipped_rate_limit" };
|
|
21
|
+
const claim = store.claimGlobalPhase2Job();
|
|
22
|
+
if (claim.type !== "claimed")
|
|
23
|
+
return { status: claim.type };
|
|
24
|
+
try {
|
|
25
|
+
ensureLayout();
|
|
26
|
+
// Preserves an existing baseline (only initializes a missing one): the
|
|
27
|
+
// diff below must span last-successful-run -> now so user edits and
|
|
28
|
+
// ad-hoc notes added since then reach consolidation. Stale stage-1
|
|
29
|
+
// output pruning happens in phase 1, before the rate gate (codex
|
|
30
|
+
// start.rs ordering).
|
|
31
|
+
if (!await ensureBaseline()) {
|
|
32
|
+
store.markPhase2Failed(claim.ownershipToken, "git baseline failed");
|
|
33
|
+
return { status: "baseline_failed" };
|
|
34
|
+
}
|
|
35
|
+
const outputs = store.getPhase2InputSelection(opts.maxRaw, opts.maxUnusedDays);
|
|
36
|
+
rebuildRawMemories(outputs);
|
|
37
|
+
writeRolloutSummaries(outputs);
|
|
38
|
+
pruneExtensionResources(opts.extensionRetentionDays);
|
|
39
|
+
const diff = await captureWorkspaceDiff();
|
|
40
|
+
if (diff.changes.length === 0) {
|
|
41
|
+
store.markPhase2Succeeded(claim.ownershipToken, outputs);
|
|
42
|
+
return { status: "no_workspace_changes" };
|
|
43
|
+
}
|
|
44
|
+
writeWorkspaceDiff(diff);
|
|
45
|
+
let heartbeatLost = false;
|
|
46
|
+
const heartbeat = setInterval(() => {
|
|
47
|
+
if (!store.heartbeatPhase2Job(claim.ownershipToken)) {
|
|
48
|
+
heartbeatLost = true;
|
|
49
|
+
}
|
|
50
|
+
}, 90_000);
|
|
51
|
+
try {
|
|
52
|
+
await consolidateViaSubagent(memoryRoot(), DIFF_ARTIFACT, opts.consolidationModel);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
clearInterval(heartbeat);
|
|
56
|
+
}
|
|
57
|
+
// Final synchronous ownership confirmation before the destructive
|
|
58
|
+
// baseline reset (codex phase2.rs does the same): the periodic flag can
|
|
59
|
+
// be up to 90s stale, and a stale worker resetting the baseline would
|
|
60
|
+
// swallow the diff a re-claiming worker is about to consume. The
|
|
61
|
+
// heartbeat is token+status guarded, so it fails once ownership is lost;
|
|
62
|
+
// markPhase2Failed is equally guarded and becomes a no-op then.
|
|
63
|
+
if (heartbeatLost || !store.heartbeatPhase2Job(claim.ownershipToken)) {
|
|
64
|
+
store.markPhase2Failed(claim.ownershipToken, "ownership lost");
|
|
65
|
+
return { status: "heartbeat_lost" };
|
|
66
|
+
}
|
|
67
|
+
if (!await resetBaseline()) {
|
|
68
|
+
store.markPhase2Failed(claim.ownershipToken, "baseline reset failed");
|
|
69
|
+
return { status: "baseline_reset_failed" };
|
|
70
|
+
}
|
|
71
|
+
store.markPhase2Succeeded(claim.ownershipToken, outputs);
|
|
72
|
+
invalidateCache();
|
|
73
|
+
return { status: "succeeded" };
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
store.markPhase2Failed(claim.ownershipToken, err.message);
|
|
77
|
+
return { status: "failed" };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
phase2InFlight = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
let lastPhase1 = 0;
|
|
2
|
+
let lastPhase2 = 0;
|
|
3
|
+
const MIN_PHASE1_INTERVAL_MS = 30_000;
|
|
4
|
+
const MIN_PHASE2_INTERVAL_MS = 5 * 60 * 1000;
|
|
5
|
+
export async function checkRateLimit(kind = "phase1") {
|
|
6
|
+
const now = Date.now();
|
|
7
|
+
if (kind === "phase1") {
|
|
8
|
+
if (now - lastPhase1 < MIN_PHASE1_INTERVAL_MS) {
|
|
9
|
+
return { ok: false, reason: "phase1 rate limit (30s)" };
|
|
10
|
+
}
|
|
11
|
+
lastPhase1 = now;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
if (now - lastPhase2 < MIN_PHASE2_INTERVAL_MS) {
|
|
15
|
+
return { ok: false, reason: "phase2 rate limit (5min)" };
|
|
16
|
+
}
|
|
17
|
+
lastPhase2 = now;
|
|
18
|
+
}
|
|
19
|
+
return { ok: true };
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function redact(text: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Mirrors codex is_memory_excluded_contextual_user_fragment (phase1.rs):
|
|
4
|
+
* injected AGENTS.md instruction blocks and <skill> payloads inside user
|
|
5
|
+
* content are contextual boilerplate, not conversation — they must not be
|
|
6
|
+
* mined for memories.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isMemoryExcludedFragment(text: string): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const REDACTIONS = [
|
|
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
|
+
export function redact(text) {
|
|
17
|
+
let out = text;
|
|
18
|
+
for (const { re, replacement } of REDACTIONS) {
|
|
19
|
+
out = out.replace(re, replacement);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function matchesMarkedFragment(text, startMarker, endMarker) {
|
|
24
|
+
const trimmed = text.trim();
|
|
25
|
+
return (trimmed.slice(0, startMarker.length).toLowerCase() === startMarker.toLowerCase() &&
|
|
26
|
+
trimmed.slice(-endMarker.length).toLowerCase() === endMarker.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Mirrors codex is_memory_excluded_contextual_user_fragment (phase1.rs):
|
|
30
|
+
* injected AGENTS.md instruction blocks and <skill> payloads inside user
|
|
31
|
+
* content are contextual boilerplate, not conversation — they must not be
|
|
32
|
+
* mined for memories.
|
|
33
|
+
*/
|
|
34
|
+
export function isMemoryExcludedFragment(text) {
|
|
35
|
+
return (matchesMarkedFragment(text, "# AGENTS.md instructions", "</INSTRUCTIONS>") ||
|
|
36
|
+
matchesMarkedFragment(text, "<skill>", "</skill>"));
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
const MEMORY_SUMMARY_TOKEN_LIMIT = 2500;
|
|
7
|
+
const READ_PATH_TEMPLATE = "read_path.md";
|
|
8
|
+
let cached = null;
|
|
9
|
+
function readTemplate() {
|
|
10
|
+
const templatePath = path.join(import.meta.dirname, "templates", READ_PATH_TEMPLATE);
|
|
11
|
+
return fs.readFileSync(templatePath, "utf8");
|
|
12
|
+
}
|
|
13
|
+
function readMemorySummary() {
|
|
14
|
+
const summaryPath = memorySummaryPath();
|
|
15
|
+
if (!fs.existsSync(summaryPath))
|
|
16
|
+
return null;
|
|
17
|
+
const stat = fs.statSync(summaryPath);
|
|
18
|
+
if (cached && cached.mtime === stat.mtimeMs) {
|
|
19
|
+
return cached.content;
|
|
20
|
+
}
|
|
21
|
+
const raw = fs.readFileSync(summaryPath, "utf8").trim();
|
|
22
|
+
if (!raw)
|
|
23
|
+
return null;
|
|
24
|
+
const truncated = truncateToTokens(raw, MEMORY_SUMMARY_TOKEN_LIMIT);
|
|
25
|
+
cached = {
|
|
26
|
+
content: truncated,
|
|
27
|
+
mtime: stat.mtimeMs,
|
|
28
|
+
};
|
|
29
|
+
return truncated;
|
|
30
|
+
}
|
|
31
|
+
export function invalidateCache() {
|
|
32
|
+
cached = null;
|
|
33
|
+
}
|
|
34
|
+
export function buildMemorySystemPrompt() {
|
|
35
|
+
const summary = readMemorySummary();
|
|
36
|
+
if (!summary)
|
|
37
|
+
return null;
|
|
38
|
+
const template = readTemplate();
|
|
39
|
+
return fillTemplate(template, {
|
|
40
|
+
base_path: memoryRoot(),
|
|
41
|
+
memory_summary: summary,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function ensureMemoryLayout() {
|
|
45
|
+
fs.mkdirSync(memoryRoot(), { recursive: true });
|
|
46
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
export declare const DEFAULT_RETRY_REMAINING = 3;
|
|
3
|
+
export declare const STAGE1_LEASE_SECONDS = 3600;
|
|
4
|
+
export declare const PHASE2_LEASE_SECONDS = 3600;
|
|
5
|
+
export declare const STAGE1_RETRY_DELAY_SECONDS = 3600;
|
|
6
|
+
export declare const PHASE2_RETRY_DELAY_SECONDS = 3600;
|
|
7
|
+
export declare const PHASE2_COOLDOWN_MS: number;
|
|
8
|
+
export declare const STAGE1_CONCURRENCY = 8;
|
|
9
|
+
export declare const SCAN_LIMIT = 5000;
|
|
10
|
+
export declare const PRUNE_BATCH_SIZE = 200;
|
|
11
|
+
export type JobKind = "memory_stage1" | "memory_consolidate_global";
|
|
12
|
+
export type JobStatus = "pending" | "running" | "done" | "failed";
|
|
13
|
+
export interface Stage1Output {
|
|
14
|
+
session_id: string;
|
|
15
|
+
source_updated_at: number;
|
|
16
|
+
raw_memory: string;
|
|
17
|
+
rollout_summary: string;
|
|
18
|
+
rollout_slug: string | null;
|
|
19
|
+
cwd?: string | null;
|
|
20
|
+
generated_at: number;
|
|
21
|
+
usage_count: number;
|
|
22
|
+
last_usage: number | null;
|
|
23
|
+
}
|
|
24
|
+
export interface Stage1Claim {
|
|
25
|
+
sessionId: string;
|
|
26
|
+
ownershipToken: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ClaimableSession {
|
|
29
|
+
id: string;
|
|
30
|
+
updated_at: number;
|
|
31
|
+
}
|
|
32
|
+
export type Phase2ClaimResult = {
|
|
33
|
+
type: "claimed";
|
|
34
|
+
workerId: string;
|
|
35
|
+
ownershipToken: string;
|
|
36
|
+
} | {
|
|
37
|
+
type: "skipped_cooldown";
|
|
38
|
+
} | {
|
|
39
|
+
type: "skipped_running";
|
|
40
|
+
} | {
|
|
41
|
+
type: "skipped_retry_unavailable";
|
|
42
|
+
};
|
|
43
|
+
export declare class MemoryStore {
|
|
44
|
+
private db;
|
|
45
|
+
constructor(db?: Database);
|
|
46
|
+
stage1Outputs(): Stage1Output[];
|
|
47
|
+
/**
|
|
48
|
+
* Deletes stale rows; snapshots consumed by the last successful Phase 2 are
|
|
49
|
+
* protected. Stalest-first, capped per run (codex PRUNE_BATCH_SIZE).
|
|
50
|
+
*/
|
|
51
|
+
pruneStage1Outputs(maxUnusedDays: number): number;
|
|
52
|
+
upsertStage1Output(out: Omit<Stage1Output, "usage_count" | "last_usage">): boolean;
|
|
53
|
+
recordUsage(sessionIds: string[]): void;
|
|
54
|
+
claimStage1Jobs(sessions: ClaimableSession[], excludeSession?: string, maxClaimed?: number): Stage1Claim[];
|
|
55
|
+
markStage1Succeeded(sessionId: string, ownershipToken: string, out: Omit<Stage1Output, "usage_count" | "last_usage">): void;
|
|
56
|
+
/** Extraction succeeded but produced nothing worth keeping: finish the job and drop any stale output. */
|
|
57
|
+
markStage1SucceededNoOutput(sessionId: string, ownershipToken: string, sourceUpdatedAt: number): void;
|
|
58
|
+
markStage1Failed(sessionId: string, ownershipToken: string, error: string): void;
|
|
59
|
+
claimGlobalPhase2Job(): Phase2ClaimResult;
|
|
60
|
+
heartbeatPhase2Job(ownershipToken: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Marks the phase-2 job done and records exactly which stage-1 snapshots the
|
|
63
|
+
* run consumed (selected_for_phase2), so pruning cannot delete inputs that
|
|
64
|
+
* still back the consolidated artifacts.
|
|
65
|
+
*/
|
|
66
|
+
markPhase2Succeeded(ownershipToken: string, selected?: Pick<Stage1Output, "session_id" | "source_updated_at">[]): void;
|
|
67
|
+
markPhase2Failed(ownershipToken: string, error: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Phase 2 input set, mirroring codex get_phase2_input_selection:
|
|
70
|
+
* - excludes sessions marked disabled/polluted (their summary files then
|
|
71
|
+
* disappear from the workspace and the diff drives forgetting)
|
|
72
|
+
* - recency: last_usage when the memory has ever been used, otherwise
|
|
73
|
+
* source_updated_at
|
|
74
|
+
* - ranked by usage, then recency
|
|
75
|
+
*/
|
|
76
|
+
getPhase2InputSelection(maxRaw: number, maxUnusedDays: number): Stage1Output[];
|
|
77
|
+
/** Mirrors codex delete_thread_memory: remove a deleted session's output + job. */
|
|
78
|
+
deleteSessionMemory(sessionId: string): void;
|
|
79
|
+
/**
|
|
80
|
+
* codex clear_memory_data deletes extracted memories and jobs but explicitly
|
|
81
|
+
* preserves per-session memory modes: a reset must not re-enable sessions
|
|
82
|
+
* the user disabled or that were marked polluted.
|
|
83
|
+
*/
|
|
84
|
+
clearMemoryData(): void;
|
|
85
|
+
setMemoryMode(sessionId: string, mode: "enabled" | "disabled" | "polluted"): void;
|
|
86
|
+
/**
|
|
87
|
+
* Stamp a mode only when the session has no meta row yet — used to mark
|
|
88
|
+
* sessions seen while generate_memories=false as permanently 'disabled'
|
|
89
|
+
* (codex stamps memory_mode at thread creation, session.rs), without
|
|
90
|
+
* overriding an explicit user-set or polluted mode.
|
|
91
|
+
*/
|
|
92
|
+
stampMemoryModeIfAbsent(sessionId: string, mode: "enabled" | "disabled"): void;
|
|
93
|
+
getMemoryMode(sessionId: string): "enabled" | "disabled" | "polluted" | null;
|
|
94
|
+
markPolluted(sessionId: string): void;
|
|
95
|
+
isPolluted(sessionId: string): boolean;
|
|
96
|
+
}
|