om-memory-system 4.30.1 → 4.31.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 (144) hide show
  1. package/README.md +1 -0
  2. package/dist/adapters/opencode/conversation.js +22 -17
  3. package/dist/adapters/opencode/skills-tool.d.ts +3 -0
  4. package/dist/adapters/opencode/skills-tool.js +31 -0
  5. package/dist/adapters/pi/capture.js +39 -23
  6. package/dist/adapters/pi/extension.d.ts +1 -0
  7. package/dist/adapters/pi/extension.js +3 -0
  8. package/dist/adapters/pi/skills-tool.d.ts +3 -0
  9. package/dist/adapters/pi/skills-tool.js +34 -0
  10. package/dist/cli/index.js +5 -0
  11. package/dist/cli/skills-command.d.ts +12 -0
  12. package/dist/cli/skills-command.js +242 -0
  13. package/dist/cli/skills-record-past.d.ts +5 -0
  14. package/dist/cli/skills-record-past.js +85 -0
  15. package/dist/config-template.d.ts +1 -1
  16. package/dist/config-template.js +12 -0
  17. package/dist/config.d.ts +64 -48
  18. package/dist/config.js +28 -5
  19. package/dist/core/host.d.ts +7 -0
  20. package/dist/core/profile-backoff.d.ts +16 -8
  21. package/dist/core/profile-backoff.js +25 -16
  22. package/dist/core/skill-agent-modes.d.ts +4 -0
  23. package/dist/core/skill-agent-modes.js +145 -0
  24. package/dist/core/skill-operation-reply.d.ts +16 -0
  25. package/dist/core/skill-operation-reply.js +35 -0
  26. package/dist/core/skill-operations.d.ts +58 -0
  27. package/dist/core/skill-operations.js +296 -0
  28. package/dist/core/skills-tool.d.ts +90 -0
  29. package/dist/core/skills-tool.js +64 -0
  30. package/dist/core/tool-input.d.ts +7 -0
  31. package/dist/core/tool-input.js +21 -0
  32. package/dist/core/tool-outcome.d.ts +13 -0
  33. package/dist/core/tool-outcome.js +22 -0
  34. package/dist/core/turn-outputs.d.ts +30 -0
  35. package/dist/core/turn-outputs.js +22 -0
  36. package/dist/importer/auto-backfill.js +13 -1
  37. package/dist/importer/claude-conversation.d.ts +1 -0
  38. package/dist/importer/claude-conversation.js +44 -10
  39. package/dist/importer/claude-hook-api.d.ts +0 -8
  40. package/dist/importer/claude-hook-api.js +36 -65
  41. package/dist/importer/claude-import.js +3 -1
  42. package/dist/importer/claude-reader.d.ts +2 -0
  43. package/dist/importer/claude-reader.js +2 -2
  44. package/dist/importer/claude-turn-units.d.ts +26 -0
  45. package/dist/importer/claude-turn-units.js +85 -0
  46. package/dist/importer/import-args.d.ts +3 -0
  47. package/dist/importer/import-args.js +8 -2
  48. package/dist/importer/import-project.d.ts +6 -0
  49. package/dist/importer/import-project.js +9 -0
  50. package/dist/importer/import-runs.d.ts +8 -2
  51. package/dist/importer/import-runs.js +3 -3
  52. package/dist/importer/import-sessions.d.ts +8 -1
  53. package/dist/importer/import-sessions.js +17 -4
  54. package/dist/importer/import-turn-outputs.d.ts +49 -0
  55. package/dist/importer/import-turn-outputs.js +91 -0
  56. package/dist/importer/importer.d.ts +5 -2
  57. package/dist/importer/importer.js +18 -32
  58. package/dist/importer/opencode-import.d.ts +2 -0
  59. package/dist/importer/opencode-import.js +6 -3
  60. package/dist/importer/opencode-reader.js +11 -5
  61. package/dist/importer/pi-conversation.d.ts +5 -0
  62. package/dist/importer/pi-conversation.js +33 -9
  63. package/dist/importer/profile-catch-up.d.ts +1 -1
  64. package/dist/importer/record-past-turns.d.ts +28 -0
  65. package/dist/importer/record-past-turns.js +63 -0
  66. package/dist/importer/run-import.d.ts +4 -0
  67. package/dist/importer/run-import.js +24 -3
  68. package/dist/importer/web-import-api.d.ts +14 -0
  69. package/dist/importer/web-import-api.js +6 -0
  70. package/dist/importer/web-import-group.d.ts +4 -1
  71. package/dist/importer/web-import-group.js +26 -6
  72. package/dist/importer/web-import-jobs.js +6 -2
  73. package/dist/index.js +6 -1
  74. package/dist/services/ai/providers/base-provider.d.ts +15 -0
  75. package/dist/services/ai/providers/base-provider.js +26 -0
  76. package/dist/services/ai/providers/openai-chat-completion.js +3 -6
  77. package/dist/services/ai/session/ai-session-manager.js +13 -0
  78. package/dist/services/auto-capture.d.ts +1 -1
  79. package/dist/services/auto-capture.js +71 -19
  80. package/dist/services/chat/chat-runtime.d.ts +2 -0
  81. package/dist/services/chat/chat-runtime.js +3 -2
  82. package/dist/services/global-config-writer.js +32 -2
  83. package/dist/services/profile-cleanup-routes.d.ts +2 -2
  84. package/dist/services/profile-cleanup-routes.js +19 -2
  85. package/dist/services/settings-snapshot.js +7 -1
  86. package/dist/services/skill-learning/agent-proposals.d.ts +76 -0
  87. package/dist/services/skill-learning/agent-proposals.js +180 -0
  88. package/dist/services/skill-learning/apply.d.ts +50 -0
  89. package/dist/services/skill-learning/apply.js +247 -0
  90. package/dist/services/skill-learning/auto-review.d.ts +40 -0
  91. package/dist/services/skill-learning/auto-review.js +132 -0
  92. package/dist/services/skill-learning/draft-turns.d.ts +60 -0
  93. package/dist/services/skill-learning/draft-turns.js +131 -0
  94. package/dist/services/skill-learning/drafting.d.ts +73 -0
  95. package/dist/services/skill-learning/drafting.js +264 -0
  96. package/dist/services/skill-learning/line-diff.d.ts +6 -0
  97. package/dist/services/skill-learning/line-diff.js +94 -0
  98. package/dist/services/skill-learning/operations-deps.d.ts +7 -0
  99. package/dist/services/skill-learning/operations-deps.js +19 -0
  100. package/dist/services/skill-learning/proposal-store.d.ts +82 -0
  101. package/dist/services/skill-learning/proposal-store.js +164 -0
  102. package/dist/services/skill-learning/ranking.d.ts +36 -0
  103. package/dist/services/skill-learning/ranking.js +140 -0
  104. package/dist/services/skill-learning/settings-for.d.ts +11 -0
  105. package/dist/services/skill-learning/settings-for.js +21 -0
  106. package/dist/services/skill-learning/skill-config.d.ts +45 -0
  107. package/dist/services/skill-learning/skill-config.js +70 -0
  108. package/dist/services/skill-learning/skill-detect.d.ts +29 -0
  109. package/dist/services/skill-learning/skill-detect.js +130 -0
  110. package/dist/services/skill-learning/skill-folders.d.ts +24 -0
  111. package/dist/services/skill-learning/skill-folders.js +42 -0
  112. package/dist/services/skill-learning/skill-index.d.ts +28 -0
  113. package/dist/services/skill-learning/skill-index.js +118 -0
  114. package/dist/services/skill-learning/skill-model.d.ts +10 -0
  115. package/dist/services/skill-learning/skill-model.js +15 -0
  116. package/dist/services/skill-learning/store.d.ts +79 -0
  117. package/dist/services/skill-learning/store.js +245 -0
  118. package/dist/services/skill-learning/turn-output.d.ts +34 -0
  119. package/dist/services/skill-learning/turn-output.js +98 -0
  120. package/dist/services/skill-learning/turn-summaries.d.ts +24 -0
  121. package/dist/services/skill-learning/turn-summaries.js +64 -0
  122. package/dist/services/skill-learning/web-review.d.ts +18 -0
  123. package/dist/services/skill-learning/web-review.js +44 -0
  124. package/dist/services/skill-routes.d.ts +20 -0
  125. package/dist/services/skill-routes.js +249 -0
  126. package/dist/services/turso/connection-manager.js +11 -7
  127. package/dist/services/turso/table-lease.d.ts +40 -0
  128. package/dist/services/turso/table-lease.js +77 -0
  129. package/dist/services/turso/turso-db.d.ts +4 -0
  130. package/dist/services/turso/turso-db.js +13 -0
  131. package/dist/services/user-prompt/profile-catch-up-lease.d.ts +3 -22
  132. package/dist/services/user-prompt/profile-catch-up-lease.js +9 -65
  133. package/dist/services/web-jobs.d.ts +7 -0
  134. package/dist/services/web-jobs.js +14 -0
  135. package/dist/services/web-route-modules.d.ts +2 -2
  136. package/dist/services/web-route-modules.js +6 -1
  137. package/dist/services/web-server.js +9 -21
  138. package/dist/v2/adapter.js +13 -8
  139. package/dist/web/assets/index-B_xLhsQO.css +2 -0
  140. package/dist/web/assets/index-BetjjXGs.js +129 -0
  141. package/dist/web/index.html +2 -2
  142. package/package.json +1 -1
  143. package/dist/web/assets/index-BGDKFzoC.css +0 -2
  144. package/dist/web/assets/index-CE8tXPA_.js +0 -128
