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.
Files changed (59) hide show
  1. package/dist/src/capture.d.ts +19 -0
  2. package/dist/src/capture.js +120 -0
  3. package/dist/src/citation.d.ts +14 -0
  4. package/dist/src/citation.js +81 -0
  5. package/dist/src/db.d.ts +3 -0
  6. package/dist/src/db.js +78 -0
  7. package/dist/src/git-baseline.d.ts +24 -0
  8. package/dist/src/git-baseline.js +150 -0
  9. package/dist/src/index.d.ts +163 -0
  10. package/dist/src/index.js +365 -0
  11. package/dist/src/llm.d.ts +19 -0
  12. package/dist/src/llm.js +251 -0
  13. package/dist/src/path-guard.d.ts +10 -0
  14. package/dist/src/path-guard.js +44 -0
  15. package/dist/src/paths.d.ts +4 -0
  16. package/dist/src/paths.js +23 -0
  17. package/dist/src/phase1.d.ts +11 -0
  18. package/dist/src/phase1.js +104 -0
  19. package/dist/src/phase2.d.ts +11 -0
  20. package/dist/src/phase2.js +83 -0
  21. package/dist/src/ratelimit.d.ts +5 -0
  22. package/dist/src/ratelimit.js +20 -0
  23. package/dist/src/redact.d.ts +8 -0
  24. package/dist/src/redact.js +37 -0
  25. package/dist/src/source.d.ts +3 -0
  26. package/dist/src/source.js +46 -0
  27. package/dist/src/store.d.ts +96 -0
  28. package/dist/src/store.js +346 -0
  29. package/dist/src/token.d.ts +8 -0
  30. package/dist/src/token.js +19 -0
  31. package/dist/src/workspace.d.ts +8 -0
  32. package/dist/src/workspace.js +194 -0
  33. package/dist/tools/control.d.ts +29 -0
  34. package/dist/tools/control.js +153 -0
  35. package/dist/tools/memory.d.ts +52 -0
  36. package/dist/tools/memory.js +322 -0
  37. package/package.json +23 -6
  38. package/src/capture.ts +0 -135
  39. package/src/citation.ts +0 -94
  40. package/src/db.ts +0 -80
  41. package/src/git-baseline.ts +0 -162
  42. package/src/index.ts +0 -366
  43. package/src/llm.ts +0 -267
  44. package/src/path-guard.ts +0 -44
  45. package/src/paths.ts +0 -29
  46. package/src/phase1.ts +0 -116
  47. package/src/phase2.ts +0 -99
  48. package/src/ratelimit.ts +0 -26
  49. package/src/redact.ts +0 -44
  50. package/src/source.ts +0 -59
  51. package/src/store.ts +0 -430
  52. package/src/templates/consolidation.md +0 -448
  53. package/src/templates/read_path.md +0 -104
  54. package/src/templates/stage_one_input.md +0 -11
  55. package/src/templates/stage_one_system.md +0 -333
  56. package/src/token.ts +0 -21
  57. package/src/workspace.ts +0 -181
  58. package/tools/control.ts +0 -145
  59. package/tools/memory.ts +0 -318
@@ -0,0 +1,4 @@
1
+ export declare function memoryRoot(): string;
2
+ export declare function memoryDbPath(): string;
3
+ export declare function opencodeDbPath(): string;
4
+ export declare function memorySummaryPath(): string;
@@ -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,5 @@
1
+ export interface RateLimitInfo {
2
+ ok: boolean;
3
+ reason?: string;
4
+ }
5
+ export declare function checkRateLimit(kind?: "phase1" | "phase2"): Promise<RateLimitInfo>;
@@ -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,3 @@
1
+ export declare function invalidateCache(): void;
2
+ export declare function buildMemorySystemPrompt(): string | null;
3
+ export declare function ensureMemoryLayout(): void;
@@ -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
+ }