@oh-my-pi/pi-coding-agent 15.5.10 → 15.5.12
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/CHANGELOG.md +34 -0
- package/dist/types/cli-commands.d.ts +19 -0
- package/dist/types/commands/install.d.ts +51 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +43 -0
- package/dist/types/discovery/omp-plugins.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +14 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/extensibility/plugins/loader.d.ts +12 -2
- package/dist/types/index.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/ultrathink.d.ts +10 -0
- package/dist/types/session/redis-session-storage.d.ts +124 -0
- package/dist/types/session/sql-session-storage.d.ts +141 -0
- package/dist/types/tools/todo-write.d.ts +30 -0
- package/examples/sdk/12-redis-sessions.ts +54 -0
- package/examples/sdk/13-sql-sessions.ts +61 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +14 -9
- package/src/cli-commands.ts +44 -0
- package/src/cli.ts +2 -32
- package/src/commands/install.ts +107 -0
- package/src/discovery/index.ts +1 -0
- package/src/discovery/omp-extension-roots.ts +190 -0
- package/src/discovery/omp-plugins.ts +383 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +15 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +63 -22
- package/src/extensibility/plugins/loader.ts +43 -18
- package/src/index.ts +3 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +12 -0
- package/src/memories/index.ts +8 -3
- package/src/modes/components/custom-editor.ts +3 -0
- package/src/modes/interactive-mode.ts +243 -12
- package/src/modes/ultrathink.ts +79 -0
- package/src/prompts/system/ultrathink-notice.md +3 -0
- package/src/session/agent-session.ts +28 -0
- package/src/session/redis-session-storage.ts +481 -0
- package/src/session/sql-session-storage.ts +565 -0
- package/src/tools/read.ts +23 -6
- package/src/tools/todo-write.ts +64 -0
- package/src/tools/write.ts +40 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.12] - 2026-05-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added the `omp-plugins` discovery provider, which scans every extension package directory configured via `extensions:` (in `~/.omp/agent/settings.json` or `<cwd>/.omp/settings.json`) or `--extension`/`-e` on the CLI for `skills/`, `hooks/pre|post/`, `tools/`, `commands/`, `rules/`, `prompts/`, and `.mcp.json`. Prior to this, only the extension's TypeScript factory module ran; every sibling capability the docs (https://omp.sh/docs/extension-authoring) advertised was silently ignored ([#1496](https://github.com/can1357/oh-my-pi/issues/1496)).
|
|
10
|
+
- Added the top-level `omp install <target>` subcommand documented at https://omp.sh/docs/extension-authoring. Local paths route to `omp plugin link` (so the directory is symlinked into the plugin set), and npm/marketplace specs route to `omp plugin install`. Before this, `install` was not a registered subcommand and the CLI runner silently forwarded `install ./my-extension` to `launch` as an initial LLM prompt ([#1496](https://github.com/can1357/oh-my-pi/issues/1496)).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Changed the sticky `Todos` panel above the editor to advance as tasks close, instead of pinning to the first 5 tasks of the active phase. `selectStickyTodoWindow` now shows up to 5 open (pending / in_progress) tasks in original phase order and reports the count of remaining open tasks for the `+N more` hint, so every `todo_write` flip produces a visible row shift. Closed-phase tail falls back to the last 5 tasks (with the `+N more` line suppressed) until `getActivePhase` walks to the next phase.
|
|
15
|
+
- Linked the sticky `Todos` panel to the live `SessionObserverRegistry` so pending todos that have an in-flight subagent doing their work light up green with an animated spinner — the same `theme.spinnerFrames` ("status" preset) the `task` tool uses for its agent rows — instead of staying greyed out as if nothing is happening. A new exported `todoMatchesAnyDescription(content, descriptions)` does case- and whitespace-insensitive equality first with a 6-char minimum-overlap substring fallback in either direction, so "Sonnet #2: shallow bug scan" and a subagent description of "Sonnet #2" still link up. Completed todos now render with `theme.status.success` (✔ / `\uf00c` / `[ok]` per symbol preset, still wrapped in the `success` colour so themed palettes can keep their purple/green/whatever) and in_progress rows render with `theme.status.running`, matching the `task` tool's icon vocabulary. The spinner interval only ticks while at least one visible open todo has a matched active subagent, and self-stops once subagents finish, so plain in_progress todos do not animate forever in the absence of subagent activity.
|
|
16
|
+
- Extracted the top-level CLI command table from `src/cli.ts` into a side-effect-free `src/cli-commands.ts` so test code can introspect the registered subcommands without triggering the entrypoint's top-level await.
|
|
17
|
+
|
|
18
|
+
## [15.5.11] - 2026-05-29
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Added `SqlSessionStorage`, a `bun:sql`-backed implementation of `SessionStorage` that persists session JSONL into PostgreSQL, MySQL/MariaDB, or SQLite. Pass a connected `Bun.SQL` instance (the constructor accepts `postgres://`, `mysql://`, or `sqlite:` URLs) to `SqlSessionStorage.create({ client, table?, adapter?, createTable? })` and hand the returned storage to any `SessionManager` factory. The dialect is auto-detected from `client.options.adapter` and used to pick the correct DDL plus upsert-with-append syntax (`ON CONFLICT … DO UPDATE` for PG/SQLite, `ON DUPLICATE KEY UPDATE` for MySQL), so the agent's append-only persist pattern works in a single round-trip per line. Same in-memory mirror and `drain()` semantics as the Redis backend; blobs and tool artifacts still live on disk via `ArtifactManager`/`BlobStore`.
|
|
23
|
+
- Added `RedisSessionStorage`, a `bun:redis`-backed implementation of the `SessionStorage` interface that lets API consumers route session JSONL through Redis instead of local disk. Pass a connected `Bun.RedisClient` (or any compatible adapter) to `RedisSessionStorage.create({ client, prefix? })` and hand the returned storage to `SessionManager.create(cwd, sessionDir, storage)` (or any other static factory that accepts a storage argument). An in-memory mirror is loaded on creation so the interface's synchronous methods (`existsSync`, `statSync`, `listFilesSync`, …) keep their contracts; `drain()` waits for queued background writes. Tool artifacts and image blobs still live on disk via `ArtifactManager`/`BlobStore` — Redis only owns the session JSONL keyspace under the configured prefix.
|
|
24
|
+
- Exported the `SessionStorage` / `SessionStorageWriter` / `FileSessionStorage` / `MemorySessionStorage` symbols (already reachable via the `./session/session-storage` subpath) from the package root so SDK consumers can construct alternative storage backends without deep-importing.
|
|
25
|
+
- Added a fresh `¶<relative-path>#TAG` snapshot header to the `write` tool's success text in hashline display mode, covering plain disk writes, ACP-bridge writes, and conflict resolutions (bulk resolutions emit a trailing `Snapshots:` block with one header per successfully written file). The header records a current snapshot in the file-snapshot store so the next `edit` can land without an extra `read` round-trip. Suppressed when the session is not in hashline mode and skipped for archive/SQLite writes and host-managed internal URL targets where hashline anchors do not apply.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- The `edit` tool's stale-snapshot rejection message now distinguishes "file changed between read and edit" (the section's hash was recorded in this session but the file has since drifted — a prior in-session edit advanced it, or an external write changed it) from "hash #X is not from this session" (a fabricated or carried-over cross-session tag), the latter carrying explicit "never invent the tag" guidance. Both messages include the current file hash plus 2 lines of context around each anchor so the next attempt has everything it needs. Snapshot-based recovery still runs first; the sharper diagnostics only surface when recovery cannot reconcile the edit.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Fixed Autonomous Memory phase 1/phase 2 failing with `Thinking effort low is not supported by <provider>/<model>` on models whose supported reasoning efforts exclude `low`/`medium` (e.g. `deepseek/deepseek-v4-pro`). Both stage1 (`Effort.Low`) and consolidation (`Effort.Medium`) call sites in `packages/coding-agent/src/memories/index.ts` now route through `clampThinkingLevelForModel`, lifting the requested effort to the model's lowest supported level instead of letting `requireSupportedEffort` throw ([#1480](https://github.com/can1357/oh-my-pi/issues/1480)).
|
|
34
|
+
|
|
5
35
|
## [15.5.10] - 2026-05-28
|
|
6
36
|
|
|
7
37
|
### Added
|
|
@@ -12,6 +42,10 @@
|
|
|
12
42
|
|
|
13
43
|
- Fixed compaction surfacing raw HTTP 401/403 envelopes (e.g. `Compaction failed: 401 {"type":"error","error":{"type":"authentication_error",…}}`) instead of routing to an authenticated fallback model. The compaction layer now attaches the provider-reported HTTP status onto the thrown error, and `AgentSession`'s auth-failure detector branches on `error.status === 401 || 403` in addition to the existing `auth_unavailable` regex. When a fallback model role (e.g. `modelRoles.smol`) is configured, compaction retries it transparently; otherwise the user sees the actionable "Compaction requires usable credentials for …" hint instead of the raw provider envelope.
|
|
14
44
|
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Fixed compiled-binary legacy plugin loading for `@earendil-works/*` imports of bundled package roots such as `@earendil-works/pi-coding-agent`; compat now rewrites all bundled pi package roots to bunfs entrypoints and resolves fallback peer dependencies through the canonical `@oh-my-pi/*` specifier.
|
|
48
|
+
|
|
15
49
|
## [15.5.8] - 2026-05-28
|
|
16
50
|
|
|
17
51
|
### Breaking Changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level CLI command table.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own module (importable without side effects) so that tests can
|
|
5
|
+
* inspect the registered subcommands without triggering the side-effectful
|
|
6
|
+
* top-level await in `cli.ts`. Adding a new subcommand here is enough to make
|
|
7
|
+
* `runCli` route to it instead of forwarding the argv as a prompt to
|
|
8
|
+
* `launch` — see #1496 for the original "args silently leak to the LLM"
|
|
9
|
+
* regression that motivated the split.
|
|
10
|
+
*/
|
|
11
|
+
import type { CommandEntry } from "@oh-my-pi/pi-utils/cli";
|
|
12
|
+
export declare const commands: CommandEntry[];
|
|
13
|
+
/**
|
|
14
|
+
* Return true when `first` matches a registered subcommand name or alias.
|
|
15
|
+
*
|
|
16
|
+
* Flags (`-…`) and `@file` arguments are never subcommands; for those the CLI
|
|
17
|
+
* runner skips ahead to the default `launch` command.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isSubcommand(first: string | undefined): boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `omp install <target>` — top-level convenience over `omp plugin install` /
|
|
3
|
+
* `omp plugin link`.
|
|
4
|
+
*
|
|
5
|
+
* The docs (omp.sh/docs/extension-authoring) advertise
|
|
6
|
+
*
|
|
7
|
+
* omp install ./my-extension
|
|
8
|
+
*
|
|
9
|
+
* as a third loading mechanism that "symlinks the directory into the plugin
|
|
10
|
+
* set and watches it for changes". Before this command existed, `install` was
|
|
11
|
+
* not a registered subcommand, so the CLI runner forwarded the argv to the
|
|
12
|
+
* default `launch` command and the model received `install ./my-extension`
|
|
13
|
+
* as an initial prompt — see #1496.
|
|
14
|
+
*
|
|
15
|
+
* Local-path targets (`./foo`, `/abs/foo`, `~/foo`, or an existing directory)
|
|
16
|
+
* route to `plugin link` so they are symlinked into the plugin set, matching
|
|
17
|
+
* the documented behavior. Everything else (`pkg`, `pkg@1.2.3`,
|
|
18
|
+
* `name@marketplace`) routes to `plugin install`.
|
|
19
|
+
*/
|
|
20
|
+
import { Command } from "@oh-my-pi/pi-utils/cli";
|
|
21
|
+
/**
|
|
22
|
+
* Heuristic used to decide whether `omp install <target>` should `link` a
|
|
23
|
+
* local directory or `install` a remote spec. Exported for tests.
|
|
24
|
+
*/
|
|
25
|
+
export declare function looksLikeLocalPath(target: string): boolean;
|
|
26
|
+
export default class Install extends Command {
|
|
27
|
+
static description: string;
|
|
28
|
+
static args: {
|
|
29
|
+
targets: import("@oh-my-pi/pi-utils/cli").ArgDescriptor & {
|
|
30
|
+
description: string;
|
|
31
|
+
required: false;
|
|
32
|
+
multiple: true;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
static flags: {
|
|
36
|
+
json: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"boolean"> & {
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
force: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"boolean"> & {
|
|
40
|
+
description: string;
|
|
41
|
+
};
|
|
42
|
+
"dry-run": import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"boolean"> & {
|
|
43
|
+
description: string;
|
|
44
|
+
};
|
|
45
|
+
scope: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"string"> & {
|
|
46
|
+
description: string;
|
|
47
|
+
options: string[];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
run(): Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { LoadContext } from "../capability/types";
|
|
2
|
+
/** A resolved extension package directory wired into the discovery surfaces. */
|
|
3
|
+
export interface OmpExtensionRoot {
|
|
4
|
+
/** Absolute path to the package directory. */
|
|
5
|
+
path: string;
|
|
6
|
+
/** Stable display name (basename of the package directory). */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Scope from which the path was sourced. */
|
|
9
|
+
level: "user" | "project";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Register CLI-provided extension package paths (e.g. from `--extension`/`-e`)
|
|
13
|
+
* so the sub-discovery providers can find their sibling `skills/`, `hooks/`,
|
|
14
|
+
* etc. Paths that do not resolve to a directory are silently dropped — file
|
|
15
|
+
* entrypoints have no package sub-tree to scan.
|
|
16
|
+
*
|
|
17
|
+
* Call once during startup before any capability load. Repeated calls extend
|
|
18
|
+
* the registered set; {@link clearOmpExtensionCliRoots} resets for tests.
|
|
19
|
+
*/
|
|
20
|
+
export declare function injectOmpExtensionCliRoots(paths: readonly string[], home: string, cwd: string): void;
|
|
21
|
+
/** Drop every CLI-injected root. Tests use this between cases. */
|
|
22
|
+
export declare function clearOmpExtensionCliRoots(): void;
|
|
23
|
+
/** Inspect currently-injected CLI roots (read-only). Exposed for diagnostics + tests. */
|
|
24
|
+
export declare function getInjectedOmpExtensionCliRoots(): readonly OmpExtensionRoot[];
|
|
25
|
+
/**
|
|
26
|
+
* Resolve every configured extension package directory for the given context.
|
|
27
|
+
*
|
|
28
|
+
* Sources, in order of precedence (later entries with the same absolute path
|
|
29
|
+
* are dropped):
|
|
30
|
+
*
|
|
31
|
+
* 1. CLI roots injected via {@link injectOmpExtensionCliRoots}
|
|
32
|
+
* 2. Project `<cwd>/.omp/settings.json#extensions`
|
|
33
|
+
* 3. User `~/.omp/agent/settings.json#extensions`
|
|
34
|
+
* 4. Enabled plugins installed under `<plugins>/node_modules/` (e.g. via
|
|
35
|
+
* `omp install <pkg>` / `omp plugin install` / `omp plugin link`)
|
|
36
|
+
*
|
|
37
|
+
* Only entries that resolve to a directory on disk are returned; file
|
|
38
|
+
* entrypoints contribute zero sub-discovery surface and are filtered out.
|
|
39
|
+
* Installed-plugin enumeration failures (missing lockfile, unreadable
|
|
40
|
+
* `package.json`, etc.) are logged at `debug` and degrade gracefully — the
|
|
41
|
+
* other sources still surface.
|
|
42
|
+
*/
|
|
43
|
+
export declare function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtensionRoot[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility shim for legacy extensions importing the package root of
|
|
3
|
+
* `@oh-my-pi/pi-coding-agent` (or one of its aliased scopes like
|
|
4
|
+
* `@earendil-works/pi-coding-agent` or `@mariozechner/pi-coding-agent`).
|
|
5
|
+
*
|
|
6
|
+
* The coding-agent package's own barrel (`./src/index.ts`) cannot be listed
|
|
7
|
+
* as a `bun --compile` extra entrypoint alongside the CLI entry without
|
|
8
|
+
* silently breaking the main binary's startup (see issue #1474 follow-up).
|
|
9
|
+
* Routing legacy plugin imports through this sibling shim sidesteps that
|
|
10
|
+
* conflict: bun bundles a distinct entry whose path differs from the CLI
|
|
11
|
+
* entry, while still re-exporting the canonical surface so plugins observe
|
|
12
|
+
* the same module identity as a direct `@oh-my-pi/pi-coding-agent` import.
|
|
13
|
+
*/
|
|
14
|
+
export * from "../index";
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare function loadLegacyPiModule(resolvedPath: string): Promise<unknown>;
|
|
2
2
|
export declare function installLegacyPiSpecifierShim(): void;
|
|
3
|
+
/** Test seam: clears the memoized canonical specifier resolutions. */
|
|
4
|
+
export declare function __resetLegacyPiResolutionCache(): void;
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import type { InstalledPlugin } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Get list of enabled plugins with their resolved configurations.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Respects both global runtime config and project overrides. Iterates the
|
|
6
|
+
* union of `<plugins>/package.json#dependencies` (`bun install`-installed
|
|
7
|
+
* packages) and `<plugins>/omp-plugins.lock.json#plugins` (so locally
|
|
8
|
+
* `plugin link`-symlinked extensions, which never get a dependency entry,
|
|
9
|
+
* are still discovered). The optional `home` parameter pins the plugins
|
|
10
|
+
* root for callers that need to enumerate plugins relative to a non-default
|
|
11
|
+
* home (tests with a tempdir, discovery loaders threaded with
|
|
12
|
+
* `LoadContext.home`).
|
|
5
13
|
*/
|
|
6
|
-
export declare function getEnabledPlugins(cwd: string
|
|
14
|
+
export declare function getEnabledPlugins(cwd: string, opts?: {
|
|
15
|
+
home?: string;
|
|
16
|
+
}): Promise<InstalledPlugin[]>;
|
|
7
17
|
export declare function resolvePluginToolPaths(plugin: InstalledPlugin): string[];
|
|
8
18
|
export declare function resolvePluginHookPaths(plugin: InstalledPlugin): string[];
|
|
9
19
|
export declare function resolvePluginCommandPaths(plugin: InstalledPlugin): string[];
|
package/dist/types/index.d.ts
CHANGED
|
@@ -23,8 +23,11 @@ export * from "./sdk";
|
|
|
23
23
|
export * from "./session/agent-session";
|
|
24
24
|
export * from "./session/auth-storage";
|
|
25
25
|
export * from "./session/messages";
|
|
26
|
+
export * from "./session/redis-session-storage";
|
|
26
27
|
export * from "./session/session-dump-format";
|
|
27
28
|
export * from "./session/session-manager";
|
|
29
|
+
export * from "./session/session-storage";
|
|
30
|
+
export * from "./session/sql-session-storage";
|
|
28
31
|
export * from "./task/executor";
|
|
29
32
|
export type * from "./task/types";
|
|
30
33
|
export * from "./tools";
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Editor, type KeyId } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import type { AppKeybinding } from "../../config/keybindings";
|
|
3
|
+
import { highlightUltrathink } from "../ultrathink";
|
|
3
4
|
type ConfigurableEditorAction = Extract<AppKeybinding, "app.interrupt" | "app.clear" | "app.exit" | "app.suspend" | "app.thinking.cycle" | "app.model.cycleForward" | "app.model.cycleBackward" | "app.model.select" | "app.model.selectTemporary" | "app.tools.expand" | "app.thinking.toggle" | "app.editor.external" | "app.history.search" | "app.message.dequeue" | "app.clipboard.pasteImage" | "app.clipboard.copyPrompt">;
|
|
4
5
|
/**
|
|
5
6
|
* Custom editor that handles configurable app-level shortcuts for coding-agent.
|
|
6
7
|
*/
|
|
7
8
|
export declare class CustomEditor extends Editor {
|
|
8
9
|
#private;
|
|
10
|
+
/** Rainbow-highlight the "ultrathink" keyword as the user types it. */
|
|
11
|
+
decorateText: typeof highlightUltrathink;
|
|
9
12
|
onEscape?: () => void;
|
|
10
13
|
shouldBypassAutocompleteOnEscape?: () => boolean;
|
|
11
14
|
onClear?: () => void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Hidden system notice appended after a user message that mentions "ultrathink". */
|
|
2
|
+
export declare const ULTRATHINK_NOTICE: string;
|
|
3
|
+
/** Whether `text` contains the standalone keyword "ultrathink" (any case). */
|
|
4
|
+
export declare function containsUltrathink(text: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Rainbow-highlight every standalone "ultrathink" in `text` for editor display.
|
|
7
|
+
* Adds only zero-width SGR escapes — the visible width is unchanged — and returns
|
|
8
|
+
* the input untouched when the keyword is absent.
|
|
9
|
+
*/
|
|
10
|
+
export declare function highlightUltrathink(text: string): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { SessionStorage, SessionStorageStat, SessionStorageWriter } from "./session-storage";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal subset of the `bun:redis` `RedisClient` surface used by
|
|
4
|
+
* {@link RedisSessionStorage}. Keeping the contract narrow (and accepting any
|
|
5
|
+
* client that conforms) lets callers swap in test doubles or shared clients
|
|
6
|
+
* without dragging the entire Bun typings into this module.
|
|
7
|
+
*/
|
|
8
|
+
export interface RedisSessionStorageClient {
|
|
9
|
+
get(key: string): Promise<string | null>;
|
|
10
|
+
set(key: string, value: string): Promise<unknown>;
|
|
11
|
+
append(key: string, value: string): Promise<number>;
|
|
12
|
+
del(...keys: string[]): Promise<number>;
|
|
13
|
+
rename(src: string, dst: string): Promise<unknown>;
|
|
14
|
+
scan(cursor: string, ...args: string[]): Promise<[string, string[]]>;
|
|
15
|
+
hset(key: string, field: string, value: string): Promise<unknown>;
|
|
16
|
+
hgetall(key: string): Promise<Record<string, string>>;
|
|
17
|
+
hdel(key: string, ...fields: string[]): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface RedisSessionStorageOptions {
|
|
20
|
+
/** A connected `bun:redis` RedisClient (or any compatible adapter). */
|
|
21
|
+
client: RedisSessionStorageClient;
|
|
22
|
+
/**
|
|
23
|
+
* Key prefix applied to every Redis key this storage owns. Default `omp:sessions:`.
|
|
24
|
+
* Trailing colon is preserved verbatim — set to a project-scoped prefix to share
|
|
25
|
+
* one Redis instance between multiple agents.
|
|
26
|
+
*/
|
|
27
|
+
prefix?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Maximum number of keys returned per SCAN batch when warming the mirror.
|
|
30
|
+
* Default 500.
|
|
31
|
+
*/
|
|
32
|
+
scanCount?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Redis-backed implementation of {@link SessionStorage}. Each session JSONL
|
|
36
|
+
* file maps to a Redis STRING key, with per-key metadata (mtime) tracked in a
|
|
37
|
+
* single sibling HASH. An in-memory mirror is loaded on construction so the
|
|
38
|
+
* interface's synchronous methods (`existsSync`, `statSync`, `listFilesSync`,
|
|
39
|
+
* `readTextSync`, `writeTextSync`) keep their contracts — Bun's Redis client
|
|
40
|
+
* is async only, and the persist hot path (`writer.writeLineSync`) cannot
|
|
41
|
+
* wait on a network round-trip.
|
|
42
|
+
*
|
|
43
|
+
* Trade-offs vs `FileSessionStorage`:
|
|
44
|
+
* - Mirror state is process-local. Two processes writing the same session key
|
|
45
|
+
* will diverge until one of them reloads via {@link refresh}. This matches
|
|
46
|
+
* `FileSessionStorage`'s existing single-writer assumption.
|
|
47
|
+
* - `writeLineSync` updates the mirror synchronously and queues an async
|
|
48
|
+
* `APPEND`. The promise is awaited by `flush()` / `close()` / {@link drain}.
|
|
49
|
+
* A SIGKILL landing between the sync mirror update and the network round
|
|
50
|
+
* trip loses the last line; the file-backed implementation survives that
|
|
51
|
+
* window because bytes are handed to the kernel page cache before
|
|
52
|
+
* returning.
|
|
53
|
+
* - Blobs (image data) and tool artifact files still live on disk via
|
|
54
|
+
* `BlobStore` / `ArtifactManager`. Those are out of scope for this storage.
|
|
55
|
+
*/
|
|
56
|
+
export declare class RedisSessionStorage implements SessionStorage {
|
|
57
|
+
#private;
|
|
58
|
+
private constructor();
|
|
59
|
+
/**
|
|
60
|
+
* Warm the in-memory mirror with every existing session key under the
|
|
61
|
+
* configured prefix and return the ready-to-use storage. Must be awaited
|
|
62
|
+
* before passing the storage into `SessionManager.create()` so synchronous
|
|
63
|
+
* lookups (session resume, recent sessions, EPERM-backup recovery) see
|
|
64
|
+
* the existing keyspace.
|
|
65
|
+
*/
|
|
66
|
+
static create(options: RedisSessionStorageOptions): Promise<RedisSessionStorage>;
|
|
67
|
+
/**
|
|
68
|
+
* Re-scan Redis and replace the mirror's contents. Call this from a
|
|
69
|
+
* different process that took over a session keyspace, or after an
|
|
70
|
+
* out-of-band write made by another agent.
|
|
71
|
+
*/
|
|
72
|
+
refresh(): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Resolve once every pending background write (issued via `writeTextSync`
|
|
75
|
+
* or `writer.writeLineSync`) has been acknowledged by Redis. Throws if any
|
|
76
|
+
* background write failed since the last drain.
|
|
77
|
+
*
|
|
78
|
+
* Call this on graceful shutdown to avoid losing the last unflushed line.
|
|
79
|
+
* The session-manager's own `flush()` / `close()` already drain through
|
|
80
|
+
* the writer chain — this method exists for callers (test harnesses,
|
|
81
|
+
* subprocess-style consumers) that bypass the writer.
|
|
82
|
+
*/
|
|
83
|
+
drain(): Promise<void>;
|
|
84
|
+
ensureDirSync(_dir: string): void;
|
|
85
|
+
existsSync(path: string): boolean;
|
|
86
|
+
writeTextSync(path: string, content: string): void;
|
|
87
|
+
readTextSync(path: string): string;
|
|
88
|
+
statSync(path: string): SessionStorageStat;
|
|
89
|
+
listFilesSync(dir: string, pattern: string): string[];
|
|
90
|
+
exists(path: string): Promise<boolean>;
|
|
91
|
+
readText(path: string): Promise<string>;
|
|
92
|
+
readTextPrefix(path: string, maxBytes: number): Promise<string>;
|
|
93
|
+
writeText(path: string, content: string): Promise<void>;
|
|
94
|
+
rename(src: string, dst: string): Promise<void>;
|
|
95
|
+
unlink(path: string): Promise<void>;
|
|
96
|
+
deleteSessionWithArtifacts(sessionPath: string): Promise<void>;
|
|
97
|
+
openWriter(path: string, options?: {
|
|
98
|
+
flags?: "a" | "w";
|
|
99
|
+
onError?: (err: Error) => void;
|
|
100
|
+
}): SessionStorageWriter;
|
|
101
|
+
_writerClosed(writer: RedisSessionStorageWriter): void;
|
|
102
|
+
/** Mirror-only mutation, no Redis call. Used by writers to update local state synchronously. */
|
|
103
|
+
_mirrorAppend(path: string, line: string): void;
|
|
104
|
+
/** Mirror-only mutation, no Redis call. Used by writers opened with `flags: "w"` to truncate. */
|
|
105
|
+
_mirrorTruncate(path: string): void;
|
|
106
|
+
_remoteTruncate(path: string): Promise<void>;
|
|
107
|
+
_remoteAppend(path: string, line: string): Promise<void>;
|
|
108
|
+
/** Record a writer's pending promise on the storage-level tail so `drain()` waits for it. */
|
|
109
|
+
_attachPending(promise: Promise<void>): void;
|
|
110
|
+
}
|
|
111
|
+
declare class RedisSessionStorageWriter implements SessionStorageWriter {
|
|
112
|
+
#private;
|
|
113
|
+
constructor(storage: RedisSessionStorage, path: string, options?: {
|
|
114
|
+
flags?: "a" | "w";
|
|
115
|
+
onError?: (err: Error) => void;
|
|
116
|
+
});
|
|
117
|
+
writeLineSync(line: string): void;
|
|
118
|
+
writeLine(line: string): Promise<void>;
|
|
119
|
+
flush(): Promise<void>;
|
|
120
|
+
fsync(): Promise<void>;
|
|
121
|
+
close(): Promise<void>;
|
|
122
|
+
getError(): Error | undefined;
|
|
123
|
+
}
|
|
124
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { SessionStorage, SessionStorageStat, SessionStorageWriter } from "./session-storage";
|
|
2
|
+
/**
|
|
3
|
+
* Supported `bun:sql` adapter dialects. `Bun.SQL` reports this string on
|
|
4
|
+
* `client.options.adapter`; we detect it once at construction and pick the
|
|
5
|
+
* correct DDL / upsert / concat syntax for the underlying engine.
|
|
6
|
+
*/
|
|
7
|
+
export type SqlSessionStorageAdapter = "postgres" | "mysql" | "sqlite";
|
|
8
|
+
/**
|
|
9
|
+
* Minimal subset of the `Bun.SQL` instance surface used by
|
|
10
|
+
* {@link SqlSessionStorage}. The real client exposes a callable
|
|
11
|
+
* tagged-template too; we only ever call `unsafe()` so the contract here is
|
|
12
|
+
* narrow — making it trivial to swap in a test double or wrap a pooled
|
|
13
|
+
* client.
|
|
14
|
+
*/
|
|
15
|
+
export interface SqlSessionStorageClient {
|
|
16
|
+
unsafe(query: string, values?: unknown[]): Promise<unknown[]>;
|
|
17
|
+
/**
|
|
18
|
+
* `Bun.SQL` exposes the parsed connection options here. We only consult
|
|
19
|
+
* `adapter` to pick the dialect; the field is typed as
|
|
20
|
+
* `string | undefined` so the real `Bun.SQL` instance type slots in
|
|
21
|
+
* without casting (it reports `string | undefined` across adapters).
|
|
22
|
+
*/
|
|
23
|
+
options: {
|
|
24
|
+
adapter?: string;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
};
|
|
27
|
+
end?(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export interface SqlSessionStorageOptions {
|
|
30
|
+
/** Connected `Bun.SQL` instance (PostgreSQL, MySQL, or SQLite). */
|
|
31
|
+
client: SqlSessionStorageClient;
|
|
32
|
+
/**
|
|
33
|
+
* Override the auto-detected adapter. Useful when the client is wrapped
|
|
34
|
+
* (e.g. by a pool) and `client.options.adapter` is unreliable.
|
|
35
|
+
*/
|
|
36
|
+
adapter?: SqlSessionStorageAdapter;
|
|
37
|
+
/**
|
|
38
|
+
* Table name to use. Default: `omp_session_files`. Must match
|
|
39
|
+
* `[A-Za-z_][A-Za-z0-9_]{0,62}` — inlined into prepared statements at
|
|
40
|
+
* startup, so we accept identifier-safe inputs only (no quoted/dotted
|
|
41
|
+
* names).
|
|
42
|
+
*/
|
|
43
|
+
table?: string;
|
|
44
|
+
/**
|
|
45
|
+
* If true, run `CREATE TABLE IF NOT EXISTS` during `create()`.
|
|
46
|
+
* Default: true. Disable when the table is owned by an external
|
|
47
|
+
* migration.
|
|
48
|
+
*/
|
|
49
|
+
createTable?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* SQL-backed implementation of {@link SessionStorage} using `bun:sql`. Each
|
|
53
|
+
* session JSONL file maps to a row keyed by `path`; one table stores
|
|
54
|
+
* everything.
|
|
55
|
+
*
|
|
56
|
+
* Works against PostgreSQL, MySQL/MariaDB, and SQLite by selecting the
|
|
57
|
+
* dialect-correct DDL, upsert, and string-concat syntax at construction.
|
|
58
|
+
*
|
|
59
|
+
* Trade-offs vs `FileSessionStorage`:
|
|
60
|
+
* - An in-memory mirror is loaded on construction so the interface's
|
|
61
|
+
* synchronous methods (`existsSync`, `statSync`, `listFilesSync`, …) keep
|
|
62
|
+
* their contracts; `bun:sql` is async only. Mirror state is process-local,
|
|
63
|
+
* matching `FileSessionStorage`'s existing single-writer assumption — peer
|
|
64
|
+
* processes need {@link refresh} to pick up out-of-band writes.
|
|
65
|
+
* - `writeLineSync` updates the mirror synchronously and queues an async
|
|
66
|
+
* upsert that appends the line to the existing row (or inserts it as the
|
|
67
|
+
* first chunk). The promise is awaited by `flush()` / `close()` /
|
|
68
|
+
* {@link drain}. A SIGKILL between the sync mirror update and the network
|
|
69
|
+
* round-trip loses the last line.
|
|
70
|
+
* - Blobs (image data) and tool artifact files still live on disk via
|
|
71
|
+
* `BlobStore` / `ArtifactManager`. Those are out of scope for this storage.
|
|
72
|
+
*/
|
|
73
|
+
export declare class SqlSessionStorage implements SessionStorage {
|
|
74
|
+
#private;
|
|
75
|
+
private constructor();
|
|
76
|
+
/**
|
|
77
|
+
* Apply the dialect-correct DDL (unless `createTable: false` is set) and
|
|
78
|
+
* warm the in-memory mirror with every existing row. Must be awaited
|
|
79
|
+
* before passing the storage into `SessionManager.create()`.
|
|
80
|
+
*/
|
|
81
|
+
static create(options: SqlSessionStorageOptions): Promise<SqlSessionStorage>;
|
|
82
|
+
get adapter(): SqlSessionStorageAdapter;
|
|
83
|
+
get table(): string;
|
|
84
|
+
/**
|
|
85
|
+
* Re-load the mirror from the database. Call this from a different
|
|
86
|
+
* process that took over the table, or after an out-of-band write made
|
|
87
|
+
* by another agent.
|
|
88
|
+
*/
|
|
89
|
+
refresh(): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Resolve once every pending background write (issued via `writeTextSync`
|
|
92
|
+
* or `writer.writeLineSync`) has been acknowledged by the database.
|
|
93
|
+
* Throws if any background write failed since the last drain. Call on
|
|
94
|
+
* graceful shutdown to avoid losing the last unflushed line.
|
|
95
|
+
*/
|
|
96
|
+
drain(): Promise<void>;
|
|
97
|
+
ensureDirSync(_dir: string): void;
|
|
98
|
+
existsSync(path: string): boolean;
|
|
99
|
+
writeTextSync(path: string, content: string): void;
|
|
100
|
+
readTextSync(path: string): string;
|
|
101
|
+
statSync(path: string): SessionStorageStat;
|
|
102
|
+
listFilesSync(dir: string, pattern: string): string[];
|
|
103
|
+
exists(path: string): Promise<boolean>;
|
|
104
|
+
readText(path: string): Promise<string>;
|
|
105
|
+
readTextPrefix(path: string, maxBytes: number): Promise<string>;
|
|
106
|
+
writeText(path: string, content: string): Promise<void>;
|
|
107
|
+
rename(src: string, dst: string): Promise<void>;
|
|
108
|
+
unlink(path: string): Promise<void>;
|
|
109
|
+
deleteSessionWithArtifacts(sessionPath: string): Promise<void>;
|
|
110
|
+
openWriter(path: string, options?: {
|
|
111
|
+
flags?: "a" | "w";
|
|
112
|
+
onError?: (err: Error) => void;
|
|
113
|
+
}): SessionStorageWriter;
|
|
114
|
+
_writerClosed(writer: SqlSessionStorageWriter): void;
|
|
115
|
+
_mirrorAppend(path: string, line: string): {
|
|
116
|
+
content: string;
|
|
117
|
+
mtimeMs: number;
|
|
118
|
+
};
|
|
119
|
+
_mirrorTruncate(path: string): void;
|
|
120
|
+
_remoteTruncate(path: string): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Append a chunk to the row at `path`, inserting if the row doesn't
|
|
123
|
+
* exist yet. Single round-trip via the dialect-specific `upsertAppend`.
|
|
124
|
+
*/
|
|
125
|
+
_remoteAppend(path: string, line: string, mtimeMs: number): Promise<void>;
|
|
126
|
+
_attachPending(promise: Promise<void>): void;
|
|
127
|
+
}
|
|
128
|
+
declare class SqlSessionStorageWriter implements SessionStorageWriter {
|
|
129
|
+
#private;
|
|
130
|
+
constructor(storage: SqlSessionStorage, path: string, options?: {
|
|
131
|
+
flags?: "a" | "w";
|
|
132
|
+
onError?: (err: Error) => void;
|
|
133
|
+
});
|
|
134
|
+
writeLineSync(line: string): void;
|
|
135
|
+
writeLine(line: string): Promise<void>;
|
|
136
|
+
flush(): Promise<void>;
|
|
137
|
+
fsync(): Promise<void>;
|
|
138
|
+
close(): Promise<void>;
|
|
139
|
+
getError(): Error | undefined;
|
|
140
|
+
}
|
|
141
|
+
export {};
|
|
@@ -49,6 +49,36 @@ declare const todoWriteSchema: z.ZodObject<{
|
|
|
49
49
|
type TodoWriteParams = z.infer<typeof todoWriteSchema>;
|
|
50
50
|
export declare const USER_TODO_EDIT_CUSTOM_TYPE = "user_todo_edit";
|
|
51
51
|
export declare function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPhase[];
|
|
52
|
+
/**
|
|
53
|
+
* Pick the actionable window of tasks to display in the sticky todo panel.
|
|
54
|
+
*
|
|
55
|
+
* Returns up to `maxVisible` open (pending / in_progress) tasks in their
|
|
56
|
+
* original phase order, plus the count of remaining open tasks not shown so
|
|
57
|
+
* the caller can render a `+N more` hint. When every task in `tasks` is
|
|
58
|
+
* closed (completed or abandoned), returns the trailing `maxVisible` tasks
|
|
59
|
+
* with `hiddenOpenCount = 0`, so the panel keeps useful context until the
|
|
60
|
+
* active-phase pointer advances on the next `todo_write`.
|
|
61
|
+
*
|
|
62
|
+
* Task identity and order are preserved — this is a slice, never a sort.
|
|
63
|
+
*/
|
|
64
|
+
export declare function selectStickyTodoWindow(tasks: TodoItem[], maxVisible?: number): {
|
|
65
|
+
visible: TodoItem[];
|
|
66
|
+
hiddenOpenCount: number;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Report whether `content` likely names the same work as any entry in
|
|
70
|
+
* `descriptions`. Used by the sticky todo panel to light up a pending todo
|
|
71
|
+
* when an in-flight subagent is doing the work for it, without requiring
|
|
72
|
+
* the caller to flip the todo's status.
|
|
73
|
+
*
|
|
74
|
+
* Matching is normalize-then-equal first (lowercased; punctuation and
|
|
75
|
+
* whitespace runs both collapsed to a single space; trimmed), with a
|
|
76
|
+
* substring fallback in either direction so minor wording drift
|
|
77
|
+
* ("Sonnet #2: bug scan" vs "Sonnet #2") still links up. The substring
|
|
78
|
+
* fallback requires at least {@link TODO_DESCRIPTION_MIN_OVERLAP} chars on
|
|
79
|
+
* the contained side.
|
|
80
|
+
*/
|
|
81
|
+
export declare function todoMatchesAnyDescription(content: string, descriptions: readonly string[]): boolean;
|
|
52
82
|
/** Apply an array of `todo_write`-style ops to existing phases. Used by /todo slash command. */
|
|
53
83
|
export declare function applyOpsToPhases(currentPhases: TodoPhase[], ops: TodoWriteParams["ops"]): {
|
|
54
84
|
phases: TodoPhase[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-Backed Sessions
|
|
3
|
+
*
|
|
4
|
+
* Store session JSONL in Redis (or Valkey) instead of the local filesystem.
|
|
5
|
+
* Useful when the agent runs in an ephemeral container, behind a load
|
|
6
|
+
* balancer, or anywhere a shared session store beats per-host disk state.
|
|
7
|
+
*
|
|
8
|
+
* The storage substrate is the only thing that changes — every other SDK
|
|
9
|
+
* surface (extensions, hooks, custom tools, slash commands, branching,
|
|
10
|
+
* `SessionManager.list`, …) continues to work unmodified.
|
|
11
|
+
*
|
|
12
|
+
* Tool artifacts and image blobs are out of scope: `ArtifactManager` /
|
|
13
|
+
* `BlobStore` keep writing to `~/.omp/agent/...`. Reach for an object store
|
|
14
|
+
* (S3, R2, GCS) if you need those off-host too.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createAgentSession, RedisSessionStorage, SessionManager } from "@oh-my-pi/pi-coding-agent";
|
|
18
|
+
import { RedisClient } from "bun";
|
|
19
|
+
|
|
20
|
+
// `bun:redis` picks up `REDIS_URL` / `VALKEY_URL` from the environment, or
|
|
21
|
+
// you can pass an explicit `redis://`/`rediss://` URL.
|
|
22
|
+
const redis = new RedisClient();
|
|
23
|
+
await redis.ping();
|
|
24
|
+
|
|
25
|
+
// `create()` warms an in-memory mirror with every existing key under the
|
|
26
|
+
// prefix so SessionManager's synchronous lookups (resume, recent sessions,
|
|
27
|
+
// list) work without per-call network round-trips.
|
|
28
|
+
const storage = await RedisSessionStorage.create({
|
|
29
|
+
client: redis,
|
|
30
|
+
prefix: "omp:sessions:", // optional, this is the default
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const sessionDir = "/sessions/my-project";
|
|
34
|
+
|
|
35
|
+
// 1) Fresh persistent session, JSONL backed by Redis.
|
|
36
|
+
const { session } = await createAgentSession({
|
|
37
|
+
sessionManager: SessionManager.create(process.cwd(), sessionDir, storage),
|
|
38
|
+
});
|
|
39
|
+
console.log("New Redis session:", session.sessionFile);
|
|
40
|
+
|
|
41
|
+
// 2) Continue the most recent session for this `sessionDir`.
|
|
42
|
+
const { session: continued } = await createAgentSession({
|
|
43
|
+
sessionManager: await SessionManager.continueRecent(process.cwd(), sessionDir, storage),
|
|
44
|
+
});
|
|
45
|
+
console.log("Resumed:", continued.sessionFile);
|
|
46
|
+
|
|
47
|
+
// 3) List every Redis-backed session under this directory key prefix.
|
|
48
|
+
const sessions = await SessionManager.list(process.cwd(), sessionDir, storage);
|
|
49
|
+
console.log(`Found ${sessions.length} sessions under ${sessionDir}`);
|
|
50
|
+
|
|
51
|
+
// On graceful shutdown, drain any background writes the writer queued and
|
|
52
|
+
// close the Redis connection so containerized hosts can exit cleanly.
|
|
53
|
+
await storage.drain();
|
|
54
|
+
redis.close();
|