om-memory-system 4.30.1 → 4.31.1
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 +1 -0
- package/dist/adapters/opencode/conversation.js +22 -17
- package/dist/adapters/opencode/skills-tool.d.ts +3 -0
- package/dist/adapters/opencode/skills-tool.js +31 -0
- package/dist/adapters/pi/capture.js +39 -23
- package/dist/adapters/pi/extension.d.ts +1 -0
- package/dist/adapters/pi/extension.js +3 -0
- package/dist/adapters/pi/skills-tool.d.ts +3 -0
- package/dist/adapters/pi/skills-tool.js +34 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/skills-command.d.ts +12 -0
- package/dist/cli/skills-command.js +242 -0
- package/dist/cli/skills-record-past.d.ts +5 -0
- package/dist/cli/skills-record-past.js +85 -0
- package/dist/config-template.d.ts +1 -1
- package/dist/config-template.js +12 -0
- package/dist/config.d.ts +64 -48
- package/dist/config.js +28 -5
- package/dist/core/host.d.ts +7 -0
- package/dist/core/profile-backoff.d.ts +16 -8
- package/dist/core/profile-backoff.js +25 -16
- package/dist/core/skill-agent-modes.d.ts +4 -0
- package/dist/core/skill-agent-modes.js +145 -0
- package/dist/core/skill-operation-reply.d.ts +16 -0
- package/dist/core/skill-operation-reply.js +35 -0
- package/dist/core/skill-operations.d.ts +58 -0
- package/dist/core/skill-operations.js +296 -0
- package/dist/core/skills-tool.d.ts +90 -0
- package/dist/core/skills-tool.js +64 -0
- package/dist/core/tool-input.d.ts +7 -0
- package/dist/core/tool-input.js +21 -0
- package/dist/core/tool-outcome.d.ts +13 -0
- package/dist/core/tool-outcome.js +22 -0
- package/dist/core/turn-outputs.d.ts +30 -0
- package/dist/core/turn-outputs.js +22 -0
- package/dist/importer/auto-backfill.js +13 -1
- package/dist/importer/claude-conversation.d.ts +1 -0
- package/dist/importer/claude-conversation.js +44 -10
- package/dist/importer/claude-hook-api.d.ts +0 -8
- package/dist/importer/claude-hook-api.js +36 -65
- package/dist/importer/claude-import.js +3 -1
- package/dist/importer/claude-reader.d.ts +2 -0
- package/dist/importer/claude-reader.js +2 -2
- package/dist/importer/claude-turn-units.d.ts +26 -0
- package/dist/importer/claude-turn-units.js +85 -0
- package/dist/importer/import-args.d.ts +3 -0
- package/dist/importer/import-args.js +8 -2
- package/dist/importer/import-project.d.ts +6 -0
- package/dist/importer/import-project.js +9 -0
- package/dist/importer/import-runs.d.ts +8 -2
- package/dist/importer/import-runs.js +3 -3
- package/dist/importer/import-sessions.d.ts +8 -1
- package/dist/importer/import-sessions.js +17 -4
- package/dist/importer/import-turn-outputs.d.ts +49 -0
- package/dist/importer/import-turn-outputs.js +88 -0
- package/dist/importer/importer.d.ts +5 -2
- package/dist/importer/importer.js +18 -32
- package/dist/importer/opencode-import.d.ts +2 -0
- package/dist/importer/opencode-import.js +6 -3
- package/dist/importer/opencode-reader.js +11 -5
- package/dist/importer/pi-conversation.d.ts +5 -0
- package/dist/importer/pi-conversation.js +33 -9
- package/dist/importer/profile-catch-up.d.ts +1 -1
- package/dist/importer/record-past-turns.d.ts +28 -0
- package/dist/importer/record-past-turns.js +63 -0
- package/dist/importer/run-import.d.ts +4 -0
- package/dist/importer/run-import.js +24 -3
- package/dist/importer/web-import-api.d.ts +14 -0
- package/dist/importer/web-import-api.js +6 -0
- package/dist/importer/web-import-group.d.ts +4 -1
- package/dist/importer/web-import-group.js +26 -6
- package/dist/importer/web-import-jobs.js +6 -2
- package/dist/index.js +6 -1
- package/dist/services/ai/session/ai-session-manager.js +13 -0
- package/dist/services/auto-capture.d.ts +1 -1
- package/dist/services/auto-capture.js +71 -19
- package/dist/services/chat/chat-runtime.d.ts +2 -0
- package/dist/services/chat/chat-runtime.js +3 -2
- package/dist/services/global-config-writer.js +32 -2
- package/dist/services/profile-cleanup-routes.d.ts +2 -2
- package/dist/services/profile-cleanup-routes.js +19 -2
- package/dist/services/settings-snapshot.js +7 -1
- package/dist/services/skill-learning/agent-proposals.d.ts +76 -0
- package/dist/services/skill-learning/agent-proposals.js +180 -0
- package/dist/services/skill-learning/apply.d.ts +50 -0
- package/dist/services/skill-learning/apply.js +247 -0
- package/dist/services/skill-learning/auto-review.d.ts +40 -0
- package/dist/services/skill-learning/auto-review.js +132 -0
- package/dist/services/skill-learning/draft-turns.d.ts +60 -0
- package/dist/services/skill-learning/draft-turns.js +131 -0
- package/dist/services/skill-learning/drafting.d.ts +73 -0
- package/dist/services/skill-learning/drafting.js +263 -0
- package/dist/services/skill-learning/line-diff.d.ts +6 -0
- package/dist/services/skill-learning/line-diff.js +94 -0
- package/dist/services/skill-learning/operations-deps.d.ts +7 -0
- package/dist/services/skill-learning/operations-deps.js +19 -0
- package/dist/services/skill-learning/proposal-store.d.ts +82 -0
- package/dist/services/skill-learning/proposal-store.js +164 -0
- package/dist/services/skill-learning/ranking.d.ts +36 -0
- package/dist/services/skill-learning/ranking.js +140 -0
- package/dist/services/skill-learning/settings-for.d.ts +11 -0
- package/dist/services/skill-learning/settings-for.js +21 -0
- package/dist/services/skill-learning/skill-config.d.ts +45 -0
- package/dist/services/skill-learning/skill-config.js +70 -0
- package/dist/services/skill-learning/skill-detect.d.ts +29 -0
- package/dist/services/skill-learning/skill-detect.js +130 -0
- package/dist/services/skill-learning/skill-folders.d.ts +24 -0
- package/dist/services/skill-learning/skill-folders.js +42 -0
- package/dist/services/skill-learning/skill-index.d.ts +28 -0
- package/dist/services/skill-learning/skill-index.js +118 -0
- package/dist/services/skill-learning/skill-model.d.ts +10 -0
- package/dist/services/skill-learning/skill-model.js +15 -0
- package/dist/services/skill-learning/store.d.ts +79 -0
- package/dist/services/skill-learning/store.js +245 -0
- package/dist/services/skill-learning/turn-output.d.ts +34 -0
- package/dist/services/skill-learning/turn-output.js +98 -0
- package/dist/services/skill-learning/turn-summaries.d.ts +24 -0
- package/dist/services/skill-learning/turn-summaries.js +64 -0
- package/dist/services/skill-learning/web-review.d.ts +18 -0
- package/dist/services/skill-learning/web-review.js +44 -0
- package/dist/services/skill-routes.d.ts +20 -0
- package/dist/services/skill-routes.js +249 -0
- package/dist/services/turso/connection-manager.js +11 -7
- package/dist/services/turso/table-lease.d.ts +40 -0
- package/dist/services/turso/table-lease.js +77 -0
- package/dist/services/turso/turso-db.d.ts +4 -0
- package/dist/services/turso/turso-db.js +13 -0
- package/dist/services/user-prompt/profile-catch-up-lease.d.ts +3 -22
- package/dist/services/user-prompt/profile-catch-up-lease.js +9 -65
- package/dist/services/web-jobs.d.ts +7 -0
- package/dist/services/web-jobs.js +14 -0
- package/dist/services/web-route-modules.d.ts +2 -2
- package/dist/services/web-route-modules.js +6 -1
- package/dist/services/web-server.js +9 -21
- package/dist/v2/adapter.js +13 -8
- package/dist/web/assets/index-9hu4Z7SV.css +2 -0
- package/dist/web/assets/index-BMt9gPNK.js +129 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BGDKFzoC.css +0 -2
- package/dist/web/assets/index-CE8tXPA_.js +0 -128
|
@@ -31,6 +31,11 @@ export async function runHistoryImport(host, args, run) {
|
|
|
31
31
|
try {
|
|
32
32
|
const { startImportRun } = await import("./import-runs.js");
|
|
33
33
|
const { workProgress } = await import("./import-progress.js");
|
|
34
|
+
const { importSkillTurnsOn, skillTurnsOnly } = await import("./import-turn-outputs.js");
|
|
35
|
+
// Without memories, the source pass records skill turns only (no model call).
|
|
36
|
+
const skillTurnsPass = args.skipMemories && importSkillTurnsOn(args);
|
|
37
|
+
// A skills-only run reads sessions whose folder is gone, so its report misses them as unresolved.
|
|
38
|
+
const keepsMissingFolders = skillTurnsOnly(args, !args.skipProfile);
|
|
34
39
|
const controller = new AbortController();
|
|
35
40
|
const onAbort = () => controller.abort();
|
|
36
41
|
run.signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -38,12 +43,16 @@ export async function runHistoryImport(host, args, run) {
|
|
|
38
43
|
controller.abort();
|
|
39
44
|
const recorder = await startImportRun(host, run.track.surface, {
|
|
40
45
|
onPause: () => controller.abort(),
|
|
46
|
+
...(skillTurnsPass ? { phase: "skill-turns" } : {}),
|
|
41
47
|
});
|
|
48
|
+
// Every unit the source pass reached is finished: the loop checks the signal before it.
|
|
49
|
+
let reached = 0;
|
|
42
50
|
try {
|
|
43
51
|
const report = await runUntracked(host, args, {
|
|
44
52
|
...run,
|
|
45
53
|
signal: controller.signal,
|
|
46
54
|
onProgress: (processed, total, preview, alreadyHandled = 0) => {
|
|
55
|
+
reached = processed;
|
|
47
56
|
// Units already in the ledger need no model call: leave them out of
|
|
48
57
|
// the progress so the bar and the time left describe real work.
|
|
49
58
|
const work = workProgress(processed, total, alreadyHandled, run.track.expectedTotal);
|
|
@@ -64,14 +73,14 @@ export async function runHistoryImport(host, args, run) {
|
|
|
64
73
|
? "failed"
|
|
65
74
|
: "done", {
|
|
66
75
|
// The total stays the one progress last reported; a real run has no
|
|
67
|
-
// "would import" count of its own.
|
|
68
|
-
done: handled,
|
|
76
|
+
// "would import" count of its own. A skill-turns pass counts the turns it read.
|
|
77
|
+
done: skillTurnsPass ? reached : handled,
|
|
69
78
|
imported: report.unitsImported,
|
|
70
79
|
skipped: report.unitsSkipped,
|
|
71
80
|
failed: report.unitsFailed,
|
|
72
81
|
}, report.profile?.error ? profileFailureCode(report.profile.error) : undefined);
|
|
73
82
|
// Only a run over every project and every session sees the full list.
|
|
74
|
-
if (args.scope === "all-projects" && !run.selection)
|
|
83
|
+
if (args.scope === "all-projects" && !run.selection && !keepsMissingFolders)
|
|
75
84
|
await recordUnresolved(host, report, args.since !== undefined || args.until !== undefined);
|
|
76
85
|
return report;
|
|
77
86
|
}
|
|
@@ -129,6 +138,7 @@ async function runUntracked(host, args, run) {
|
|
|
129
138
|
force: args.force,
|
|
130
139
|
skipMemories: args.skipMemories,
|
|
131
140
|
skipProfile: args.skipProfile,
|
|
141
|
+
skipSkills: args.skipSkills,
|
|
132
142
|
...(run.signal ? { signal: run.signal } : {}),
|
|
133
143
|
...(run.onProgress ? { onProgress: run.onProgress } : {}),
|
|
134
144
|
...(run.models.capture ? { provider: capture } : {}),
|
|
@@ -164,6 +174,7 @@ async function runUntracked(host, args, run) {
|
|
|
164
174
|
force: args.force,
|
|
165
175
|
dryRun: args.dryRun,
|
|
166
176
|
skipMemories: args.skipMemories,
|
|
177
|
+
skipSkills: args.skipSkills,
|
|
167
178
|
pathMaps,
|
|
168
179
|
};
|
|
169
180
|
if (host === "claude-code") {
|
|
@@ -195,6 +206,12 @@ export function formatHistoryImportReport(host, report, model) {
|
|
|
195
206
|
: ""));
|
|
196
207
|
lines.push(` memory units: ${report.unitsWouldImport} pending, ${report.unitsAlreadyHandled} already done, ` +
|
|
197
208
|
`${report.unitsImported} imported, ${report.unitsSkipped} skipped, ${report.unitsFailed} failed`);
|
|
209
|
+
if (report.skillTurnsWouldRecord !== undefined) {
|
|
210
|
+
lines.push(` skill turns: ${report.skillTurnsWouldRecord} pending, ${report.skillTurnsAlreadyRecorded ?? 0} already recorded`);
|
|
211
|
+
}
|
|
212
|
+
if (report.skillTurnsRecorded !== undefined) {
|
|
213
|
+
lines.push(` skill turns: ${report.skillTurnsRecorded} recorded, ${report.skillTurnsAlreadyPresent ?? 0} already present`);
|
|
214
|
+
}
|
|
198
215
|
for (const project of report.projects) {
|
|
199
216
|
lines.push(` project ${project.directory}: ${project.sessions} sessions, ${project.units} units`);
|
|
200
217
|
}
|
|
@@ -236,6 +253,10 @@ export function summarizeHistoryImportReport(report) {
|
|
|
236
253
|
unitsAlreadyHandled: report.unitsAlreadyHandled,
|
|
237
254
|
unitsHeldBack: report.unitsHeldBack ?? 0,
|
|
238
255
|
unitsUntimed: report.unitsUntimed ?? 0,
|
|
256
|
+
skillTurnsRecorded: report.skillTurnsRecorded ?? 0,
|
|
257
|
+
skillTurnsAlreadyPresent: report.skillTurnsAlreadyPresent ?? 0,
|
|
258
|
+
skillTurnsWouldRecord: report.skillTurnsWouldRecord ?? 0,
|
|
259
|
+
skillTurnsAlreadyRecorded: report.skillTurnsAlreadyRecorded ?? 0,
|
|
239
260
|
projects: report.projects.length,
|
|
240
261
|
unresolved: (report.unresolvedProjects?.length ?? 0) + report.unresolvableSessions.length,
|
|
241
262
|
loadErrors: report.loadErrors.length,
|
|
@@ -11,3 +11,17 @@ export { BackfillControls } from "./backfill-controls.js";
|
|
|
11
11
|
export { directoryMapsView, readStoreProjects, type KnownProject } from "./map-suggestions.js";
|
|
12
12
|
export { testExternalApi } from "./external-api-test.js";
|
|
13
13
|
export { catchUpState, pauseCatchUp, previewCatchUp, startCatchUp } from "./profile-catch-up.js";
|
|
14
|
+
import type { SettingsImportJobs } from "./web-import-jobs.js";
|
|
15
|
+
/** Start Record past turns in the web app's import job slot. */
|
|
16
|
+
export declare function startRecordPast(jobs: () => Promise<SettingsImportJobs>, cwd: string): Promise<{
|
|
17
|
+
id: string;
|
|
18
|
+
host: import("./import-args.js").ImportHost;
|
|
19
|
+
dryRun: boolean;
|
|
20
|
+
state: "failed" | "done" | "running" | "cancelling" | "cancelled";
|
|
21
|
+
sessions: number;
|
|
22
|
+
processed: number;
|
|
23
|
+
total: number;
|
|
24
|
+
report?: string;
|
|
25
|
+
summary?: ReturnType<typeof import("./run-import.js").summarizeHistoryImportReport>;
|
|
26
|
+
error?: string;
|
|
27
|
+
} | import("./web-import-group.js").GroupImportJob>;
|
|
@@ -11,3 +11,9 @@ export { BackfillControls } from "./backfill-controls.js";
|
|
|
11
11
|
export { directoryMapsView, readStoreProjects } from "./map-suggestions.js";
|
|
12
12
|
export { testExternalApi } from "./external-api-test.js";
|
|
13
13
|
export { catchUpState, pauseCatchUp, previewCatchUp, startCatchUp } from "./profile-catch-up.js";
|
|
14
|
+
import { importReadiness } from "./import-readiness.js";
|
|
15
|
+
import { startRecordPastTurns } from "./record-past-turns.js";
|
|
16
|
+
/** Start Record past turns in the web app's import job slot. */
|
|
17
|
+
export async function startRecordPast(jobs, cwd) {
|
|
18
|
+
return startRecordPastTurns(await jobs(), cwd, await importReadiness());
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ImportHost } from "./import-args.js";
|
|
2
2
|
import { type ImportReadiness } from "./import-readiness.js";
|
|
3
|
+
import type { ImportSurface } from "./import-runs.js";
|
|
3
4
|
import { resolveImportSelection } from "./import-sessions.js";
|
|
4
5
|
import { type ImportSourceIdentity } from "./import-sources.js";
|
|
5
6
|
import { runHistoryImport, summarizeHistoryImportReport, type HistoryImportRun } from "./run-import.js";
|
|
@@ -17,7 +18,7 @@ export interface GroupHostJob {
|
|
|
17
18
|
host: ImportHost;
|
|
18
19
|
state: GroupHostState;
|
|
19
20
|
sessions: number;
|
|
20
|
-
phase: "preparing" | "memory" | "profile";
|
|
21
|
+
phase: "preparing" | "memory" | "skill-turns" | "profile";
|
|
21
22
|
processed: number;
|
|
22
23
|
total: number;
|
|
23
24
|
profileProcessed: number;
|
|
@@ -46,6 +47,8 @@ export interface GroupImportDeps {
|
|
|
46
47
|
prepareModels?: (choice: string, directory: string, signal: AbortSignal, host: ImportHost) => Promise<HistoryImportRun["models"]>;
|
|
47
48
|
discardSelection?: (identity: ImportSourceIdentity) => Promise<void>;
|
|
48
49
|
waitingPrompts?: () => Promise<Set<string>>;
|
|
50
|
+
/** Where the run record says the import started; `web` by default. */
|
|
51
|
+
surface?: ImportSurface;
|
|
49
52
|
}
|
|
50
53
|
type Deps = GroupImportDeps & {
|
|
51
54
|
runner: typeof runHistoryImport;
|
|
@@ -3,6 +3,7 @@ import { CONFIG } from "../config.js";
|
|
|
3
3
|
import { hostLabel } from "../types/host-label.js";
|
|
4
4
|
import { parseHistoryImportArgs, webImportTokens, } from "./import-args.js";
|
|
5
5
|
import { importBlockedReason } from "./import-readiness.js";
|
|
6
|
+
import { importSkillTurnsOn, skillTurnsOnly } from "./import-turn-outputs.js";
|
|
6
7
|
import { resolveImportSelection } from "./import-sessions.js";
|
|
7
8
|
import { importSourceKey, readImportSourceToken, } from "./import-sources.js";
|
|
8
9
|
import { historyImportFailed, ImportAlreadyRunningError, runHistoryImport, summarizeHistoryImportReport, } from "./run-import.js";
|
|
@@ -53,10 +54,19 @@ function matchOptions(child, directory, signal) {
|
|
|
53
54
|
cwd: directory,
|
|
54
55
|
allowEmpty: true,
|
|
55
56
|
signal,
|
|
56
|
-
|
|
57
|
+
// Skill turns have no session ledger: while they are recorded, no session counts as handled.
|
|
58
|
+
outputs: importSkillTurnsOn(child.args)
|
|
59
|
+
? { memories: false, profile: false }
|
|
60
|
+
: { memories: !child.args.skipMemories, profile: !child.args.skipProfile },
|
|
57
61
|
...(child.args.force ? { force: true } : {}),
|
|
62
|
+
// The list the selection came from passed the same flag.
|
|
63
|
+
...(skillTurnsOnly(child.args, !child.args.skipProfile) ? { skillTurnsOnly: true } : {}),
|
|
58
64
|
};
|
|
59
65
|
}
|
|
66
|
+
/** A skill-turns-only run reads history and calls no model. */
|
|
67
|
+
function needsModel(args) {
|
|
68
|
+
return !args.skipMemories || !args.skipProfile;
|
|
69
|
+
}
|
|
60
70
|
function hostError(host, error) {
|
|
61
71
|
return new ImportJobError(`${hostLabel(host)}: ${safeHealthError(error, [CONFIG.memoryApiKey, CONFIG.embeddingApiKey])}`, error.status ?? 400);
|
|
62
72
|
}
|
|
@@ -74,6 +84,10 @@ function emptySummary(dryRun) {
|
|
|
74
84
|
unitsAlreadyHandled: 0,
|
|
75
85
|
unitsHeldBack: 0,
|
|
76
86
|
unitsUntimed: 0,
|
|
87
|
+
skillTurnsRecorded: 0,
|
|
88
|
+
skillTurnsAlreadyPresent: 0,
|
|
89
|
+
skillTurnsWouldRecord: 0,
|
|
90
|
+
skillTurnsAlreadyRecorded: 0,
|
|
77
91
|
projects: 0,
|
|
78
92
|
unresolved: 0,
|
|
79
93
|
loadErrors: 0,
|
|
@@ -171,7 +185,7 @@ export async function startImportGroup(input, directory, deps, publish) {
|
|
|
171
185
|
child.row.blocker =
|
|
172
186
|
importBlockedReason(readiness, {
|
|
173
187
|
host,
|
|
174
|
-
needsModel:
|
|
188
|
+
needsModel: needsModel(child.args),
|
|
175
189
|
modelChoice: child.input.modelChoice,
|
|
176
190
|
}) ?? undefined;
|
|
177
191
|
if (!job.dryRun && child.row.blocker)
|
|
@@ -269,12 +283,12 @@ async function executeGroup(children, job, signal, directory, deps, waiting) {
|
|
|
269
283
|
// Reader/model readiness may change while earlier children finish their profile work.
|
|
270
284
|
const blocked = importBlockedReason(await deps.readiness(), {
|
|
271
285
|
host,
|
|
272
|
-
needsModel: !job.dryRun,
|
|
286
|
+
needsModel: !job.dryRun && needsModel(child.args),
|
|
273
287
|
modelChoice: child.input.modelChoice,
|
|
274
288
|
});
|
|
275
289
|
if (blocked)
|
|
276
290
|
throw new Error(blocked);
|
|
277
|
-
const models = job.dryRun
|
|
291
|
+
const models = job.dryRun || !needsModel(child.args)
|
|
278
292
|
? {}
|
|
279
293
|
: await (deps.prepareModels ?? prepareModels)(child.input.modelChoice, directory, signal, host);
|
|
280
294
|
if (signal.aborted) {
|
|
@@ -282,12 +296,16 @@ async function executeGroup(children, job, signal, directory, deps, waiting) {
|
|
|
282
296
|
continue;
|
|
283
297
|
}
|
|
284
298
|
const eligible = new Set();
|
|
285
|
-
child.row.phase = child.args.skipMemories
|
|
299
|
+
child.row.phase = !child.args.skipMemories
|
|
300
|
+
? "memory"
|
|
301
|
+
: importSkillTurnsOn(child.args)
|
|
302
|
+
? "skill-turns"
|
|
303
|
+
: "profile";
|
|
286
304
|
const report = await deps.runner(host, child.args, {
|
|
287
305
|
cwd: directory,
|
|
288
306
|
models,
|
|
289
307
|
signal,
|
|
290
|
-
track: { surface: "web" },
|
|
308
|
+
track: { surface: deps.surface ?? "web" },
|
|
291
309
|
selection: {
|
|
292
310
|
keys: pinned.keys,
|
|
293
311
|
cutoff: pinned.cutoff,
|
|
@@ -340,6 +358,8 @@ async function executeGroup(children, job, signal, directory, deps, waiting) {
|
|
|
340
358
|
else {
|
|
341
359
|
const pending = report.unitsImported +
|
|
342
360
|
report.unitsWouldImport +
|
|
361
|
+
(report.skillTurnsRecorded ?? 0) +
|
|
362
|
+
(report.skillTurnsWouldRecord ?? 0) +
|
|
343
363
|
(report.profile?.promptsRecorded ?? 0) +
|
|
344
364
|
(report.profile?.promptsWouldRecord ?? 0) +
|
|
345
365
|
(report.profile?.batchesBuilt ?? 0) +
|
|
@@ -23,6 +23,7 @@ const fields = new Set([
|
|
|
23
23
|
"force",
|
|
24
24
|
"skipMemories",
|
|
25
25
|
"skipProfile",
|
|
26
|
+
"skipSkills",
|
|
26
27
|
"scope",
|
|
27
28
|
"project",
|
|
28
29
|
"since",
|
|
@@ -65,7 +66,7 @@ export function validateWebImportRequest(value) {
|
|
|
65
66
|
for (const [key, option] of Object.entries(options)) {
|
|
66
67
|
if (!fields.has(key))
|
|
67
68
|
throw new Error(`Unknown import option: ${key}`);
|
|
68
|
-
if (["dryRun", "force", "skipMemories", "skipProfile"].includes(key)) {
|
|
69
|
+
if (["dryRun", "force", "skipMemories", "skipProfile", "skipSkills"].includes(key)) {
|
|
69
70
|
if (typeof option !== "boolean")
|
|
70
71
|
throw new Error(`Invalid ${key}`);
|
|
71
72
|
}
|
|
@@ -92,7 +93,10 @@ export function validateWebImportRequest(value) {
|
|
|
92
93
|
throw new Error("The project must be an absolute path");
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
|
-
|
|
96
|
+
// Skill turns count as a target only while skillLearning is on.
|
|
97
|
+
if (options.skipMemories === true &&
|
|
98
|
+
options.skipProfile === true &&
|
|
99
|
+
(options.skipSkills === true || !CONFIG.skillLearning))
|
|
96
100
|
throw new Error("Choose Project memories or User profile");
|
|
97
101
|
if (input.modelChoice !== undefined &&
|
|
98
102
|
(typeof input.modelChoice !== "string" ||
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { getLanguageName } from "./services/language-detector.js";
|
|
|
21
21
|
import { getHostClientConfig } from "./adapters/opencode/opencode-host-config.js";
|
|
22
22
|
import { applySkillsPathConfig } from "./adapters/opencode/package-skills.js";
|
|
23
23
|
import { loadOpencodeProvider } from "./adapters/opencode/opencode-provider-loader.js";
|
|
24
|
+
import { opencodeSkillsTool } from "./adapters/opencode/skills-tool.js";
|
|
24
25
|
import { STRUCTURED_OUTPUT_AGENT, STRUCTURED_OUTPUT_TOOLS, } from "./adapters/opencode/opencode-provider.js";
|
|
25
26
|
import { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle, isTrackedInternalCaptureSession, } from "./adapters/opencode/internal-capture-sessions.js";
|
|
26
27
|
import { registerOpencodeProfileModel } from "./adapters/opencode/profile-model.js";
|
|
@@ -544,6 +545,7 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
544
545
|
return JSON.stringify(result);
|
|
545
546
|
},
|
|
546
547
|
}),
|
|
548
|
+
skills: opencodeSkillsTool(directory),
|
|
547
549
|
},
|
|
548
550
|
event: async (input) => {
|
|
549
551
|
const event = input.event;
|
|
@@ -559,7 +561,8 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
559
561
|
return;
|
|
560
562
|
}
|
|
561
563
|
if (event.type === "session.idle") {
|
|
562
|
-
|
|
564
|
+
// Skill learning records turns even when memory capture is off (ADR-036).
|
|
565
|
+
if (!isConfigured() || !(CONFIG.autoCaptureEnabled || CONFIG.skillLearning))
|
|
563
566
|
return;
|
|
564
567
|
const sessionID = event.properties?.sessionID;
|
|
565
568
|
if (!sessionID)
|
|
@@ -575,6 +578,8 @@ export const OmmsPlugin = async (ctx) => {
|
|
|
575
578
|
idleTimeout = setTimeout(async () => {
|
|
576
579
|
try {
|
|
577
580
|
await performAutoCapture(autoCaptureHost, sessionID, directory);
|
|
581
|
+
if (!CONFIG.autoCaptureEnabled)
|
|
582
|
+
return; // Skill turns only; the memory work stays off.
|
|
578
583
|
await performUserProfileLearning(ctx, directory);
|
|
579
584
|
const { cleanupService } = await import("./services/cleanup-service.js");
|
|
580
585
|
if (await cleanupService.shouldRunCleanup())
|
|
@@ -24,6 +24,19 @@ export class AISessionManager {
|
|
|
24
24
|
this.initPromise = (async () => {
|
|
25
25
|
try {
|
|
26
26
|
this.db = await tursoConnectionManager.getConnection(this.dbPath);
|
|
27
|
+
// Sessions hold prompts, including skill drafts built from recorded
|
|
28
|
+
// turns. Zero deleted rows so their text does not stay in free pages.
|
|
29
|
+
await this.db.execute("PRAGMA secure_delete = ON");
|
|
30
|
+
// A file written before secure_delete was on can still hold deleted
|
|
31
|
+
// text in free pages. Rebuild it at open when it has free pages.
|
|
32
|
+
try {
|
|
33
|
+
const free = await this.db.get("PRAGMA freelist_count");
|
|
34
|
+
if (Number(free?.freelist_count ?? 0) > 0)
|
|
35
|
+
await this.db.execute("VACUUM");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Best effort: another process may hold the file; the next open tries again.
|
|
39
|
+
}
|
|
27
40
|
await this.initDatabase();
|
|
28
41
|
}
|
|
29
42
|
catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CaptureResult, CaptureWorkUnit } from "../core/capture.js";
|
|
2
2
|
import type { AutoCaptureHost } from "../core/host.js";
|
|
3
3
|
import { type CaptureRetryDrainResult } from "./capture-retry-drain.js";
|
|
4
4
|
export { buildBoundedSummaryPrompt, buildMarkdownContext, getAutoCaptureMarkdownBudget, } from "../core/capture-context.js";
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { CONFIG, refreshConfigIfChanged } from "../config.js";
|
|
2
|
-
import { captureConversation } from "../core/capture.js";
|
|
3
2
|
import { quickRetryDelayMs } from "../core/capture-retry-policy.js";
|
|
3
|
+
import { handleFinishedTurn } from "../core/turn-outputs.js";
|
|
4
4
|
import { drainCaptureRetries, queueFailedCapture, startCaptureRetryDrain, } from "./capture-retry-drain.js";
|
|
5
5
|
import { log } from "./logger.js";
|
|
6
|
+
import { skillTurnRecorder } from "./skill-learning/settings-for.js";
|
|
6
7
|
import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
|
|
7
8
|
export { buildBoundedSummaryPrompt, buildMarkdownContext, getAutoCaptureMarkdownBudget, } from "../core/capture-context.js";
|
|
8
9
|
let isCaptureRunning = false;
|
|
10
|
+
/**
|
|
11
|
+
* With memory capture off, prompts stay uncaptured, so each idle event lists
|
|
12
|
+
* them again. This keeps, per session, the time of the last prompt recorded for
|
|
13
|
+
* skill learning, so no conversation is read twice. The prompts are not marked
|
|
14
|
+
* captured, so memory capture still sees them once it is turned on.
|
|
15
|
+
*/
|
|
16
|
+
const skillRecordedUpTo = new Map();
|
|
9
17
|
async function notifySafely(host, notification) {
|
|
10
18
|
try {
|
|
11
19
|
await host.notify?.(notification);
|
|
@@ -20,18 +28,74 @@ export async function performAutoCapture(host, sessionID, directory) {
|
|
|
20
28
|
isCaptureRunning = true;
|
|
21
29
|
try {
|
|
22
30
|
const prompts = await userPromptManager.getUncapturedPromptsForSession(sessionID);
|
|
23
|
-
if (prompts.length === 0
|
|
31
|
+
if (prompts.length === 0)
|
|
24
32
|
return;
|
|
33
|
+
const recorder = skillTurnRecorder(directory);
|
|
34
|
+
// Skill learning records turns even when memory capture is off (ADR-036).
|
|
35
|
+
if (CONFIG.autoCaptureEnabled === false || !host.isCaptureReady()) {
|
|
36
|
+
for (const prompt of prompts) {
|
|
37
|
+
if (prompt.createdAt <= (skillRecordedUpTo.get(sessionID) ?? -Infinity))
|
|
38
|
+
continue;
|
|
39
|
+
if (await recordSkillTurnOnly(host, sessionID, directory, prompt, recorder))
|
|
40
|
+
skillRecordedUpTo.set(sessionID, prompt.createdAt);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
25
44
|
const maxRetries = CONFIG.autoCaptureMaxRetries ?? 3;
|
|
26
45
|
for (const prompt of prompts) {
|
|
27
|
-
await capturePrompt(host, sessionID, directory, prompt, maxRetries);
|
|
46
|
+
await capturePrompt(host, sessionID, directory, prompt, maxRetries, recorder);
|
|
28
47
|
}
|
|
29
48
|
}
|
|
30
49
|
finally {
|
|
31
50
|
isCaptureRunning = false;
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
|
-
|
|
53
|
+
function opencodeWorkUnit(host, sessionID, directory, prompt, conversation) {
|
|
54
|
+
return {
|
|
55
|
+
host: host.host,
|
|
56
|
+
hostSessionId: sessionID,
|
|
57
|
+
sourceType: "live-capture",
|
|
58
|
+
projectDirectory: directory,
|
|
59
|
+
userPrompt: prompt.content,
|
|
60
|
+
promptId: prompt.id,
|
|
61
|
+
prompt: {
|
|
62
|
+
id: prompt.id,
|
|
63
|
+
providerId: prompt.providerId,
|
|
64
|
+
modelId: prompt.modelId,
|
|
65
|
+
},
|
|
66
|
+
...conversation,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The skill output keys a turn by OpenCode's message id, the id the history
|
|
71
|
+
* import reads, so a live turn imported later stays one row. The memory unit
|
|
72
|
+
* keeps the OMMS prompt id.
|
|
73
|
+
*/
|
|
74
|
+
function opencodeSkillRecorder(prompt, recorder) {
|
|
75
|
+
return recorder && ((unit) => recorder({ ...unit, promptId: prompt.messageId }));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Memory capture is off or not ready: record the turn for skill learning only.
|
|
79
|
+
* Returns true when the finished turn went to the skill output.
|
|
80
|
+
*/
|
|
81
|
+
async function recordSkillTurnOnly(host, sessionID, directory, prompt, recorder) {
|
|
82
|
+
if (!recorder)
|
|
83
|
+
return false;
|
|
84
|
+
try {
|
|
85
|
+
const conversation = await host.getConversation(sessionID, prompt.messageId);
|
|
86
|
+
if (!conversation)
|
|
87
|
+
return false;
|
|
88
|
+
if (conversation.textResponses.length === 0 && conversation.toolCalls.length === 0)
|
|
89
|
+
return false;
|
|
90
|
+
await handleFinishedTurn(opencodeWorkUnit(host, sessionID, directory, prompt, conversation), { memories: false, skills: true }, { recordSkillTurn: opencodeSkillRecorder(prompt, recorder) });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
log("OpenCode skill turn read failed", { sessionID, code: "conversation-read-failed" }, "warn");
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function capturePrompt(host, sessionID, directory, prompt, maxRetries, recorder) {
|
|
35
99
|
let claimedPromptId = null;
|
|
36
100
|
let attempt = prompt.capture_attempts || 0;
|
|
37
101
|
let workUnit = null;
|
|
@@ -47,21 +111,9 @@ async function capturePrompt(host, sessionID, directory, prompt, maxRetries) {
|
|
|
47
111
|
return;
|
|
48
112
|
if (conversation.textResponses.length === 0 && conversation.toolCalls.length === 0)
|
|
49
113
|
return;
|
|
50
|
-
workUnit =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
sourceType: "live-capture",
|
|
54
|
-
projectDirectory: directory,
|
|
55
|
-
userPrompt: prompt.content,
|
|
56
|
-
promptId: prompt.id,
|
|
57
|
-
prompt: {
|
|
58
|
-
id: prompt.id,
|
|
59
|
-
providerId: prompt.providerId,
|
|
60
|
-
modelId: prompt.modelId,
|
|
61
|
-
},
|
|
62
|
-
...conversation,
|
|
63
|
-
};
|
|
64
|
-
const captureResult = await captureConversation(workUnit, host);
|
|
114
|
+
workUnit = opencodeWorkUnit(host, sessionID, directory, prompt, conversation);
|
|
115
|
+
const record = opencodeSkillRecorder(prompt, recorder);
|
|
116
|
+
const captureResult = await handleFinishedTurn(workUnit, { memories: true, skills: record !== undefined }, { provider: host, recordSkillTurn: record });
|
|
65
117
|
if (captureResult.status === "skipped") {
|
|
66
118
|
log("Auto-capture skipped", {
|
|
67
119
|
promptId: prompt.id,
|
|
@@ -15,6 +15,8 @@ export interface ChatRuntimeConfig {
|
|
|
15
15
|
}
|
|
16
16
|
/** The chat store for this storage path, opened and migrated once. */
|
|
17
17
|
export declare function chatStore(config: Pick<ChatRuntimeConfig, "storagePath">): Promise<ChatStore>;
|
|
18
|
+
/** One structured call to the external memory model. Skill drafting reuses it (design §6). */
|
|
19
|
+
export declare function callTool(config: ChatRuntimeConfig, system: string, user: string, tool: object, timeoutMs?: number): Promise<import("../ai/providers/base-provider.js").ToolCallResult>;
|
|
18
20
|
/** The memory ids from `ids` that still exist in any project or user shard. */
|
|
19
21
|
export declare function existingMemoryIds(ids: string[]): Promise<Set<string>>;
|
|
20
22
|
/** Real dependencies for `askQuestion`, built from the settings passed in. */
|
|
@@ -16,11 +16,12 @@ export function chatStore(config) {
|
|
|
16
16
|
}
|
|
17
17
|
return store;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
/** One structured call to the external memory model. Skill drafting reuses it (design §6). */
|
|
20
|
+
export async function callTool(config, system, user, tool, timeoutMs = CHAT_MODEL_TIMEOUT_MS) {
|
|
20
21
|
const { AIProviderFactory } = await import("../ai/ai-provider-factory.js");
|
|
21
22
|
const { buildMemoryProviderConfig } = await import("../ai/provider-config.js");
|
|
22
23
|
const { aiSessionManager } = await import("../ai/session/ai-session-manager.js");
|
|
23
|
-
const provider = AIProviderFactory.createProvider(config.memoryProvider, buildMemoryProviderConfig(config, { maxIterations: 1, iterationTimeout:
|
|
24
|
+
const provider = AIProviderFactory.createProvider(config.memoryProvider, buildMemoryProviderConfig(config, { maxIterations: 1, iterationTimeout: timeoutMs }));
|
|
24
25
|
const sessionId = `chat_${crypto.randomUUID()}`;
|
|
25
26
|
try {
|
|
26
27
|
return await provider.executeToolCall(system, user, tool, sessionId);
|
|
@@ -5,6 +5,7 @@ import { applyEdits, modify, parse } from "jsonc-parser";
|
|
|
5
5
|
import { getGlobalConfigSourcePath, getGlobalConfigWritePath, validateGlobalConfig, } from "../config.js";
|
|
6
6
|
import { CONFIG_TEMPLATE } from "../config-template.js";
|
|
7
7
|
import { isValidClaudeConfigDir } from "./claude-folder.js";
|
|
8
|
+
import { missingSkillFolder } from "./skill-learning/skill-config.js";
|
|
8
9
|
import { KEYWORD_MODES } from "../utils/keyword-boost.js";
|
|
9
10
|
import { getMemoryLimitRule } from "../utils/memory-limits.js";
|
|
10
11
|
import { RERANKER_TIMEOUT_MAX_MS, RERANKER_TIMEOUT_MIN_MS } from "./request-timeouts.js";
|
|
@@ -43,7 +44,18 @@ const keys = new Set([
|
|
|
43
44
|
"rerankerModel",
|
|
44
45
|
"rerankerApiKey",
|
|
45
46
|
"rerankerTimeoutMs",
|
|
47
|
+
"skillLearning",
|
|
48
|
+
"skillLearningRedact",
|
|
49
|
+
"skillFolders",
|
|
46
50
|
]);
|
|
51
|
+
function isFolder(path) {
|
|
52
|
+
try {
|
|
53
|
+
return statSync(path).isDirectory();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
47
59
|
/**
|
|
48
60
|
* Written only by the embedding apply route, together, after a passing test.
|
|
49
61
|
* The general settings save refuses them, so a change always re-embeds.
|
|
@@ -70,6 +82,8 @@ export function isSecretReference(value) {
|
|
|
70
82
|
}
|
|
71
83
|
/** Reranker keys that a null edit removes, the same wire form as a limit reset. */
|
|
72
84
|
const OPTIONAL_RERANKER_KEYS = ["rerankerApiUrl", "rerankerModel", "rerankerApiKey"];
|
|
85
|
+
/** Other keys whose `null` edit removes them, so they follow the default again. */
|
|
86
|
+
const RESETTABLE_KEYS = ["keywordMode", "skillFolders"];
|
|
73
87
|
function isValidEdit(key, value) {
|
|
74
88
|
// `undefined` removes the key from the file. `null` is the wire form of
|
|
75
89
|
// "reset to default", accepted for the six memory limits and the optional reranker keys.
|
|
@@ -78,6 +92,9 @@ function isValidEdit(key, value) {
|
|
|
78
92
|
return true;
|
|
79
93
|
if (OPTIONAL_RERANKER_KEYS.includes(key) && value === null)
|
|
80
94
|
return true;
|
|
95
|
+
// skillFolders: null removes the key, so the default skill folders apply again.
|
|
96
|
+
if (key === "skillFolders" && value === null)
|
|
97
|
+
return true;
|
|
81
98
|
// keywordMode: null removes the key, so the mode follows the default again.
|
|
82
99
|
if (key === "keywordMode")
|
|
83
100
|
return value === null || KEYWORD_MODES.includes(value);
|
|
@@ -114,9 +131,15 @@ function isValidEdit(key, value) {
|
|
|
114
131
|
if (key === "captureTrace" ||
|
|
115
132
|
key === "autoBackfill" ||
|
|
116
133
|
key === "webServerAutoStart" ||
|
|
117
|
-
key === "userProfileAutoCleanupEnabled"
|
|
134
|
+
key === "userProfileAutoCleanupEnabled" ||
|
|
135
|
+
key === "skillLearning" ||
|
|
136
|
+
key === "skillLearningRedact") {
|
|
118
137
|
return typeof value === "boolean";
|
|
119
138
|
}
|
|
139
|
+
// Path rules are checked by the startup validation below; existence just after this check.
|
|
140
|
+
if (key === "skillFolders") {
|
|
141
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
142
|
+
}
|
|
120
143
|
// Each automatic cleanup is paid for, so the page refuses an interval that would run one
|
|
121
144
|
// after almost every learning pass. A hand edit of the file has no floor.
|
|
122
145
|
if (key === "userProfileAutoCleanupInterval") {
|
|
@@ -201,6 +224,11 @@ options = {}) {
|
|
|
201
224
|
? `Invalid ${key} setting: accepted ${rerankerAccepted[key]}`
|
|
202
225
|
: `Invalid ${key} setting`);
|
|
203
226
|
}
|
|
227
|
+
const missing = key === "skillFolders" && value !== null
|
|
228
|
+
? missingSkillFolder(value, isFolder)
|
|
229
|
+
: null;
|
|
230
|
+
if (missing !== null)
|
|
231
|
+
throw new Error(`Skill folder not found: ${missing}`);
|
|
204
232
|
}
|
|
205
233
|
const wasQueued = pending > 0;
|
|
206
234
|
pending++;
|
|
@@ -224,7 +252,9 @@ options = {}) {
|
|
|
224
252
|
// null is the wire form of "reset to default": remove the key so the
|
|
225
253
|
// value follows the default again (undefined removes in jsonc modify).
|
|
226
254
|
const jsoncValue = value === null &&
|
|
227
|
-
(getMemoryLimitRule(key) ||
|
|
255
|
+
(getMemoryLimitRule(key) ||
|
|
256
|
+
OPTIONAL_RERANKER_KEYS.includes(key) ||
|
|
257
|
+
RESETTABLE_KEYS.includes(key))
|
|
228
258
|
? undefined
|
|
229
259
|
: value;
|
|
230
260
|
content = applyEdits(content, modify(content, jsoncPath, jsoncValue, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The profile cleanup routes: AI Cleanup (start, status, stop, apply)
|
|
3
|
-
* last automatic cleanup. Returns the JSON body, or undefined when the path and method are
|
|
2
|
+
* The profile cleanup routes: AI Cleanup (start, status, stop, apply), the undo of the
|
|
3
|
+
* last automatic cleanup, and category cleanup (start, status, apply, undo). Returns the JSON body, or undefined when the path and method are
|
|
4
4
|
* not a cleanup route. The body is read only for the routes that need it.
|
|
5
5
|
*/
|
|
6
6
|
export declare function routeProfileCleanup(path: string, method: string, url: URL, readBody: () => Promise<any>): Promise<unknown | undefined>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as aiCleanup from "./ai-cleanup-handlers.js";
|
|
2
2
|
import * as autoCleanup from "./auto-cleanup-handlers.js";
|
|
3
|
+
import * as categoryCleanup from "./api-handlers.js";
|
|
3
4
|
/**
|
|
4
|
-
* The profile cleanup routes: AI Cleanup (start, status, stop, apply)
|
|
5
|
-
* last automatic cleanup. Returns the JSON body, or undefined when the path and method are
|
|
5
|
+
* The profile cleanup routes: AI Cleanup (start, status, stop, apply), the undo of the
|
|
6
|
+
* last automatic cleanup, and category cleanup (start, status, apply, undo). Returns the JSON body, or undefined when the path and method are
|
|
6
7
|
* not a cleanup route. The body is read only for the routes that need it.
|
|
7
8
|
*/
|
|
8
9
|
export async function routeProfileCleanup(path, method, url, readBody) {
|
|
@@ -28,5 +29,21 @@ export async function routeProfileCleanup(path, method, url, readBody) {
|
|
|
28
29
|
const body = await readBody();
|
|
29
30
|
return autoCleanup.handleAutoCleanupUndo(body.userId || undefined);
|
|
30
31
|
}
|
|
32
|
+
if (path === "/api/user-profile/category-cleanup") {
|
|
33
|
+
if (method === "POST") {
|
|
34
|
+
const body = await readBody();
|
|
35
|
+
return categoryCleanup.handleCategoryCleanupStart(body.userId || undefined);
|
|
36
|
+
}
|
|
37
|
+
if (method === "GET")
|
|
38
|
+
return categoryCleanup.handleCategoryCleanupStatus(queryUser);
|
|
39
|
+
}
|
|
40
|
+
if (path === "/api/user-profile/category-cleanup/apply" && method === "POST") {
|
|
41
|
+
const body = await readBody();
|
|
42
|
+
return categoryCleanup.handleCategoryCleanupApply(body.userId || undefined, body);
|
|
43
|
+
}
|
|
44
|
+
if (path === "/api/user-profile/category-cleanup/undo" && method === "POST") {
|
|
45
|
+
const body = await readBody();
|
|
46
|
+
return categoryCleanup.handleCategoryCleanupUndo(body.userId || undefined);
|
|
47
|
+
}
|
|
31
48
|
return undefined;
|
|
32
49
|
}
|
|
@@ -34,6 +34,9 @@ const editable = [
|
|
|
34
34
|
"rerankerModel",
|
|
35
35
|
"rerankerTimeoutMs",
|
|
36
36
|
"keywordMode",
|
|
37
|
+
"skillLearning",
|
|
38
|
+
"skillLearningRedact",
|
|
39
|
+
"skillFolders",
|
|
37
40
|
...MEMORY_LIMIT_SETTINGS,
|
|
38
41
|
];
|
|
39
42
|
/** Keys a project config cannot override, so the page always shows the global value. */
|
|
@@ -55,6 +58,8 @@ const globalOnly = [
|
|
|
55
58
|
"rerankerModel",
|
|
56
59
|
"rerankerApiKey",
|
|
57
60
|
"rerankerTimeoutMs",
|
|
61
|
+
"skillLearningRedact",
|
|
62
|
+
"skillFolders",
|
|
58
63
|
];
|
|
59
64
|
function readSettingsFile(path) {
|
|
60
65
|
if (!path)
|
|
@@ -112,7 +117,8 @@ export function getSettingsSnapshot(directory) {
|
|
|
112
117
|
return [key, nestedMemorySetting(limitRule, global, project)];
|
|
113
118
|
}
|
|
114
119
|
const projectOverrides = Object.hasOwn(project, key) &&
|
|
115
|
-
|
|
120
|
+
// A project may turn these off, never on.
|
|
121
|
+
(key === "captureTrace" || key === "skillLearning"
|
|
116
122
|
? project[key] === false
|
|
117
123
|
: !key.endsWith("RetentionDays") && !globalOnly.includes(key));
|
|
118
124
|
const source = projectOverrides
|