@dzhi/ocpg 0.15.0 → 0.16.0
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/README.md +12 -3
- package/ocpg.ts +73 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,11 +45,20 @@ export OCPG_SSL="disable"
|
|
|
45
45
|
|
|
46
46
|
## How memory works
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Visibility is **type-based**:
|
|
49
|
+
|
|
50
|
+
| Type | Visibility | What it's for |
|
|
51
|
+
| -------------- | ----------------------------- | ---------------------------------------------- |
|
|
52
|
+
| `preference` | Global | Personal to the operator, not about any codebase |
|
|
53
|
+
| `stack_fact` | Global | True about the tooling/stack itself - portable to any project using the same stack |
|
|
54
|
+
| `project_fact` | Origin project only (default) | True about this specific project/customer; pass `global: true` on recall to reach across |
|
|
55
|
+
| `episodic` | Reserved, unused | - |
|
|
56
|
+
|
|
57
|
+
`memory_forget` / `memory_update` follow the same rule: a foreign project's `project_fact` is off-limits; global types are maintainable from any project.
|
|
49
58
|
|
|
50
59
|
## How injection picks memories
|
|
51
60
|
|
|
52
|
-
By default the block is **relevance-ranked, not recency-ranked**: the user's latest prompt is turned into a full-text query (OR of stemmed words) over **
|
|
61
|
+
By default the block is **relevance-ranked, not recency-ranked**: the user's latest prompt is turned into a full-text query (OR of stemmed words) over every **visible** memory (global types from anywhere, `project_fact` from its origin project), ranked by relevance with a small same-project tiebreak, top 5 injected - each line labeled with its origin project. When nothing matches the prompt, it falls back to the latest visible memories (preferences first). Set `OCPG_INJECTION=recency` for the blind-last-5 behavior.
|
|
53
62
|
|
|
54
63
|
This is keyword relevance, not embedding-based semantic search - close phrasing wins, paraphrases may not. Note the OR ranking: common words in a prompt surface more rows; the ranking favors rows matching more distinctive terms.
|
|
55
64
|
|
|
@@ -57,7 +66,7 @@ This is keyword relevance, not embedding-based semantic search - close phrasing
|
|
|
57
66
|
|
|
58
67
|
Four agent tools are registered: `memory_remember` (store), `memory_recall` (search), `memory_forget` (delete by id), `memory_update` (rewrite an existing memory, keeping its original learned date), and `memory_consolidate` (remove near-duplicates on demand). The agent reads their usage rules from the tool schemas - as the user, the things worth knowing are:
|
|
59
68
|
|
|
60
|
-
-
|
|
69
|
+
- Visibility follows the type (see the table above); `memory_recall` takes `global: true` to search other projects' `project_fact` memories.
|
|
61
70
|
- Duplicate writes are **never rejected** - they land, the injection block collapses them, and `memory_consolidate` cleans them up when you ask: it keeps the newest of each >=80%-similar group and reports the removed texts so the agent can merge any unique fact back.
|
|
62
71
|
|
|
63
72
|
Writes are capped at 4000 characters of content, 10 tags, and 64 characters per tag; oversized writes are rejected with the actual size rather than silently truncated. Memories carry a `type` (`preference`, `project_fact` default, or `episodic`); `preference` memories come first in recency mode.
|
package/ocpg.ts
CHANGED
|
@@ -13,7 +13,7 @@ type DbConfig = {
|
|
|
13
13
|
database: string;
|
|
14
14
|
ssl: SslMode;
|
|
15
15
|
};
|
|
16
|
-
type RecallArgs = { query?: string; limit?: number; tags?: string[] };
|
|
16
|
+
type RecallArgs = { query?: string; limit?: number; tags?: string[]; global?: boolean };
|
|
17
17
|
type RememberArgs = {
|
|
18
18
|
content: string;
|
|
19
19
|
tags?: string[];
|
|
@@ -242,34 +242,46 @@ function extractPromptQuery(
|
|
|
242
242
|
return "";
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
245
|
+
// Visibility rule shared by recall and both injection builders: global types
|
|
246
|
+
// (preference, stack_fact) are visible everywhere; project_fact is visible
|
|
247
|
+
// only from its origin project unless explicitly opted out with
|
|
248
|
+
// global: true (recall) - at injection time there is no caller to opt in, so
|
|
249
|
+
// other projects' project_fact rows never surface as ambient context.
|
|
250
|
+
function visibleRows(client: SQL, directory: string) {
|
|
251
|
+
return client`(memory_type != 'project_fact' OR project = ${directory})`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Recency query shared by the recency mode and the no-match fallback: latest
|
|
255
|
+
// visible rows, preferences first. Global by design - the project column
|
|
256
|
+
// records origin, not visibility... for global types; project_fact is
|
|
257
|
+
// origin-scoped (see visibleRows).
|
|
248
258
|
// Query builders are pure functions of the client so the benchmark
|
|
249
259
|
// (bench/run.ts) can execute the EXACT production SQL against bench
|
|
250
260
|
// databases - no drift between what is measured and what runs.
|
|
251
|
-
function buildRecencyQuery(client: SQL) {
|
|
261
|
+
function buildRecencyQuery(client: SQL, directory: string) {
|
|
252
262
|
// LIMIT 20: a candidate slice for collapseDupes, not the final block.
|
|
253
263
|
return client`
|
|
254
264
|
SELECT content, coalesce(tags, '{}') AS tags,
|
|
255
265
|
to_char(created_at, 'YYYY-MM-DD') AS date,
|
|
256
266
|
project
|
|
257
267
|
FROM memories
|
|
268
|
+
WHERE ${visibleRows(client, directory)}
|
|
258
269
|
ORDER BY (memory_type = 'preference') DESC, created_at DESC
|
|
259
270
|
LIMIT 20
|
|
260
271
|
`;
|
|
261
272
|
}
|
|
262
273
|
|
|
263
274
|
function buildRelevanceQuery(client: SQL, tsQuery: string, directory: string) {
|
|
264
|
-
// Relevance: full-text search
|
|
265
|
-
//
|
|
266
|
-
// memories.
|
|
275
|
+
// Relevance: full-text search over every visible memory - global types
|
|
276
|
+
// from anywhere, project_fact from the origin project - with a small
|
|
277
|
+
// same-project boost to break rank ties toward locally stored memories.
|
|
267
278
|
return client`
|
|
268
279
|
SELECT content, coalesce(tags, '{}') AS tags,
|
|
269
280
|
to_char(created_at, 'YYYY-MM-DD') AS date,
|
|
270
281
|
project
|
|
271
282
|
FROM memories
|
|
272
283
|
WHERE search_vector @@ to_tsquery('english', ${tsQuery})
|
|
284
|
+
AND ${visibleRows(client, directory)}
|
|
273
285
|
ORDER BY ts_rank(search_vector, to_tsquery('english', ${tsQuery}))
|
|
274
286
|
+ (CASE WHEN project = ${directory} THEN 0.01 ELSE 0 END) DESC,
|
|
275
287
|
created_at DESC
|
|
@@ -312,10 +324,10 @@ async function handleTransform(
|
|
|
312
324
|
);
|
|
313
325
|
if (rows.length === 0) {
|
|
314
326
|
// No keyword match for this prompt - recency beats an empty block.
|
|
315
|
-
rows = await withDeadline(buildRecencyQuery(sql) as unknown as PromiseLike<InjectionRow[]>, 1000);
|
|
327
|
+
rows = await withDeadline(buildRecencyQuery(sql, directory) as unknown as PromiseLike<InjectionRow[]>, 1000);
|
|
316
328
|
}
|
|
317
329
|
} else {
|
|
318
|
-
rows = await withDeadline(buildRecencyQuery(sql) as unknown as PromiseLike<InjectionRow[]>, 1000);
|
|
330
|
+
rows = await withDeadline(buildRecencyQuery(sql, directory) as unknown as PromiseLike<InjectionRow[]>, 1000);
|
|
319
331
|
}
|
|
320
332
|
const block = formatBlock(collapseDupes(rows), directory);
|
|
321
333
|
// Evict oldest entry when cache exceeds 32
|
|
@@ -363,9 +375,12 @@ const MAX_TAG_LENGTH = 64;
|
|
|
363
375
|
|
|
364
376
|
// The stored vocabulary mirrors the DB CHECK constraint (memories_type_check);
|
|
365
377
|
// rows predate the column, so "required" would break every existing caller -
|
|
366
|
-
// type is always defaulted.
|
|
378
|
+
// type is always defaulted. Visibility follows the type: preference and
|
|
379
|
+
// stack_fact are global (tooling knowledge ports across every project using
|
|
380
|
+
// the stack), project_fact is origin-project-only (customer-specific facts
|
|
381
|
+
// must not surface elsewhere), episodic is reserved for the (cut, opt-in) 1.3
|
|
367
382
|
// feature; remember accepts it so the vocabulary stays in one place.
|
|
368
|
-
const MEMORY_TYPES = ["preference", "project_fact", "episodic"] as const;
|
|
383
|
+
const MEMORY_TYPES = ["preference", "stack_fact", "project_fact", "episodic"] as const;
|
|
369
384
|
type MemoryType = (typeof MEMORY_TYPES)[number];
|
|
370
385
|
|
|
371
386
|
function resolveMemoryType(raw: unknown): MemoryType {
|
|
@@ -396,11 +411,14 @@ function toolError(kind: string, action: string, e: unknown): string {
|
|
|
396
411
|
|
|
397
412
|
async function recall(
|
|
398
413
|
args: RecallArgs,
|
|
414
|
+
ctx: { directory: string },
|
|
399
415
|
): Promise<string> {
|
|
400
416
|
try {
|
|
401
417
|
const limit = resolveLimit(args.limit);
|
|
402
|
-
//
|
|
403
|
-
//
|
|
418
|
+
// Visibility: global types (preference, stack_fact) everywhere;
|
|
419
|
+
// project_fact only from the origin project unless the caller opts in
|
|
420
|
+
// with global: true.
|
|
421
|
+
const visibleCond = args.global ? sql`` : sql`AND ${visibleRows(sql, ctx.directory)}`;
|
|
404
422
|
const queryCond = args.query
|
|
405
423
|
? sql`AND search_vector @@ to_tsquery('english', ${orTsQuery(args.query)})`
|
|
406
424
|
: sql``;
|
|
@@ -425,7 +443,7 @@ async function recall(
|
|
|
425
443
|
to_char(created_at, 'YYYY-MM-DD') AS date,
|
|
426
444
|
project, memory_type
|
|
427
445
|
FROM memories
|
|
428
|
-
WHERE 1=1 ${queryCond} ${tagCond}
|
|
446
|
+
WHERE 1=1 ${visibleCond} ${queryCond} ${tagCond}
|
|
429
447
|
${orderBy}
|
|
430
448
|
LIMIT ${limit}
|
|
431
449
|
` as (MemoryRow & { memory_type: string })[];
|
|
@@ -562,9 +580,9 @@ async function captureFromPrompt(
|
|
|
562
580
|
}
|
|
563
581
|
}
|
|
564
582
|
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
583
|
+
// project_fact rows are origin-scoped (a customer-B agent must not delete
|
|
584
|
+
// customer-A's customer facts); global types (preference, stack_fact) are
|
|
585
|
+
// maintainable from any project - that's what global means for them.
|
|
568
586
|
async function forget(
|
|
569
587
|
args: ForgetArgs,
|
|
570
588
|
ctx: { directory: string },
|
|
@@ -576,11 +594,15 @@ async function forget(
|
|
|
576
594
|
}
|
|
577
595
|
const deleted = await sql`
|
|
578
596
|
DELETE FROM memories
|
|
579
|
-
WHERE id = ${id}
|
|
597
|
+
WHERE id = ${id} AND ${visibleRows(sql, ctx.directory)}
|
|
580
598
|
RETURNING id
|
|
581
599
|
` as { id: number }[];
|
|
582
600
|
|
|
583
601
|
if (deleted.length === 0) {
|
|
602
|
+
const exists = await sql`SELECT project, memory_type FROM memories WHERE id = ${id}` as { project: string; memory_type: string }[];
|
|
603
|
+
if (exists.length > 0) {
|
|
604
|
+
return `Memory #${id} is a ${exists[0].memory_type} belonging to ${exists[0].project}; not deleted. Only that project's agent can delete it.`;
|
|
605
|
+
}
|
|
584
606
|
return `No memory #${id}; nothing deleted.`;
|
|
585
607
|
}
|
|
586
608
|
invalidateInjection(ctx.directory);
|
|
@@ -620,11 +642,15 @@ async function updateMemory(
|
|
|
620
642
|
${tagCond}
|
|
621
643
|
${typeCond}
|
|
622
644
|
updated_at = now()
|
|
623
|
-
WHERE id = ${id}
|
|
645
|
+
WHERE id = ${id} AND ${visibleRows(sql, ctx.directory)}
|
|
624
646
|
RETURNING id
|
|
625
647
|
` as { id: number }[];
|
|
626
648
|
|
|
627
649
|
if (updated.length === 0) {
|
|
650
|
+
const exists = await sql`SELECT project, memory_type FROM memories WHERE id = ${id}` as { project: string; memory_type: string }[];
|
|
651
|
+
if (exists.length > 0) {
|
|
652
|
+
return `Memory #${id} is a ${exists[0].memory_type} belonging to ${exists[0].project}; not updated. Only that project's agent can edit it.`;
|
|
653
|
+
}
|
|
628
654
|
return `No memory #${id}; nothing updated.`;
|
|
629
655
|
}
|
|
630
656
|
invalidateInjection(ctx.directory);
|
|
@@ -753,7 +779,7 @@ const ocpg = Plugin.define({
|
|
|
753
779
|
// breaks direct invocation without adding value.
|
|
754
780
|
options: { codemode: false },
|
|
755
781
|
description:
|
|
756
|
-
"Search past memories
|
|
782
|
+
"Search past memories. Global types (preference, stack_fact) are always searched; this project's project_fact memories are searched by default. Use before non-trivial work to check for relevant lessons, fixes, and decisions.",
|
|
757
783
|
input: {
|
|
758
784
|
type: "object",
|
|
759
785
|
properties: {
|
|
@@ -765,12 +791,19 @@ const ocpg = Plugin.define({
|
|
|
765
791
|
"Only return memories carrying all of these tags. Tags are not full-text " +
|
|
766
792
|
"searchable (search covers content only), so this filter is the only way to reach them.",
|
|
767
793
|
},
|
|
794
|
+
global: {
|
|
795
|
+
type: "boolean",
|
|
796
|
+
description:
|
|
797
|
+
"Also search other projects' project_fact memories (default: only this " +
|
|
798
|
+
"project's project_fact memories, plus all preference/stack_fact memories, " +
|
|
799
|
+
"which are always global).",
|
|
800
|
+
},
|
|
768
801
|
limit: { type: "number", description: "1-20, default 5" },
|
|
769
802
|
},
|
|
770
803
|
additionalProperties: false,
|
|
771
804
|
},
|
|
772
805
|
execute: async (input) => {
|
|
773
|
-
return { content: await recall(input as RecallArgs) };
|
|
806
|
+
return { content: await recall(input as RecallArgs, { directory }) };
|
|
774
807
|
},
|
|
775
808
|
});
|
|
776
809
|
editor.add({
|
|
@@ -781,8 +814,10 @@ const ocpg = Plugin.define({
|
|
|
781
814
|
// injected block is skipped entirely for projects with no memories and
|
|
782
815
|
// the user may have no project instructions at all.
|
|
783
816
|
description:
|
|
784
|
-
"Store a durable memory shared across all projects
|
|
785
|
-
"
|
|
817
|
+
"Store a durable memory. preference and stack_fact are shared across all projects; " +
|
|
818
|
+
"project_fact (the default) is visible only in this project unless recalled with " +
|
|
819
|
+
"global: true. Use after user corrections (immediately), architecture decisions, " +
|
|
820
|
+
"non-trivial fixes, environment facts, and stated preferences. " +
|
|
786
821
|
"Do not store session progress, secrets, or anything the code itself already states.",
|
|
787
822
|
input: {
|
|
788
823
|
type: "object",
|
|
@@ -795,8 +830,15 @@ const ocpg = Plugin.define({
|
|
|
795
830
|
type: "string",
|
|
796
831
|
enum: [...MEMORY_TYPES],
|
|
797
832
|
description:
|
|
798
|
-
"preference = a standing user preference (
|
|
799
|
-
"
|
|
833
|
+
"preference = a standing user preference (global, injected first). " +
|
|
834
|
+
"stack_fact = true about the tooling/stack itself, portable to any project " +
|
|
835
|
+
"using the same stack (e.g. a Terraform module quirk, an ArgoCD gotcha, a " +
|
|
836
|
+
"Helm chart convention) - global, like preference. " +
|
|
837
|
+
"project_fact (default) = true about THIS specific project/customer only " +
|
|
838
|
+
"(an environment quirk, a customer's specific request, a one-off workaround) " +
|
|
839
|
+
"- visible only in this project unless the caller asks for global search. " +
|
|
840
|
+
"Test: would this fact help in a different customer's repo using the same " +
|
|
841
|
+
"tools? If yes, stack_fact. If no, project_fact.",
|
|
800
842
|
},
|
|
801
843
|
tags: {
|
|
802
844
|
type: "array",
|
|
@@ -820,7 +862,8 @@ const ocpg = Plugin.define({
|
|
|
820
862
|
options: { codemode: false },
|
|
821
863
|
description:
|
|
822
864
|
"Delete a memory by id (get ids from memory_recall). Use for memories that are wrong or obsolete; prefer storing a corrected memory when the old one is still useful history. " +
|
|
823
|
-
"
|
|
865
|
+
"preference and stack_fact can be deleted from any project; project_fact can only be " +
|
|
866
|
+
"deleted by its origin project (the delete will fail with the owning project's name).",
|
|
824
867
|
input: {
|
|
825
868
|
type: "object",
|
|
826
869
|
properties: {
|
|
@@ -838,7 +881,9 @@ const ocpg = Plugin.define({
|
|
|
838
881
|
options: { codemode: false },
|
|
839
882
|
description:
|
|
840
883
|
"Rewrite an existing memory by id (get ids from memory_recall). Use when a memory is outdated but still worth keeping: the corrected content replaces the old, keeping the original learned date. " +
|
|
841
|
-
"Omitted tags/type are kept as-is.
|
|
884
|
+
"Omitted tags/type are kept as-is. preference and stack_fact can be edited from any " +
|
|
885
|
+
"project; project_fact can only be edited by its origin project. " +
|
|
886
|
+
"For obsolete memories use memory_forget; for genuinely new memories use memory_remember.",
|
|
842
887
|
input: {
|
|
843
888
|
type: "object",
|
|
844
889
|
properties: {
|