om-memory-system 4.14.0-next.106 → 4.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,191 +1,148 @@
1
+ <p align="center">
2
+ <img src=".github/screenshots/omms-logo.png" alt="OMMS logo" width="96" height="96" />
3
+ </p>
4
+
1
5
  # OMMS: Opinionated Modular Memory System
2
6
 
3
7
  [![npm version](https://img.shields.io/npm/v/om-memory-system.svg)](https://www.npmjs.com/package/om-memory-system)
4
8
  [![npm downloads](https://img.shields.io/npm/dm/om-memory-system.svg)](https://www.npmjs.com/package/om-memory-system)
5
- [![license](https://img.shields.io/npm/l/om-memory-system.svg)](https://www.npmjs.com/package/om-memory-system)
9
+ [![MIT licence](https://img.shields.io/npm/l/om-memory-system.svg)](LICENSE.md)
10
+
11
+ OMMS gives AI coding agents long-term project memory. It captures technical
12
+ work, recalls relevant notes in later sessions and learns your working
13
+ preferences. One shared engine serves **OpenCode**, **Pi** and **Claude Code**,
14
+ so each host can retrieve memories captured by the others in the same project.
6
15
 
7
- ![OMMS banner](.github/banner.png)
16
+ Memories are stored locally, by default in `~/.omms/data`. Embeddings run
17
+ locally by default; the first use downloads a search model. Capture and profile
18
+ learning make additional model calls that can send conversation content to your
19
+ chosen provider and incur costs. Remote embeddings are optional. OMMS removes
20
+ text inside `<private>` tags before storage. See [Configuration](docs/configuration.md)
21
+ for model choices and privacy controls.
8
22
 
9
- OMMS gives your AI coding agent a long-term memory. As you work, it writes
10
- short notes about what was done and decided in each project: fixes, design
11
- choices, things that did not work. It brings the relevant notes back in later
12
- sessions, so the agent does not start from zero every time. It also learns how
13
- you like to work and keeps that as a user profile.
23
+ ## Set up
14
24
 
15
- It runs inside [OpenCode](https://opencode.ai) and the
25
+ Use Node.js **22.14 or later** for the terminal command and Claude Code hooks.
26
+ OpenCode v1 needs **1.18.29 or later**. OMMS also supports OpenCode v2 and the
16
27
  [Pi coding agent](https://www.npmjs.com/package/@earendil-works/pi-coding-agent).
17
- Both share one memory per project, so a note written in one is available in
18
- the other. Everything is stored locally on your machine.
19
28
 
20
- Claude Code is also supported, through a plugin of hooks. It needs Node.js and
21
- your own external API for capture. A global install is optional. See the
22
- [Claude Code adapter](docs/claude-code-adapter.md) guide.
29
+ > Automatic history import is enabled by default and makes model calls.
30
+ > To disable it before your first session, set `"autoBackfill": false` in
31
+ > `~/.config/omms/omms.jsonc`. See [Automatic import](docs/web-ui-memory.md#automatic-import).
23
32
 
24
- ## What it does
33
+ ### OpenCode
25
34
 
26
- - **Remembers automatically.** After each piece of work, a background model
27
- call summarises it into a memory. You do not have to ask.
28
- - **Recalls when relevant.** Matching memories are added to the agent's
29
- context: on every prompt in OpenCode v2 and Pi, at the start of a session in
30
- OpenCode v1.
31
- - **Learns your preferences.** A user profile of your habits builds up over
32
- time and follows you across projects.
33
- - **Imports your past sessions.** Pi and OpenCode import older history after
34
- startup. A command gives you a preview and manual control.
35
- - **Lets you look and edit.** A local web page shows every memory and your
36
- profile.
37
- - **Keeps private things private.** Text inside `<private>` tags is never
38
- stored.
35
+ Add the package to `~/.config/opencode/opencode.json`. Keep your existing settings.
36
+ On Windows, use `%USERPROFILE%\.config\opencode\opencode.json`.
39
37
 
40
- ![Project memory timeline](.github/screenshot-project-memory.png)
38
+ **OpenCode v2:**
41
39
 
42
- ![User profile viewer](.github/screenshot-user-profile.png)
40
+ ```json
41
+ { "plugins": ["om-memory-system"] }
42
+ ```
43
43
 
44
- ## Before you start
44
+ **OpenCode v1:**
45
45
 
46
- You need one of:
46
+ ```json
47
+ { "plugin": ["om-memory-system"] }
48
+ ```
47
49
 
48
- - **OpenCode** 1.18.29 or later (v1 plugin API) or OpenCode v2
49
- - **Pi coding agent**
50
- - **Claude Code**, through a plugin of hooks. Capture needs your own external API. See
51
- the [Claude Code adapter](docs/claude-code-adapter.md) guide for setup.
50
+ OpenCode v2 also supports:
52
51
 
53
- Nothing else is required. OMMS brings its own database. On first use it
54
- downloads a small embedding model (the part that makes memories searchable),
55
- so you need internet access once. The terminal import command also needs
56
- Node.js 22.14 or later.
52
+ ```bash
53
+ opencode plugin add om-memory-system
54
+ ```
57
55
 
58
- ## Set up
56
+ Restart OpenCode. The [OpenCode guide](docs/opencode-adapter.md#installation)
57
+ covers plugin management and its additional `cli.json` configuration.
59
58
 
60
- ### 1. Install
59
+ ### Pi
60
+
61
+ ```bash
62
+ pi install npm:om-memory-system
63
+ ```
61
64
 
62
- **OpenCode.** Add `om-memory-system` to `~/.config/opencode/opencode.json`
63
- (on Windows, `%USERPROFILE%\.config\opencode\opencode.json`):
65
+ Restart Pi. See the [Pi guide](docs/pi-adapter.md).
64
66
 
65
- ```jsonc
66
- // OpenCode v2
67
- { "plugins": ["om-memory-system"] }
67
+ ### Claude Code
68
68
 
69
- // OpenCode v1
70
- { "plugin": ["om-memory-system"] }
71
- ```
69
+ Install the OMMS plugin using the [Claude Code guide](docs/claude-code-adapter.md#installation).
70
+ Capture and profile learning require a configured external API; retrieval and
71
+ manual memory operations remain available without it. A global npm install is optional.
72
72
 
73
- On OpenCode v2 you can run `opencode plugin add om-memory-system` instead.
74
- Restart OpenCode.
73
+ ### Model and web app
75
74
 
76
- **Pi.** Run:
75
+ With no model settings, OpenCode and Pi use the session model. You can choose a
76
+ host model or configure an external API under **Settings**. Claude Code uses
77
+ only the external API. See [Choosing the model](docs/configuration.md#choosing-the-model)
78
+ for the selection order and credentials.
77
79
 
78
- ```bash
79
- pi install npm:om-memory-system
80
- ```
80
+ Start a host session, then open [http://127.0.0.1:4747](http://127.0.0.1:4747).
81
+ Each host starts the shared web app when needed, unless it is disabled. Browse
82
+ captured notes on **Project memories**, or ask your agent to search memory for
83
+ an earlier decision.
81
84
 
82
- Restart Pi. You can install OMMS in both agents; they share the same memory.
85
+ ### Terminal command (optional)
83
86
 
84
- **Terminal command (optional).** A global install puts the `om-memory-system`
85
- command on your `PATH`. You do not need it. OMMS runs the newest copy it finds on
86
- the machine, so updating one host is enough. A global install from 4.3 or earlier
87
- needs one update. See
88
- [Updating and upgrading](docs/upgrading.md#one-update-updates-every-host).
89
- To install it:
87
+ A global install puts `om-memory-system` on your `PATH`:
90
88
 
91
89
  ```bash
92
- npm i -g om-memory-system # or: bun add -g om-memory-system
90
+ npm install -g om-memory-system
93
91
  om-memory-system --version
94
92
  ```
95
93
 
96
- ### 2. Choose which model writes memories (optional)
97
-
98
- With no settings, OMMS uses the model of the session you are working in. To
99
- use a different one, add it to `~/.config/omms/omms.jsonc`. If you have no
100
- config file yet, OMMS creates this one with comments on first start.
101
-
102
- ```jsonc
103
- {
104
- // A model you are already signed in to in OpenCode or Pi.
105
- // Use "inherit" to always follow the session's model.
106
- "opencodeProvider": "openai",
107
- "opencodeModel": "gpt-5.6-luna",
108
- "piProvider": "openai-codex",
109
- "piModel": "gpt-5.6-luna",
110
- }
111
- ```
94
+ See the [CLI reference](docs/cli.md) for manual memory operations and history imports.
112
95
 
113
- You can also use any OpenAI-compatible or Anthropic API with your own key, set
114
- up on the Settings page's **External API** card, and choose `"external"` as a
115
- host's model. See [Configuration](docs/configuration.md#choosing-the-model).
96
+ ## Inside OMMS
116
97
 
117
- ### 3. Check it works
98
+ These screenshots use synthetic examples and configuration. All visible prompts,
99
+ code snippets and outcomes are made up for the demonstration.
118
100
 
119
- Work normally for a few turns, then open `http://127.0.0.1:4747` in your
120
- browser. OpenCode or the login web app serves this page. New memories appear
121
- on the timeline. You can also ask the agent: "search memory for what we changed today".
101
+ ### Project memories
122
102
 
123
- ## Import your past history
103
+ Browse captured notes alongside example user input and technical outcomes.
124
104
 
125
- Older sessions import automatically after a host starts. This makes model
126
- calls. To turn it off, set `"autoBackfill": false` in
127
- `~/.config/omms/omms.jsonc` before you start the agent. For a manual import or
128
- a custom source, preview it inside the agent:
105
+ ![Project memories with synthetic English prompts, code and outcomes](.github/screenshots/project-memories.png)
129
106
 
130
- ```text
131
- /memory-import-opencode-history --dry-run
132
- /memory-import-pi-history --dry-run
133
- ```
107
+ ### Memory
108
+
109
+ Review history imports, manage profile learning and set memory limits. This page
110
+ also helps resolve missing project folders.
111
+
112
+ ![Memory page with synthetic import and profile examples](.github/screenshots/memory.png)
113
+
114
+ ### Settings
115
+
116
+ Choose models and manage access. Diagnostics show capture outcomes and component health.
134
117
 
135
- The preview shows how many model calls a real import needs. It uses the
136
- session's model; add `--model provider/id` for a cheaper one. There is also a
137
- terminal version, `npx om-memory-system`, that uses an API key. See the
138
- [OpenCode](docs/opencode-history-import.md) and [Pi](docs/pi-history-import.md)
139
- import guides, and the [CLI reference](docs/cli.md).
140
-
141
- ## Keeping OMMS up to date
142
-
143
- Pi shows a notice when a new version is out; update with
144
- `pi update npm:om-memory-system`. On OpenCode v2, the footer shows when a new version is out;
145
- run `opencode plugin update om-memory-system`. Restart the agent afterwards.
146
- [UPDATES.md](UPDATES.md) explains how updates work on every host.
147
- To stay on one version, install it with the number, for example
148
- `pi install npm:om-memory-system@3.1.1` or
149
- `opencode plugin add om-memory-system@3.1.1`; a pinned install is never
150
- updated. Coming from `opencode-mem`? Your memories move over automatically, with a
151
- backup first. See [Updating and upgrading](docs/upgrading.md) and
152
- [CHANGELOG.md](CHANGELOG.md).
118
+ ![Settings page with synthetic model configuration](.github/screenshots/settings.png)
153
119
 
154
120
  ## Documentation
155
121
 
156
- | Read this | To learn about |
157
- | ---------------------------------------------------------------- | ---------------------------------------------------------------- |
158
- | [Using memory day to day](docs/using-memory.md) | How capture and recall work, the `memory` tool, the user profile |
159
- | [Configuration](docs/configuration.md) | Settings, choosing the model, embeddings, troubleshooting |
160
- | [Web UI](docs/web-ui.md) | The memory explorer, opening it on a network safely |
161
- | [Settings page](docs/web-ui-settings.md) | Every card and control on the web Settings page |
162
- | [Moving projects](docs/moving-projects.md) | Nested repositories, moved folders, backup and restore |
163
- | [Updating and upgrading](docs/upgrading.md) | Updates, pinning a version, older stores |
164
- | [OpenCode adapter](docs/opencode-adapter.md) | How the OpenCode plugin hooks in |
165
- | [Pi adapter](docs/pi-adapter.md) | How the Pi extension hooks in |
166
- | [Claude Code adapter](docs/claude-code-adapter.md) | Installing the Claude Code plugin, hooks, the `memory` command |
167
- | [OpenCode history import](docs/opencode-history-import.md) | Importing past OpenCode sessions |
168
- | [Pi history import](docs/pi-history-import.md) | Importing past Pi sessions, moving machines |
169
- | [Claude Code history import](docs/claude-code-history-import.md) | Importing past Claude Code transcripts |
170
- | [CLI reference](docs/cli.md) | The `om-memory-system` terminal command |
171
- | [Migrating from opencode-mem](docs/omms-migration.md) | What changes when upgrading from the original plugin |
172
- | [For developers](docs/developers.md) | Building, testing, the public `tags` export, architecture |
173
- | [Contributing](CONTRIBUTING.md) | How to set up, test, and send a pull request |
174
-
175
- ## About this fork
176
-
177
- OMMS is [`cmdaltctr/omms`](https://github.com/cmdaltctr/omms), a fork of
178
- [`tickernelz/opencode-mem`](https://github.com/tickernelz/opencode-mem),
179
- published on npm as `om-memory-system`. The fork runs the memory engine as a
180
- shared core for both OpenCode and Pi (see [shared core](docs/shared-core.md)).
181
- Existing OpenCode memories and settings keep working.
182
-
183
- ## License and links
184
-
185
- MIT License. See [LICENSE.md](LICENSE.md).
186
-
187
- - Repository: https://github.com/cmdaltctr/omms
188
- - Upstream: https://github.com/tickernelz/opencode-mem
189
- - Issues: https://github.com/cmdaltctr/omms/issues
190
-
191
- Inspired by [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory).
122
+ | Guide | Covers |
123
+ | ------------------------------------------- | ------------------------------------------------------------ |
124
+ | [Using memory](docs/using-memory.md) | Daily use, capture, recall and the user profile |
125
+ | [Web UI](docs/web-ui.md) | Starting the app, browsing memories and safe network access |
126
+ | [Memory page](docs/web-ui-memory.md) | History imports, profile actions, limits and missing folders |
127
+ | [Settings page](docs/web-ui-settings.md) | Models, credentials, diagnostics and profile identities |
128
+ | [Configuration](docs/configuration.md) | Config files, model selection, privacy and embeddings |
129
+ | [Moving projects](docs/moving-projects.md) | Folder moves, backup and restore |
130
+ | [Updating and upgrading](docs/upgrading.md) | Updates, version pinning and older stores |
131
+ | [CLI reference](docs/cli.md) | Terminal commands |
132
+ | [For developers](docs/developers.md) | Building, testing and architecture |
133
+ | [Contributing](CONTRIBUTING.md) | Development workflow and pull requests |
134
+
135
+ History import guides: [OpenCode](docs/opencode-history-import.md),
136
+ [Pi](docs/pi-history-import.md) and [Claude Code](docs/claude-code-history-import.md).
137
+ See [UPDATES.md](UPDATES.md) for host update commands and
138
+ [CHANGELOG.md](CHANGELOG.md) for release changes. Existing `opencode-mem` users
139
+ can follow the [migration guide](docs/omms-migration.md).
140
+
141
+ ## Licence
142
+
143
+ MIT. See [LICENSE.md](LICENSE.md) for copyright and attribution, and
144
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for bundled dependencies.
145
+
146
+ [Repository](https://github.com/cmdaltctr/omms) ·
147
+ [Issues](https://github.com/cmdaltctr/omms/issues) ·
148
+ [Documentation index](docs/README.md)
@@ -4,4 +4,6 @@ import { type ClaudeStatusOptions } from "./status.js";
4
4
  * `om-memory-system claude-hook <event>`. Always returns 0: a hook must never
5
5
  * block or break a Claude Code session, whatever goes wrong.
6
6
  */
7
- export declare function runClaudeHookCommand(argv: string[], io?: ClaudeHookOptions & ClaudeStatusOptions): Promise<number>;
7
+ export declare function runClaudeHookCommand(argv: string[], io?: ClaudeHookOptions & ClaudeStatusOptions & {
8
+ skipSelfUpdate?: boolean;
9
+ }): Promise<number>;
@@ -9,6 +9,18 @@ async function printStatus(io) {
9
9
  const status = await buildClaudeStatus({ ...inputs, fetch: io.fetch ?? globalThis.fetch });
10
10
  (io.writeStdout ?? ((text) => process.stdout.write(text)))(`${JSON.stringify(status)}\n`);
11
11
  }
12
+ /** Update the Claude plugin in the background when npm has a newer release. Never throws. */
13
+ async function selfUpdate(io) {
14
+ try {
15
+ const { startPluginSelfUpdate, pluginSelfUpdateDeps } = await import("./plugin-self-update.js");
16
+ const log = io.log ??
17
+ (async (message, data) => (await import("../../services/logger.js")).log(message, data));
18
+ await startPluginSelfUpdate(pluginSelfUpdateDeps(log));
19
+ }
20
+ catch {
21
+ /* The memory hook already ran; a failed update check changes nothing. */
22
+ }
23
+ }
12
24
  /**
13
25
  * `om-memory-system claude-hook <event>`. Always returns 0: a hook must never
14
26
  * block or break a Claude Code session, whatever goes wrong.
@@ -22,6 +34,8 @@ export async function runClaudeHookCommand(argv, io = {}) {
22
34
  }
23
35
  if (isHookEvent(event)) {
24
36
  await runClaudeHook(event, io);
37
+ if (event === "session-start" && !io.skipSelfUpdate)
38
+ await selfUpdate(io);
25
39
  return 0;
26
40
  }
27
41
  // Only a known name reaches the log; argv is not trusted text.
@@ -0,0 +1,27 @@
1
+ /** A second session does not start another update for the same release within this time. */
2
+ export declare const SELF_UPDATE_RETRY_MS: number;
3
+ export type PluginSelfUpdateCode = "started" | "current" | "recent" | "disabled" | "no-plugin" | "unreadable-plugin" | "unreachable" | "spawn-failed";
4
+ export interface PluginSelfUpdateDeps {
5
+ /** False when `OMMS_DISABLE_UPDATE_CHECK` is `1`. */
6
+ enabled: boolean;
7
+ /** `CLAUDE_PLUGIN_ROOT`; undefined outside a Claude plugin. */
8
+ pluginRoot: string | undefined;
9
+ readPluginVersion: (root: string) => string;
10
+ fetch: typeof fetch;
11
+ now: () => number;
12
+ readMarker: () => {
13
+ version: string;
14
+ startedAt: number;
15
+ } | null;
16
+ writeMarker: (value: {
17
+ version: string;
18
+ startedAt: number;
19
+ }) => void;
20
+ /** Start a process that outlives this hook. */
21
+ spawnDetached: (command: string, args: string[]) => void;
22
+ log: (message: string, data: Record<string, unknown>) => void | Promise<void>;
23
+ }
24
+ /** Start a background plugin update when npm has a newer release. Never throws. */
25
+ export declare function startPluginSelfUpdate(deps: PluginSelfUpdateDeps): Promise<PluginSelfUpdateCode>;
26
+ /** Production inputs: the real plugin manifest, npm, and a marker file in ~/.omms. */
27
+ export declare function pluginSelfUpdateDeps(log: PluginSelfUpdateDeps["log"], env?: NodeJS.ProcessEnv): PluginSelfUpdateDeps;
@@ -0,0 +1,103 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { availableUpdate, latestNpmVersion } from "../../services/update-check.js";
6
+ // Claude Code installs the OMMS plugin from the `stable` branch, but its own
7
+ // update pass runs at its own time and can leave a new session on the old
8
+ // plugin. At session start OMMS compares the plugin with npm `latest` and, when
9
+ // it is older, runs Claude Code's own update commands in the background. The
10
+ // new copy loads in the next session or after /reload-plugins.
11
+ /** A second session does not start another update for the same release within this time. */
12
+ export const SELF_UPDATE_RETRY_MS = 30 * 60 * 1000;
13
+ /** Node script that refreshes the marketplace, then updates the plugin. Run detached. */
14
+ const UPDATE_SCRIPT = `
15
+ const { spawnSync } = require("node:child_process");
16
+ const shell = process.platform === "win32";
17
+ for (const args of [["plugin", "marketplace", "update", "omms"], ["plugin", "update", "omms@omms"]]) {
18
+ const run = spawnSync("claude", args, { stdio: "ignore", shell, timeout: 120000 });
19
+ if (run.status !== 0) process.exit(1);
20
+ }
21
+ `;
22
+ /** Start a background plugin update when npm has a newer release. Never throws. */
23
+ export async function startPluginSelfUpdate(deps) {
24
+ const code = await decide(deps);
25
+ if (code.code === "started" || code.code === "spawn-failed") {
26
+ await deps.log("Claude plugin self-update", {
27
+ code: code.code,
28
+ plugin: code.plugin,
29
+ latest: code.latest,
30
+ });
31
+ }
32
+ return code.code;
33
+ }
34
+ async function decide(deps) {
35
+ if (!deps.enabled)
36
+ return { code: "disabled" };
37
+ if (!deps.pluginRoot)
38
+ return { code: "no-plugin" };
39
+ let plugin;
40
+ try {
41
+ plugin = deps.readPluginVersion(deps.pluginRoot);
42
+ }
43
+ catch {
44
+ return { code: "unreadable-plugin" };
45
+ }
46
+ const latest = await latestNpmVersion(deps.fetch);
47
+ if (latest === null)
48
+ return { code: "unreachable" };
49
+ const newer = availableUpdate(plugin, latest);
50
+ if (!newer)
51
+ return { code: "current" };
52
+ const marker = deps.readMarker();
53
+ const now = deps.now();
54
+ if (marker?.version === newer && now - marker.startedAt <= SELF_UPDATE_RETRY_MS) {
55
+ return { code: "recent" };
56
+ }
57
+ // Write first, so a session that starts during the update does not start another.
58
+ deps.writeMarker({ version: newer, startedAt: now });
59
+ try {
60
+ deps.spawnDetached(process.execPath, ["-e", UPDATE_SCRIPT]);
61
+ }
62
+ catch {
63
+ return { code: "spawn-failed", plugin, latest: newer };
64
+ }
65
+ return { code: "started", plugin, latest: newer };
66
+ }
67
+ /** Production inputs: the real plugin manifest, npm, and a marker file in ~/.omms. */
68
+ export function pluginSelfUpdateDeps(log, env = process.env) {
69
+ const markerPath = join(homedir(), ".omms", "claude-plugin-update.json");
70
+ return {
71
+ enabled: env.OMMS_DISABLE_UPDATE_CHECK !== "1",
72
+ pluginRoot: env.CLAUDE_PLUGIN_ROOT || undefined,
73
+ readPluginVersion: (root) => {
74
+ const manifest = JSON.parse(readFileSync(join(root, ".claude-plugin", "plugin.json"), "utf8"));
75
+ if (typeof manifest.version !== "string")
76
+ throw new Error("no version");
77
+ return manifest.version;
78
+ },
79
+ fetch: globalThis.fetch,
80
+ now: Date.now,
81
+ readMarker: () => {
82
+ try {
83
+ const value = JSON.parse(readFileSync(markerPath, "utf8"));
84
+ return typeof value.version === "string" && typeof value.startedAt === "number"
85
+ ? { version: value.version, startedAt: value.startedAt }
86
+ : null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ },
92
+ writeMarker: (value) => {
93
+ mkdirSync(dirname(markerPath), { recursive: true });
94
+ writeFileSync(markerPath, JSON.stringify(value));
95
+ },
96
+ spawnDetached: (command, args) => {
97
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
98
+ child.on("error", () => { });
99
+ child.unref();
100
+ },
101
+ log,
102
+ };
103
+ }
@@ -58,6 +58,7 @@ export async function importClaudeHistory(deps, filters) {
58
58
  model: deps.profile.model,
59
59
  batchSize: deps.profile.batchSize,
60
60
  ...(deps.profile.onProgress ? { onProgress: deps.profile.onProgress } : {}),
61
+ ...(deps.profile.onEligiblePrompt ? { onEligiblePrompt: deps.profile.onEligiblePrompt } : {}),
61
62
  });
62
63
  }
63
64
  return report;
@@ -27,6 +27,9 @@ export interface ImportMatchOptions {
27
27
  project?: string;
28
28
  pathMaps: ImportPathMap[];
29
29
  cwd: string;
30
+ /** Grouped imports can retain a valid source with no matching sessions as no-work. */
31
+ allowEmpty?: boolean;
32
+ signal?: AbortSignal;
30
33
  }
31
34
  export declare class StaleSelectionError extends Error {
32
35
  readonly status = 409;
@@ -91,6 +94,8 @@ export interface ResolvedImportSelection {
91
94
  identity: ImportSourceIdentity;
92
95
  keys: string[];
93
96
  cutoff: number;
97
+ /** Source sessions still missing a project, excluding ignored directories; never import keys. */
98
+ unresolvedCount?: number;
94
99
  }
95
100
  /**
96
101
  * Re-resolve a selection against the source right before a job. New sessions
@@ -39,7 +39,10 @@ async function readRows(identity, options, snapshotMode) {
39
39
  }));
40
40
  }
41
41
  const { openOpencodeHistory } = await import("./opencode-reader.js");
42
- const history = await openOpencodeHistory(identity.realPath, {}, { shared: { key: importSourceKey(identity), mode: snapshotMode } });
42
+ const history = await openOpencodeHistory(identity.realPath, {}, {
43
+ shared: { key: importSourceKey(identity), mode: snapshotMode },
44
+ ...(options.signal ? { signal: options.signal } : {}),
45
+ });
43
46
  try {
44
47
  return history.sessions.map((session) => ({
45
48
  key: session.sessionId,
@@ -71,6 +74,7 @@ function revisionOf(identity, options, projectTag, keys) {
71
74
  .digest("hex");
72
75
  }
73
76
  export async function matchImportSessions(identity, options, snapshotMode) {
77
+ options.signal?.throwIfAborted();
74
78
  assertImportSourceUnchanged(identity);
75
79
  const { projectFilterTag } = await import("./importer.js");
76
80
  const projectTag = options.scope === "current-project"
@@ -207,6 +211,7 @@ export function validateImportSelection(value) {
207
211
  * "ids" selection stale. Neither ever imports a session the user did not see.
208
212
  */
209
213
  export async function resolveImportSelection(sourceToken, selection, options) {
214
+ options.signal?.throwIfAborted();
210
215
  const identity = readImportSourceToken(sourceToken, options.host);
211
216
  const matched = await matchImportSessions(identity, options, "reuse").catch((error) => {
212
217
  if (error.code === "expired")
@@ -228,7 +233,14 @@ export async function resolveImportSelection(sourceToken, selection, options) {
228
233
  }
229
234
  keys = selection.sessions.map((item) => item.key);
230
235
  }
231
- if (keys.length === 0)
236
+ if (keys.length === 0 && !options.allowEmpty)
232
237
  throw new Error("Choose sessions to import");
233
- return { identity, keys, cutoff: selection.listedAt };
238
+ const { CONFIG } = await import("../config.js");
239
+ const { visibleUnresolvedCount } = await import("../services/backfill-state.js");
240
+ return {
241
+ identity,
242
+ keys,
243
+ cutoff: selection.listedAt,
244
+ unresolvedCount: visibleUnresolvedCount(matched.unresolved.length, matched.unresolved.map((row) => ({ directory: row.recordedDirectory ?? "", sessions: 1 })), CONFIG.importIgnoredDirectories ?? []),
245
+ };
234
246
  }
@@ -95,6 +95,7 @@ export interface ImporterDeps {
95
95
  model?: ModelPort;
96
96
  batchSize?: number;
97
97
  onProgress?: (done: number, total: number) => void;
98
+ onEligiblePrompt?: (identity: string) => void;
98
99
  };
99
100
  }
100
101
  export interface ImportWindow extends CaptureConversation {
@@ -392,6 +392,7 @@ export async function importPiHistory(deps, filters) {
392
392
  model: deps.profile.model,
393
393
  batchSize: deps.profile.batchSize,
394
394
  ...(deps.profile.onProgress ? { onProgress: deps.profile.onProgress } : {}),
395
+ ...(deps.profile.onEligiblePrompt ? { onEligiblePrompt: deps.profile.onEligiblePrompt } : {}),
395
396
  });
396
397
  }
397
398
  return report;
@@ -28,6 +28,7 @@ export interface OpencodeImportOptions {
28
28
  profileModel?: ModelPort;
29
29
  /** Profile batches done and planned, during the profile step. */
30
30
  onProfileProgress?: (done: number, total: number) => void;
31
+ onProfilePrompt?: (identity: string) => void;
31
32
  /** `alreadyHandled`: units found already done so far, which need no model call. */
32
33
  onProgress?: (processed: number, total: number, promptPreview: string, alreadyHandled?: number) => void;
33
34
  signal?: AbortSignal;
@@ -143,6 +143,7 @@ export async function importOpencodeHistory(options) {
143
143
  batchSize: options.profileBatch,
144
144
  model: options.profileModel,
145
145
  ...(options.onProfileProgress ? { onProgress: options.onProfileProgress } : {}),
146
+ ...(options.onProfilePrompt ? { onEligiblePrompt: options.onProfilePrompt } : {}),
146
147
  });
147
148
  }
148
149
  return report;
@@ -28,6 +28,8 @@ export interface ProfileImportOptions {
28
28
  profileStore?: ProfileStore;
29
29
  /** Profile batches done and planned; called before the first batch and after each one. */
30
30
  onProgress?: (done: number, total: number) => void;
31
+ /** Dry-run estimate input only; no prompt content leaves this pipeline. */
32
+ onEligiblePrompt?: (identity: string) => void;
31
33
  }
32
34
  /** Record historical prompts and build or update the user profile in batches. */
33
35
  export declare function importProfileFromHistory(source: AsyncIterable<ImportSourceSession> | Iterable<ImportSourceSession>, options: ProfileImportOptions): Promise<ProfileImportReport>;
@@ -7,6 +7,8 @@ import { buildImportKey } from "./importer.js";
7
7
  import { existsSync } from "node:fs";
8
8
  import { ImportLedger, importLedgerDbPath } from "./ledger.js";
9
9
  import { drainProfileBacklog } from "./profile-backlog.js";
10
+ import { isTrivialPrompt } from "../core/trivial-prompt.js";
11
+ import { profilePromptIdentity } from "./web-import-profile-estimate.js";
10
12
  /** Record historical prompts and build or update the user profile in batches. */
11
13
  export async function importProfileFromHistory(source, options) {
12
14
  if (!options.dryRun && !options.model)
@@ -62,6 +64,8 @@ export async function importProfileFromHistory(source, options) {
62
64
  continue;
63
65
  }
64
66
  if (options.dryRun) {
67
+ if (!isTrivialPrompt(prompt))
68
+ options.onEligiblePrompt?.(profilePromptIdentity(session.sessionId, unit.userEntryId));
65
69
  report.promptsWouldRecord++;
66
70
  continue;
67
71
  }
@@ -18,6 +18,8 @@ export interface HistoryImportRun {
18
18
  signal?: AbortSignal;
19
19
  /** Profile batches done and planned, during the profile step after the exchanges. */
20
20
  onProfileProgress?: (done: number, total: number) => void;
21
+ /** Eligible dry-run profile prompt identities, kept out of public reports. */
22
+ onProfilePrompt?: (identity: string) => void;
21
23
  /** Record progress and take the host's lock for a run that calls models. */
22
24
  track?: {
23
25
  surface: ImportSurface;