opencode-codex-memory 0.1.2 → 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 -137
  39. package/src/citation.ts +0 -94
  40. package/src/db.ts +0 -84
  41. package/src/git-baseline.ts +0 -162
  42. package/src/index.ts +0 -366
  43. package/src/llm.ts +0 -266
  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 -101
  48. package/src/ratelimit.ts +0 -26
  49. package/src/redact.ts +0 -44
  50. package/src/source.ts +0 -62
  51. package/src/store.ts +0 -434
  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 -190
  58. package/tools/control.ts +0 -145
  59. package/tools/memory.ts +0 -318
package/src/store.ts DELETED
@@ -1,434 +0,0 @@
1
- import type { Database } from "bun:sqlite"
2
- import { openDb } from "./db.js"
3
-
4
- export const DEFAULT_RETRY_REMAINING = 3
5
- export const STAGE1_LEASE_SECONDS = 3600
6
- export const PHASE2_LEASE_SECONDS = 3600
7
- export const STAGE1_RETRY_DELAY_SECONDS = 3600
8
- export const PHASE2_RETRY_DELAY_SECONDS = 3600
9
- export const PHASE2_COOLDOWN_MS = 6 * 60 * 60 * 1000
10
- export const STAGE1_CONCURRENCY = 8
11
- export const SCAN_LIMIT = 5000
12
- export const PRUNE_BATCH_SIZE = 200
13
-
14
- export type JobKind = "memory_stage1" | "memory_consolidate_global"
15
- export type JobStatus = "pending" | "running" | "done" | "failed"
16
-
17
- export interface Stage1Output {
18
- session_id: string
19
- source_updated_at: number
20
- raw_memory: string
21
- rollout_summary: string
22
- rollout_slug: string | null
23
- cwd?: string | null
24
- generated_at: number
25
- usage_count: number
26
- last_usage: number | null
27
- }
28
-
29
- export type ClaimResult =
30
- | { type: "claimed"; sessionId: string; workerId: string; ownershipToken: string }
31
- | { type: "skipped" }
32
-
33
- export interface Stage1Claim {
34
- sessionId: string
35
- ownershipToken: string
36
- }
37
-
38
- export interface ClaimableSession {
39
- id: string
40
- updated_at: number
41
- }
42
-
43
- export type Phase2ClaimResult =
44
- | { type: "claimed"; workerId: string; ownershipToken: string }
45
- | { type: "skipped_cooldown" }
46
- | { type: "skipped_running" }
47
- | { type: "skipped_retry_unavailable" }
48
-
49
- function newId(): string {
50
- return crypto.randomUUID()
51
- }
52
- function now(): number {
53
- return Date.now()
54
- }
55
- function nowSec(): number {
56
- return Math.floor(Date.now() / 1000)
57
- }
58
-
59
- export class MemoryStore {
60
- constructor(private db: Database = openDb()) {}
61
-
62
- stage1Outputs(): Stage1Output[] {
63
- return this.db
64
- .prepare("SELECT * FROM memory_stage1_outputs ORDER BY source_updated_at DESC")
65
- .all() as Stage1Output[]
66
- }
67
-
68
- /**
69
- * Deletes stale rows; snapshots consumed by the last successful Phase 2 are
70
- * protected. Stalest-first, capped per run (codex PRUNE_BATCH_SIZE).
71
- */
72
- pruneStage1Outputs(maxUnusedDays: number): number {
73
- const cutoff = now() - maxUnusedDays * 24 * 60 * 60 * 1000
74
- return this.db
75
- .prepare(
76
- `DELETE FROM memory_stage1_outputs
77
- WHERE rowid IN (
78
- SELECT rowid FROM memory_stage1_outputs
79
- WHERE selected_for_phase2 = 0
80
- AND ((last_usage IS NOT NULL AND last_usage < ?)
81
- OR (last_usage IS NULL AND source_updated_at < ?))
82
- ORDER BY COALESCE(last_usage, source_updated_at) ASC
83
- LIMIT ?
84
- )`,
85
- )
86
- .run(cutoff, cutoff, PRUNE_BATCH_SIZE).changes
87
- }
88
-
89
- upsertStage1Output(out: Omit<Stage1Output, "usage_count" | "last_usage">): boolean {
90
- const existing = this.db
91
- .prepare("SELECT source_updated_at FROM memory_stage1_outputs WHERE session_id = ?")
92
- .get(out.session_id) as { source_updated_at: number } | null
93
- // codex replaces when the incoming watermark is >= the stored one; only a
94
- // strictly newer stored row wins.
95
- if (existing && existing.source_updated_at > out.source_updated_at) {
96
- return false
97
- }
98
- this.db
99
- .prepare(
100
- `INSERT INTO memory_stage1_outputs
101
- (session_id, source_updated_at, raw_memory, rollout_summary, rollout_slug, cwd, generated_at, usage_count, last_usage)
102
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, NULL)
103
- ON CONFLICT(session_id) DO UPDATE SET
104
- source_updated_at = excluded.source_updated_at,
105
- raw_memory = excluded.raw_memory,
106
- rollout_summary = excluded.rollout_summary,
107
- rollout_slug = excluded.rollout_slug,
108
- cwd = excluded.cwd,
109
- generated_at = excluded.generated_at`,
110
- )
111
- .run(out.session_id, out.source_updated_at, out.raw_memory, out.rollout_summary, out.rollout_slug, out.cwd ?? null, out.generated_at)
112
- return true
113
- }
114
-
115
- recordUsage(sessionIds: string[]): void {
116
- if (sessionIds.length === 0) return
117
- const ts = now()
118
- const stmt = this.db.prepare(
119
- "UPDATE memory_stage1_outputs SET usage_count = usage_count + 1, last_usage = ? WHERE session_id = ?",
120
- )
121
- for (const id of sessionIds) stmt.run(ts, id)
122
- }
123
-
124
- claimStage1Jobs(sessions: ClaimableSession[], excludeSession?: string, maxClaimed?: number): Stage1Claim[] {
125
- const workerId = newId()
126
- // Cap per-pass claims at codex's max_rollouts_per_startup (max_claimed) when
127
- // provided, never exceeding the hard concurrency ceiling. codex also uses
128
- // max_claimed as the cross-process running-jobs cap.
129
- const claimCap = Math.max(1, Math.min(maxClaimed ?? STAGE1_CONCURRENCY, STAGE1_CONCURRENCY))
130
- const claimed: Stage1Claim[] = []
131
- const claimOne = this.db.transaction((s: ClaimableSession, ownershipToken: string, lease: number): boolean => {
132
- const activeRow = this.db
133
- .prepare("SELECT COUNT(*) AS c FROM memory_jobs WHERE kind='memory_stage1' AND status='running' AND (lease_until IS NULL OR lease_until > ?)")
134
- .get(nowSec()) as { c: number }
135
- if (activeRow.c >= claimCap) return false
136
- // Mirrors codex try_claim_stage1_job: a newer input watermark (session
137
- // activity) overrides retry backoff and resets exhausted retries; done
138
- // jobs are reclaimed only when the session advanced past the last
139
- // success watermark.
140
- const result = this.db
141
- .prepare(
142
- `INSERT INTO memory_jobs
143
- (kind, job_key, status, worker_id, ownership_token, started_at, lease_until, retry_remaining, input_watermark)
144
- VALUES ('memory_stage1', ?, 'running', ?, ?, ?, ?, ?, ?)
145
- ON CONFLICT(kind, job_key) DO UPDATE SET
146
- status = 'running',
147
- worker_id = excluded.worker_id,
148
- ownership_token = excluded.ownership_token,
149
- started_at = excluded.started_at,
150
- lease_until = excluded.lease_until,
151
- finished_at = NULL,
152
- retry_at = NULL,
153
- last_error = NULL,
154
- retry_remaining = CASE
155
- WHEN excluded.input_watermark > COALESCE(memory_jobs.input_watermark, -1) THEN excluded.retry_remaining
156
- ELSE memory_jobs.retry_remaining
157
- END,
158
- input_watermark = excluded.input_watermark
159
- WHERE (memory_jobs.status != 'running' OR memory_jobs.lease_until IS NULL OR memory_jobs.lease_until <= excluded.started_at)
160
- AND (memory_jobs.retry_at IS NULL
161
- OR memory_jobs.retry_at <= excluded.started_at
162
- OR excluded.input_watermark > COALESCE(memory_jobs.input_watermark, -1))
163
- AND (memory_jobs.retry_remaining > 0
164
- OR excluded.input_watermark > COALESCE(memory_jobs.input_watermark, -1))
165
- AND (memory_jobs.status != 'done'
166
- OR memory_jobs.last_success_watermark IS NULL
167
- OR memory_jobs.last_success_watermark < excluded.input_watermark)`,
168
- )
169
- .run(s.id, workerId, ownershipToken, nowSec(), lease, DEFAULT_RETRY_REMAINING, s.updated_at)
170
- return result.changes > 0
171
- })
172
- for (const s of sessions) {
173
- if (s.id === excludeSession) continue
174
- if (claimed.length >= claimCap) break
175
- // Per-claim ownership token (codex uses a fresh UUID per claim) so a
176
- // zombie worker cannot finalize a job another worker re-claimed.
177
- const ownershipToken = newId()
178
- const lease = nowSec() + STAGE1_LEASE_SECONDS
179
- if (claimOne.immediate(s, ownershipToken, lease)) claimed.push({ sessionId: s.id, ownershipToken })
180
- }
181
- return claimed
182
- }
183
-
184
- markStage1Succeeded(sessionId: string, ownershipToken: string, out: Omit<Stage1Output, "usage_count" | "last_usage">): void {
185
- this.db.transaction(() => {
186
- const res = this.db
187
- .prepare(
188
- `UPDATE memory_jobs SET status='done', finished_at=?, lease_until=NULL, last_error=NULL,
189
- last_success_watermark=?, retry_at=NULL
190
- WHERE kind='memory_stage1' AND job_key=? AND status='running' AND ownership_token=?`,
191
- )
192
- .run(nowSec(), out.source_updated_at, sessionId, ownershipToken)
193
- // Ownership lost (lease expired, job re-claimed): do not clobber the new
194
- // owner's output. Mirrors codex mark_stage1_job_succeeded.
195
- if (res.changes > 0) this.upsertStage1Output(out)
196
- }).immediate()
197
- }
198
-
199
- /** Extraction succeeded but produced nothing worth keeping: finish the job and drop any stale output. */
200
- markStage1SucceededNoOutput(sessionId: string, ownershipToken: string, sourceUpdatedAt: number): void {
201
- this.db.transaction(() => {
202
- const res = this.db
203
- .prepare(
204
- `UPDATE memory_jobs SET status='done', finished_at=?, lease_until=NULL, last_error=NULL,
205
- last_success_watermark=?, retry_at=NULL
206
- WHERE kind='memory_stage1' AND job_key=? AND status='running' AND ownership_token=?`,
207
- )
208
- .run(nowSec(), sourceUpdatedAt, sessionId, ownershipToken)
209
- if (res.changes > 0) this.db.prepare("DELETE FROM memory_stage1_outputs WHERE session_id = ?").run(sessionId)
210
- }).immediate()
211
- }
212
-
213
- markStage1Failed(sessionId: string, ownershipToken: string, error: string): void {
214
- this.db
215
- .prepare(
216
- `UPDATE memory_jobs SET
217
- status = CASE WHEN retry_remaining > 1 THEN 'pending' ELSE 'failed' END,
218
- retry_remaining = MAX(0, retry_remaining - 1),
219
- last_error = ?,
220
- retry_at = ?,
221
- finished_at = ?,
222
- lease_until = NULL
223
- WHERE kind='memory_stage1' AND job_key=? AND status='running' AND ownership_token=?`,
224
- )
225
- .run(error.slice(0, 4000), nowSec() + STAGE1_RETRY_DELAY_SECONDS, nowSec(), sessionId, ownershipToken)
226
- }
227
-
228
- claimGlobalPhase2Job(): Phase2ClaimResult {
229
- const workerId = newId()
230
- const ownershipToken = newId()
231
- const tNow = nowSec()
232
- const lease = tNow + PHASE2_LEASE_SECONDS
233
- return this.db
234
- .transaction((): Phase2ClaimResult => {
235
- const row = this.db
236
- .prepare("SELECT * FROM memory_jobs WHERE kind='memory_consolidate_global' AND job_key='global'")
237
- .get() as
238
- | {
239
- status: string
240
- lease_until: number | null
241
- retry_at: number | null
242
- finished_at: number | null
243
- last_error: string | null
244
- }
245
- | null
246
- if (!row) {
247
- this.db
248
- .prepare(
249
- `INSERT INTO memory_jobs
250
- (kind, job_key, status, worker_id, ownership_token, started_at, lease_until, retry_remaining)
251
- VALUES ('memory_consolidate_global', 'global', 'running', ?, ?, ?, ?, ?)`,
252
- )
253
- .run(workerId, ownershipToken, tNow, lease, DEFAULT_RETRY_REMAINING)
254
- return { type: "claimed", workerId, ownershipToken }
255
- }
256
- if (row.status === "running" && row.lease_until != null && row.lease_until > tNow) {
257
- return { type: "skipped_running" }
258
- }
259
- // codex: cooldown after a clean success (last_error IS NULL AND
260
- // finished_at within the window); failures fall through to retry_at.
261
- if (row.last_error == null && row.finished_at != null && tNow - row.finished_at < PHASE2_COOLDOWN_MS / 1000) {
262
- return { type: "skipped_cooldown" }
263
- }
264
- // codex gates on retry_at regardless of status and never exhausts
265
- // phase-2 retries; retry_remaining is informational only.
266
- if (row.retry_at != null && row.retry_at > tNow) {
267
- return { type: "skipped_retry_unavailable" }
268
- }
269
- this.db
270
- .prepare(
271
- `UPDATE memory_jobs SET
272
- status='running',
273
- worker_id=?,
274
- ownership_token=?,
275
- started_at=?,
276
- lease_until=?,
277
- finished_at=NULL,
278
- retry_at=NULL,
279
- last_error=NULL
280
- WHERE kind='memory_consolidate_global' AND job_key='global'`,
281
- )
282
- .run(workerId, ownershipToken, tNow, lease)
283
- return { type: "claimed", workerId, ownershipToken }
284
- })
285
- .immediate()
286
- }
287
-
288
- heartbeatPhase2Job(ownershipToken: string): boolean {
289
- const lease = nowSec() + PHASE2_LEASE_SECONDS
290
- const res = this.db
291
- .prepare(
292
- `UPDATE memory_jobs SET lease_until=? WHERE kind='memory_consolidate_global' AND job_key='global' AND ownership_token=? AND status='running'`,
293
- )
294
- .run(lease, ownershipToken)
295
- return res.changes > 0
296
- }
297
-
298
- /**
299
- * Marks the phase-2 job done and records exactly which stage-1 snapshots the
300
- * run consumed (selected_for_phase2), so pruning cannot delete inputs that
301
- * still back the consolidated artifacts.
302
- */
303
- markPhase2Succeeded(ownershipToken: string, selected: Pick<Stage1Output, "session_id" | "source_updated_at">[] = []): void {
304
- // codex stores the completion watermark = max source_updated_at consumed;
305
- // the 6h cooldown is keyed on finished_at, not on this value.
306
- const watermark = selected.reduce((max, s) => Math.max(max, s.source_updated_at), 0)
307
- const res = this.db
308
- .prepare(
309
- `UPDATE memory_jobs SET status='done', finished_at=?, lease_until=NULL, last_error=NULL, retry_remaining=?,
310
- last_success_watermark=MAX(COALESCE(last_success_watermark, 0), ?), retry_at=NULL
311
- WHERE kind='memory_consolidate_global' AND job_key='global' AND ownership_token=? AND status='running'`,
312
- )
313
- .run(nowSec(), DEFAULT_RETRY_REMAINING, watermark, ownershipToken)
314
- if (res.changes === 0) return
315
- this.db.exec("UPDATE memory_stage1_outputs SET selected_for_phase2 = 0, selected_for_phase2_source_updated_at = NULL")
316
- const mark = this.db.prepare(
317
- `UPDATE memory_stage1_outputs
318
- SET selected_for_phase2 = 1, selected_for_phase2_source_updated_at = ?
319
- WHERE session_id = ? AND source_updated_at = ?`,
320
- )
321
- for (const s of selected) mark.run(s.source_updated_at, s.session_id, s.source_updated_at)
322
- }
323
-
324
- markPhase2Failed(ownershipToken: string, error: string): void {
325
- this.db
326
- .prepare(
327
- `UPDATE memory_jobs SET
328
- status = 'failed',
329
- retry_remaining = MAX(0, retry_remaining - 1),
330
- last_error = ?,
331
- retry_at = ?,
332
- finished_at = ?,
333
- lease_until = NULL
334
- WHERE kind='memory_consolidate_global' AND job_key='global' AND ownership_token=? AND status='running'`,
335
- )
336
- .run(error.slice(0, 4000), nowSec() + PHASE2_RETRY_DELAY_SECONDS, nowSec(), ownershipToken)
337
- }
338
-
339
- /**
340
- * Phase 2 input set, mirroring codex get_phase2_input_selection:
341
- * - excludes sessions marked disabled/polluted (their summary files then
342
- * disappear from the workspace and the diff drives forgetting)
343
- * - recency: last_usage when the memory has ever been used, otherwise
344
- * source_updated_at
345
- * - ranked by usage, then recency
346
- */
347
- getPhase2InputSelection(maxRaw: number, maxUnusedDays: number): Stage1Output[] {
348
- const cutoff = now() - maxUnusedDays * 24 * 60 * 60 * 1000
349
- return this.db
350
- .prepare(
351
- `SELECT so.* FROM memory_stage1_outputs so
352
- LEFT JOIN memory_session_meta m ON m.session_id = so.session_id
353
- WHERE (m.memory_mode IS NULL OR m.memory_mode = 'enabled')
354
- AND (length(trim(so.raw_memory)) > 0 OR length(trim(so.rollout_summary)) > 0)
355
- AND ((so.last_usage IS NOT NULL AND so.last_usage >= ?)
356
- OR (so.last_usage IS NULL AND so.source_updated_at >= ?))
357
- ORDER BY COALESCE(so.usage_count, 0) DESC,
358
- COALESCE(so.last_usage, so.source_updated_at) DESC,
359
- so.source_updated_at DESC,
360
- so.session_id DESC
361
- LIMIT ?`,
362
- )
363
- .all(cutoff, cutoff, maxRaw) as Stage1Output[]
364
- }
365
-
366
- /** Mirrors codex delete_thread_memory: remove a deleted session's output + job. */
367
- deleteSessionMemory(sessionId: string): void {
368
- this.db.transaction(() => {
369
- this.db.prepare("DELETE FROM memory_stage1_outputs WHERE session_id = ?").run(sessionId)
370
- this.db.prepare("DELETE FROM memory_jobs WHERE kind='memory_stage1' AND job_key = ?").run(sessionId)
371
- }).immediate()
372
- }
373
-
374
- /**
375
- * codex clear_memory_data deletes extracted memories and jobs but explicitly
376
- * preserves per-session memory modes: a reset must not re-enable sessions
377
- * the user disabled or that were marked polluted.
378
- */
379
- clearMemoryData(): void {
380
- this.db.transaction(() => {
381
- this.db.exec("DELETE FROM memory_stage1_outputs")
382
- this.db.exec("DELETE FROM memory_jobs")
383
- }).immediate()
384
- }
385
-
386
- setMemoryMode(sessionId: string, mode: "enabled" | "disabled" | "polluted"): void {
387
- this.db
388
- .prepare(
389
- `INSERT INTO memory_session_meta (session_id, memory_mode, polluted, updated_at)
390
- VALUES (?, ?, ?, ?)
391
- ON CONFLICT(session_id) DO UPDATE SET memory_mode=excluded.memory_mode, polluted=excluded.polluted, updated_at=excluded.updated_at`,
392
- )
393
- .run(sessionId, mode, mode === "polluted" ? 1 : 0, now())
394
- }
395
-
396
- /**
397
- * Stamp a mode only when the session has no meta row yet — used to mark
398
- * sessions seen while generate_memories=false as permanently 'disabled'
399
- * (codex stamps memory_mode at thread creation, session.rs), without
400
- * overriding an explicit user-set or polluted mode.
401
- */
402
- stampMemoryModeIfAbsent(sessionId: string, mode: "enabled" | "disabled"): void {
403
- this.db
404
- .prepare(
405
- `INSERT OR IGNORE INTO memory_session_meta (session_id, memory_mode, polluted, updated_at)
406
- VALUES (?, ?, 0, ?)`,
407
- )
408
- .run(sessionId, mode, now())
409
- }
410
-
411
- getMemoryMode(sessionId: string): "enabled" | "disabled" | "polluted" | null {
412
- const row = this.db
413
- .prepare("SELECT memory_mode AS mode FROM memory_session_meta WHERE session_id = ?")
414
- .get(sessionId) as { mode: "enabled" | "disabled" | "polluted" } | null
415
- return row?.mode ?? null
416
- }
417
-
418
- markPolluted(sessionId: string): void {
419
- this.db
420
- .prepare(
421
- `INSERT INTO memory_session_meta (session_id, memory_mode, polluted, updated_at)
422
- VALUES (?, 'polluted', 1, ?)
423
- ON CONFLICT(session_id) DO UPDATE SET polluted=1, memory_mode='polluted', updated_at=excluded.updated_at`,
424
- )
425
- .run(sessionId, now())
426
- }
427
-
428
- isPolluted(sessionId: string): boolean {
429
- const row = this.db
430
- .prepare("SELECT polluted AS p FROM memory_session_meta WHERE session_id = ?")
431
- .get(sessionId) as { p: number } | null
432
- return row?.p === 1
433
- }
434
- }