@chendpoc/pi-memory 0.2.0 → 0.2.2

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 (55) hide show
  1. package/README.md +352 -63
  2. package/assets/pi-memory-logo.png +0 -0
  3. package/dist/config/index.d.ts +1 -0
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +1 -0
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/config/preflight.d.ts +8 -0
  8. package/dist/config/preflight.d.ts.map +1 -0
  9. package/dist/config/preflight.js +22 -0
  10. package/dist/config/preflight.js.map +1 -0
  11. package/dist/constants/env.d.ts +3 -0
  12. package/dist/constants/env.d.ts.map +1 -1
  13. package/dist/constants/env.js +3 -0
  14. package/dist/constants/env.js.map +1 -1
  15. package/dist/constants/preflight.d.ts +7 -0
  16. package/dist/constants/preflight.d.ts.map +1 -1
  17. package/dist/constants/preflight.js +7 -0
  18. package/dist/constants/preflight.js.map +1 -1
  19. package/dist/constants/timing.d.ts +2 -0
  20. package/dist/constants/timing.d.ts.map +1 -1
  21. package/dist/constants/timing.js +2 -0
  22. package/dist/constants/timing.js.map +1 -1
  23. package/dist/pi-extension.d.ts.map +1 -1
  24. package/dist/pi-extension.js +21 -2
  25. package/dist/pi-extension.js.map +1 -1
  26. package/dist/preflight/episodic.d.ts +7 -1
  27. package/dist/preflight/episodic.d.ts.map +1 -1
  28. package/dist/preflight/episodic.js +20 -4
  29. package/dist/preflight/episodic.js.map +1 -1
  30. package/dist/preflight/intentCache.d.ts +13 -0
  31. package/dist/preflight/intentCache.d.ts.map +1 -0
  32. package/dist/preflight/intentCache.js +37 -0
  33. package/dist/preflight/intentCache.js.map +1 -0
  34. package/dist/preflight/queryIntent.d.ts +14 -5
  35. package/dist/preflight/queryIntent.d.ts.map +1 -1
  36. package/dist/preflight/queryIntent.js +27 -11
  37. package/dist/preflight/queryIntent.js.map +1 -1
  38. package/dist/sidecar/warmup.d.ts +6 -0
  39. package/dist/sidecar/warmup.d.ts.map +1 -0
  40. package/dist/sidecar/warmup.js +39 -0
  41. package/dist/sidecar/warmup.js.map +1 -0
  42. package/doc/README-zh.md +394 -0
  43. package/doc/ROADMAP-zh.md +52 -0
  44. package/doc/ROADMAP.md +52 -0
  45. package/package.json +5 -1
  46. package/src/config/index.ts +1 -0
  47. package/src/config/preflight.ts +34 -0
  48. package/src/constants/env.ts +3 -0
  49. package/src/constants/preflight.ts +9 -0
  50. package/src/constants/timing.ts +2 -0
  51. package/src/pi-extension.ts +22 -2
  52. package/src/preflight/episodic.ts +28 -4
  53. package/src/preflight/intentCache.ts +44 -0
  54. package/src/preflight/queryIntent.ts +46 -12
  55. package/src/sidecar/warmup.ts +50 -0
@@ -1,3 +1,4 @@
1
+ import { readPreflightRuntimeConfig } from "../config/preflight.js";
1
2
  import { resolvePreflightBudget } from "../config/preflightBudget.js";
2
3
  import type { LlmClient } from "../adapters/llm/types.js";
3
4
  import { debugMemory } from "../utils/debugLog.js";
