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.
Files changed (141) 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 +88 -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/session/ai-session-manager.js +13 -0
  75. package/dist/services/auto-capture.d.ts +1 -1
  76. package/dist/services/auto-capture.js +71 -19
  77. package/dist/services/chat/chat-runtime.d.ts +2 -0
  78. package/dist/services/chat/chat-runtime.js +3 -2
  79. package/dist/services/global-config-writer.js +32 -2
  80. package/dist/services/profile-cleanup-routes.d.ts +2 -2
  81. package/dist/services/profile-cleanup-routes.js +19 -2
  82. package/dist/services/settings-snapshot.js +7 -1
  83. package/dist/services/skill-learning/agent-proposals.d.ts +76 -0
  84. package/dist/services/skill-learning/agent-proposals.js +180 -0
  85. package/dist/services/skill-learning/apply.d.ts +50 -0
  86. package/dist/services/skill-learning/apply.js +247 -0
  87. package/dist/services/skill-learning/auto-review.d.ts +40 -0
  88. package/dist/services/skill-learning/auto-review.js +132 -0
  89. package/dist/services/skill-learning/draft-turns.d.ts +60 -0
  90. package/dist/services/skill-learning/draft-turns.js +131 -0
  91. package/dist/services/skill-learning/drafting.d.ts +73 -0
  92. package/dist/services/skill-learning/drafting.js +263 -0
  93. package/dist/services/skill-learning/line-diff.d.ts +6 -0
  94. package/dist/services/skill-learning/line-diff.js +94 -0
  95. package/dist/services/skill-learning/operations-deps.d.ts +7 -0
  96. package/dist/services/skill-learning/operations-deps.js +19 -0
  97. package/dist/services/skill-learning/proposal-store.d.ts +82 -0
  98. package/dist/services/skill-learning/proposal-store.js +164 -0
  99. package/dist/services/skill-learning/ranking.d.ts +36 -0
  100. package/dist/services/skill-learning/ranking.js +140 -0
  101. package/dist/services/skill-learning/settings-for.d.ts +11 -0
  102. package/dist/services/skill-learning/settings-for.js +21 -0
  103. package/dist/services/skill-learning/skill-config.d.ts +45 -0
  104. package/dist/services/skill-learning/skill-config.js +70 -0
  105. package/dist/services/skill-learning/skill-detect.d.ts +29 -0
  106. package/dist/services/skill-learning/skill-detect.js +130 -0
  107. package/dist/services/skill-learning/skill-folders.d.ts +24 -0
  108. package/dist/services/skill-learning/skill-folders.js +42 -0
  109. package/dist/services/skill-learning/skill-index.d.ts +28 -0
  110. package/dist/services/skill-learning/skill-index.js +118 -0
  111. package/dist/services/skill-learning/skill-model.d.ts +10 -0
  112. package/dist/services/skill-learning/skill-model.js +15 -0
  113. package/dist/services/skill-learning/store.d.ts +79 -0
  114. package/dist/services/skill-learning/store.js +245 -0
  115. package/dist/services/skill-learning/turn-output.d.ts +34 -0
  116. package/dist/services/skill-learning/turn-output.js +98 -0
  117. package/dist/services/skill-learning/turn-summaries.d.ts +24 -0
  118. package/dist/services/skill-learning/turn-summaries.js +64 -0
  119. package/dist/services/skill-learning/web-review.d.ts +18 -0
  120. package/dist/services/skill-learning/web-review.js +44 -0
  121. package/dist/services/skill-routes.d.ts +20 -0
  122. package/dist/services/skill-routes.js +249 -0
  123. package/dist/services/turso/connection-manager.js +11 -7
  124. package/dist/services/turso/table-lease.d.ts +40 -0
  125. package/dist/services/turso/table-lease.js +77 -0
  126. package/dist/services/turso/turso-db.d.ts +4 -0
  127. package/dist/services/turso/turso-db.js +13 -0
  128. package/dist/services/user-prompt/profile-catch-up-lease.d.ts +3 -22
  129. package/dist/services/user-prompt/profile-catch-up-lease.js +9 -65
  130. package/dist/services/web-jobs.d.ts +7 -0
  131. package/dist/services/web-jobs.js +14 -0
  132. package/dist/services/web-route-modules.d.ts +2 -2
  133. package/dist/services/web-route-modules.js +6 -1
  134. package/dist/services/web-server.js +9 -21
  135. package/dist/v2/adapter.js +13 -8
  136. package/dist/web/assets/index-9hu4Z7SV.css +2 -0
  137. package/dist/web/assets/index-BMt9gPNK.js +129 -0
  138. package/dist/web/index.html +2 -2
  139. package/package.json +1 -1
  140. package/dist/web/assets/index-BGDKFzoC.css +0 -2
  141. package/dist/web/assets/index-CE8tXPA_.js +0 -128
