@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 +21 -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/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/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/extensibility/legacy-pi-coding-agent-shim.ts +15 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +63 -22
- package/src/index.ts +3 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/memories/index.ts +8 -3
- package/src/modes/components/custom-editor.ts +3 -0
- 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/write.ts +40 -6
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;
|
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 {};
|
|
@@ -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.
|
|
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.
|
|
51
|
-
"@oh-my-pi/omp-stats": "15.5.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "15.5.
|
|
53
|
-
"@oh-my-pi/pi-ai": "15.5.
|
|
54
|
-
"@oh-my-pi/pi-natives": "15.5.
|
|
55
|
-
"@oh-my-pi/pi-tui": "15.5.
|
|
56
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
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",
|
package/scripts/build-binary.ts
CHANGED
|
@@ -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
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// `legacy-pi-
|
|
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";
|