om-memory-system 4.31.1 → 4.32.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.
@@ -13,12 +13,18 @@ export interface SkillTurnSummary {
13
13
  failedToolCalls: number;
14
14
  }
15
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.
16
+ * Turns that used one skill, newest first, `limit` of them after skipping
17
+ * `offset`. `path` is null for a skill that no watched folder holds. A missing
18
+ * `skills.db` gives no turns and stays missing.
18
19
  */
19
20
  export declare function recentSkillTurns(store: SkillStore, skill: {
20
21
  name: string;
21
22
  path: string | null;
22
- }, index: SkillIndex, limit?: number): Promise<SkillTurnSummary[]>;
23
+ }, index: SkillIndex, limit?: number, offset?: number): Promise<SkillTurnSummary[]>;
24
+ /** How many recorded turns used this skill. */
25
+ export declare function countSkillTurns(store: SkillStore, skill: {
26
+ name: string;
27
+ path: string | null;
28
+ }, index: SkillIndex): Promise<number>;
23
29
  /** The turns with these ids, newest first; ids of deleted turns are left out. */
24
30
  export declare function skillTurnsByIds(store: SkillStore, ids: readonly number[]): Promise<SkillTurnSummary[]>;
@@ -17,42 +17,54 @@ function toSummary(row) {
17
17
  // The same use-to-skill rule as the ranking: a recorded path counts while the
18
18
  // index lists it; a use with no path belongs to the one watched skill with its name.
19
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 ?`;
20
+ SELECT su.turn_id FROM skill_uses su
21
+ WHERE su.skill_path = ? OR (su.skill_path IS NULL AND su.skill_name = ? AND ? = 1)`;
24
22
  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 ?`;
23
+ SELECT su.turn_id FROM skill_uses su
24
+ WHERE su.skill_name = ? AND (
25
+ (su.skill_path IS NOT NULL AND su.skill_path NOT IN (SELECT value FROM json_each(?)))
26
+ OR (su.skill_path IS NULL AND ? = 0)
27
+ )`;
28
+ /** The SQL that selects a skill's turn ids, and its parameters. */
29
+ function turnIdsFor(skill, index) {
30
+ const named = index.entries.filter((e) => e.name === skill.name);
31
+ return skill.path
32
+ ? {
33
+ sql: BY_PATH,
34
+ args: [skill.path, skill.name, named.length === 1 && named[0].path === skill.path ? 1 : 0],
35
+ }
36
+ : {
37
+ sql: BY_NAME,
38
+ args: [
39
+ skill.name,
40
+ JSON.stringify(index.entries.map((e) => e.path)),
41
+ named.length === 1 ? 1 : 0,
42
+ ],
43
+ };
44
+ }
32
45
  /**
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.
46
+ * Turns that used one skill, newest first, `limit` of them after skipping
47
+ * `offset`. `path` is null for a skill that no watched folder holds. A missing
48
+ * `skills.db` gives no turns and stays missing.
35
49
  */
36
- export async function recentSkillTurns(store, skill, index, limit = RECENT_TURN_LIMIT) {
50
+ export async function recentSkillTurns(store, skill, index, limit = RECENT_TURN_LIMIT, offset = 0) {
37
51
  const db = await store.existingDb();
38
52
  if (!db)
39
53
  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
+ const ids = turnIdsFor(skill, index);
55
+ const rows = await db.all(`SELECT ${COLUMNS} FROM turns t WHERE t.id IN (${ids.sql})
56
+ ORDER BY t.created_at DESC, t.id DESC LIMIT ? OFFSET ?`, [...ids.args, limit, offset]);
54
57
  return rows.map(toSummary);
55
58
  }
59
+ /** How many recorded turns used this skill. */
60
+ export async function countSkillTurns(store, skill, index) {
61
+ const db = await store.existingDb();
62
+ if (!db)
63
+ return 0;
64
+ const ids = turnIdsFor(skill, index);
65
+ const row = await db.get(`SELECT COUNT(*) AS n FROM turns t WHERE t.id IN (${ids.sql})`, ids.args);
66
+ return Number(row?.n ?? 0);
67
+ }
56
68
  /** The turns with these ids, newest first; ids of deleted turns are left out. */
57
69
  export async function skillTurnsByIds(store, ids) {
58
70
  const db = await store.existingDb();
@@ -1,14 +1,20 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { RECORDING_OFF_TEXT, runSkillOperation, } from "../core/skill-operations.js";
3
3
  import { log } from "./logger.js";
4
+ import { skillActivity, skillDaily } from "./skill-learning/activity.js";
5
+ import { parseActivityQuery, topParam } from "./skill-learning/activity-range.js";
4
6
  import { undoProposal } from "./skill-learning/apply.js";
7
+ import { skillHistory, withCanUndo } from "./skill-learning/history.js";
5
8
  import { skillOperationsDeps } from "./skill-learning/operations-deps.js";
6
9
  import { getProposal, listProposals, setSkillMode, } from "./skill-learning/proposal-store.js";
10
+ import { intParam } from "./skill-learning/query-params.js";
7
11
  import { SKILL_ORDERS, rankSkills } from "./skill-learning/ranking.js";
8
12
  import { skillTurnSettingsFor } from "./skill-learning/settings-for.js";
9
13
  import { getSkillIndex } from "./skill-learning/skill-index.js";
10
14
  import { SkillStore, SkillStoreError, skillsDbPath } from "./skill-learning/store.js";
11
- import { recentSkillTurns, skillTurnsByIds } from "./skill-learning/turn-summaries.js";
15
+ import { RECENT_TURN_LIMIT, countSkillTurns, recentSkillTurns, skillTurnsByIds, } from "./skill-learning/turn-summaries.js";
16
+ const MAX_PAGE = 200;
17
+ const DEFAULT_PAGE = 20;
12
18
  const PROPOSAL_STATES = [
13
19
  "pending",
14
20
  "applied",
@@ -123,10 +129,19 @@ async function dispatch(path, method, url, readBody, ctx) {
123
129
  redact: settings.redact,
124
130
  });
125
131
  }
126
- if (path === "/api/skills/skill")
127
- return showSkill(url, store, index(), now);
132
+ if (path === "/api/skills/skill") {
133
+ return showSkill(url, { store, folders: settings.folders }, index(), now);
134
+ }
135
+ if (path === "/api/skills/activity") {
136
+ const query = parseActivityQuery(url, now);
137
+ if ("error" in query)
138
+ return fail("invalid-args", query.error);
139
+ return ok("ok", await skillActivity(store, index(), now, query, topParam(url)));
140
+ }
141
+ if (path === "/api/skills/history")
142
+ return history(url, store, settings.folders);
128
143
  if (path === "/api/skills/proposals")
129
- return proposals(url, store);
144
+ return proposals(url, store, settings.folders);
130
145
  const one = /^\/api\/skills\/proposals\/([^/]+)$/.exec(path);
131
146
  if (one) {
132
147
  const proposal = await getProposal(store, decodeURIComponent(one[1]));
@@ -168,7 +183,8 @@ async function dispatch(path, method, url, readBody, ctx) {
168
183
  }
169
184
  return undefined;
170
185
  }
171
- async function showSkill(url, store, index, now) {
186
+ async function showSkill(url, deps, index, now) {
187
+ const { store } = deps;
172
188
  const path = text(url.searchParams.get("path"));
173
189
  const name = text(url.searchParams.get("name"));
174
190
  if (!path && !name)
@@ -176,13 +192,19 @@ async function showSkill(url, store, index, now) {
176
192
  const skill = (await rankSkills(store, index, now)).find((s) => path ? s.path === path : s.path === null && s.name === name);
177
193
  if (!skill)
178
194
  return fail("not-found", "No such skill");
195
+ const limit = intParam(url, "turnLimit", 1, MAX_PAGE, RECENT_TURN_LIMIT);
196
+ const offset = intParam(url, "turnOffset", 0, Number.MAX_SAFE_INTEGER, 0);
179
197
  return ok("ok", {
180
198
  skill,
181
- recentTurns: await recentSkillTurns(store, skill, index),
182
- proposals: skill.path ? await listProposals(store, { skillPath: skill.path }) : [],
199
+ recentTurns: await recentSkillTurns(store, skill, index, limit, offset),
200
+ turnTotal: await countSkillTurns(store, skill, index),
201
+ daily: await skillDaily(store, index, skill, now),
202
+ proposals: skill.path
203
+ ? await withCanUndo(deps, await listProposals(store, { skillPath: skill.path }))
204
+ : [],
183
205
  });
184
206
  }
185
- async function proposals(url, store) {
207
+ async function proposals(url, store, folders) {
186
208
  const state = text(url.searchParams.get("state"));
187
209
  const skill = text(url.searchParams.get("skill"));
188
210
  if (state && !PROPOSAL_STATES.includes(state)) {
@@ -196,7 +218,22 @@ async function proposals(url, store) {
196
218
  filter.skillPath = skill;
197
219
  else if (skill)
198
220
  filter.skillName = skill;
199
- return ok("ok", { proposals: await listProposals(store, filter) });
221
+ const all = await listProposals(store, filter);
222
+ // Without limit or offset the whole list comes back, as before.
223
+ const offset = intParam(url, "offset", 0, Number.MAX_SAFE_INTEGER, 0);
224
+ const limit = intParam(url, "limit", 1, MAX_PAGE, all.length || 1);
225
+ const paged = url.searchParams.has("limit") || url.searchParams.has("offset");
226
+ const shown = paged ? all.slice(offset, offset + limit) : all;
227
+ return ok("ok", { proposals: await withCanUndo({ store, folders }, shown), total: all.length });
228
+ }
229
+ async function history(url, store, folders) {
230
+ const path = text(url.searchParams.get("path"));
231
+ const name = text(url.searchParams.get("name"));
232
+ if (!path && !name)
233
+ return fail("invalid-args", "Give path or name");
234
+ const limit = intParam(url, "limit", 1, MAX_PAGE, DEFAULT_PAGE);
235
+ const offset = intParam(url, "offset", 0, Number.MAX_SAFE_INTEGER, 0);
236
+ return ok("ok", await skillHistory({ store, folders }, { path, name }, limit, offset));
200
237
  }
201
238
  async function undo(id, store, folders) {
202
239
  if (!id)