@@ -20,7 +21,13 @@ export type EpisodicPreflightOptions = {
20
21
  agentDir: string;
21
22
  store: MemoryStore;
22
23
  llm?: LlmClient | null;
24
+ /** Force episodic gate (first turn); does not force helper LLM. */
25
+ forceEpisodic?: boolean;
26
+ /** Force helper LLM intent extraction. */
27
+ forceIntent?: boolean;
28
+ /** @deprecated Use forceEpisodic */
23
29
  force?: boolean;
30
+ sessionId?: string;
24
31
  budgetMs?: number;
25
32
  signal?: AbortSignal;
26
33
  onProgress?: (message: string) => void;
@@ -67,16 +74,23 @@ async function withTimeout<T>(
67
74
  }
68
75
 
69
76
  /**
70
- * Fail-silent episodic preflight: QueryIntent (Zod) → buildRetrievalQuery → sidecar.query → fallback.
77
+ * Fail-silent episodic preflight: QueryIntent (optional) → buildRetrievalQuery → sidecar.query → fallback.
71
78
  */
72
79
  export async function runEpisodicPreflight(
73
80
  userInput: string,
74
81
  options: EpisodicPreflightOptions,
75
82
  ): Promise<EpisodicPreflightResult | null> {
76
83
  const startedAt = nowMs();
84
+ const forceEpisodic = options.forceEpisodic ?? options.force ?? false;
85
+ const runtime = readPreflightRuntimeConfig();
77
86
 
78
87
  try {
79
- if (!shouldRunEpisodicPreflight(userInput, options.force)) {
88
+ if (await options.store.isEmpty()) {
89
+ debugMemory("preflight", "skipped", { reason: "empty" });
90
+ return null;
91
+ }
92
+
93
+ if (!shouldRunEpisodicPreflight(userInput, forceEpisodic)) {
80
94
  debugMemory("preflight", "skipped", { reason: "gate" });
81
95
  return null;
82
96
  }
@@ -88,15 +102,23 @@ export async function runEpisodicPreflight(
88
102
  const intentStartedAt = nowMs();
89
103
  const intentBudget = Math.min(budget.intentMs, remainingMs(deadline));
90
104
  let intent;
105
+ let intentSkipped = false;
106
+ let intentCacheHit = false;
107
+
91
108
  try {
92
- intent = await withTimeout(
109
+ const extracted = await withTimeout(
93
110
  extractQueryIntent(userInput, options.llm ?? null, {
94
- force: options.force,
111
+ forceIntent: options.forceIntent,
95
112
  signal: options.signal,
113
+ sessionId: options.sessionId,
114
+ intentCache: runtime.intentCache,
96
115
  }),
97
116
  intentBudget,
98
117
  options.signal,
99
118
  );
119
+ intent = extracted.intent;
120
+ intentSkipped = extracted.skipped;
121
+ intentCacheHit = extracted.cacheHit;
100
122
  } catch {
101
123
  intent = { raw_query: userInput.trim() };
102
124
  }
@@ -143,6 +165,8 @@ export async function runEpisodicPreflight(
143
165
 
144
166
  debugMemory("preflight", "recall", {
145
167
  intent_ms: intentMs,
168
+ intent_skipped: intentSkipped,
169
+ intent_cache_hit: intentCacheHit,
146
170
  sidecar_ms: sidecarMs,
147
171
  total_ms: nowMs() - startedAt,
148
172
  cache_hit: cacheHit,
@@ -0,0 +1,44 @@
1
+ import { LRUCache } from "lru-cache";
2
+
3
+ import { INTENT_CACHE_MAX_ENTRIES } from "../constants/preflight.js";
4
+ import type { QueryIntent } from "./queryIntent.js";
5
+
6
+ function normalizeInput(input: string): string {
7
+ return input.trim().replace(/\s+/g, " ").toLowerCase();
8
+ }
9
+
10
+ function cacheKey(sessionId: string, userInput: string): string {
11
+ return `${sessionId}\0${normalizeInput(userInput)}`;
12
+ }
13
+
14
+ class QueryIntentCache {
15
+ private readonly lru = new LRUCache<string, QueryIntent>({ max: INTENT_CACHE_MAX_ENTRIES });
16
+
17
+ get(sessionId: string | undefined, userInput: string): QueryIntent | null {
18
+ if (!sessionId) return null;
19
+ return this.lru.get(cacheKey(sessionId, userInput)) ?? null;
20
+ }
21
+
22
+ set(sessionId: string | undefined, userInput: string, intent: QueryIntent): void {
23
+ if (!sessionId) return;
24
+ this.lru.set(cacheKey(sessionId, userInput), intent);
25
+ }
26
+
27
+ clearSession(sessionId: string): void {
28
+ const prefix = `${sessionId}\0`;
29
+ for (const key of this.lru.keys()) {
30
+ if (key.startsWith(prefix)) this.lru.delete(key);
31
+ }
32
+ }
33
+
34
+ /** @internal test hook */
35
+ resetForTests(): void {
36
+ this.lru.clear();
37
+ }
38
+ }
39
+
40
+ export const queryIntentCache = new QueryIntentCache();
41
+
42
+ export function resetQueryIntentCacheForTests(): void {
43
+ queryIntentCache.resetForTests();
44
+ }
@@ -2,12 +2,15 @@ import compact from "lodash/compact.js";
2
2
  import { Type, type Static } from "typebox";
3
3
  import { Value } from "typebox/value";
4
4
 
5
+ import { readPreflightRuntimeConfig } from "../config/preflight.js";
5
6
  import {
7
+ DEFAULT_INTENT_RETRIES,
6
8
  MEMORY_CUE_RE,
7
9
  PREFLIGHT_EXTRACT_MIN_LENGTH,
8
10
  PREFLIGHT_SKIP_MIN_LENGTH,
9
11
  } from "../constants/preflight.js";
10
12
  import type { LlmClient } from "../adapters/llm/types.js";
13
+ import { queryIntentCache } from "./intentCache.js";
11
14
 
12
15
  /** Structured intent from helper LLM — simpler than crafting a search query directly. */
13
16
  export const QueryIntentSchema = Type.Object(
@@ -35,17 +38,17 @@ Use raw_query when the whole message should be searched verbatim.
35
38
  User message:
36
39
  `;
37
40
 
38
- export function shouldRunEpisodicPreflight(query: string, force = false): boolean {
41
+ export function shouldRunEpisodicPreflight(query: string, forceEpisodic = false): boolean {
39
42
  const trimmed = query.trim();
40
43
  if (!trimmed) return false;
41
- if (force) return true;
44
+ if (forceEpisodic) return true;
42
45
  if (trimmed.startsWith("/")) return false;
43
46
  if (trimmed.length < PREFLIGHT_SKIP_MIN_LENGTH && !MEMORY_CUE_RE.test(trimmed)) return false;
44
47
  return MEMORY_CUE_RE.test(trimmed) || trimmed.length >= PREFLIGHT_EXTRACT_MIN_LENGTH;
45
48
  }
46
49
 
47
- export function shouldExtractIntent(query: string, force = false): boolean {
48
- if (force) return true;
50
+ export function shouldExtractIntent(query: string, forceIntent = false): boolean {
51
+ if (forceIntent) return true;
49
52
  if (query.trim().length >= PREFLIGHT_EXTRACT_MIN_LENGTH) return true;
50
53
  return MEMORY_CUE_RE.test(query);
51
54
  }
@@ -69,24 +72,55 @@ function extractJsonObject(text: string): unknown {
69
72
  return JSON.parse(text.slice(start, end + 1));
70
73
  }
71
74
 
75
+ export type ExtractQueryIntentOptions = {
76
+ forceIntent?: boolean;
77
+ signal?: AbortSignal;
78
+ sessionId?: string;
79
+ intentCache?: boolean;
80
+ intentRetries?: number;
81
+ };
82
+
83
+ export type ExtractQueryIntentResult = {
84
+ intent: QueryIntent;
85
+ skipped: boolean;
86
+ cacheHit: boolean;
87
+ };
88
+
72
89
  export async function extractQueryIntent(
73
90
  userInput: string,
74
91
  llm: LlmClient | null,
75
- options: { force?: boolean; signal?: AbortSignal } = {},
76
- ): Promise<QueryIntent> {
92
+ options: ExtractQueryIntentOptions = {},
93
+ ): Promise<ExtractQueryIntentResult> {
77
94
  const fallback: QueryIntent = { raw_query: userInput.trim() };
78
- if (!llm || !shouldExtractIntent(userInput, options.force)) {
79
- return fallback;
95
+ const runtime = readPreflightRuntimeConfig();
96
+ const useCache = options.intentCache ?? runtime.intentCache;
97
+
98
+ if (useCache && options.sessionId) {
99
+ const cached = queryIntentCache.get(options.sessionId, userInput);
100
+ if (cached) {
101
+ return { intent: cached, skipped: false, cacheHit: true };
102
+ }
80
103
  }
81
104
 
82
- for (let attempt = 0; attempt < 2; attempt++) {
105
+ if (!llm || !shouldExtractIntent(userInput, options.forceIntent)) {
106
+ return { intent: fallback, skipped: true, cacheHit: false };
107
+ }
108
+
109
+ const retries = options.intentRetries ?? runtime.intentRetries ?? DEFAULT_INTENT_RETRIES;
110
+ const maxAttempts = Math.max(1, retries + 1);
111
+
112
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
83
113
  try {
84
114
  const raw = await llm.complete(`${INTENT_PROMPT}${userInput}`, options.signal);
85
- return parseQueryIntent(extractJsonObject(raw));
115
+ const intent = parseQueryIntent(extractJsonObject(raw));
116
+ if (useCache && options.sessionId) {
117
+ queryIntentCache.set(options.sessionId, userInput, intent);
118
+ }
119
+ return { intent, skipped: false, cacheHit: false };
86
120
  } catch {
87
- // one retry, then fallback
121
+ // retry or fallback
88
122
  }
89
123
  }
90
124
 
91
- return fallback;
125
+ return { intent: fallback, skipped: false, cacheHit: false };
92
126
  }
@@ -0,0 +1,50 @@
1
+ import { fetchIndexStats, ping, query } from "./client.js";
2
+ import { SIDECAR_WARMUP_QUERY_TIMEOUT_MS } from "../constants/timing.js";
3
+ import { debugMemory } from "../utils/debugLog.js";
4
+ import { nowMs } from "../utils/time.js";
5
+
6
+ export type WarmSidecarOptions = {
7
+ queryTimeoutMs?: number;
8
+ };
9
+
10
+ /** Pre-open sidecar DB and optionally smoke-test embed + vec paths. Fail-silent. */
11
+ export async function warmSidecar(
12
+ socketPath: string,
13
+ options: WarmSidecarOptions = {},
14
+ ): Promise<void> {
15
+ const startedAt = nowMs();
16
+
17
+ if (!(await ping(socketPath))) {
18
+ debugMemory("sidecar", "warm", { ok: false, reason: "ping" });
19
+ return;
20
+ }
21
+
22
+ const statsStartedAt = nowMs();
23
+ const statsResult = await fetchIndexStats(socketPath);
24
+ const statsMs = nowMs() - statsStartedAt;
25
+
26
+ if ("error" in statsResult) {
27
+ debugMemory("sidecar", "warm", { ok: false, stats_ms: statsMs, reason: "stats" });
28
+ return;
29
+ }
30
+
31
+ let queryMs = 0;
32
+ if (statsResult.stats.chunk_count > 0) {
33
+ const queryStartedAt = nowMs();
34
+ const queryTimeoutMs = options.queryTimeoutMs ?? SIDECAR_WARMUP_QUERY_TIMEOUT_MS;
35
+ try {
36
+ await query(socketPath, ".", queryTimeoutMs);
37
+ } catch {
38
+ // warm query is best-effort
39
+ }
40
+ queryMs = nowMs() - queryStartedAt;
41
+ }
42
+
43
+ debugMemory("sidecar", "warm", {
44
+ ok: true,
45
+ stats_ms: statsMs,
46
+ query_ms: queryMs,
47
+ chunks: statsResult.stats.chunk_count,
48
+ total_ms: nowMs() - startedAt,
49
+ });
50
+ }