@@ -0,0 +1,245 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tursoConnectionManager } from "../turso/connection-manager.js";
4
+ /** Bump when the tables change, and add the step to `init`. */
5
+ export const SKILL_SCHEMA_VERSION = 1;
6
+ export const SKILLS_DB_FILE = "skills.db";
7
+ export class SkillStoreError extends Error {
8
+ code;
9
+ constructor(message, code) {
10
+ super(message);
11
+ this.code = code;
12
+ this.name = "SkillStoreError";
13
+ }
14
+ }
15
+ /** `skills.db` always sits in the global storage path, never in a project folder. */
16
+ export function skillsDbPath(storagePath) {
17
+ return join(storagePath, SKILLS_DB_FILE);
18
+ }
19
+ // Like chats.db, the file keeps the default rollback journal, so committed
20
+ // data lives in skills.db alone.
21
+ const SCHEMA = [
22
+ `CREATE TABLE IF NOT EXISTS skill_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)`,
23
+ `CREATE TABLE IF NOT EXISTS turns (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ turn_key TEXT NOT NULL UNIQUE,
26
+ host TEXT NOT NULL,
27
+ session_id TEXT NOT NULL,
28
+ project_tag TEXT,
29
+ project_name TEXT,
30
+ provider TEXT,
31
+ model TEXT,
32
+ created_at INTEGER NOT NULL,
33
+ prompt TEXT NOT NULL,
34
+ replies_json TEXT NOT NULL,
35
+ redacted INTEGER NOT NULL
36
+ )`,
37
+ `CREATE TABLE IF NOT EXISTS tool_calls (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ turn_id INTEGER NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
40
+ position INTEGER NOT NULL,
41
+ name TEXT NOT NULL,
42
+ input TEXT NOT NULL,
43
+ outcome TEXT NOT NULL,
44
+ error TEXT
45
+ )`,
46
+ `CREATE TABLE IF NOT EXISTS skill_uses (
47
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ turn_id INTEGER NOT NULL REFERENCES turns(id) ON DELETE CASCADE,
49
+ skill_name TEXT NOT NULL,
50
+ skill_path TEXT,
51
+ method TEXT NOT NULL,
52
+ position INTEGER
53
+ )`,
54
+ `CREATE TABLE IF NOT EXISTS skill_settings (
55
+ skill_path TEXT PRIMARY KEY,
56
+ mode TEXT NOT NULL,
57
+ last_review_at INTEGER,
58
+ uses_at_last_review INTEGER NOT NULL DEFAULT 0
59
+ )`,
60
+ // Evidence turn ids are a JSON list with no foreign key: deleting turns keeps proposals.
61
+ `CREATE TABLE IF NOT EXISTS skill_proposals (
62
+ id TEXT PRIMARY KEY,
63
+ kind TEXT NOT NULL,
64
+ skill_name TEXT NOT NULL,
65
+ skill_path TEXT,
66
+ base_hash TEXT,
67
+ proposed_content TEXT NOT NULL,
68
+ diff TEXT,
69
+ summary TEXT,
70
+ evidence_turn_ids TEXT NOT NULL DEFAULT '[]',
71
+ state TEXT NOT NULL,
72
+ applied_by TEXT,
73
+ drafted_by TEXT,
74
+ created_at INTEGER NOT NULL,
75
+ updated_at INTEGER NOT NULL
76
+ )`,
77
+ `CREATE TABLE IF NOT EXISTS skill_versions (
78
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
79
+ proposal_id TEXT NOT NULL,
80
+ path TEXT NOT NULL,
81
+ previous_content TEXT,
82
+ written_hash TEXT NOT NULL,
83
+ saved_at INTEGER NOT NULL
84
+ )`,
85
+ `CREATE INDEX IF NOT EXISTS turns_created ON turns(created_at)`,
86
+ `CREATE INDEX IF NOT EXISTS tool_calls_turn ON tool_calls(turn_id, position)`,
87
+ `CREATE INDEX IF NOT EXISTS skill_uses_turn ON skill_uses(turn_id)`,
88
+ `CREATE INDEX IF NOT EXISTS skill_uses_name ON skill_uses(skill_name)`,
89
+ `CREATE INDEX IF NOT EXISTS skill_proposals_state ON skill_proposals(state, skill_name)`,
90
+ ];
91
+ /** Adds a column to an existing table; a missing table or a present column is left as it is. */
92
+ async function addMissingColumn(db, table, column) {
93
+ const exists = await db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", [
94
+ table,
95
+ ]);
96
+ if (!exists)
97
+ return;
98
+ try {
99
+ await db.run(`ALTER TABLE ${table} ADD COLUMN ${column}`);
100
+ }
101
+ catch (error) {
102
+ if (!String(error?.message ?? error).includes("duplicate column"))
103
+ throw error;
104
+ }
105
+ }
106
+ /**
107
+ * Recorded turns, proposals and saved versions in `skills.db`. One instance
108
+ * per file. Writes check the schema version, so a file from a newer OMMS
109
+ * stays untouched. Reads on a missing file return empty values and do not
110
+ * create the file.
111
+ */
112
+ export class SkillStore {
113
+ dbPath;
114
+ connect;
115
+ readOnly = false;
116
+ constructor(dbPath, connect = (path) => tursoConnectionManager.getConnection(path)) {
117
+ this.dbPath = dbPath;
118
+ this.connect = connect;
119
+ }
120
+ /** The open connection. `proposal-store.ts` and `ranking.ts` share it. */
121
+ db() {
122
+ return this.connect(this.dbPath);
123
+ }
124
+ /** Creates or upgrades the tables. Call it before any write. */
125
+ async init() {
126
+ const db = await this.db();
127
+ await db.execute(SCHEMA[0]);
128
+ const row = await db.get("SELECT value FROM skill_meta WHERE key = 'schema_version'");
129
+ const version = row ? Number(row.value) : 0;
130
+ if (version > SKILL_SCHEMA_VERSION) {
131
+ this.readOnly = true;
132
+ return;
133
+ }
134
+ this.readOnly = false;
135
+ // A dev skills.db made before a column has the table without it. The
136
+ // column must exist before the batch, which creates indexes on the table.
137
+ await addMissingColumn(db, "skill_uses", "position INTEGER");
138
+ await addMissingColumn(db, "skill_proposals", "drafted_by TEXT");
139
+ await db.batch(SCHEMA.slice(1).map((sql) => ({ sql })));
140
+ if (version < SKILL_SCHEMA_VERSION) {
141
+ await db.run("INSERT INTO skill_meta (key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", [String(SKILL_SCHEMA_VERSION)]);
142
+ }
143
+ }
144
+ isReadOnly() {
145
+ return this.readOnly;
146
+ }
147
+ writable() {
148
+ if (this.readOnly) {
149
+ throw new SkillStoreError("skills.db is from a newer OMMS and is read-only", "read_only");
150
+ }
151
+ }
152
+ /** Opens the file only when it and the `turns` table exist, so a read never creates it. */
153
+ async existingDb() {
154
+ if (!existsSync(this.dbPath))
155
+ return null;
156
+ const db = await this.db();
157
+ const table = await db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'turns'");
158
+ return table ? db : null;
159
+ }
160
+ /**
161
+ * Writes the turn, its tool calls and its skill uses in one transaction.
162
+ * Returns false when the `turn_key` is already stored; nothing is written then.
163
+ */
164
+ async insertTurn(turn) {
165
+ this.writable();
166
+ const db = await this.db();
167
+ return db.transaction("write", async (tx) => {
168
+ const inserted = await tx.execute({
169
+ sql: `INSERT OR IGNORE INTO turns
170
+ (turn_key, host, session_id, project_tag, project_name, provider, model, created_at, prompt, replies_json, redacted)
171
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
172
+ args: [
173
+ turn.turnKey,
174
+ turn.host,
175
+ turn.sessionId,
176
+ turn.projectTag,
177
+ turn.projectName,
178
+ turn.provider,
179
+ turn.model,
180
+ turn.createdAt,
181
+ turn.prompt,
182
+ JSON.stringify(turn.replies),
183
+ turn.redacted ? 1 : 0,
184
+ ],
185
+ });
186
+ if (Number(inserted.rowsAffected) === 0)
187
+ return false;
188
+ const turnId = inserted.lastInsertRowid;
189
+ for (const [position, call] of turn.toolCalls.entries()) {
190
+ await tx.execute({
191
+ sql: "INSERT INTO tool_calls (turn_id, position, name, input, outcome, error) VALUES (?, ?, ?, ?, ?, ?)",
192
+ args: [turnId, position, call.name, call.input, call.outcome, call.error],
193
+ });
194
+ }
195
+ for (const use of turn.skillUses) {
196
+ await tx.execute({
197
+ sql: "INSERT INTO skill_uses (turn_id, skill_name, skill_path, method, position) VALUES (?, ?, ?, ?, ?)",
198
+ args: [turnId, use.name, use.path, use.method, use.position ?? null],
199
+ });
200
+ }
201
+ return true;
202
+ });
203
+ }
204
+ /** Read-only lookup; a missing file gives false and stays missing. */
205
+ async hasTurn(turnKey) {
206
+ const db = await this.existingDb();
207
+ if (!db)
208
+ return false;
209
+ return (await db.get("SELECT 1 AS found FROM turns WHERE turn_key = ?", [turnKey])) !== null;
210
+ }
211
+ async countTurns() {
212
+ const db = await this.existingDb();
213
+ if (!db)
214
+ return 0;
215
+ const row = await db.get("SELECT COUNT(*) AS n FROM turns");
216
+ return Number(row?.n ?? 0);
217
+ }
218
+ /** Deletes every turn with its tool calls and skill uses. Proposals and versions stay. */
219
+ async deleteAllTurns() {
220
+ this.writable();
221
+ const db = await this.existingDb();
222
+ if (!db)
223
+ return 0;
224
+ const deleted = await db.transaction("write", async (tx) => {
225
+ const result = await tx.execute("DELETE FROM turns");
226
+ return Number(result.rowsAffected);
227
+ });
228
+ // A delete only marks pages free, so the turn text stays readable in the
229
+ // file. VACUUM rewrites it. `secure_delete` would not hold: after each
230
+ // transaction the libSQL client opens a fresh handle with default pragmas,
231
+ // which is also why busy_timeout is set again here.
232
+ await db.execute("PRAGMA busy_timeout = 5000");
233
+ await db.execute("VACUUM");
234
+ return deleted;
235
+ }
236
+ /** Size of `skills.db` in bytes, or 0 when the file does not exist. */
237
+ fileSize() {
238
+ try {
239
+ return statSync(this.dbPath).size;
240
+ }
241
+ catch {
242
+ return 0;
243
+ }
244
+ }
245
+ }
@@ -0,0 +1,34 @@
1
+ import type { CaptureWorkUnit } from "../../core/capture.js";
2
+ import { type CaptureDiagnosticsConfig } from "../capture-diagnostics.js";
3
+ /**
4
+ * The resolved settings for one turn. The caller reads them from `CONFIG` for
5
+ * the turn's project (`settings-for.ts`), so this module never imports it.
6
+ */
7
+ export interface SkillTurnSettings {
8
+ /** `skillLearning` after the project override. */
9
+ enabled: boolean;
10
+ /** `skillLearningRedact`. */
11
+ redact: boolean;
12
+ /** `skillFolders`, or the default folders when it is unset. */
13
+ folders: string[];
14
+ storagePath: string;
15
+ /** The configured secrets that `redactTraceText` removes. */
16
+ secrets: CaptureDiagnosticsConfig;
17
+ }
18
+ export type SkillTurnStatus = "disabled" | "private" | "new" | "duplicate";
19
+ /**
20
+ * The turn's identity in `skills.db`: host, session and the host's id for the
21
+ * user message. Live capture and history import build the same key.
22
+ */
23
+ export declare function skillTurnKey(unit: CaptureWorkUnit): string;
24
+ /**
25
+ * Records one finished turn into `skills.db` (ADR-036). Off: returns at once
26
+ * and never opens the file. The log record holds counts and sizes only.
27
+ * Errors are thrown; `handleFinishedTurn` catches them.
28
+ */
29
+ export declare function recordSkillTurn(unit: CaptureWorkUnit, settings: SkillTurnSettings): Promise<SkillTurnStatus>;
30
+ /**
31
+ * What `recordSkillTurn` would return, for a dry run. Read-only: a missing
32
+ * `skills.db` stays missing.
33
+ */
34
+ export declare function peekSkillTurn(unit: CaptureWorkUnit, settings: SkillTurnSettings): Promise<SkillTurnStatus>;
@@ -0,0 +1,98 @@
1
+ import { redactTraceText } from "../capture-diagnostics.js";
2
+ import { log } from "../logger.js";
3
+ import { isFullyPrivate } from "../privacy.js";
4
+ import { getTags } from "../tags.js";
5
+ import { detectSkillUses } from "./skill-detect.js";
6
+ import { getSkillIndex } from "./skill-index.js";
7
+ import { SkillStore, skillsDbPath } from "./store.js";
8
+ // One initialised store for each file, shared by every turn.
9
+ const stores = new Map();
10
+ function storeFor(storagePath) {
11
+ const path = skillsDbPath(storagePath);
12
+ let store = stores.get(path);
13
+ if (!store) {
14
+ const created = new SkillStore(path);
15
+ store = created.init().then(() => created);
16
+ // A failed open is tried again on the next turn.
17
+ store.catch(() => stores.delete(path));
18
+ stores.set(path, store);
19
+ }
20
+ return store;
21
+ }
22
+ /**
23
+ * The turn's identity in `skills.db`: host, session and the host's id for the
24
+ * user message. Live capture and history import build the same key.
25
+ */
26
+ export function skillTurnKey(unit) {
27
+ return `${unit.host}:${unit.hostSessionId}:${unit.promptId}`;
28
+ }
29
+ function buildRecord(unit, settings) {
30
+ // redactTraceText removes `<private>` text first, then secrets.
31
+ const clean = (text) => settings.redact ? redactTraceText(text, settings.secrets) : text;
32
+ const project = getTags(unit.projectDirectory).project;
33
+ // Detection reads the unredacted calls, so a redacted path still matches.
34
+ const skillUses = detectSkillUses({ prompt: unit.userPrompt, toolCalls: unit.toolCalls }, getSkillIndex(settings.folders));
35
+ return {
36
+ turnKey: skillTurnKey(unit),
37
+ host: unit.host,
38
+ sessionId: unit.hostSessionId,
39
+ projectTag: project.tag,
40
+ projectName: project.projectName ?? null,
41
+ provider: unit.prompt?.providerId ?? null,
42
+ model: unit.prompt?.modelId ?? null,
43
+ createdAt: unit.sourceTimestamp ?? Date.now(),
44
+ prompt: clean(unit.userPrompt),
45
+ replies: unit.textResponses.map(clean),
46
+ redacted: settings.redact,
47
+ toolCalls: unit.toolCalls.map((call) => ({
48
+ name: call.name,
49
+ input: clean(call.input),
50
+ outcome: call.outcome ?? "unknown",
51
+ error: call.error === undefined ? null : clean(call.error),
52
+ })),
53
+ skillUses,
54
+ };
55
+ }
56
+ function storedChars(record) {
57
+ let chars = record.prompt.length;
58
+ for (const reply of record.replies)
59
+ chars += reply.length;
60
+ for (const call of record.toolCalls)
61
+ chars += call.input.length + (call.error?.length ?? 0);
62
+ return chars;
63
+ }
64
+ /**
65
+ * Records one finished turn into `skills.db` (ADR-036). Off: returns at once
66
+ * and never opens the file. The log record holds counts and sizes only.
67
+ * Errors are thrown; `handleFinishedTurn` catches them.
68
+ */
69
+ export async function recordSkillTurn(unit, settings) {
70
+ if (!settings.enabled)
71
+ return "disabled";
72
+ if (settings.redact && isFullyPrivate(unit.userPrompt)) {
73
+ log("Skill turn skipped", { host: unit.host, code: "private" });
74
+ return "private";
75
+ }
76
+ const record = buildRecord(unit, settings);
77
+ const store = await storeFor(settings.storagePath);
78
+ const status = (await store.insertTurn(record)) ? "new" : "duplicate";
79
+ log("Skill turn recorded", {
80
+ host: unit.host,
81
+ toolCalls: record.toolCalls.length,
82
+ chars: storedChars(record),
83
+ result: status,
84
+ });
85
+ return status;
86
+ }
87
+ /**
88
+ * What `recordSkillTurn` would return, for a dry run. Read-only: a missing
89
+ * `skills.db` stays missing.
90
+ */
91
+ export async function peekSkillTurn(unit, settings) {
92
+ if (!settings.enabled)
93
+ return "disabled";
94
+ if (settings.redact && isFullyPrivate(unit.userPrompt))
95
+ return "private";
96
+ const store = new SkillStore(skillsDbPath(settings.storagePath));
97
+ return (await store.hasTurn(skillTurnKey(unit))) ? "duplicate" : "new";
98
+ }
@@ -0,0 +1,24 @@
1
+ import type { SkillIndex } from "./skill-index.js";
2
+ import type { SkillStore } from "./store.js";
3
+ /** The Skills page shows this much of a prompt. */
4
+ export declare const PROMPT_PREVIEW_CHARS = 200;
5
+ export declare const RECENT_TURN_LIMIT = 20;
6
+ /** One recorded turn as the Skills page lists it: no replies, no tool input. */
7
+ export interface SkillTurnSummary {
8
+ id: number;
9
+ createdAt: number;
10
+ host: string;
11
+ projectName: string | null;
12
+ promptPreview: string;
13
+ failedToolCalls: number;
14
+ }
15
+ /**
16
+ * The newest turns that used one skill. `path` is null for a skill that no
17
+ * watched folder holds. A missing `skills.db` gives no turns and stays missing.
18
+ */
19
+ export declare function recentSkillTurns(store: SkillStore, skill: {
20
+ name: string;
21
+ path: string | null;
22
+ }, index: SkillIndex, limit?: number): Promise<SkillTurnSummary[]>;
23
+ /** The turns with these ids, newest first; ids of deleted turns are left out. */
24
+ export declare function skillTurnsByIds(store: SkillStore, ids: readonly number[]): Promise<SkillTurnSummary[]>;
@@ -0,0 +1,64 @@
1
+ /** The Skills page shows this much of a prompt. */
2
+ export const PROMPT_PREVIEW_CHARS = 200;
3
+ export const RECENT_TURN_LIMIT = 20;
4
+ const COLUMNS = `t.id, t.created_at, t.host, t.project_name,
5
+ substr(t.prompt, 1, ${PROMPT_PREVIEW_CHARS}) AS preview,
6
+ (SELECT COUNT(*) FROM tool_calls tc WHERE tc.turn_id = t.id AND tc.outcome = 'error') AS failed`;
7
+ function toSummary(row) {
8
+ return {
9
+ id: Number(row.id),
10
+ createdAt: Number(row.created_at),
11
+ host: String(row.host),
12
+ projectName: row.project_name == null ? null : String(row.project_name),
13
+ promptPreview: String(row.preview),
14
+ failedToolCalls: Number(row.failed),
15
+ };
16
+ }
17
+ // The same use-to-skill rule as the ranking: a recorded path counts while the
18
+ // index lists it; a use with no path belongs to the one watched skill with its name.
19
+ const BY_PATH = `
20
+ SELECT ${COLUMNS} FROM turns t WHERE t.id IN (
21
+ SELECT su.turn_id FROM skill_uses su
22
+ WHERE su.skill_path = ? OR (su.skill_path IS NULL AND su.skill_name = ? AND ? = 1)
23
+ ) ORDER BY t.created_at DESC, t.id DESC LIMIT ?`;
24
+ const BY_NAME = `
25
+ SELECT ${COLUMNS} FROM turns t WHERE t.id IN (
26
+ SELECT su.turn_id FROM skill_uses su
27
+ WHERE su.skill_name = ? AND (
28
+ (su.skill_path IS NOT NULL AND su.skill_path NOT IN (SELECT value FROM json_each(?)))
29
+ OR (su.skill_path IS NULL AND ? = 0)
30
+ )
31
+ ) ORDER BY t.created_at DESC, t.id DESC LIMIT ?`;
32
+ /**
33
+ * The newest turns that used one skill. `path` is null for a skill that no
34
+ * watched folder holds. A missing `skills.db` gives no turns and stays missing.
35
+ */
36
+ export async function recentSkillTurns(store, skill, index, limit = RECENT_TURN_LIMIT) {
37
+ const db = await store.existingDb();
38
+ if (!db)
39
+ return [];
40
+ const named = index.entries.filter((e) => e.name === skill.name);
41
+ const rows = skill.path
42
+ ? await db.all(BY_PATH, [
43
+ skill.path,
44
+ skill.name,
45
+ named.length === 1 && named[0].path === skill.path ? 1 : 0,
46
+ limit,
47
+ ])
48
+ : await db.all(BY_NAME, [
49
+ skill.name,
50
+ JSON.stringify(index.entries.map((e) => e.path)),
51
+ named.length === 1 ? 1 : 0,
52
+ limit,
53
+ ]);
54
+ return rows.map(toSummary);
55
+ }
56
+ /** The turns with these ids, newest first; ids of deleted turns are left out. */
57
+ export async function skillTurnsByIds(store, ids) {
58
+ const db = await store.existingDb();
59
+ if (!db || ids.length === 0)
60
+ return [];
61
+ const rows = await db.all(`SELECT ${COLUMNS} FROM turns t WHERE t.id IN (SELECT value FROM json_each(?))
62
+ ORDER BY t.created_at DESC, t.id DESC`, [JSON.stringify(ids)]);
63
+ return rows.map(toSummary);
64
+ }
@@ -0,0 +1,18 @@
1
+ import type { ChatRuntimeConfig } from "../chat/chat-runtime.js";
2
+ import { type AutoReviewDeps } from "./auto-review.js";
3
+ /** The settings the web app's review loop reads. The caller passes a getter for `CONFIG`. */
4
+ export interface WebSkillReviewConfig extends ChatRuntimeConfig {
5
+ skillLearning?: boolean;
6
+ skillFolders?: string[];
7
+ claudeConfigDir?: string;
8
+ skillStoragePath?: string;
9
+ }
10
+ /**
11
+ * The deps for each tick. `getConfig` is called at the start of every tick,
12
+ * because a config reload replaces the `CONFIG` object: a saved Settings
13
+ * change then takes effect without a restart. Null while skill learning is
14
+ * off or before any turn is recorded; `skills.db` is never created here.
15
+ */
16
+ export declare function webReviewDeps(getConfig: () => WebSkillReviewConfig): () => Promise<AutoReviewDeps | null>;
17
+ /** Starts the automatic skill review in the web app that owns the port. Once per process. */
18
+ export declare function startWebSkillReview(getConfig: () => WebSkillReviewConfig): void;
@@ -0,0 +1,44 @@
1
+ import { skillReviewLease, startAutoReviewLoop } from "./auto-review.js";
2
+ import { skillStoragePath, watchedSkillFolders } from "./skill-folders.js";
3
+ import { getSkillIndex } from "./skill-index.js";
4
+ import { skillModel } from "./skill-model.js";
5
+ import { SkillStore, skillsDbPath } from "./store.js";
6
+ let stopLoop = null;
7
+ const opened = new Map();
8
+ /** The store and lease for this storage path, initialised once. */
9
+ function openStore(storagePath) {
10
+ let entry = opened.get(storagePath);
11
+ if (!entry) {
12
+ const store = new SkillStore(skillsDbPath(storagePath));
13
+ entry = store.init().then(() => ({ store, lease: skillReviewLease(store) }));
14
+ entry.catch(() => opened.delete(storagePath));
15
+ opened.set(storagePath, entry);
16
+ }
17
+ return entry;
18
+ }
19
+ /**
20
+ * The deps for each tick. `getConfig` is called at the start of every tick,
21
+ * because a config reload replaces the `CONFIG` object: a saved Settings
22
+ * change then takes effect without a restart. Null while skill learning is
23
+ * off or before any turn is recorded; `skills.db` is never created here.
24
+ */
25
+ export function webReviewDeps(getConfig) {
26
+ return async () => {
27
+ const config = getConfig();
28
+ if (!config.skillLearning)
29
+ return null;
30
+ const storagePath = skillStoragePath(config);
31
+ if (!opened.has(storagePath)) {
32
+ const probe = new SkillStore(skillsDbPath(storagePath));
33
+ if (!(await probe.existingDb()))
34
+ return null;
35
+ }
36
+ const folders = watchedSkillFolders(config);
37
+ const { store, lease } = await openStore(storagePath);
38
+ return { store, lease, folders, index: getSkillIndex(folders), model: skillModel(config) };
39
+ };
40
+ }
41
+ /** Starts the automatic skill review in the web app that owns the port. Once per process. */
42
+ export function startWebSkillReview(getConfig) {
43
+ stopLoop ??= startAutoReviewLoop(webReviewDeps(getConfig));
44
+ }
@@ -0,0 +1,20 @@
1
+ import type { RouteResult } from "./chat/chat-routes.js";
2
+ /**
3
+ * What the web server lends the skill routes: its folder, and a way to start
4
+ * Record past turns in its own import job slot. The importer stays behind
5
+ * web-server.ts, the one services file allowed to load it.
6
+ */
7
+ export interface SkillRouteContext {
8
+ cwd: string;
9
+ startRecordPast: (cwd: string) => Promise<{
10
+ id: string;
11
+ }>;
12
+ }
13
+ type Body = Record<string, unknown>;
14
+ /**
15
+ * The Skills page routes under `/api/skills`. Returns undefined for any other
16
+ * path. The web server applies the origin, token and JSON checks before this.
17
+ * Reads never create `skills.db`; a write opens it with `init` first.
18
+ */
19
+ export declare function routeSkills(path: string, method: string, url: URL, readBody: () => Promise<Body>, ctx: SkillRouteContext): Promise<RouteResult | undefined>;
20
+ export {};