@@ -68,6 +68,8 @@ export async function scheduleAutoBackfill(options) {
68
68
  force: false,
69
69
  skipMemories: false,
70
70
  skipProfile: false,
71
+ // Skill turns follow the same cutoff and sessions as memories.
72
+ skipSkills: !CONFIG.skillLearning,
71
73
  scope: "all-projects",
72
74
  pathMaps: [],
73
75
  until: cutoff,
@@ -89,8 +91,18 @@ export async function scheduleAutoBackfill(options) {
89
91
  return;
90
92
  }
91
93
  if (!current.pending) {
94
+ // No memory work, but turns to record: a skill-turns pass needs no model.
95
+ const skillTurnsOnly = Boolean(dry.skillTurnsWouldRecord);
96
+ if (skillTurnsOnly) {
97
+ await run(options.host, { ...args, dryRun: false, skipMemories: true, skipProfile: true }, {
98
+ cwd: options.cwd,
99
+ models: {},
100
+ signal: controller.signal,
101
+ track: { surface: options.surface ?? "auto", lockHeld: true },
102
+ });
103
+ }
92
104
  // A pressed Run now needs an answer on the page: record a finished run.
93
- if (options.userStarted) {
105
+ if (options.userStarted && !skillTurnsOnly) {
94
106
  await startImportRun(options.host, options.surface ?? "web")
95
107
  .then((recorder) => recorder.finish("done", {}))
96
108
  .catch((error) => log("Backfill run record write failed", {
@@ -17,6 +17,7 @@ export interface ClaudeContentBlock {
17
17
  input?: unknown;
18
18
  tool_use_id?: string;
19
19
  content?: unknown;
20
+ is_error?: boolean;
20
21
  }
21
22
  export interface ClaudeTranscriptEntry {
22
23
  type: string;
@@ -1,3 +1,5 @@
1
+ import { boundToolInput, readToolPath } from "../core/tool-input.js";
2
+ import { errorOutcome } from "../core/tool-outcome.js";
1
3
  /**
2
4
  * Entry types seen in real transcripts. A line with any other type is skipped
3
5
  * and counted, so a new Claude Code version shows up in the import report.
@@ -35,7 +37,6 @@ const NOISE_PREFIXES = [
35
37
  "<bash-stderr>",
36
38
  "<task-notification>",
37
39
  ];
38
- const MAX_TOOL_INPUT_LENGTH = 100;
39
40
  /** Parse one transcript line. A JSON value without a string `type` is unreadable. */
40
41
  export function parseClaudeTranscriptLine(line) {
41
42
  if (!line.trim())
@@ -121,12 +122,6 @@ function formatToolInput(input) {
121
122
  .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
122
123
  .join(", ");
123
124
  }
124
- function boundToolInput(input) {
125
- if (input.length > MAX_TOOL_INPUT_LENGTH) {
126
- return input.substring(0, MAX_TOOL_INPUT_LENGTH) + "...";
127
- }
128
- return input;
129
- }
130
125
  function closeWindow(open) {
131
126
  if (open.textResponses.length === 0 && open.toolCalls.length === 0)
132
127
  return null;
@@ -153,10 +148,45 @@ function addAssistantWork(open, entry) {
153
148
  if (text)
154
149
  open.textResponses.push(text);
155
150
  for (const block of blocks.filter((item) => item.type === "tool_use")) {
156
- open.toolCalls.push({
157
- name: typeof block.name === "string" ? block.name : "unknown",
151
+ // A call stays `unknown` until a `tool_result` block answers it.
152
+ const name = typeof block.name === "string" ? block.name : "unknown";
153
+ const call = {
154
+ name,
158
155
  input: boundToolInput(formatToolInput(block.input)),
159
- });
156
+ outcome: "unknown",
157
+ };
158
+ const path = readToolPath(name, block.input);
159
+ if (path !== undefined)
160
+ call.path = path;
161
+ open.toolCalls.push(call);
162
+ if (typeof block.id === "string")
163
+ open.callsById.set(block.id, call);
164
+ }
165
+ }
166
+ /** Text of a `tool_result` block: a string, or an array of text blocks. */
167
+ function toolResultText(content) {
168
+ if (typeof content === "string")
169
+ return content;
170
+ if (!Array.isArray(content))
171
+ return "";
172
+ return content
173
+ .filter((block) => block && block.type === "text" && typeof block.text === "string")
174
+ .map((block) => block.text)
175
+ .join("\n");
176
+ }
177
+ /**
178
+ * Set the outcome of each call a user entry's `tool_result` blocks answer.
179
+ * Only the error text of a failed call is kept; tool output is never stored.
180
+ */
181
+ function addToolResults(open, entry) {
182
+ for (const block of blocksOf(entry).filter((item) => item.type === "tool_result")) {
183
+ const call = open.callsById.get(block.tool_use_id ?? "");
184
+ if (!call)
185
+ continue;
186
+ if (block.is_error === true)
187
+ Object.assign(call, errorOutcome(toolResultText(block.content)));
188
+ else
189
+ call.outcome = "ok";
160
190
  }
161
191
  }
162
192
  function windowsWithIndex(entries) {
@@ -180,6 +210,7 @@ function windowsWithIndex(entries) {
180
210
  textResponses: [],
181
211
  toolCalls: [],
182
212
  sourceEntryIds: [entry.uuid],
213
+ callsById: new Map(),
183
214
  };
184
215
  }
185
216
  return;
@@ -187,6 +218,9 @@ function windowsWithIndex(entries) {
187
218
  if (open && entry.type === "assistant" && entry.isSidechain !== true) {
188
219
  addAssistantWork(open, entry);
189
220
  }
221
+ if (open && entry.type === "user" && entry.isSidechain !== true) {
222
+ addToolResults(open, entry);
223
+ }
190
224
  });
191
225
  flush();
192
226
  return result;
@@ -1,6 +1,5 @@
1
1
  import type { CaptureSummaryProvider } from "../core/host.js";
2
2
  import type { ModelPort } from "../core/profile-analysis.js";
3
- import { type ClaudeConversationWindow, type ClaudeTranscriptEntry } from "./claude-conversation.js";
4
3
  export interface ClaudeHookDeps {
5
4
  /** The capture model. Defaults to the external API. */
6
5
  captureProvider?: () => CaptureSummaryProvider;
@@ -34,13 +33,6 @@ export declare function handleClaudeCapture(body: unknown, deps?: ClaudeHookDeps
34
33
  };
35
34
  /** Resolves when every queued capture has finished. */
36
35
  export declare function whenClaudeCaptureIdle(): Promise<void>;
37
- /**
38
- * The windows after the cursor. The transcript file can lag the hook, so the
39
- * hook's final text is added as a last assistant reply when the transcript
40
- * does not already end with it. It carries no entry id, so `sourceEntryIds`
41
- * stay the reader's ids.
42
- */
43
- export declare function claudeWindowsToCapture(entries: ClaudeTranscriptEntry[], cursor: string | null, finalText: string | undefined): ClaudeConversationWindow[];
44
36
  /**
45
37
  * Register the Claude Code retry drain in this web app and start one retry
46
38
  * pass. The drain uses the external API. When capture is off, the first
@@ -2,9 +2,9 @@ import { realpathSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
4
4
  import { CONFIG, refreshConfigIfChanged } from "../config.js";
5
- import { captureConversation } from "../core/capture.js";
6
5
  import { classifyCaptureFailure, quickRetryDelayMs } from "../core/capture-retry-policy.js";
7
- import { buildRecentMemoriesSection, buildRetrievalSection, formatMemoriesForCompaction, retrievalWrapperBytes, stripRetrievalSections, wrapRetrievalSection, } from "../core/retrieval.js";
6
+ import { buildRecentMemoriesSection, buildRetrievalSection, formatMemoriesForCompaction, retrievalWrapperBytes, wrapRetrievalSection, } from "../core/retrieval.js";
7
+ import { handleFinishedTurn } from "../core/turn-outputs.js";
8
8
  import { DEFAULT_RETRIEVAL_MAX_TOKENS, tokensToByteCeiling } from "../core/context-budget.js";
9
9
  import { resolveClaudeCodeLiveModel } from "../services/ai/live-model-choice.js";
10
10
  import { captureFailureOf, drainCaptureRetries, queueFailedCapture, registerCaptureRetryDrain, startCaptureRetryDrain, } from "../services/capture-retry-drain.js";
@@ -12,9 +12,11 @@ import { readClaudeCaptureCursor, writeClaudeCaptureCursor, } from "../services/
12
12
  import { memoryClient } from "../services/client.js";
13
13
  import { log } from "../services/logger.js";
14
14
  import { isFullyPrivate, stripPrivateContent } from "../services/privacy.js";
15
+ import { skillTurnRecorder } from "../services/skill-learning/settings-for.js";
15
16
  import { getTags } from "../services/tags.js";
16
- import { extractClaudeWindowsAfter, parseClaudeTranscript, } from "./claude-conversation.js";
17
+ import { parseClaudeTranscript } from "./claude-conversation.js";
17
18
  import { learnClaudeProfile } from "./claude-profile-learning.js";
19
+ import { claudeWindowsToCapture, recordClaudeSkillTurns, sessionDirectory, skillUnitOf, workUnitOf, } from "./claude-turn-units.js";
18
20
  import { defaultClaudeProjectsRoot } from "./claude-reader.js";
19
21
  // The web app's side of the Claude Code hooks (design decisions 4, 6, 7):
20
22
  // retrieval for SessionStart and UserPromptSubmit, a one-worker capture queue
@@ -251,63 +253,13 @@ async function readTranscript(request) {
251
253
  }
252
254
  return parsed.entries;
253
255
  }
254
- /**
255
- * The windows after the cursor. The transcript file can lag the hook, so the
256
- * hook's final text is added as a last assistant reply when the transcript
257
- * does not already end with it. It carries no entry id, so `sourceEntryIds`
258
- * stay the reader's ids.
259
- */
260
- export function claudeWindowsToCapture(entries, cursor, finalText) {
261
- const tail = finalText?.trim();
262
- if (!tail)
263
- return extractClaudeWindowsAfter(entries, cursor);
264
- const hookReply = {
265
- type: "assistant",
266
- message: { role: "assistant", content: [{ type: "text", text: tail }] },
267
- };
268
- const windows = extractClaudeWindowsAfter([...entries, hookReply], cursor);
269
- const last = windows.at(-1);
270
- const replies = last?.textResponses ?? [];
271
- if (replies.length > 1 && replies[replies.length - 2].trim().endsWith(tail))
272
- replies.pop();
273
- return windows;
274
- }
275
- /**
276
- * The project a session belongs to: the first prompt's directory, the same rule
277
- * the history import uses. Live capture must file a turn under the project the
278
- * import would choose, or the import cannot see that the turn is already saved.
279
- */
280
- function sessionDirectory(entries, fallback) {
281
- const first = entries.find((entry) => entry.type === "user" && entry.isSidechain !== true);
282
- return typeof first?.cwd === "string" && first.cwd ? first.cwd : fallback;
283
- }
284
- /** A cleaned work unit, or null for a prompt with nothing left to capture. */
285
- function workUnitOf(request, window, directory) {
286
- const prompt = stripRetrievalSections(window.userPrompt);
287
- if (!prompt || isFullyPrivate(prompt))
288
- return null;
289
- return {
290
- host: HOST,
291
- hostSessionId: request.sessionId,
292
- sourceType: "live-capture",
293
- projectDirectory: directory,
294
- userPrompt: stripPrivateContent(prompt),
295
- promptId: window.userEntryId,
296
- textResponses: window.textResponses.map(stripPrivateContent),
297
- toolCalls: window.toolCalls.map((call) => ({
298
- name: call.name,
299
- input: stripPrivateContent(call.input),
300
- })),
301
- sourceEntryIds: window.sourceEntryIds,
302
- ...(window.sourceTimestamp !== undefined ? { sourceTimestamp: window.sourceTimestamp } : {}),
303
- };
304
- }
305
256
  /** Try the turn up to `autoCaptureMaxRetries` times, as the other hosts do. */
306
- async function captureWithQuickRetries(unit, provider, sleep) {
257
+ async function captureWithQuickRetries(unit, provider, sleep, recordSkillTurn) {
307
258
  const maxRetries = Math.max(1, CONFIG.autoCaptureMaxRetries ?? 3);
308
259
  for (let attempt = 1;; attempt++) {
309
260
  try {
310
- return await captureConversation(unit, provider);
261
+ const skills = recordSkillTurn !== undefined;
262
+ return await handleFinishedTurn(unit, { memories: true, skills }, { provider, recordSkillTurn });
311
263
  }
312
264
  catch (error) {
313
265
  if (attempt >= maxRetries)
@@ -320,16 +272,26 @@ async function externalModels() {
320
272
  const { selectImportModel } = await import("./model-selection.js");
321
273
  return selectImportModel({});
322
274
  }
275
+ /** Memory capture is on and ready. Logs why when it is not, as before. */
276
+ function memoryCaptureOn(request) {
277
+ if (!CONFIG.autoCaptureEnabled)
278
+ return false;
279
+ const live = resolveClaudeCodeLiveModel(CONFIG);
280
+ if (!live.ready)
281
+ reportCaptureOff(live.issues);
282
+ else if (memoryClient.getEmbeddingInitError?.())
283
+ skipped(request, "not-ready");
284
+ else
285
+ return true;
286
+ return false;
287
+ }
323
288
  async function captureTurn(request, deps) {
324
289
  const started = Date.now();
325
290
  refreshConfigIfChanged(request.cwd);
326
- if (!CONFIG.autoCaptureEnabled)
291
+ const memories = memoryCaptureOn(request);
292
+ // Skill learning records turns even when memory capture is off (ADR-036).
293
+ if (!memories && !CONFIG.skillLearning)
327
294
  return;
328
- const live = resolveClaudeCodeLiveModel(CONFIG);
329
- if (!live.ready)
330
- return reportCaptureOff(live.issues);
331
- if (memoryClient.getEmbeddingInitError?.())
332
- return skipped(request, "not-ready");
333
295
  const entries = await readTranscript(request);
334
296
  if (!entries)
335
297
  return;
@@ -342,29 +304,38 @@ async function captureTurn(request, deps) {
342
304
  return;
343
305
  }
344
306
  const directory = sessionDirectory(entries, request.cwd);
307
+ const recorder = skillTurnRecorder(directory);
308
+ const recordOnly = (only) => recordClaudeSkillTurns(only, request.sessionId, directory, recorder);
309
+ if (!memories)
310
+ return recordOnly(windows);
345
311
  let provider;
346
312
  try {
347
313
  provider = deps.captureProvider?.() ?? (await externalModels()).capture;
348
314
  }
349
315
  catch (error) {
350
- return skipped(request, "model-unavailable", { error: errorCode(error) });
316
+ skipped(request, "model-unavailable", { error: errorCode(error) });
317
+ return recordOnly(windows);
351
318
  }
352
319
  const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
353
320
  const counts = { captured: 0, skipped: 0, failed: 0, private: 0, queued: 0 };
354
321
  let blocked = null;
355
322
  for (const window of windows) {
356
- const unit = workUnitOf(request, window, directory);
323
+ const unit = workUnitOf(request.sessionId, window, directory);
357
324
  if (!unit) {
358
325
  counts.private++;
326
+ await recordOnly([window]);
359
327
  }
360
328
  else if (blocked) {
361
329
  // The model is out of reach: queue the rest of the turns without calling it.
330
+ await recordOnly([window]);
362
331
  if (await queueFailedCapture(unit, blocked, CONFIG))
363
332
  counts.queued++;
364
333
  }
365
334
  else {
366
335
  try {
367
- const result = await captureWithQuickRetries(unit, provider, sleep);
336
+ // Memory capture reads the cleaned unit; skill learning records the raw one.
337
+ const record = recorder && (() => recorder(skillUnitOf(request.sessionId, window, directory)));
338
+ const result = await captureWithQuickRetries(unit, provider, sleep, record);
368
339
  counts[result.status]++;
369
340
  await recordPrompt(request, unit, result.status === "captured" ? result.memoryId : null);
370
341
  }
@@ -1,5 +1,6 @@
1
1
  import { importHistorySource, } from "./importer.js";
2
2
  import { openClaudeHistorySource } from "./claude-reader.js";
3
+ import { importSkillTurnsOn, skillTurnsOnly } from "./import-turn-outputs.js";
3
4
  /**
4
5
  * Import Claude Code history through the shared capture, ledger, and profile
5
6
  * steps, with host `claude-code`. Mirrors `importPiHistory`: the same filters,
@@ -16,6 +17,7 @@ export async function importClaudeHistory(deps, filters) {
16
17
  ...(filters.maxSessions !== undefined ? { maxSessions: filters.maxSessions } : {}),
17
18
  ...(filters.pathMaps ? { pathMaps: filters.pathMaps } : {}),
18
19
  ...(filters.scope === "current-project" ? { project: filters.currentDirectory } : {}),
20
+ keepMissingFolders: skillTurnsOnly(filters, Boolean(deps.profile)),
19
21
  ...(deps.signal ? { signal: deps.signal } : {}),
20
22
  }, deps.loadSession ? { loadSession: deps.loadSession } : {});
21
23
  const report = {
@@ -42,7 +44,7 @@ export async function importClaudeHistory(deps, filters) {
42
44
  unreadableLines: source.unreadableLines,
43
45
  unknownTypes: source.unknownTypes,
44
46
  };
45
- if (filters.skipMemories) {
47
+ if (filters.skipMemories && !importSkillTurnsOn(filters)) {
46
48
  report.unitsTotal = source.unitsKept;
47
49
  }
48
50
  else {
@@ -64,6 +64,8 @@ export interface ClaudeHistoryOptions {
64
64
  pathMaps?: ImportPathMap[];
65
65
  /** Keep only sessions of this project directory (current-project scope). */
66
66
  project?: string;
67
+ /** A skills-only run keeps a session whose folder is gone, under its recorded path. */
68
+ keepMissingFolders?: boolean;
67
69
  signal?: AbortSignal;
68
70
  }
69
71
  export interface ClaudeHistoryDeps {
@@ -6,7 +6,7 @@ import { claudeProjectsRoot } from "../services/claude-folder.js";
6
6
  import { extractScopeFromContainerTag } from "../services/memory-scope.js";
7
7
  import { getTags } from "../services/tags.js";
8
8
  import { extractClaudeConversationWindows, parseClaudeTranscript, parseClaudeTranscriptLine, } from "./claude-conversation.js";
9
- import { resolveImportProject } from "./import-project.js";
9
+ import { importDirectory } from "./import-project.js";
10
10
  import { countImportableUnits, projectFilterTag, selectImportWindows, } from "./importer.js";
11
11
  /**
12
12
  * Discovery and loading of Claude Code transcripts for the history import.
@@ -213,7 +213,7 @@ export async function openClaudeHistorySource(options = {}, deps = {}) {
213
213
  source.sessionsFilteredOut++;
214
214
  continue;
215
215
  }
216
- const { directory } = resolveImportProject(discovered.cwd, options.pathMaps ?? []);
216
+ const directory = importDirectory(discovered.cwd, options.pathMaps ?? [], options.keepMissingFolders === true);
217
217
  if (!directory) {
218
218
  source.unresolvableSessions.push({ file: discovered.file, cwd: discovered.cwd });
219
219
  continue;
@@ -0,0 +1,26 @@
1
+ import type { CaptureWorkUnit } from "../core/capture.js";
2
+ import type { SkillTurnRecorder } from "../services/skill-learning/settings-for.js";
3
+ import { type ClaudeConversationWindow, type ClaudeTranscriptEntry } from "./claude-conversation.js";
4
+ /**
5
+ * The windows after the cursor. The transcript file can lag the hook, so the
6
+ * hook's final text is added as a last assistant reply when the transcript
7
+ * does not already end with it. It carries no entry id, so `sourceEntryIds`
8
+ * stay the reader's ids.
9
+ */
10
+ export declare function claudeWindowsToCapture(entries: ClaudeTranscriptEntry[], cursor: string | null, finalText: string | undefined): ClaudeConversationWindow[];
11
+ /**
12
+ * The project a session belongs to: the first prompt's directory, the same rule
13
+ * the history import uses. Live capture must file a turn under the project the
14
+ * import would choose, or the import cannot see that the turn is already saved.
15
+ */
16
+ export declare function sessionDirectory(entries: ClaudeTranscriptEntry[], fallback: string): string;
17
+ /** A cleaned work unit, or null for a prompt with nothing left to capture. */
18
+ export declare function workUnitOf(sessionId: string, window: ClaudeConversationWindow, directory: string): CaptureWorkUnit | null;
19
+ /**
20
+ * The unit skill learning records: OMMS's injected memories removed, nothing
21
+ * else. Private text and secrets are the skill output's job, by its setting,
22
+ * and the tool calls keep their outcome, error and path.
23
+ */
24
+ export declare function skillUnitOf(sessionId: string, window: ClaudeConversationWindow, directory: string): CaptureWorkUnit;
25
+ /** Records windows that memory capture does not run for. Never moves the capture cursor. */
26
+ export declare function recordClaudeSkillTurns(windows: ClaudeConversationWindow[], sessionId: string, directory: string, recorder: SkillTurnRecorder | undefined): Promise<void>;
@@ -0,0 +1,85 @@
1
+ import { stripRetrievalSections } from "../core/retrieval.js";
2
+ import { handleFinishedTurn } from "../core/turn-outputs.js";
3
+ import { isFullyPrivate, stripPrivateContent } from "../services/privacy.js";
4
+ import { extractClaudeWindowsAfter, } from "./claude-conversation.js";
5
+ // Work units for the Claude Code Stop hook (`claude-hook-api.ts`): the cleaned
6
+ // unit memory capture reads, and the raw unit skill learning records.
7
+ const HOST = "claude-code";
8
+ /**
9
+ * The windows after the cursor. The transcript file can lag the hook, so the
10
+ * hook's final text is added as a last assistant reply when the transcript
11
+ * does not already end with it. It carries no entry id, so `sourceEntryIds`
12
+ * stay the reader's ids.
13
+ */
14
+ export function claudeWindowsToCapture(entries, cursor, finalText) {
15
+ const tail = finalText?.trim();
16
+ if (!tail)
17
+ return extractClaudeWindowsAfter(entries, cursor);
18
+ const hookReply = {
19
+ type: "assistant",
20
+ message: { role: "assistant", content: [{ type: "text", text: tail }] },
21
+ };
22
+ const windows = extractClaudeWindowsAfter([...entries, hookReply], cursor);
23
+ const last = windows.at(-1);
24
+ const replies = last?.textResponses ?? [];
25
+ if (replies.length > 1 && replies[replies.length - 2].trim().endsWith(tail))
26
+ replies.pop();
27
+ return windows;
28
+ }
29
+ /**
30
+ * The project a session belongs to: the first prompt's directory, the same rule
31
+ * the history import uses. Live capture must file a turn under the project the
32
+ * import would choose, or the import cannot see that the turn is already saved.
33
+ */
34
+ export function sessionDirectory(entries, fallback) {
35
+ const first = entries.find((entry) => entry.type === "user" && entry.isSidechain !== true);
36
+ return typeof first?.cwd === "string" && first.cwd ? first.cwd : fallback;
37
+ }
38
+ /** A cleaned work unit, or null for a prompt with nothing left to capture. */
39
+ export function workUnitOf(sessionId, window, directory) {
40
+ const prompt = stripRetrievalSections(window.userPrompt);
41
+ if (!prompt || isFullyPrivate(prompt))
42
+ return null;
43
+ return {
44
+ host: HOST,
45
+ hostSessionId: sessionId,
46
+ sourceType: "live-capture",
47
+ projectDirectory: directory,
48
+ userPrompt: stripPrivateContent(prompt),
49
+ promptId: window.userEntryId,
50
+ textResponses: window.textResponses.map(stripPrivateContent),
51
+ toolCalls: window.toolCalls.map((call) => ({
52
+ name: call.name,
53
+ input: stripPrivateContent(call.input),
54
+ })),
55
+ sourceEntryIds: window.sourceEntryIds,
56
+ ...(window.sourceTimestamp !== undefined ? { sourceTimestamp: window.sourceTimestamp } : {}),
57
+ };
58
+ }
59
+ /**
60
+ * The unit skill learning records: OMMS's injected memories removed, nothing
61
+ * else. Private text and secrets are the skill output's job, by its setting,
62
+ * and the tool calls keep their outcome, error and path.
63
+ */
64
+ export function skillUnitOf(sessionId, window, directory) {
65
+ return {
66
+ host: HOST,
67
+ hostSessionId: sessionId,
68
+ sourceType: "live-capture",
69
+ projectDirectory: directory,
70
+ userPrompt: stripRetrievalSections(window.userPrompt),
71
+ promptId: window.userEntryId,
72
+ textResponses: window.textResponses,
73
+ toolCalls: window.toolCalls,
74
+ sourceEntryIds: window.sourceEntryIds,
75
+ ...(window.sourceTimestamp !== undefined ? { sourceTimestamp: window.sourceTimestamp } : {}),
76
+ };
77
+ }
78
+ /** Records windows that memory capture does not run for. Never moves the capture cursor. */
79
+ export async function recordClaudeSkillTurns(windows, sessionId, directory, recorder) {
80
+ if (!recorder)
81
+ return;
82
+ for (const window of windows) {
83
+ await handleFinishedTurn(skillUnitOf(sessionId, window, directory), { memories: false, skills: true }, { recordSkillTurn: recorder });
84
+ }
85
+ }
@@ -14,6 +14,8 @@ export interface HistoryImportArgs {
14
14
  force: boolean;
15
15
  skipMemories: boolean;
16
16
  skipProfile: boolean;
17
+ /** Record no skill turns. Skill turns are recorded only while `skillLearning` is on. */
18
+ skipSkills: boolean;
17
19
  profileBatch?: number;
18
20
  /** Session: `provider/id` from the host's signed-in models. CLI: a model id for `--provider`. */
19
21
  model?: string;
@@ -49,6 +51,7 @@ export interface WebImportOptions {
49
51
  force?: boolean;
50
52
  skipMemories?: boolean;
51
53
  skipProfile?: boolean;
54
+ skipSkills?: boolean;
52
55
  model?: string;
53
56
  scope?: HistoryImportArgs["scope"];
54
57
  project?: string;
@@ -11,6 +11,7 @@ const BOOLEAN_FLAGS = new Set([
11
11
  "--force",
12
12
  "--skip-memories",
13
13
  "--skip-profile",
14
+ "--skip-skills",
14
15
  ]);
15
16
  const VALUE_FLAGS = new Set([
16
17
  "--profile-batch",
@@ -55,6 +56,7 @@ export function parseHistoryImportArgs(tokens, options) {
55
56
  force: false,
56
57
  skipMemories: false,
57
58
  skipProfile: false,
59
+ skipSkills: false,
58
60
  scope: "current-project",
59
61
  pathMaps: [],
60
62
  errors: [],
@@ -77,6 +79,8 @@ export function parseHistoryImportArgs(tokens, options) {
77
79
  result.force = true;
78
80
  else if (flag === "--skip-memories")
79
81
  result.skipMemories = true;
82
+ else if (flag === "--skip-skills")
83
+ result.skipSkills = true;
80
84
  else
81
85
  result.skipProfile = true;
82
86
  continue;
@@ -188,6 +192,7 @@ export function webImportTokens(options, _host) {
188
192
  ["force", "--force"],
189
193
  ["skipMemories", "--skip-memories"],
190
194
  ["skipProfile", "--skip-profile"],
195
+ ["skipSkills", "--skip-skills"],
191
196
  ]) {
192
197
  if (options[field])
193
198
  tokens.push(flag);
@@ -244,8 +249,9 @@ ${model}
244
249
  --max-sessions <n> Read at most n sessions, oldest first
245
250
  --map <old>=<new> Map a recorded directory that no longer exists (repeatable)
246
251
  ${source}
247
- --skip-memories Record profile prompts only
248
- --skip-profile Import memories only
252
+ --skip-memories Save no memories
253
+ --skip-profile Record no profile prompts
254
+ --skip-skills Record no skill turns (recorded only while skillLearning is on)
249
255
  --profile-batch <n> Prompts per profile analysis batch (default: 50)
250
256
  --force Reprocess already-handled memory units and profile prompts
251
257
  --help Show this help
@@ -14,3 +14,9 @@ export interface ResolvedImportProject {
14
14
  * of the same project.
15
15
  */
16
16
  export declare function resolveImportProject(recordedDirectory: string | null, maps?: ImportPathMap[], projectWorktree?: string | null): ResolvedImportProject;
17
+ /**
18
+ * The folder an importer uses for one session. Skill turns need no project
19
+ * folder, so a skills-only run (`keepMissing`) keeps a session whose folder is
20
+ * gone under its recorded path. Every other run leaves it unresolved.
21
+ */
22
+ export declare function importDirectory(recordedDirectory: string | null, maps: ImportPathMap[], keepMissing: boolean, projectWorktree?: string | null): string | null;
@@ -33,3 +33,12 @@ export function resolveImportProject(recordedDirectory, maps = [], projectWorktr
33
33
  }
34
34
  return { directory: null, via: "unresolved" };
35
35
  }
36
+ /**
37
+ * The folder an importer uses for one session. Skill turns need no project
38
+ * folder, so a skills-only run (`keepMissing`) keeps a session whose folder is
39
+ * gone under its recorded path. Every other run leaves it unresolved.
40
+ */
41
+ export function importDirectory(recordedDirectory, maps, keepMissing, projectWorktree = null) {
42
+ const { directory } = resolveImportProject(recordedDirectory, maps, projectWorktree);
43
+ return directory ?? (keepMissing ? recordedDirectory : null);
44
+ }
@@ -2,8 +2,12 @@ import type { BackfillHost } from "./backfill-model.js";
2
2
  import { type ProgressEstimate } from "./import-progress.js";
3
3
  export type ImportSurface = "auto" | "web" | "cli" | "slash";
4
4
  export type ImportRunState = "running" | "paused" | "stopped" | "done" | "failed";
5
- /** A run imports exchanges first, then learns the profile from the imported prompts. */
6
- export type ImportRunPhase = "exchanges" | "profile";
5
+ /**
6
+ * A run imports exchanges first, then learns the profile from the imported
7
+ * prompts. A run without memories that records skill turns starts in
8
+ * `skill-turns` instead of `exchanges`.
9
+ */
10
+ export type ImportRunPhase = "exchanges" | "skill-turns" | "profile";
7
11
  export interface ImportRunCounts {
8
12
  total: number;
9
13
  done: number;
@@ -48,4 +52,6 @@ export declare function startImportRun(host: BackfillHost, surface: ImportSurfac
48
52
  intervalMs?: number;
49
53
  sampleIntervalMs?: number;
50
54
  onPause?: () => void;
55
+ /** The first phase; `exchanges` unless the run records skill turns only. */
56
+ phase?: Exclude<ImportRunPhase, "profile">;
51
57
  }): Promise<ImportRunRecorder>;
@@ -67,7 +67,7 @@ export async function readImportRun(host) {
67
67
  failed: Number(row.failed),
68
68
  error: row.error === null ? null : String(row.error),
69
69
  paused: Number(row.paused) === 1,
70
- phase: row.phase === "profile" ? "profile" : "exchanges",
70
+ phase: row.phase === "profile" || row.phase === "skill-turns" ? row.phase : "exchanges",
71
71
  profileDone: Number(row.profile_done ?? 0),
72
72
  profileTotal: Number(row.profile_total ?? 0),
73
73
  ...(state === "running"
@@ -102,8 +102,8 @@ export async function startImportRun(host, surface, options = {}) {
102
102
  const startedAt = clock();
103
103
  await db.run(`UPDATE import_runs SET surface = ?, state = 'running', pid = ?, started_at = ?, total = 0,
104
104
  done = 0, imported = 0, skipped = 0, failed = 0, error = NULL, updated_at = ?, samples = '[]',
105
- phase = 'exchanges', profile_done = 0, profile_total = 0
106
- WHERE host = ?`, [surface, process.pid, startedAt, startedAt, host]);
105
+ phase = ?, profile_done = 0, profile_total = 0
106
+ WHERE host = ?`, [surface, process.pid, startedAt, startedAt, options.phase ?? "exchanges", host]);
107
107
  let samples = [];
108
108
  let lastWrite = Number.NEGATIVE_INFINITY;
109
109
  let lastSample = Number.NEGATIVE_INFINITY;
@@ -39,6 +39,11 @@ export interface ImportMatchOptions {
39
39
  outputs?: ImportOutputs;
40
40
  /** A forced run re-analyses handled history, so it keeps handled sessions. */
41
41
  force?: boolean;
42
+ /**
43
+ * The run writes skill turns only. Skill turns need no project folder, so a
44
+ * session whose folder is gone matches under its recorded path.
45
+ */
46
+ skillTurnsOnly?: boolean;
42
47
  }
43
48
  export declare class StaleSelectionError extends Error {
44
49
  readonly status = 409;
@@ -48,8 +53,10 @@ interface Matched {
48
53
  identity: ImportSourceIdentity;
49
54
  /** Resolvable sessions in scope, newest first. */
50
55
  matching: ImportSessionRow[];
51
- /** Sessions whose directory cannot be resolved, in any scope. */
56
+ /** Sessions this run cannot use because their directory cannot be resolved, in any scope. */
52
57
  unresolved: ImportSessionRow[];
58
+ /** Every session whose directory cannot be resolved; feeds the Directory maps list. */
59
+ missing: ImportSessionRow[];
53
60
  revision: string;
54
61
  }
55
62
  export declare function matchImportSessions(identity: ImportSourceIdentity, options: ImportMatchOptions, snapshotMode: "fresh" | "reuse" | "any"): Promise<Matched>;