om-memory-system 4.29.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 (143) 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/capture-attempt-store.d.ts +2 -0
  78. package/dist/services/capture-attempt-store.js +11 -0
  79. package/dist/services/chat/chat-runtime.d.ts +2 -0
  80. package/dist/services/chat/chat-runtime.js +3 -2
  81. package/dist/services/global-config-writer.js +32 -2
  82. package/dist/services/profile-cleanup-routes.d.ts +2 -2
  83. package/dist/services/profile-cleanup-routes.js +19 -2
  84. package/dist/services/settings-snapshot.js +7 -1
  85. package/dist/services/skill-learning/agent-proposals.d.ts +76 -0
  86. package/dist/services/skill-learning/agent-proposals.js +180 -0
  87. package/dist/services/skill-learning/apply.d.ts +50 -0
  88. package/dist/services/skill-learning/apply.js +247 -0
  89. package/dist/services/skill-learning/auto-review.d.ts +40 -0
  90. package/dist/services/skill-learning/auto-review.js +132 -0
  91. package/dist/services/skill-learning/draft-turns.d.ts +60 -0
  92. package/dist/services/skill-learning/draft-turns.js +131 -0
  93. package/dist/services/skill-learning/drafting.d.ts +73 -0
  94. package/dist/services/skill-learning/drafting.js +263 -0
  95. package/dist/services/skill-learning/line-diff.d.ts +6 -0
  96. package/dist/services/skill-learning/line-diff.js +94 -0
  97. package/dist/services/skill-learning/operations-deps.d.ts +7 -0
  98. package/dist/services/skill-learning/operations-deps.js +19 -0
  99. package/dist/services/skill-learning/proposal-store.d.ts +82 -0
  100. package/dist/services/skill-learning/proposal-store.js +164 -0
  101. package/dist/services/skill-learning/ranking.d.ts +36 -0
  102. package/dist/services/skill-learning/ranking.js +140 -0
  103. package/dist/services/skill-learning/settings-for.d.ts +11 -0
  104. package/dist/services/skill-learning/settings-for.js +21 -0
  105. package/dist/services/skill-learning/skill-config.d.ts +45 -0
  106. package/dist/services/skill-learning/skill-config.js +70 -0
  107. package/dist/services/skill-learning/skill-detect.d.ts +29 -0
  108. package/dist/services/skill-learning/skill-detect.js +130 -0
  109. package/dist/services/skill-learning/skill-folders.d.ts +24 -0
  110. package/dist/services/skill-learning/skill-folders.js +42 -0
  111. package/dist/services/skill-learning/skill-index.d.ts +28 -0
  112. package/dist/services/skill-learning/skill-index.js +118 -0
  113. package/dist/services/skill-learning/skill-model.d.ts +10 -0
  114. package/dist/services/skill-learning/skill-model.js +15 -0
  115. package/dist/services/skill-learning/store.d.ts +79 -0
  116. package/dist/services/skill-learning/store.js +245 -0
  117. package/dist/services/skill-learning/turn-output.d.ts +34 -0
  118. package/dist/services/skill-learning/turn-output.js +98 -0
  119. package/dist/services/skill-learning/turn-summaries.d.ts +24 -0
  120. package/dist/services/skill-learning/turn-summaries.js +64 -0
  121. package/dist/services/skill-learning/web-review.d.ts +18 -0
  122. package/dist/services/skill-learning/web-review.js +44 -0
  123. package/dist/services/skill-routes.d.ts +20 -0
  124. package/dist/services/skill-routes.js +249 -0
  125. package/dist/services/turso/connection-manager.js +11 -7
  126. package/dist/services/turso/table-lease.d.ts +40 -0
  127. package/dist/services/turso/table-lease.js +77 -0
  128. package/dist/services/turso/turso-db.d.ts +4 -0
  129. package/dist/services/turso/turso-db.js +13 -0
  130. package/dist/services/user-prompt/profile-catch-up-lease.d.ts +3 -22
  131. package/dist/services/user-prompt/profile-catch-up-lease.js +9 -65
  132. package/dist/services/web-jobs.d.ts +7 -0
  133. package/dist/services/web-jobs.js +14 -0
  134. package/dist/services/web-route-modules.d.ts +2 -2
  135. package/dist/services/web-route-modules.js +6 -1
  136. package/dist/services/web-server.js +13 -21
  137. package/dist/v2/adapter.js +13 -8
  138. package/dist/web/assets/index-9hu4Z7SV.css +2 -0
  139. package/dist/web/assets/index-BMt9gPNK.js +129 -0
  140. package/dist/web/index.html +2 -2
  141. package/package.json +1 -1
  142. package/dist/web/assets/index-CHBKLTiT.js +0 -126
  143. package/dist/web/assets/index-DsQXsY3T.css +0 -2
