pi-hermes-memory 0.7.15 → 0.7.18
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 +8 -8
- package/package.json +1 -1
- package/src/config.ts +2 -5
- package/src/constants.ts +11 -7
- package/src/handlers/auto-consolidate.ts +4 -1
- package/src/handlers/index-sessions.ts +3 -3
- package/src/handlers/learn-memory.ts +1 -1
- package/src/handlers/pi-child-process.ts +99 -6
- package/src/handlers/session-backfill.ts +135 -0
- package/src/handlers/session-live-index.ts +89 -0
- package/src/handlers/skills-command.ts +1 -1
- package/src/handlers/switch-project.ts +2 -4
- package/src/index.ts +45 -2
- package/src/paths.ts +6 -1
- package/src/store/fts-query.ts +6 -2
- package/src/store/memory-store.ts +11 -2
- package/src/store/schema.ts +6 -0
- package/src/store/session-indexer.ts +208 -26
- package/src/store/session-parser.ts +16 -9
- package/src/store/session-search.ts +133 -62
- package/src/store/skill-store.ts +2 -2
- package/src/tools/session-search-tool.ts +2 -2
- package/src/tools/skill-tool.ts +7 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseManager } from './db.js';
|
|
2
|
-
import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
|
|
2
|
+
import { buildFallbackFts5Query, hasExplicitFts5Operator, isFts5QueryError, normalizeFts5Query } from './fts-query.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Search result from session history.
|
|
@@ -27,6 +27,52 @@ export interface SessionSearchOptions {
|
|
|
27
27
|
since?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
type SearchMatch =
|
|
31
|
+
| { type: 'fts'; query: string }
|
|
32
|
+
| { type: 'like'; terms: string[] };
|
|
33
|
+
|
|
34
|
+
const QUERY_TOKEN_PATTERN = /"([^"]*)"|(\S+)/g;
|
|
35
|
+
const NATURAL_LANGUAGE_CONNECTORS = new Set(['and', 'or', 'not', 'near']);
|
|
36
|
+
|
|
37
|
+
function escapeLikePattern(text: string): string {
|
|
38
|
+
return text.replace(/[\\%_]/g, '\\$&');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectLikeTerms(query: string): string[] {
|
|
42
|
+
const terms: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const match of query.matchAll(QUERY_TOKEN_PATTERN)) {
|
|
45
|
+
const phrase = match[1];
|
|
46
|
+
const term = match[2];
|
|
47
|
+
if (phrase === undefined && term && NATURAL_LANGUAGE_CONNECTORS.has(term.toLowerCase())) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rawValue = phrase ?? term ?? '';
|
|
52
|
+
if (rawValue.length > 0) terms.push(rawValue);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return terms;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mapRows(rows: Array<{
|
|
59
|
+
session_id: string;
|
|
60
|
+
project: string;
|
|
61
|
+
role: string;
|
|
62
|
+
content: string;
|
|
63
|
+
timestamp: string;
|
|
64
|
+
snippet: string;
|
|
65
|
+
}>): SessionSearchResult[] {
|
|
66
|
+
return rows.map(row => ({
|
|
67
|
+
sessionId: row.session_id,
|
|
68
|
+
project: row.project,
|
|
69
|
+
role: row.role,
|
|
70
|
+
content: row.content,
|
|
71
|
+
timestamp: row.timestamp,
|
|
72
|
+
snippet: row.snippet,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
30
76
|
/**
|
|
31
77
|
* Search across indexed session messages using FTS5.
|
|
32
78
|
*
|
|
@@ -47,79 +93,104 @@ export function searchSessions(
|
|
|
47
93
|
const db = dbManager.getDb();
|
|
48
94
|
const { limit = 10, project, role, since } = options;
|
|
49
95
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
96
|
+
const executeSearch = (match: SearchMatch): SessionSearchResult[] => {
|
|
97
|
+
const conditions: string[] = [];
|
|
98
|
+
const params: unknown[] = [];
|
|
99
|
+
|
|
100
|
+
if (match.type === 'fts') {
|
|
101
|
+
// FTS5 match condition — use subquery for reliable rowid matching
|
|
102
|
+
conditions.push('m.rowid IN (SELECT rowid FROM message_fts WHERE message_fts MATCH ?)');
|
|
103
|
+
params.push(match.query);
|
|
104
|
+
} else {
|
|
105
|
+
if (match.terms.length === 0) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const likeConditions = match.terms.map(() => `m.content LIKE ? ESCAPE '\\'`);
|
|
109
|
+
conditions.push(`(${likeConditions.join(' OR ')})`);
|
|
110
|
+
for (const term of match.terms) {
|
|
111
|
+
params.push(`%${escapeLikePattern(term)}%`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Project filter
|
|
116
|
+
if (project) {
|
|
117
|
+
conditions.push('s.project = ?');
|
|
118
|
+
params.push(project);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Role filter
|
|
122
|
+
if (role) {
|
|
123
|
+
conditions.push('m.role = ?');
|
|
124
|
+
params.push(role);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Date filter
|
|
128
|
+
if (since) {
|
|
129
|
+
conditions.push('m.timestamp >= ?');
|
|
130
|
+
params.push(since);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
134
|
+
|
|
135
|
+
const sql = `
|
|
136
|
+
SELECT
|
|
137
|
+
m.session_id,
|
|
138
|
+
s.project,
|
|
139
|
+
m.role,
|
|
140
|
+
m.content,
|
|
141
|
+
m.timestamp,
|
|
142
|
+
m.content as snippet
|
|
143
|
+
FROM messages m
|
|
144
|
+
JOIN sessions s ON s.id = m.session_id
|
|
145
|
+
${whereClause}
|
|
146
|
+
ORDER BY m.timestamp DESC
|
|
147
|
+
LIMIT ?
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const rows = db.prepare(sql).all(...params, limit) as Array<{
|
|
152
|
+
session_id: string;
|
|
153
|
+
project: string;
|
|
154
|
+
role: string;
|
|
155
|
+
content: string;
|
|
156
|
+
timestamp: string;
|
|
157
|
+
snippet: string;
|
|
158
|
+
}>;
|
|
159
|
+
|
|
160
|
+
return mapRows(rows);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (match.type === 'fts' && isFts5QueryError(err)) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
53
168
|
|
|
54
|
-
// FTS5 match condition — use subquery for reliable rowid matching
|
|
55
169
|
const normalizedQuery = normalizeFts5Query(query);
|
|
56
170
|
if (normalizedQuery.length === 0) {
|
|
57
171
|
return [];
|
|
58
172
|
}
|
|
59
|
-
conditions.push('m.rowid IN (SELECT rowid FROM message_fts WHERE message_fts MATCH ?)');
|
|
60
|
-
params.push(normalizedQuery);
|
|
61
|
-
|
|
62
|
-
// Project filter
|
|
63
|
-
if (project) {
|
|
64
|
-
conditions.push('s.project = ?');
|
|
65
|
-
params.push(project);
|
|
66
|
-
}
|
|
67
173
|
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
params.push(role);
|
|
174
|
+
const exactResults = executeSearch({ type: 'fts', query: normalizedQuery });
|
|
175
|
+
if (exactResults.length > 0) {
|
|
176
|
+
return exactResults;
|
|
72
177
|
}
|
|
73
178
|
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
params.push(since);
|
|
179
|
+
const explicitOperatorQuery = hasExplicitFts5Operator(query);
|
|
180
|
+
if (explicitOperatorQuery) {
|
|
181
|
+
return exactResults;
|
|
78
182
|
}
|
|
79
183
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
s.project,
|
|
86
|
-
m.role,
|
|
87
|
-
m.content,
|
|
88
|
-
m.timestamp,
|
|
89
|
-
m.content as snippet
|
|
90
|
-
FROM messages m
|
|
91
|
-
JOIN sessions s ON s.id = m.session_id
|
|
92
|
-
${whereClause}
|
|
93
|
-
ORDER BY m.timestamp DESC
|
|
94
|
-
LIMIT ?
|
|
95
|
-
`;
|
|
96
|
-
params.push(limit);
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const rows = db.prepare(sql).all(...params) as Array<{
|
|
100
|
-
session_id: string;
|
|
101
|
-
project: string;
|
|
102
|
-
role: string;
|
|
103
|
-
content: string;
|
|
104
|
-
timestamp: string;
|
|
105
|
-
snippet: string;
|
|
106
|
-
}>;
|
|
107
|
-
|
|
108
|
-
// Map snake_case column names to camelCase
|
|
109
|
-
return rows.map(row => ({
|
|
110
|
-
sessionId: row.session_id,
|
|
111
|
-
project: row.project,
|
|
112
|
-
role: row.role,
|
|
113
|
-
content: row.content,
|
|
114
|
-
timestamp: row.timestamp,
|
|
115
|
-
snippet: row.snippet,
|
|
116
|
-
}));
|
|
117
|
-
} catch (err) {
|
|
118
|
-
if (isFts5QueryError(err)) {
|
|
119
|
-
return [];
|
|
184
|
+
const fallbackQuery = buildFallbackFts5Query(query);
|
|
185
|
+
if (fallbackQuery && fallbackQuery !== normalizedQuery) {
|
|
186
|
+
const fallbackResults = executeSearch({ type: 'fts', query: fallbackQuery });
|
|
187
|
+
if (fallbackResults.length > 0) {
|
|
188
|
+
return fallbackResults;
|
|
120
189
|
}
|
|
121
|
-
throw err;
|
|
122
190
|
}
|
|
191
|
+
|
|
192
|
+
const likeTerms = collectLikeTerms(query);
|
|
193
|
+
return executeSearch({ type: 'like', terms: likeTerms });
|
|
123
194
|
}
|
|
124
195
|
|
|
125
196
|
/**
|
package/src/store/skill-store.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
import * as os from "node:os";
|
|
11
10
|
import { scanContent } from "./content-scanner.js";
|
|
12
11
|
import {
|
|
13
12
|
buildSkillId,
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
tokenizeForSimilarity,
|
|
22
21
|
} from "./skill-utils.js";
|
|
23
22
|
import type { SkillDocument, SkillIndex, SkillResult, SkillScope } from "../types.js";
|
|
23
|
+
import { AGENT_ROOT } from "../paths.js";
|
|
24
24
|
|
|
25
25
|
interface SkillStoreOptions {
|
|
26
26
|
globalSkillsDir?: string;
|
|
@@ -55,7 +55,7 @@ export class SkillStore {
|
|
|
55
55
|
private migrationSentinelPath: string;
|
|
56
56
|
|
|
57
57
|
constructor(options: SkillStoreOptions = {}) {
|
|
58
|
-
const agentRoot =
|
|
58
|
+
const agentRoot = AGENT_ROOT;
|
|
59
59
|
this.globalSkillsDir = options.globalSkillsDir ?? path.join(agentRoot, "skills");
|
|
60
60
|
this.projectSkillsDir = options.projectSkillsDir ?? null;
|
|
61
61
|
this.projectName = options.projectName ?? null;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
import { Type } from "typebox";
|
|
5
4
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
@@ -8,6 +7,7 @@ import { searchSessions, getIndexedMessageCount } from '../store/session-search.
|
|
|
8
7
|
import { searchSessionAnchors } from '../store/session-anchor-search.js';
|
|
9
8
|
import type { SessionAnchorRange, SessionAnchorSearchResult } from '../store/session-anchor-search.js';
|
|
10
9
|
import type { SessionSearchConfig } from '../types.js';
|
|
10
|
+
import { AGENT_ROOT } from '../paths.js';
|
|
11
11
|
|
|
12
12
|
interface SearchResult {
|
|
13
13
|
success: boolean;
|
|
@@ -21,7 +21,7 @@ interface SessionSearchToolOptions {
|
|
|
21
21
|
sessionsDir?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const DEFAULT_SESSIONS_DIR = path.join(
|
|
24
|
+
const DEFAULT_SESSIONS_DIR = path.join(AGENT_ROOT, 'sessions');
|
|
25
25
|
|
|
26
26
|
export function registerSessionSearchTool(
|
|
27
27
|
pi: ExtensionAPI,
|
package/src/tools/skill-tool.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill tool — registers the LLM-callable `
|
|
2
|
+
* Skill manager tool — registers the LLM-callable `skill_manage` tool for procedural memory.
|
|
3
3
|
* Complements the `memory` tool (declarative knowledge) with procedural knowledge.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -85,14 +85,16 @@ const SKILL_TOOL_PARAMETERS = Type.Object({
|
|
|
85
85
|
})),
|
|
86
86
|
}, { additionalProperties: false });
|
|
87
87
|
|
|
88
|
+
export const SKILL_MANAGE_TOOL_NAME = "skill_manage";
|
|
89
|
+
|
|
88
90
|
export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
89
91
|
pi.registerTool({
|
|
90
|
-
name:
|
|
91
|
-
label: "Skill",
|
|
92
|
+
name: SKILL_MANAGE_TOOL_NAME,
|
|
93
|
+
label: "Skill Manager",
|
|
92
94
|
description: SKILL_TOOL_DESCRIPTION,
|
|
93
|
-
promptSnippet: "
|
|
95
|
+
promptSnippet: "Create, inspect, and update reusable procedures and patterns",
|
|
94
96
|
promptGuidelines: [
|
|
95
|
-
"Use the
|
|
97
|
+
"Use the skill_manage tool after completing complex tasks that required trial and error or multiple tool calls.",
|
|
96
98
|
"Use 'create' to save a new reusable procedure, 'patch' to update a section of an existing skill by skill_id, and 'update' for a full rewrite.",
|
|
97
99
|
"Scope is required on create: choose scope='global' for transferable procedures and scope='project' when the workflow depends on this repo's paths, scripts, conventions, or deploy steps.",
|
|
98
100
|
"Prefer structured fields for create/update: when_to_use, procedure_steps, pitfalls, and verification_steps. The tool will render valid SKILL.md sections for you.",
|