@oh-my-pi/pi-coding-agent 15.5.10 → 15.5.11

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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.5.11] - 2026-05-29
6
+
7
+ ### Added
8
+
9
+ - 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`.
10
+ - 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.
11
+ - 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.
12
+ - 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.
13
+
14
+ ### Changed
15
+
16
+ - 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.
17
+
18
+ ### Fixed
19
+
20
+ - 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)).
21
+
5
22
  ## [15.5.10] - 2026-05-28
6
23
 
7
24
  ### Added
@@ -12,6 +29,10 @@
12
29
 
13
30
  - 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
31
 
32
+ ### Fixed
33
+
34
+ - 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.
35
+
15
36
  ## [15.5.8] - 2026-05-28
16
37
 
17
38
  ### Breaking Changes
@@ -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;
@@ -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 {};
@@ -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();
@@ -0,0 +1,61 @@
1
+ /**
2
+ * SQL-Backed Sessions (PostgreSQL / MySQL / SQLite)
3
+ *
4
+ * Store session JSONL in a SQL database via `bun:sql`. One table, one row
5
+ * per session file — works against PostgreSQL, MySQL/MariaDB, and SQLite
6
+ * with the dialect picked automatically from the connection URL.
7
+ *
8
+ * Useful when:
9
+ * - sessions need to be queryable from existing analytics infra (just JOIN
10
+ * against the rest of your warehouse);
11
+ * - a managed Postgres/MySQL instance is already in place and adding Redis
12
+ * isn't worth the operational surface;
13
+ * - you want a single durable file at rest (SQLite) without coding directly
14
+ * against `bun:sqlite`.
15
+ *
16
+ * Tool artifacts and image blobs are out of scope: `ArtifactManager` /
17
+ * `BlobStore` keep writing to `~/.omp/agent/...`. Reach for object storage
18
+ * if you need those off-host too.
19
+ */
20
+
21
+ import { createAgentSession, SessionManager, SqlSessionStorage } from "@oh-my-pi/pi-coding-agent";
22
+ import { SQL } from "bun";
23
+
24
+ // Pick one — Bun.SQL auto-detects the dialect from the URL scheme.
25
+ //
26
+ // postgres://user:pass@host:5432/db
27
+ // mysql://user:pass@host:3306/db
28
+ // sqlite:/absolute/path/to/sessions.sqlite
29
+ // sqlite::memory: // ephemeral
30
+ const client = new SQL(process.env.SESSIONS_DB_URL ?? "sqlite::memory:");
31
+
32
+ // `create()` runs `CREATE TABLE IF NOT EXISTS` (with the right DDL for the
33
+ // dialect) and warms the in-memory mirror with every existing row.
34
+ const storage = await SqlSessionStorage.create({
35
+ client,
36
+ table: "omp_session_files", // optional, this is the default
37
+ // createTable: false, // set if migrations are owned elsewhere
38
+ });
39
+
40
+ const sessionDir = "/sessions/my-project";
41
+
42
+ // 1) Fresh persistent session, JSONL backed by SQL.
43
+ const { session } = await createAgentSession({
44
+ sessionManager: SessionManager.create(process.cwd(), sessionDir, storage),
45
+ });
46
+ console.log(`New SQL session (${storage.adapter}):`, session.sessionFile);
47
+
48
+ // 2) Continue the most recent session for this `sessionDir`.
49
+ const { session: continued } = await createAgentSession({
50
+ sessionManager: await SessionManager.continueRecent(process.cwd(), sessionDir, storage),
51
+ });
52
+ console.log("Resumed:", continued.sessionFile);
53
+
54
+ // 3) Enumerate every session row under this directory prefix.
55
+ const sessions = await SessionManager.list(process.cwd(), sessionDir, storage);
56
+ console.log(`Found ${sessions.length} sessions under ${sessionDir}`);
57
+
58
+ // On graceful shutdown, drain any background writes the writer queued and
59
+ // close the connection.
60
+ await storage.drain();
61
+ await client.end?.();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "15.5.10",
4
+ "version": "15.5.11",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -47,13 +47,13 @@
47
47
  "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@babel/parser": "^7.29.3",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.5.10",
51
- "@oh-my-pi/omp-stats": "15.5.10",
52
- "@oh-my-pi/pi-agent-core": "15.5.10",
53
- "@oh-my-pi/pi-ai": "15.5.10",
54
- "@oh-my-pi/pi-natives": "15.5.10",
55
- "@oh-my-pi/pi-tui": "15.5.10",
56
- "@oh-my-pi/pi-utils": "15.5.10",
50
+ "@oh-my-pi/hashline": "15.5.11",
51
+ "@oh-my-pi/omp-stats": "15.5.11",
52
+ "@oh-my-pi/pi-agent-core": "15.5.11",
53
+ "@oh-my-pi/pi-ai": "15.5.11",
54
+ "@oh-my-pi/pi-natives": "15.5.11",
55
+ "@oh-my-pi/pi-tui": "15.5.11",
56
+ "@oh-my-pi/pi-utils": "15.5.11",
57
57
  "@puppeteer/browsers": "^2.13.0",
58
58
  "@types/turndown": "5.0.6",
59
59
  "@xterm/headless": "^6.0.0",
@@ -56,17 +56,22 @@ async function main(): Promise<void> {
56
56
  "../stats/src/sync-worker.ts",
57
57
  "./src/tools/browser/tab-worker-entry.ts",
58
58
  "./src/eval/js/worker-entry.ts",
59
- // Legacy pi-* extension compat shims served by `legacy-pi-compat.ts`.
60
- // Both are reached only via the computed `TYPEBOX_SHIM_PATH` /
61
- // `LEGACY_PI_AI_SHIM_PATH` constants (which `--compile`'s static
62
- // analyzer cannot trace), so each shim must be listed here to land
63
- // in bunfs alongside the workers above. The bunfs entry path is
64
- // `--root`-relative with a `.js` extension, e.g.
65
- // `/$bunfs/root/packages/coding-agent/src/extensibility/typebox.js`,
66
- // which is what the `isCompiledBinary()` branch in
67
- // `legacy-pi-compat.ts` resolves to at runtime.
59
+ // Legacy pi-* extension compat entrypoints served by
60
+ // `legacy-pi-compat.ts`. These are reached via computed bunfs paths
61
+ // (which `--compile`'s static analyzer cannot trace), so each must be
62
+ // listed here to land in bunfs at
63
+ // `/$bunfs/root/packages/<pkg>/<entry>.js`. The coding-agent's own
64
+ // `./src/index.ts` is intentionally NOT listed: bun --compile silently
65
+ // breaks the CLI entry when the same package's barrel appears as an
66
+ // extra entrypoint (issue #1474), so legacy `pi-coding-agent` imports
67
+ // resolve through `legacy-pi-coding-agent-shim.ts` instead.
68
+ "../agent/src/index.ts",
69
+ "../natives/native/index.js",
70
+ "../tui/src/index.ts",
71
+ "../utils/src/index.ts",
68
72
  "./src/extensibility/typebox.ts",
69
73
  "./src/extensibility/legacy-pi-ai-shim.ts",
74
+ "./src/extensibility/legacy-pi-coding-agent-shim.ts",
70
75
  "--outfile",
71
76
  "dist/omp",
72
77
  ],
@@ -0,0 +1,15 @@
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
+
15
+ export * from "../index";