@@ -86,8 +86,13 @@ export async function matchImportSessions(identity, options, snapshotMode) {
86
86
  const { runPathMaps } = await import("./import-path-maps.js");
87
87
  const merged = { ...options, pathMaps: await runPathMaps(options.pathMaps, options.cwd) };
88
88
  const rows = await readRows(identity, merged, snapshotMode);
89
- const unresolved = rows.filter((row) => row.directory === null);
90
- const matching = rows
89
+ const missing = rows.filter((row) => row.directory === null);
90
+ // `via` stays "unresolved", so a kept row still shows its folder is gone.
91
+ const usable = options.skillTurnsOnly
92
+ ? rows.map((row) => (row.directory ? row : { ...row, directory: row.recordedDirectory }))
93
+ : rows;
94
+ const unresolved = usable.filter((row) => row.directory === null);
95
+ const matching = usable
91
96
  .filter((row) => row.directory !== null)
92
97
  .filter((row) => projectTag === null || projectFilterTag(row.directory) === projectTag)
93
98
  .sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0) || a.key.localeCompare(b.key));
@@ -95,6 +100,7 @@ export async function matchImportSessions(identity, options, snapshotMode) {
95
100
  identity,
96
101
  matching,
97
102
  unresolved,
103
+ missing,
98
104
  revision: revisionOf(identity, merged, projectTag, matching.map((row) => row.key)),
99
105
  };
100
106
  }
@@ -138,7 +144,13 @@ export function validateSessionListRequest(value) {
138
144
  throw new Error(`Invalid ${key}`);
139
145
  }
140
146
  }
141
- for (const key of ["skipMemories", "skipProfile", "force", "includeHandled"]) {
147
+ for (const key of [
148
+ "skipMemories",
149
+ "skipProfile",
150
+ "force",
151
+ "includeHandled",
152
+ "skillTurnsOnly",
153
+ ]) {
142
154
  if (input[key] !== undefined && typeof input[key] !== "boolean") {
143
155
  throw new Error(`Invalid ${key}`);
144
156
  }
@@ -157,6 +169,7 @@ export function validateSessionListRequest(value) {
157
169
  pathMaps: maps,
158
170
  outputs: { memories: input.skipMemories !== true, profile: input.skipProfile !== true },
159
171
  ...(force ? { force } : {}),
172
+ ...(input.skillTurnsOnly === true ? { skillTurnsOnly: true } : {}),
160
173
  },
161
174
  };
162
175
  }
