pi-hermes-memory 0.7.22 → 0.8.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 +18 -2
- package/package.json +3 -2
- package/src/config.ts +14 -1
- package/src/constants.ts +72 -0
- package/src/extension-root-migration.ts +429 -5
- package/src/handlers/auto-consolidate.ts +118 -8
- package/src/handlers/background-review.ts +164 -64
- package/src/handlers/child-process-watchdog.mjs +90 -0
- package/src/handlers/correction-detector.ts +52 -36
- package/src/handlers/pi-child-process.ts +270 -37
- package/src/handlers/review-memory-ops.ts +383 -0
- package/src/handlers/session-flush.ts +71 -4
- package/src/handlers/sync-markdown-memories.ts +143 -51
- package/src/index.ts +36 -17
- package/src/store/atomic-lock-coordinator.ts +252 -0
- package/src/store/canonical-storage-path.ts +54 -0
- package/src/store/db.ts +244 -9
- package/src/store/markdown-mutation-lock.ts +38 -0
- package/src/store/memory-store.ts +455 -57
- package/src/store/sqlite-memory-store.ts +121 -4
- package/src/tools/memory-tool.ts +48 -9
- package/src/tools/session-search-tool.ts +70 -12
- package/src/types.ts +6 -0
|
@@ -3,23 +3,129 @@
|
|
|
3
3
|
* Ported from hermes-agent/run_agent.py (_spawn_background_review, _memory_nudge_interval).
|
|
4
4
|
* See PLAN.md → "Hermes Source File Reference Map" for source lines.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Default transport: in-process complete() side-channel (preserves parent LLM cache).
|
|
7
|
+
* Fallback: pi.exec("pi", ["-p", ...]) subprocess when direct path is unavailable.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { COMBINED_REVIEW_PROMPT, DIRECT_REVIEW_SYSTEM_PROMPT } from "../constants.js";
|
|
11
12
|
import { MemoryStore } from "../store/memory-store.js";
|
|
12
|
-
import {
|
|
13
|
+
import { DatabaseManager } from "../store/db.js";
|
|
13
14
|
import type { MemoryConfig } from "../types.js";
|
|
14
15
|
import { applyRecentMessageLimit, collectMessageParts } from "./message-parts.js";
|
|
15
16
|
import { execChildPrompt } from "./pi-child-process.js";
|
|
17
|
+
import { runDirectMemoryCompletion, usesDirectTransport, type DirectReviewResult } from "./review-memory-ops.js";
|
|
18
|
+
|
|
19
|
+
export interface BackgroundReviewOptions {
|
|
20
|
+
dbManager?: DatabaseManager | null;
|
|
21
|
+
projectName?: string | null;
|
|
22
|
+
deps?: BackgroundReviewDeps;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BackgroundReviewDeps {
|
|
26
|
+
runDirectReview?: typeof runDirectMemoryCompletion;
|
|
27
|
+
execChildPrompt?: typeof execChildPrompt;
|
|
28
|
+
/** Test-only hook: called once runReview() has fully settled (after the
|
|
29
|
+
* fire-and-forget review work completes and reviewInProgress resets),
|
|
30
|
+
* since production callers never await runReview() directly. */
|
|
31
|
+
onReviewSettled?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ReviewPromptInput {
|
|
35
|
+
parts: string[];
|
|
36
|
+
currentMemory: string;
|
|
37
|
+
currentUser: string;
|
|
38
|
+
currentProject: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildSubprocessReviewPrompt(input: ReviewPromptInput): string {
|
|
42
|
+
const reviewPrompt = [
|
|
43
|
+
COMBINED_REVIEW_PROMPT,
|
|
44
|
+
"",
|
|
45
|
+
"--- Current Memory ---",
|
|
46
|
+
input.currentMemory || "(empty)",
|
|
47
|
+
"",
|
|
48
|
+
"--- Current User Profile ---",
|
|
49
|
+
input.currentUser || "(empty)",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
if (input.currentProject !== null) {
|
|
53
|
+
reviewPrompt.push(
|
|
54
|
+
"",
|
|
55
|
+
"--- Current Project Memory ---",
|
|
56
|
+
input.currentProject || "(empty)",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
reviewPrompt.push(
|
|
61
|
+
"",
|
|
62
|
+
"--- Conversation to Review ---",
|
|
63
|
+
input.parts.join("\n\n"),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return reviewPrompt.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildDirectReviewUserPrompt(input: ReviewPromptInput): string {
|
|
70
|
+
const sections = [
|
|
71
|
+
"--- Current Memory ---",
|
|
72
|
+
input.currentMemory || "(empty)",
|
|
73
|
+
"",
|
|
74
|
+
"--- Current User Profile ---",
|
|
75
|
+
input.currentUser || "(empty)",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (input.currentProject !== null) {
|
|
79
|
+
sections.push(
|
|
80
|
+
"",
|
|
81
|
+
"--- Current Project Memory ---",
|
|
82
|
+
input.currentProject || "(empty)",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sections.push(
|
|
87
|
+
"",
|
|
88
|
+
"--- Conversation to Review ---",
|
|
89
|
+
input.parts.join("\n\n"),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return sections.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function shouldNotifyDirect(result: DirectReviewResult): boolean {
|
|
96
|
+
return result.ok && result.appliedCount > 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function shouldNotifySubprocess(stdout: string | undefined): boolean {
|
|
100
|
+
const output = stdout?.trim();
|
|
101
|
+
return !!output && !output.toLowerCase().includes("nothing to save");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function runSubprocessReview(
|
|
105
|
+
pi: ExtensionAPI,
|
|
106
|
+
prompt: string,
|
|
107
|
+
config: MemoryConfig,
|
|
108
|
+
execChild: typeof execChildPrompt,
|
|
109
|
+
): Promise<{ code: number; stdout?: string }> {
|
|
110
|
+
return execChild(pi, prompt, config, {
|
|
111
|
+
signal: undefined,
|
|
112
|
+
timeoutMs: 120000,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
16
115
|
|
|
17
116
|
export function setupBackgroundReview(
|
|
18
117
|
pi: ExtensionAPI,
|
|
19
118
|
store: MemoryStore,
|
|
20
119
|
projectStore: MemoryStore | null,
|
|
21
120
|
config: MemoryConfig,
|
|
121
|
+
options: BackgroundReviewOptions = {},
|
|
22
122
|
): void {
|
|
123
|
+
const dbManager = options.dbManager ?? null;
|
|
124
|
+
const projectName = options.projectName ?? null;
|
|
125
|
+
const runDirectReview = options.deps?.runDirectReview ?? runDirectMemoryCompletion;
|
|
126
|
+
const execChild = options.deps?.execChildPrompt ?? execChildPrompt;
|
|
127
|
+
const onReviewSettled = options.deps?.onReviewSettled;
|
|
128
|
+
|
|
23
129
|
let turnsSinceReview = 0;
|
|
24
130
|
let toolCallsSinceReview = 0;
|
|
25
131
|
let userTurnCount = 0;
|
|
@@ -37,9 +143,6 @@ export function setupBackgroundReview(
|
|
|
37
143
|
if (!config.reviewEnabled) return;
|
|
38
144
|
if (reviewInProgress) return;
|
|
39
145
|
|
|
40
|
-
// Count tool calls from this turn's message only (not cumulative branch scan —
|
|
41
|
-
// otherwise the counter resets to 0 at review, then immediately re-counts all
|
|
42
|
-
// historical tool calls and re-triggers on every subsequent turn).
|
|
43
146
|
try {
|
|
44
147
|
const msg = event.message;
|
|
45
148
|
if (msg?.role === "assistant") {
|
|
@@ -56,7 +159,6 @@ export function setupBackgroundReview(
|
|
|
56
159
|
// If we can't count tool calls, fall back to turn-based only
|
|
57
160
|
}
|
|
58
161
|
|
|
59
|
-
// Trigger on EITHER turn count OR tool call count
|
|
60
162
|
const turnThresholdMet = turnsSinceReview >= config.nudgeInterval;
|
|
61
163
|
const toolCallThresholdMet = toolCallsSinceReview >= config.nudgeToolCalls;
|
|
62
164
|
|
|
@@ -67,78 +169,76 @@ export function setupBackgroundReview(
|
|
|
67
169
|
toolCallsSinceReview = 0;
|
|
68
170
|
reviewInProgress = true;
|
|
69
171
|
|
|
70
|
-
// Build conversation snapshot from session entries (crash-safe)
|
|
71
172
|
let allParts: string[] = [];
|
|
72
173
|
try {
|
|
73
174
|
const entries = ctx.sessionManager.getBranch();
|
|
74
175
|
allParts = collectMessageParts(entries);
|
|
75
176
|
} catch {
|
|
76
177
|
reviewInProgress = false;
|
|
77
|
-
return;
|
|
178
|
+
return;
|
|
78
179
|
}
|
|
79
180
|
if (allParts.length < 4) {
|
|
80
181
|
reviewInProgress = false;
|
|
81
|
-
return;
|
|
182
|
+
return;
|
|
82
183
|
}
|
|
184
|
+
|
|
83
185
|
const parts = applyRecentMessageLimit(allParts, config.reviewRecentMessages);
|
|
186
|
+
const promptInput: ReviewPromptInput = {
|
|
187
|
+
parts,
|
|
188
|
+
currentMemory: store.getMemoryEntries().join("\n§\n"),
|
|
189
|
+
currentUser: store.getUserEntries().join("\n§\n"),
|
|
190
|
+
currentProject: projectStore ? projectStore.getMemoryEntries().join("\n§\n") : null,
|
|
191
|
+
};
|
|
84
192
|
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const currentProject = projectStore ? projectStore.getMemoryEntries().join("\n§\n") : null;
|
|
193
|
+
const subprocessPrompt = buildSubprocessReviewPrompt(promptInput);
|
|
194
|
+
const directPrompt = buildDirectReviewUserPrompt(promptInput);
|
|
88
195
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
currentMemory || "(empty)",
|
|
94
|
-
"",
|
|
95
|
-
"--- Current User Profile ---",
|
|
96
|
-
currentUser || "(empty)",
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
if (currentProject !== null) {
|
|
100
|
-
reviewPrompt.push(
|
|
101
|
-
"",
|
|
102
|
-
"--- Current Project Memory ---",
|
|
103
|
-
currentProject || "(empty)",
|
|
104
|
-
);
|
|
105
|
-
}
|
|
196
|
+
const finishReview = () => {
|
|
197
|
+
reviewInProgress = false;
|
|
198
|
+
onReviewSettled?.();
|
|
199
|
+
};
|
|
106
200
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
201
|
+
const notifyIfSaved = (saved: boolean) => {
|
|
202
|
+
if (saved) {
|
|
203
|
+
ctx.ui.notify("💾 Memory auto-reviewed and updated", "info");
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const runReview = async (): Promise<void> => {
|
|
208
|
+
if (usesDirectTransport(config)) {
|
|
209
|
+
try {
|
|
210
|
+
const directResult = await runDirectReview(
|
|
211
|
+
ctx as Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
212
|
+
store,
|
|
213
|
+
projectStore,
|
|
214
|
+
{ userPrompt: directPrompt, systemPrompt: DIRECT_REVIEW_SYSTEM_PROMPT, config, timeoutMs: 120000 },
|
|
215
|
+
dbManager,
|
|
216
|
+
projectName,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (directResult.ok) {
|
|
220
|
+
notifyIfSaved(shouldNotifyDirect(directResult));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
112
223
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// Notifications are delivered via .then() once the subprocess completes.
|
|
116
|
-
//
|
|
117
|
-
// We intentionally omit ctx.signal — the signal is tied to the turn
|
|
118
|
-
// lifetime and would abort the subprocess before it finishes now that
|
|
119
|
-
// we're not awaiting. The timeout (120s) provides its own safety net.
|
|
120
|
-
const reviewPromise = execChildPrompt(pi, reviewPrompt.join("\n"), config, {
|
|
121
|
-
signal: undefined,
|
|
122
|
-
timeoutMs: 120000,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
reviewPromise
|
|
126
|
-
.then((result) => {
|
|
127
|
-
reviewInProgress = false;
|
|
128
|
-
if (result.code === 0 && result.stdout) {
|
|
129
|
-
const output = result.stdout.trim();
|
|
130
|
-
if (output && !output.toLowerCase().includes("nothing to save")) {
|
|
131
|
-
ctx.ui.notify("💾 Memory auto-reviewed and updated", "info");
|
|
224
|
+
if (directResult.fallbackReason === "empty") {
|
|
225
|
+
return;
|
|
132
226
|
}
|
|
227
|
+
} catch {
|
|
228
|
+
// Fall through to subprocess below.
|
|
133
229
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const subprocessResult = await runSubprocessReview(pi, subprocessPrompt, config, execChild);
|
|
233
|
+
if (subprocessResult.code === 0) {
|
|
234
|
+
notifyIfSaved(shouldNotifySubprocess(subprocessResult.stdout));
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
runReview()
|
|
138
239
|
.catch(() => {
|
|
139
|
-
// Best-effort
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
240
|
+
// Best-effort only
|
|
241
|
+
})
|
|
242
|
+
.finally(finishReview);
|
|
143
243
|
});
|
|
144
|
-
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const [timeoutValue, cancellationPath, command, ...args] = process.argv.slice(2);
|
|
5
|
+
const timeoutMs = Number(timeoutValue);
|
|
6
|
+
|
|
7
|
+
if (!cancellationPath || !command || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
8
|
+
process.stderr.write("pi-hermes-memory watchdog: invalid invocation\n");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const child = spawn(command, args, {
|
|
13
|
+
detached: process.platform !== "win32",
|
|
14
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
child.stdout?.pipe(process.stdout);
|
|
18
|
+
child.stderr?.pipe(process.stderr);
|
|
19
|
+
|
|
20
|
+
let timedOut = false;
|
|
21
|
+
let cancelled = false;
|
|
22
|
+
let terminating = false;
|
|
23
|
+
let forceTimer;
|
|
24
|
+
|
|
25
|
+
function signalTree(signal) {
|
|
26
|
+
if (!child.pid) return;
|
|
27
|
+
if (process.platform === "win32") {
|
|
28
|
+
const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
29
|
+
stdio: "ignore",
|
|
30
|
+
windowsHide: true,
|
|
31
|
+
});
|
|
32
|
+
killer.unref();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
process.kill(-child.pid, signal);
|
|
37
|
+
} catch {
|
|
38
|
+
try { child.kill(signal); } catch {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function terminateTree() {
|
|
43
|
+
if (terminating) return;
|
|
44
|
+
terminating = true;
|
|
45
|
+
signalTree("SIGTERM");
|
|
46
|
+
forceTimer = setTimeout(() => signalTree("SIGKILL"), 500);
|
|
47
|
+
forceTimer.unref();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const timeout = setTimeout(() => {
|
|
51
|
+
timedOut = true;
|
|
52
|
+
process.stderr.write(`[pi-hermes-memory] child timed out after ${timeoutMs}ms; terminating process tree\n`);
|
|
53
|
+
terminateTree();
|
|
54
|
+
}, timeoutMs);
|
|
55
|
+
timeout.unref();
|
|
56
|
+
|
|
57
|
+
const cancellationPoll = cancellationPath === "-" ? undefined : setInterval(() => {
|
|
58
|
+
if (!existsSync(cancellationPath)) return;
|
|
59
|
+
cancelled = true;
|
|
60
|
+
process.stderr.write("[pi-hermes-memory] child cancellation requested; terminating process tree\n");
|
|
61
|
+
terminateTree();
|
|
62
|
+
}, 25);
|
|
63
|
+
cancellationPoll?.unref();
|
|
64
|
+
|
|
65
|
+
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
66
|
+
process.on(signal, terminateTree);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
child.once("error", (error) => {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
if (cancellationPoll) clearInterval(cancellationPoll);
|
|
72
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
73
|
+
process.stderr.write(`pi-hermes-memory watchdog: ${error.message}\n`);
|
|
74
|
+
process.exitCode = timedOut ? 124 : cancelled ? 143 : 127;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
child.once("close", (code, signal) => {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
if (cancellationPoll) clearInterval(cancellationPoll);
|
|
80
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
81
|
+
if (timedOut) {
|
|
82
|
+
process.exitCode = 124;
|
|
83
|
+
} else if (cancelled) {
|
|
84
|
+
process.exitCode = 143;
|
|
85
|
+
} else if (typeof code === "number") {
|
|
86
|
+
process.exitCode = code;
|
|
87
|
+
} else {
|
|
88
|
+
process.exitCode = signal === "SIGTERM" ? 143 : 1;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
@@ -11,21 +11,19 @@
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { MemoryStore } from "../store/memory-store.js";
|
|
13
13
|
import { DatabaseManager } from "../store/db.js";
|
|
14
|
-
import {
|
|
15
|
-
formatFailureMemoryContent,
|
|
16
|
-
syncMemoryEntry,
|
|
17
|
-
} from "../store/sqlite-memory-store.js";
|
|
18
14
|
import {
|
|
19
15
|
CORRECTION_SAVE_PROMPT,
|
|
20
16
|
CORRECTION_STRONG_PATTERNS,
|
|
21
17
|
CORRECTION_WEAK_PATTERNS,
|
|
22
18
|
CORRECTION_NEGATIVE_PATTERNS,
|
|
23
19
|
CORRECTION_DIRECTIVE_WORDS,
|
|
20
|
+
DIRECT_CORRECTION_SYSTEM_PROMPT,
|
|
24
21
|
ENTRY_DELIMITER,
|
|
25
22
|
} from "../constants.js";
|
|
26
23
|
import type { MemoryConfig } from "../types.js";
|
|
27
24
|
import { getMessageText } from "../types.js";
|
|
28
25
|
import { execChildPrompt } from "./pi-child-process.js";
|
|
26
|
+
import { runDirectMemoryCompletion, usesDirectTransport } from "./review-memory-ops.js";
|
|
29
27
|
|
|
30
28
|
/**
|
|
31
29
|
* Extract the directive part from a correction message.
|
|
@@ -129,12 +127,14 @@ export function setupCorrectionDetector(
|
|
|
129
127
|
config: MemoryConfig,
|
|
130
128
|
dbManager: DatabaseManager | null = null,
|
|
131
129
|
projectName?: string | null,
|
|
130
|
+
deps: { runDirectMemoryCompletion?: typeof runDirectMemoryCompletion } = {},
|
|
132
131
|
): void {
|
|
133
132
|
if (!config.correctionDetection) return;
|
|
134
133
|
|
|
135
134
|
let pendingCorrection = false;
|
|
136
135
|
let turnsSinceLastCorrection = 3; // Start at threshold so first correction can fire immediately
|
|
137
136
|
let correctionInProgress = false;
|
|
137
|
+
const runDirect = deps.runDirectMemoryCompletion ?? runDirectMemoryCompletion;
|
|
138
138
|
|
|
139
139
|
// Flag on message_end (user role)
|
|
140
140
|
pi.on("message_end", async (event, _ctx) => {
|
|
@@ -182,9 +182,7 @@ export function setupCorrectionDetector(
|
|
|
182
182
|
const currentUser = store.getUserEntries().join(ENTRY_DELIMITER);
|
|
183
183
|
const currentProject = projectStore ? projectStore.getMemoryEntries().join(ENTRY_DELIMITER) : null;
|
|
184
184
|
|
|
185
|
-
const
|
|
186
|
-
CORRECTION_SAVE_PROMPT,
|
|
187
|
-
"",
|
|
185
|
+
const promptBody = [
|
|
188
186
|
"--- Current Memory ---",
|
|
189
187
|
currentMemory || "(empty)",
|
|
190
188
|
"",
|
|
@@ -193,31 +191,67 @@ export function setupCorrectionDetector(
|
|
|
193
191
|
];
|
|
194
192
|
|
|
195
193
|
if (currentProject !== null) {
|
|
196
|
-
|
|
194
|
+
promptBody.push(
|
|
197
195
|
"",
|
|
198
196
|
"--- Current Project Memory ---",
|
|
199
197
|
currentProject || "(empty)",
|
|
200
198
|
);
|
|
201
199
|
}
|
|
202
200
|
|
|
203
|
-
|
|
201
|
+
promptBody.push(
|
|
204
202
|
"",
|
|
205
203
|
"--- Recent Conversation ---",
|
|
206
204
|
recentParts.join("\n\n"),
|
|
207
205
|
);
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
207
|
+
let savedViaLlm = false;
|
|
208
|
+
|
|
209
|
+
const runSubprocessCorrection = async (): Promise<void> => {
|
|
210
|
+
const subprocessPrompt = [CORRECTION_SAVE_PROMPT, "", ...promptBody].join("\n");
|
|
211
|
+
const result = await execChildPrompt(pi, subprocessPrompt, config, {
|
|
212
|
+
signal: ctx.signal,
|
|
213
|
+
timeoutMs: 30000,
|
|
214
|
+
});
|
|
215
|
+
if (result.code === 0 && result.stdout) {
|
|
216
|
+
const output = result.stdout.trim();
|
|
217
|
+
savedViaLlm = !!output && !output.toLowerCase().includes("nothing to save");
|
|
218
|
+
}
|
|
219
|
+
};
|
|
213
220
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
let handledDirect = false;
|
|
222
|
+
if (usesDirectTransport(config)) {
|
|
223
|
+
try {
|
|
224
|
+
const directResult = await runDirect(
|
|
225
|
+
ctx,
|
|
226
|
+
store,
|
|
227
|
+
projectStore,
|
|
228
|
+
{
|
|
229
|
+
systemPrompt: DIRECT_CORRECTION_SYSTEM_PROMPT,
|
|
230
|
+
userPrompt: promptBody.join("\n"),
|
|
231
|
+
config,
|
|
232
|
+
timeoutMs: 30000,
|
|
233
|
+
signal: ctx.signal,
|
|
234
|
+
},
|
|
235
|
+
dbManager,
|
|
236
|
+
projectName,
|
|
237
|
+
);
|
|
238
|
+
if (directResult.ok) {
|
|
239
|
+
savedViaLlm = directResult.appliedCount > 0;
|
|
240
|
+
handledDirect = true;
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// Fall through to subprocess below.
|
|
218
244
|
}
|
|
219
245
|
}
|
|
220
246
|
|
|
247
|
+
if (!handledDirect) {
|
|
248
|
+
await runSubprocessCorrection();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (savedViaLlm) {
|
|
252
|
+
ctx.ui.notify("🔧 Correction detected — memory updated", "info");
|
|
253
|
+
}
|
|
254
|
+
|
|
221
255
|
// Also save as a failure memory for learning
|
|
222
256
|
try {
|
|
223
257
|
const lastUserMsg = recentParts.find(p => p.startsWith("[USER]"));
|
|
@@ -226,29 +260,11 @@ export function setupCorrectionDetector(
|
|
|
226
260
|
const directive = extractCorrectionDirective(correctionText);
|
|
227
261
|
const failureReason = "User corrected the agent";
|
|
228
262
|
const scopedProjectName = projectStore ? projectName?.trim() || null : null;
|
|
229
|
-
|
|
263
|
+
await store.addFailure(directive, {
|
|
230
264
|
category: "correction",
|
|
231
265
|
failureReason,
|
|
232
266
|
project: scopedProjectName ?? undefined,
|
|
233
267
|
});
|
|
234
|
-
|
|
235
|
-
if (addResult.success && dbManager) {
|
|
236
|
-
try {
|
|
237
|
-
syncMemoryEntry(dbManager, {
|
|
238
|
-
content: formatFailureMemoryContent(directive, {
|
|
239
|
-
category: "correction",
|
|
240
|
-
failureReason,
|
|
241
|
-
project: scopedProjectName,
|
|
242
|
-
}),
|
|
243
|
-
target: "failure",
|
|
244
|
-
project: scopedProjectName,
|
|
245
|
-
category: "correction",
|
|
246
|
-
failureReason,
|
|
247
|
-
});
|
|
248
|
-
} catch {
|
|
249
|
-
// Best-effort — searchable sync should not block correction capture
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
268
|
}
|
|
253
269
|
} catch {
|
|
254
270
|
// Best-effort — don't block the session
|