@@ -174,7 +187,7 @@ export async function listImportSessions(request, options) {
174
187
  const matched = await matchImportSessions(identity, options, request.refresh ? "fresh" : "any");
175
188
  // Feed the Directory maps list; a failure here must not break the listing.
176
189
  const { recordUnresolvedDirectories, summarizeUnresolvedDirectories } = await import("../services/backfill-state.js");
177
- await recordUnresolvedDirectories(options.host, summarizeUnresolvedDirectories(matched.unresolved.map((row) => ({ directory: row.recordedDirectory })))).catch(() => { });
190
+ await recordUnresolvedDirectories(options.host, summarizeUnresolvedDirectories(matched.missing.map((row) => ({ directory: row.recordedDirectory })))).catch(() => { });
178
191
  const handled = await handledKeys(matched.matching, options.outputs ?? ALL_OUTPUTS);
179
192
  const listed = [
180
193
  ...matched.matching.map((row) => ({ ...row, selectable: true, handled: handled.has(row.key) })),
@@ -0,0 +1,49 @@
1
+ import type { CaptureWorkUnit } from "../core/capture.js";
2
+ import type { MemoryHost } from "../types/index.js";
3
+ import type { ImportWindow } from "./importer.js";
4
+ /**
5
+ * The outputs of one imported turn (design decision 11). Skill turns run
6
+ * first for every candidate, before the memory ledger is read, so turns the
7
+ * ledger or live capture already handled still reach `skills.db`.
8
+ */
9
+ /** Skill-turn counts on an import report. Present only when the run records skill turns. */
10
+ export interface ImportSkillTurnCounts {
11
+ /** Real run: turns written to `skills.db`. */
12
+ skillTurnsRecorded?: number;
13
+ /** Real run: turns `skills.db` already held. */
14
+ skillTurnsAlreadyPresent?: number;
15
+ /** Dry run: turns a real run would write. */
16
+ skillTurnsWouldRecord?: number;
17
+ /** Dry run: turns `skills.db` already holds. */
18
+ skillTurnsAlreadyRecorded?: number;
19
+ }
20
+ /** True when an import with these options records skill turns: `skillLearning` on and no `--skip-skills`. */
21
+ export declare function importSkillTurnsOn(options: {
22
+ skipSkills?: boolean;
23
+ }): boolean;
24
+ /**
25
+ * True when a run writes skill turns and nothing else (`profile`: the run also
26
+ * learns the profile). Such a run also reads sessions whose project folder is
27
+ * gone; memory and profile runs still skip them.
28
+ */
29
+ export declare function skillTurnsOnly(options: {
30
+ skipMemories?: boolean;
31
+ skipSkills?: boolean;
32
+ }, profile: boolean): boolean;
33
+ /** The capture work unit for one imported window, shared by the memory and skill outputs. */
34
+ export declare function importWorkUnit(host: MemoryHost, turn: {
35
+ sessionId: string;
36
+ directory: string;
37
+ sourceFile: string;
38
+ window: ImportWindow;
39
+ }, importId: string): CaptureWorkUnit;
40
+ /**
41
+ * The skill output of one run, or undefined when the run records no skill
42
+ * turns. A real run records through `handleFinishedTurn`; a dry run only
43
+ * checks `turn_key` and never creates `skills.db`. Each project folder's
44
+ * settings are read once, with its project override applied.
45
+ */
46
+ export declare function importSkillTurnOutput(options: {
47
+ skipSkills?: boolean;
48
+ dryRun?: boolean;
49
+ }, report: ImportSkillTurnCounts): ((unit: CaptureWorkUnit) => Promise<void>) | undefined;
@@ -0,0 +1,88 @@
1
+ import { CONFIG } from "../config.js";
2
+ import { handleFinishedTurn } from "../core/turn-outputs.js";
3
+ import { skillTurnSettingsFor } from "../services/skill-learning/settings-for.js";
4
+ import { peekSkillTurn, recordSkillTurn, } from "../services/skill-learning/turn-output.js";
5
+ /** True when an import with these options records skill turns: `skillLearning` on and no `--skip-skills`. */
6
+ export function importSkillTurnsOn(options) {
7
+ return options.skipSkills !== true && CONFIG.skillLearning === true;
8
+ }
9
+ /**
10
+ * True when a run writes skill turns and nothing else (`profile`: the run also
11
+ * learns the profile). Such a run also reads sessions whose project folder is
12
+ * gone; memory and profile runs still skip them.
13
+ */
14
+ export function skillTurnsOnly(options, profile) {
15
+ return options.skipMemories === true && !profile && importSkillTurnsOn(options);
16
+ }
17
+ /** The capture work unit for one imported window, shared by the memory and skill outputs. */
18
+ export function importWorkUnit(host, turn, importId) {
19
+ const { window } = turn;
20
+ return {
21
+ host,
22
+ hostSessionId: turn.sessionId,
23
+ sourceType: "history-import",
24
+ projectDirectory: turn.directory,
25
+ userPrompt: window.userPrompt,
26
+ promptId: window.userEntryId,
27
+ textResponses: window.textResponses,
28
+ toolCalls: window.toolCalls,
29
+ sourceEntryIds: window.sourceEntryIds,
30
+ ...(window.userTimestamp !== undefined
31
+ ? { sourceTimestamp: window.userTimestamp }
32
+ : window.sourceTimestamp !== undefined
33
+ ? { sourceTimestamp: window.sourceTimestamp }
34
+ : {}),
35
+ sourceFile: turn.sourceFile,
36
+ importId,
37
+ };
38
+ }
39
+ /**
40
+ * The skill output of one run, or undefined when the run records no skill
41
+ * turns. A real run records through `handleFinishedTurn`; a dry run only
42
+ * checks `turn_key` and never creates `skills.db`. Each project folder's
43
+ * settings are read once, with its project override applied.
44
+ */
45
+ export function importSkillTurnOutput(options, report) {
46
+ if (!importSkillTurnsOn(options))
47
+ return undefined;
48
+ const dryRun = Boolean(options.dryRun);
49
+ if (dryRun) {
50
+ report.skillTurnsWouldRecord ??= 0;
51
+ report.skillTurnsAlreadyRecorded ??= 0;
52
+ }
53
+ else {
54
+ report.skillTurnsRecorded ??= 0;
55
+ report.skillTurnsAlreadyPresent ??= 0;
56
+ }
57
+ const count = (status) => {
58
+ if (status === "new") {
59
+ if (dryRun)
60
+ report.skillTurnsWouldRecord++;
61
+ else
62
+ report.skillTurnsRecorded++;
63
+ }
64
+ else if (status === "duplicate") {
65
+ if (dryRun)
66
+ report.skillTurnsAlreadyRecorded++;
67
+ else
68
+ report.skillTurnsAlreadyPresent++;
69
+ }
70
+ };
71
+ const settings = new Map();
72
+ const settingsFor = (directory) => {
73
+ let found = settings.get(directory);
74
+ if (!found) {
75
+ found = skillTurnSettingsFor(directory);
76
+ settings.set(directory, found);
77
+ }
78
+ return found;
79
+ };
80
+ const output = dryRun ? peekSkillTurn : recordSkillTurn;
81
+ return async (unit) => {
82
+ const current = settingsFor(unit.projectDirectory);
83
+ if (!current.enabled)
84
+ return;
85
+ // handleFinishedTurn logs a failure as skill-record-failed, so one bad turn never stops the run.
86
+ await handleFinishedTurn(unit, { memories: false, skills: true }, { recordSkillTurn: async (turn) => count(await output(turn, current)) });
87
+ };
88
+ }
@@ -1,6 +1,7 @@
1
1
  import type { CaptureConversation, CaptureSummaryProvider } from "../core/host.js";
2
2
  import type { ModelPort } from "../core/profile-analysis.js";
3
3
  import type { ProfileImportReport } from "./profile-import.js";
4
+ import { type ImportSkillTurnCounts } from "./import-turn-outputs.js";
4
5
  import { PiImportLedger } from "./ledger.js";
5
6
  import type { LoadedPiSession } from "./session-loader.js";
6
7
  import type { MemoryHost } from "../types/index.js";
@@ -27,6 +28,8 @@ export interface ImportFilters {
27
28
  dryRun?: boolean;
28
29
  /** Record profile prompts only; leave memory work units untouched. */
29
30
  skipMemories?: boolean;
31
+ /** Record no skill turns (design decision 11). */
32
+ skipSkills?: boolean;
30
33
  pathMaps?: ImportPathMap[];
31
34
  root?: string;
32
35
  /**
@@ -54,7 +57,7 @@ export interface ImportProjectReport {
54
57
  sessions: number;
55
58
  units: number;
56
59
  }
57
- export interface ImportReport {
60
+ export interface ImportReport extends ImportSkillTurnCounts {
58
61
  dryRun: boolean;
59
62
  root: string;
60
63
  sessionsDiscovered: number;
@@ -140,5 +143,5 @@ export interface LazyImportSource {
140
143
  open: () => AsyncIterable<ImportSourceSession>;
141
144
  }
142
145
  /** Process host-labelled work units through the shared capture and ledger pipeline. */
143
- export declare function importHistorySource(source: AsyncIterable<ImportSourceSession> | Iterable<ImportSourceSession> | LazyImportSource, host: MemoryHost, deps: Pick<ImporterDeps, "provider" | "ledger" | "onProgress" | "signal">, filters: Pick<ImportFilters, "dryRun" | "force" | "since" | "until">, report: ImportReport): Promise<void>;
146
+ export declare function importHistorySource(source: AsyncIterable<ImportSourceSession> | Iterable<ImportSourceSession> | LazyImportSource, host: MemoryHost, deps: Pick<ImporterDeps, "provider" | "ledger" | "onProgress" | "signal">, filters: Pick<ImportFilters, "dryRun" | "force" | "since" | "until" | "skipMemories" | "skipSkills">, report: ImportReport): Promise<void>;
144
147
  export declare function importPiHistory(deps: ImporterDeps, filters: ImportFilters): Promise<ImportReport>;
@@ -1,5 +1,5 @@
1
1
  import { existsSync, realpathSync } from "node:fs";
2
- import { captureConversation } from "../core/capture.js";
2
+ import { handleFinishedTurn } from "../core/turn-outputs.js";
3
3
  import { memoryClient } from "../services/client.js";
4
4
  import { findLiveCapturedEntryIds } from "../services/live-captured-entries.js";
5
5
  import { extractScopeFromContainerTag } from "../services/memory-scope.js";
@@ -8,7 +8,8 @@ import { tursoConnectionManager } from "../services/turso/connection-manager.js"
8
8
  import { tursoShardManager } from "../services/turso/shard-manager.js";
9
9
  import { extractPiConversationWindows, } from "./pi-conversation.js";
10
10
  import { discoverPiSessions } from "./discovery.js";
11
- import { resolveImportProject } from "./import-project.js";
11
+ import { importDirectory } from "./import-project.js";
12
+ import { importSkillTurnOutput, importSkillTurnsOn, importWorkUnit, skillTurnsOnly, } from "./import-turn-outputs.js";
12
13
  import { PiImportLedger, importLedgerDbPath } from "./ledger.js";
13
14
  export function buildImportKey(sessionId, window, host = "pi") {
14
15
  const terminalEntryId = window.sourceEntryIds?.[window.sourceEntryIds.length - 1];
@@ -106,13 +107,14 @@ async function lookupExistingMemory(projectHash, key, options) {
106
107
  /** Process host-labelled work units through the shared capture and ledger pipeline. */
107
108
  export async function importHistorySource(source, host, deps, filters, report) {
108
109
  const dryRun = Boolean(filters.dryRun);
110
+ const memories = !filters.skipMemories;
111
+ const recordSkills = importSkillTurnOutput(filters, report);
109
112
  const ledger = deps.ledger ?? new PiImportLedger();
110
113
  const ledgerFileExists = existsSync(importLedgerDbPath());
111
114
  const ledgerAccessible = !dryRun || ledgerFileExists;
112
- // A dry run only reads the ledger (peek), so it never writes its schema.
113
- if (!dryRun)
115
+ if (!dryRun && memories) {
116
+ // A dry run only reads the ledger (peek), so it never writes its schema.
114
117
  await ledger.get("__warmup__");
115
- if (!dryRun) {
116
118
  const embeddingInitError = memoryClient.getEmbeddingInitError?.();
117
119
  if (embeddingInitError) {
118
120
  throw new Error(`Memory system is not ready for import: ${embeddingInitError}. Start the extension and retry.`);
@@ -146,7 +148,7 @@ export async function importHistorySource(source, host, deps, filters, report) {
146
148
  for await (const candidate of candidates) {
147
149
  if (deps.signal?.aborted)
148
150
  break;
149
- const { sessionId, directory, sourceFile, hash, window } = candidate;
151
+ const { sessionId, sourceFile, hash, window } = candidate;
150
152
  const key = buildImportKey(sessionId, window, host);
151
153
  processed++;
152
154
  deps.onProgress?.(processed, report.unitsTotal, preview(window.userPrompt), report.unitsAlreadyHandled);
@@ -157,6 +159,11 @@ export async function importHistorySource(source, host, deps, filters, report) {
157
159
  promptPreview: preview(window.userPrompt),
158
160
  status: "would-import",
159
161
  };
162
+ const workUnit = importWorkUnit(host, candidate, key);
163
+ await recordSkills?.(workUnit);
164
+ // A skills-only unit leaves the memory ledger untouched.
165
+ if (!memories)
166
+ continue;
160
167
  const existing = !ledgerAccessible
161
168
  ? null
162
169
  : dryRun
@@ -203,31 +210,9 @@ export async function importHistorySource(source, host, deps, filters, report) {
203
210
  report.units.push(unit);
204
211
  continue;
205
212
  }
206
- await ledger.begin({
207
- key,
208
- sessionId: sessionId,
209
- sourceFile: sourceFile,
210
- projectHash: hash,
211
- });
213
+ await ledger.begin({ key, sessionId, sourceFile, projectHash: hash });
212
214
  try {
213
- const result = await captureConversation({
214
- host,
215
- hostSessionId: sessionId,
216
- sourceType: "history-import",
217
- projectDirectory: directory,
218
- userPrompt: window.userPrompt,
219
- promptId: window.userEntryId,
220
- textResponses: window.textResponses,
221
- toolCalls: window.toolCalls,
222
- sourceEntryIds: window.sourceEntryIds,
223
- ...(window.userTimestamp !== undefined
224
- ? { sourceTimestamp: window.userTimestamp }
225
- : window.sourceTimestamp !== undefined
226
- ? { sourceTimestamp: window.sourceTimestamp }
227
- : {}),
228
- sourceFile: sourceFile,
229
- importId: key,
230
- }, deps.provider);
215
+ const result = await handleFinishedTurn(workUnit, { memories: true, skills: false }, { provider: deps.provider });
231
216
  if (result.status === "skipped") {
232
217
  await ledger.skip(key, `extractor-skip:${result.type ?? "unknown"}`);
233
218
  unit.status = "skipped";
@@ -282,6 +267,7 @@ export async function importPiHistory(deps, filters) {
282
267
  const selected = filters.selectionKeys ? new Set(filters.selectionKeys) : null;
283
268
  const projectAggregates = new Map();
284
269
  const candidates = [];
270
+ const keepMissing = skillTurnsOnly(filters, Boolean(deps.profile));
285
271
  for (const discovered of discovery.sessions) {
286
272
  if (deps.signal?.aborted)
287
273
  break;
@@ -316,7 +302,7 @@ export async function importPiHistory(deps, filters) {
316
302
  });
317
303
  continue;
318
304
  }
319
- const directory = resolveImportProject(session.cwd, pathMaps).directory;
305
+ const directory = importDirectory(session.cwd, pathMaps, keepMissing);
320
306
  if (!directory) {
321
307
  report.unresolvableSessions.push({ file: session.sourceFile, cwd: session.cwd });
322
308
  continue;
@@ -369,7 +355,7 @@ export async function importPiHistory(deps, filters) {
369
355
  }
370
356
  }
371
357
  const sourceSessions = [...grouped.values()];
372
- if (filters.skipMemories) {
358
+ if (filters.skipMemories && !importSkillTurnsOn(filters)) {
373
359
  report.unitsTotal = candidates.length;
374
360
  }
375
361
  else {
@@ -23,6 +23,8 @@ export interface OpencodeImportOptions {
23
23
  force?: boolean;
24
24
  skipMemories?: boolean;
25
25
  skipProfile?: boolean;
26
+ /** Record no skill turns. */
27
+ skipSkills?: boolean;
26
28
  profileBatch?: number;
27
29
  provider?: CaptureSummaryProvider;
28
30
  profileModel?: ModelPort;
@@ -1,7 +1,8 @@
1
1
  import { getTags } from "../services/tags.js";
2
2
  import { extractScopeFromContainerTag } from "../services/memory-scope.js";
3
3
  import { countImportableUnits, importHistorySource, projectFilterTag, selectImportWindows, } from "./importer.js";
4
- import { resolveImportProject } from "./import-project.js";
4
+ import { importDirectory } from "./import-project.js";
5
+ import { importSkillTurnsOn, skillTurnsOnly } from "./import-turn-outputs.js";
5
6
  import { DEFAULT_OPENCODE_DB, openOpencodeHistory, } from "./opencode-reader.js";
6
7
  import { importProfileFromHistory } from "./profile-import.js";
7
8
  export { DEFAULT_OPENCODE_DB };
@@ -32,8 +33,9 @@ export async function importOpencodeHistory(options) {
32
33
  const projects = new Map();
33
34
  const selected = [];
34
35
  let filtered = 0;
36
+ const keepMissing = skillTurnsOnly(options, options.skipProfile !== true);
35
37
  for (const session of history.sessions) {
36
- const { directory } = resolveImportProject(session.recordedDirectory, options.pathMaps ?? [], session.projectWorktree);
38
+ const directory = importDirectory(session.recordedDirectory, options.pathMaps ?? [], keepMissing, session.projectWorktree);
37
39
  if (!directory) {
38
40
  const entry = unresolved.get(session.recordedDirectory) ?? {
39
41
  directory: session.recordedDirectory,
@@ -120,7 +122,8 @@ export async function importOpencodeHistory(options) {
120
122
  childSessionsFolded: history.childSessions,
121
123
  unresolvedProjects: [...unresolved.values()],
122
124
  };
123
- if (!options.skipMemories) {
125
+ // The memory path or skill turns read every turn; profile-only runs only count them.
126
+ if (!options.skipMemories || importSkillTurnsOn(options)) {
124
127
  await importHistorySource({ total, open }, "opencode", {
125
128
  provider: options.provider ?? {
126
129
  summarize: async () => {
@@ -3,6 +3,8 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { DatabaseSync } from "node:sqlite";
6
+ import { boundToolInput, readToolPath } from "../core/tool-input.js";
7
+ import { opencodeToolOutcome } from "../core/tool-outcome.js";
6
8
  import { INTERNAL_CAPTURE_SESSION_TITLES } from "./opencode-internal-sessions.js";
7
9
  import { acquirePrivateSnapshot, opencodeSnapshots, OpencodeSnapshotError, } from "./opencode-snapshot.js";
8
10
  export const DEFAULT_OPENCODE_DB = join(homedir(), ".local/share/opencode/opencode.db");
@@ -37,9 +39,8 @@ function parseData(json) {
37
39
  return {};
38
40
  }
39
41
  }
40
- function boundInput(value) {
41
- const text = typeof value === "string" ? value : JSON.stringify(value ?? "");
42
- return text.length > 100 ? `${text.slice(0, 100)}...` : text;
42
+ function formatInput(value) {
43
+ return typeof value === "string" ? value : JSON.stringify(value ?? "");
43
44
  }
44
45
  function buildWindows(db, sessionId, filters) {
45
46
  const messages = db
@@ -98,9 +99,14 @@ function buildWindows(db, sessionId, filters) {
98
99
  const state = part.state && typeof part.state === "object"
99
100
  ? part.state
100
101
  : {};
102
+ const name = String(part.tool ?? part.name ?? part.type);
103
+ const args = state.input ?? part.input ?? "";
104
+ const path = readToolPath(name, args);
101
105
  current.toolCalls.push({
102
- name: String(part.tool ?? part.name ?? part.type),
103
- input: boundInput(state.input ?? part.input ?? ""),
106
+ name,
107
+ input: boundToolInput(formatInput(args)),
108
+ ...opencodeToolOutcome(state),
109
+ ...(path === undefined ? {} : { path }),
104
110
  });
105
111
  }
106
112
  }
@@ -11,10 +11,15 @@ export interface PiSessionEntry {
11
11
  message?: {
12
12
  role?: string;
13
13
  content?: string | PiContentBlock[];
14
+ /** Set on `toolResult` messages: the `id` of the matching `toolCall` block. */
15
+ toolCallId?: string;
16
+ /** Set on `toolResult` messages. */
17
+ isError?: boolean;
14
18
  };
15
19
  }
16
20
  export interface PiContentBlock {
17
21
  type: string;
22
+ id?: string;
18
23
  text?: string;
19
24
  thinking?: string;
20
25
  name?: string;
@@ -1,4 +1,5 @@
1
- const MAX_TOOL_INPUT_LENGTH = 100;
1
+ import { boundToolInput, readToolPath } from "../core/tool-input.js";
2
+ import { errorOutcome } from "../core/tool-outcome.js";
2
3
  function entryText(content) {
3
4
  if (typeof content === "string")
4
5
  return content;
@@ -16,6 +17,24 @@ function isUserEntry(entry) {
16
17
  function isAssistantEntry(entry) {
17
18
  return entry.type === "message" && entry.message?.role === "assistant";
18
19
  }
20
+ function isToolResultEntry(entry) {
21
+ return entry.type === "message" && entry.message?.role === "toolResult";
22
+ }
23
+ /**
24
+ * Set the outcome of the call a `toolResult` entry answers. Only the error
25
+ * text of a failed call is kept; a successful call's output is never stored.
26
+ */
27
+ function applyToolResult(entry, callsById) {
28
+ const call = callsById.get(entry.message?.toolCallId ?? "");
29
+ if (!call)
30
+ return;
31
+ if (entry.message?.isError === true) {
32
+ Object.assign(call, errorOutcome(entryText(entry.message.content)));
33
+ }
34
+ else {
35
+ call.outcome = "ok";
36
+ }
37
+ }
19
38
  function formatToolInput(args) {
20
39
  if (!args)
21
40
  return "";
@@ -23,12 +42,6 @@ function formatToolInput(args) {
23
42
  .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
24
43
  .join(", ");
25
44
  }
26
- function boundToolInput(input) {
27
- if (input.length > MAX_TOOL_INPUT_LENGTH) {
28
- return input.substring(0, MAX_TOOL_INPUT_LENGTH) + "...";
29
- }
30
- return input;
31
- }
32
45
  /**
33
46
  * Build the capture window for the user entry at `userIndex`: only that
34
47
  * prompt's assistant/tool work, stopping at the next user message. Hidden
@@ -46,10 +59,13 @@ function buildWindow(entries, userIndex) {
46
59
  const textResponses = [];
47
60
  const toolCalls = [];
48
61
  const sourceEntryIds = [];
62
+ const callsById = new Map();
49
63
  let sourceTimestamp;
50
64
  for (const entry of entries.slice(userIndex + 1)) {
51
65
  if (isUserEntry(entry))
52
66
  break;
67
+ if (isToolResultEntry(entry))
68
+ applyToolResult(entry, callsById);
53
69
  if (!isAssistantEntry(entry))
54
70
  continue;
55
71
  if (typeof entry.id === "string")
@@ -69,10 +85,18 @@ function buildWindow(entries, userIndex) {
69
85
  }
70
86
  for (const block of blocks.filter((item) => item.type === "toolCall")) {
71
87
  const name = typeof block.name === "string" ? block.name : "unknown";
72
- toolCalls.push({
88
+ // A call stays `unknown` until a `toolResult` entry answers it.
89
+ const call = {
73
90
  name,
74
91
  input: boundToolInput(formatToolInput(block.arguments)),
75
- });
92
+ outcome: "unknown",
93
+ };
94
+ const path = readToolPath(name, block.arguments);
95
+ if (path !== undefined)
96
+ call.path = path;
97
+ toolCalls.push(call);
98
+ if (typeof block.id === "string")
99
+ callsById.set(block.id, call);
76
100
  }
77
101
  }
78
102
  if (textResponses.length === 0 && toolCalls.length === 0)
@@ -14,7 +14,7 @@ export interface CatchUpState {
14
14
  * newest run owns the record, and waits for the older run's current batch.
15
15
  */
16
16
  export declare function catchUpLeaseHooks(signal?: AbortSignal, lease?: CatchUpLease): Promise<{
17
- beforeBatch: () => Promise<import("../services/user-prompt/profile-catch-up-lease.js").BatchStart>;
17
+ beforeBatch: () => Promise<import("../services/turso/table-lease.js").BatchStart>;
18
18
  afterBatch: () => Promise<void>;
19
19
  release: () => Promise<void>;
20
20
  }>;
@@ -0,0 +1,28 @@
1
+ import type { ImportHost } from "./import-args.js";
2
+ import type { ImportReadiness } from "./import-readiness.js";
3
+ import { type GroupImportRequest, type SettingsImportJobs } from "./web-import-jobs.js";
4
+ /** Hosts whose default history exists on this computer and whose reader loads. */
5
+ export declare function setUpHosts(readiness: ImportReadiness): ImportHost[];
6
+ /**
7
+ * The grouped request for every set-up host. Each host lists its default
8
+ * source now, so the selection is every session up to this moment, including
9
+ * sessions the memory ledger marks handled.
10
+ */
11
+ export declare function recordPastTurnsRequest(cwd: string, readiness: ImportReadiness, options?: {
12
+ dryRun?: boolean;
13
+ }): Promise<GroupImportRequest>;
14
+ /** Start **Record past turns** in a job slot: the web app's own, or a new one in the terminal. */
15
+ export declare function startRecordPastTurns(jobs: SettingsImportJobs, cwd: string, readiness: ImportReadiness, options?: {
16
+ dryRun?: boolean;
17
+ }): Promise<{
18
+ id: string;
19
+ host: ImportHost;
20
+ dryRun: boolean;
21
+ state: "failed" | "done" | "running" | "cancelling" | "cancelled";
22
+ sessions: number;
23
+ processed: number;
24
+ total: number;
25
+ report?: string;
26
+ summary?: ReturnType<typeof import("./run-import.js").summarizeHistoryImportReport>;
27
+ error?: string;
28
+ } | import("./web-import-group.js").GroupImportJob>;
@@ -0,0 +1,63 @@
1
+ import { ImportJobError, } from "./web-import-jobs.js";
2
+ /**
3
+ * **Record past turns** (design decision 11): one skill-turns-only grouped
4
+ * import over every set-up host, all projects and every session, with no date
5
+ * limit. It runs through the grouped-import job, so the Memory page shows its
6
+ * progress, and it calls no model. Safe to run again: `turn_key` drops copies.
7
+ */
8
+ const HOSTS = ["pi", "opencode", "claude-code"];
9
+ /** Hosts whose default history exists on this computer and whose reader loads. */
10
+ export function setUpHosts(readiness) {
11
+ return HOSTS.filter((host) => readiness.defaultSourceFound[host] && (host !== "pi" || readiness.piReader.available));
12
+ }
13
+ /**
14
+ * The grouped request for every set-up host. Each host lists its default
15
+ * source now, so the selection is every session up to this moment, including
16
+ * sessions the memory ledger marks handled.
17
+ */
18
+ export async function recordPastTurnsRequest(cwd, readiness, options = {}) {
19
+ const { listImportSessions } = await import("./import-sessions.js");
20
+ const hosts = [];
21
+ for (const host of setUpHosts(readiness)) {
22
+ const page = await listImportSessions({ refresh: true, includeHandled: true, limit: 1 }, {
23
+ host,
24
+ scope: "all-projects",
25
+ pathMaps: [],
26
+ cwd,
27
+ outputs: { memories: false, profile: false },
28
+ // Matches the job's skills-only options, so sessions whose folder is gone are kept.
29
+ skillTurnsOnly: true,
30
+ });
31
+ const selection = {
32
+ mode: "all",
33
+ excludedKeys: [],
34
+ revision: page.revision,
35
+ listedAt: page.listedAt,
36
+ includeHandled: true,
37
+ };
38
+ hosts.push({
39
+ host,
40
+ source: page.source.sourceToken,
41
+ selection,
42
+ // Claude Code imports name the external API; this run never calls it.
43
+ ...(host === "claude-code" ? { modelChoice: "external" } : {}),
44
+ });
45
+ }
46
+ return {
47
+ hosts,
48
+ options: {
49
+ skipMemories: true,
50
+ skipProfile: true,
51
+ scope: "all-projects",
52
+ ...(options.dryRun ? { dryRun: true } : {}),
53
+ },
54
+ };
55
+ }
56
+ /** Start **Record past turns** in a job slot: the web app's own, or a new one in the terminal. */
57
+ export async function startRecordPastTurns(jobs, cwd, readiness, options = {}) {
58
+ const request = await recordPastTurnsRequest(cwd, readiness, options);
59
+ if (request.hosts.length === 0) {
60
+ throw new ImportJobError("No Pi, OpenCode or Claude Code history was found on this computer");
61
+ }
62
+ return jobs.start(request, cwd);
63
+ }
@@ -66,6 +66,10 @@ export declare function summarizeHistoryImportReport(report: HistoryImportReport
66
66
  unitsAlreadyHandled: number;
67
67
  unitsHeldBack: number;
68
68
  unitsUntimed: number;
69
+ skillTurnsRecorded: number;
70
+ skillTurnsAlreadyPresent: number;
71
+ skillTurnsWouldRecord: number;
72
+ skillTurnsAlreadyRecorded: number;
69
73
  projects: number;
70
74
  unresolved: number;
71
75
  loadErrors: number;