@oh-my-pi/pi-coding-agent 15.8.2 → 15.9.0
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 +82 -29
- package/dist/types/capability/skill.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +36 -22
- package/dist/types/debug/protocol-probe.d.ts +38 -0
- package/dist/types/debug/terminal-info.d.ts +34 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/transports/stdio.d.ts +16 -7
- package/dist/types/modes/components/assistant-message.d.ts +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/components/todo-reminder.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +36 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +20 -1
- package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
- package/dist/types/plan-mode/plan-protection.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/indexed-session-storage.d.ts +59 -0
- package/dist/types/session/redis-session-storage.d.ts +12 -85
- package/dist/types/session/session-manager.d.ts +20 -0
- package/dist/types/session/session-storage.d.ts +5 -7
- package/dist/types/session/sql-session-storage.d.ts +16 -85
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/telemetry-export.d.ts +19 -0
- package/dist/types/tiny/compiled-runtime.d.ts +35 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/index.d.ts +4 -2
- package/dist/types/tools/path-utils.d.ts +1 -1
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
- package/dist/types/utils/jj.d.ts +1 -1
- package/dist/types/utils/session-color.d.ts +7 -2
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +65 -1
- package/package.json +15 -9
- package/scripts/build-binary.ts +12 -0
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +1 -1
- package/src/config/settings-schema.ts +22 -55
- package/src/debug/index.ts +67 -1
- package/src/debug/protocol-probe.ts +267 -0
- package/src/debug/terminal-info.ts +127 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -3
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +3 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +12 -0
- package/src/mcp/transports/stdio.ts +37 -12
- package/src/modes/acp/acp-event-mapper.ts +7 -7
- package/src/modes/components/assistant-message.ts +3 -2
- package/src/modes/components/hook-selector.ts +149 -14
- package/src/modes/components/session-selector.ts +36 -5
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/todo-reminder.ts +1 -1
- package/src/modes/components/tool-execution.ts +9 -4
- package/src/modes/components/transcript-container.ts +91 -0
- package/src/modes/controllers/event-controller.ts +12 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +18 -6
- package/src/modes/print-mode.ts +5 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/theme.ts +48 -8
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/plan-mode/plan-handoff.ts +37 -0
- package/src/plan-mode/plan-protection.ts +31 -0
- package/src/priority.json +4 -0
- package/src/prompts/goals/goal-continuation.md +1 -1
- package/src/prompts/system/eager-todo.md +3 -3
- package/src/prompts/system/orchestrate-notice.md +5 -5
- package/src/prompts/system/plan-mode-approved.md +14 -17
- package/src/prompts/system/plan-mode-reference.md +3 -6
- package/src/prompts/system/subagent-system-prompt.md +11 -0
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/browser.md +5 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +3 -1
- package/src/prompts/tools/{todo-write.md → todo.md} +8 -0
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +29 -16
- package/src/session/indexed-session-storage.ts +430 -0
- package/src/session/redis-session-storage.ts +66 -377
- package/src/session/session-manager.ts +102 -22
- package/src/session/session-storage.ts +148 -68
- package/src/session/sql-session-storage.ts +131 -382
- package/src/slash-commands/helpers/todo.ts +2 -2
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +51 -1
- package/src/telemetry-export.ts +126 -0
- package/src/tiny/compiled-runtime.ts +179 -0
- package/src/tiny/worker.ts +24 -2
- package/src/tools/ask.ts +133 -87
- package/src/tools/fetch.ts +17 -4
- package/src/tools/find.ts +2 -2
- package/src/tools/index.ts +6 -4
- package/src/tools/path-utils.ts +16 -7
- package/src/tools/renderers.ts +2 -2
- package/src/tools/search.ts +6 -3
- package/src/tools/{todo-write.ts → todo.ts} +32 -35
- package/src/utils/jj.ts +28 -5
- package/src/utils/session-color.ts +39 -14
- package/src/web/search/index.ts +1 -1
- package/src/web/search/provider.ts +18 -34
- package/src/web/search/types.ts +73 -32
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
IndexedSessionStorage,
|
|
3
|
+
type SessionStorageBackend,
|
|
4
|
+
type SessionStorageIndexEntry,
|
|
5
|
+
} from "./indexed-session-storage";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Supported `bun:sql` adapter dialects. `Bun.SQL` reports this string on
|
|
6
9
|
* `client.options.adapter`; we detect it once at construction and pick the
|
|
7
|
-
* correct DDL / upsert / concat syntax for the underlying engine.
|
|
10
|
+
* correct DDL / upsert / concat / byte-slice syntax for the underlying engine.
|
|
8
11
|
*/
|
|
9
12
|
export type SqlSessionStorageAdapter = "postgres" | "mysql" | "sqlite";
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Minimal subset of the `Bun.SQL` instance surface used by
|
|
13
|
-
* {@link SqlSessionStorage}.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* {@link SqlSessionStorage}. Bun's SQL client exposes a tagged-template API too,
|
|
17
|
+
* but this implementation intentionally uses `unsafe(query, values)` because
|
|
18
|
+
* the table identifier is validated and then inlined while values remain bound
|
|
19
|
+
* parameters.
|
|
17
20
|
*/
|
|
18
21
|
export interface SqlSessionStorageClient {
|
|
19
22
|
unsafe(query: string, values?: unknown[]): Promise<unknown[]>;
|
|
@@ -50,11 +53,6 @@ export interface SqlSessionStorageOptions {
|
|
|
50
53
|
createTable?: boolean;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
interface MirrorEntry {
|
|
54
|
-
content: string;
|
|
55
|
-
mtimeMs: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
56
|
interface DialectQueries {
|
|
59
57
|
createTable: string;
|
|
60
58
|
/** Insert or replace the full content for `path`. Used for `writeText`/`flags="w"` truncate. */
|
|
@@ -63,18 +61,34 @@ interface DialectQueries {
|
|
|
63
61
|
upsertAppend: string;
|
|
64
62
|
/** Delete a single row by path. */
|
|
65
63
|
delete: string;
|
|
66
|
-
/** Delete every row whose `path` starts with the supplied LIKE pattern. */
|
|
67
|
-
deletePrefix: string;
|
|
68
64
|
/** Move a row from one path to another (caller deletes any conflicting destination first). */
|
|
69
65
|
rename: string;
|
|
70
|
-
/**
|
|
71
|
-
|
|
66
|
+
/** Warm the synchronous index without transferring full content. */
|
|
67
|
+
loadIndex: string;
|
|
68
|
+
/** Read the full content for the async `readText` surface. */
|
|
69
|
+
readFull: string;
|
|
70
|
+
/** Read bounded byte windows from the head and tail of the content. */
|
|
71
|
+
readSlices: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface IndexRow {
|
|
75
|
+
path: string;
|
|
76
|
+
byte_len: number | bigint | string;
|
|
77
|
+
mtime_ms: number | bigint | string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ContentRow {
|
|
81
|
+
content: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface SliceRow {
|
|
85
|
+
head: unknown;
|
|
86
|
+
tail: unknown;
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
const DEFAULT_TABLE = "omp_session_files";
|
|
75
90
|
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
76
|
-
const
|
|
77
|
-
const LIKE_ESCAPE_RE = /[%_#]/g;
|
|
91
|
+
const utf8Decoder = new TextDecoder("utf-8");
|
|
78
92
|
|
|
79
93
|
function enoent(p: string): NodeJS.ErrnoException {
|
|
80
94
|
const err = new Error(`ENOENT: no such file, '${p}'`) as NodeJS.ErrnoException;
|
|
@@ -85,16 +99,6 @@ function enoent(p: string): NodeJS.ErrnoException {
|
|
|
85
99
|
return err;
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
function matchesGlob(name: string, pattern: string): boolean {
|
|
89
|
-
if (pattern === "*") return true;
|
|
90
|
-
if (pattern.startsWith("*.")) return name.endsWith(pattern.slice(1));
|
|
91
|
-
return name === pattern;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function escapeLikeLiteral(value: string): string {
|
|
95
|
-
return value.replace(LIKE_ESCAPE_RE, ch => `${LIKE_ESCAPE_CHAR}${ch}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
102
|
function detectAdapter(client: SqlSessionStorageClient): SqlSessionStorageAdapter {
|
|
99
103
|
const reported = String(client.options?.adapter ?? "").toLowerCase();
|
|
100
104
|
if (reported === "postgres" || reported === "postgresql" || reported === "pg") return "postgres";
|
|
@@ -124,17 +128,30 @@ function buildQueries(adapter: SqlSessionStorageAdapter, table: string): Dialect
|
|
|
124
128
|
`INSERT INTO ${table} (path, content, mtime_ms) VALUES (?, ?, ?) ` +
|
|
125
129
|
`ON DUPLICATE KEY UPDATE content = CONCAT(content, VALUES(content)), mtime_ms = VALUES(mtime_ms)`,
|
|
126
130
|
delete: `DELETE FROM ${table} WHERE path = ?`,
|
|
127
|
-
deletePrefix: `DELETE FROM ${table} WHERE path LIKE ? ESCAPE '${LIKE_ESCAPE_CHAR}'`,
|
|
128
131
|
rename: `UPDATE ${table} SET path = ?, mtime_ms = ? WHERE path = ?`,
|
|
129
|
-
|
|
132
|
+
loadIndex: `SELECT path, mtime_ms, length(content) AS byte_len FROM ${table}`,
|
|
133
|
+
readFull: `SELECT content AS content FROM ${table} WHERE path = ?`,
|
|
134
|
+
readSlices:
|
|
135
|
+
`SELECT substring(cast(content AS binary), 1, ?) AS head, ` +
|
|
136
|
+
`CASE WHEN ? <= 0 THEN cast('' AS binary) ` +
|
|
137
|
+
`ELSE substring(cast(content AS binary), greatest(1, length(content) - ? + 1)) END AS tail ` +
|
|
138
|
+
`FROM ${table} WHERE path = ?`,
|
|
130
139
|
};
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
// PostgreSQL + SQLite — both support `ON CONFLICT(path) DO UPDATE …` and
|
|
134
|
-
// `||` for string concatenation. The `excluded` keyword references the
|
|
135
|
-
// row that would have been inserted, in both engines.
|
|
136
142
|
const mtimeType = adapter === "postgres" ? "BIGINT" : "INTEGER";
|
|
137
143
|
const tableQualifier = `${table}.content`;
|
|
144
|
+
const byteLengthExpr = adapter === "postgres" ? "octet_length(content)" : "length(cast(content AS blob))";
|
|
145
|
+
const readSlices =
|
|
146
|
+
adapter === "postgres"
|
|
147
|
+
? `SELECT substring(convert_to(content, 'UTF8') from 1 for ${placeholder(1)}) AS head, ` +
|
|
148
|
+
`CASE WHEN ${placeholder(2)} <= 0 THEN ''::bytea ` +
|
|
149
|
+
`ELSE substring(convert_to(content, 'UTF8') from greatest(1, octet_length(content) - ${placeholder(2)} + 1)) END AS tail ` +
|
|
150
|
+
`FROM ${table} WHERE path = ${placeholder(3)}`
|
|
151
|
+
: `SELECT substr(cast(content AS blob), 1, ?) AS head, ` +
|
|
152
|
+
`CASE WHEN ? <= 0 THEN x'' ELSE substr(cast(content AS blob), -?) END AS tail ` +
|
|
153
|
+
`FROM ${table} WHERE path = ?`;
|
|
154
|
+
|
|
138
155
|
return {
|
|
139
156
|
createTable:
|
|
140
157
|
`CREATE TABLE IF NOT EXISTS ${table} (` +
|
|
@@ -151,78 +168,56 @@ function buildQueries(adapter: SqlSessionStorageAdapter, table: string): Dialect
|
|
|
151
168
|
`VALUES (${placeholder(1)}, ${placeholder(2)}, ${placeholder(3)}) ` +
|
|
152
169
|
`ON CONFLICT (path) DO UPDATE SET content = ${tableQualifier} || excluded.content, mtime_ms = excluded.mtime_ms`,
|
|
153
170
|
delete: `DELETE FROM ${table} WHERE path = ${placeholder(1)}`,
|
|
154
|
-
deletePrefix: `DELETE FROM ${table} WHERE path LIKE ${placeholder(1)} ESCAPE '${LIKE_ESCAPE_CHAR}'`,
|
|
155
171
|
rename: `UPDATE ${table} SET path = ${placeholder(1)}, mtime_ms = ${placeholder(2)} WHERE path = ${placeholder(3)}`,
|
|
156
|
-
|
|
172
|
+
loadIndex: `SELECT path, mtime_ms, ${byteLengthExpr} AS byte_len FROM ${table}`,
|
|
173
|
+
readFull: `SELECT content AS content FROM ${table} WHERE path = ${placeholder(1)}`,
|
|
174
|
+
readSlices,
|
|
157
175
|
};
|
|
158
176
|
}
|
|
159
177
|
|
|
160
|
-
|
|
161
|
-
path: string;
|
|
162
|
-
content: string;
|
|
163
|
-
mtime_ms: number | bigint | string;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function rowMtime(value: number | bigint | string): number {
|
|
178
|
+
function rowNumber(value: number | bigint | string): number {
|
|
167
179
|
if (typeof value === "number") return value;
|
|
168
180
|
if (typeof value === "bigint") return Number(value);
|
|
169
181
|
return Number.parseInt(value, 10);
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
function decodeSqlBytes(value: unknown): string {
|
|
185
|
+
if (value === null || value === undefined) return "";
|
|
186
|
+
if (typeof value === "string") return value;
|
|
187
|
+
if (value instanceof Uint8Array) return utf8Decoder.decode(value);
|
|
188
|
+
if (value instanceof ArrayBuffer) return utf8Decoder.decode(new Uint8Array(value));
|
|
189
|
+
return String(value);
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
/**
|
|
173
193
|
* SQL-backed implementation of {@link SessionStorage} using `bun:sql`. Each
|
|
174
|
-
* session JSONL file maps to a row keyed by `path`; one table stores
|
|
175
|
-
*
|
|
194
|
+
* session JSONL file maps to a row keyed by `path`; one table stores the file
|
|
195
|
+
* contents while this process keeps only a metadata index (`size`, `mtimeMs`) in
|
|
196
|
+
* memory for synchronous `existsSync` / `statSync` / `listFilesSync` calls.
|
|
176
197
|
*
|
|
177
198
|
* Works against PostgreSQL, MySQL/MariaDB, and SQLite by selecting the
|
|
178
|
-
* dialect-correct DDL, upsert,
|
|
179
|
-
*
|
|
180
|
-
* Trade-offs vs `FileSessionStorage`:
|
|
181
|
-
* - An in-memory mirror is loaded on construction so the interface's
|
|
182
|
-
* synchronous methods (`existsSync`, `statSync`, `listFilesSync`, …) keep
|
|
183
|
-
* their contracts; `bun:sql` is async only. Mirror state is process-local,
|
|
184
|
-
* matching `FileSessionStorage`'s existing single-writer assumption — peer
|
|
185
|
-
* processes need {@link refresh} to pick up out-of-band writes.
|
|
186
|
-
* - `writeLineSync` updates the mirror synchronously and queues an async
|
|
187
|
-
* upsert that appends the line to the existing row (or inserts it as the
|
|
188
|
-
* first chunk). The promise is awaited by `flush()` / `close()` /
|
|
189
|
-
* {@link drain}. A SIGKILL between the sync mirror update and the network
|
|
190
|
-
* round-trip loses the last line.
|
|
191
|
-
* - Blobs (image data) and tool artifact files still live on disk via
|
|
192
|
-
* `BlobStore` / `ArtifactManager`. Those are out of scope for this storage.
|
|
199
|
+
* dialect-correct DDL, upsert, string-concat, byte-length, and byte-slice syntax
|
|
200
|
+
* at construction.
|
|
193
201
|
*/
|
|
194
|
-
export class SqlSessionStorage
|
|
195
|
-
readonly #client: SqlSessionStorageClient;
|
|
202
|
+
export class SqlSessionStorage extends IndexedSessionStorage {
|
|
196
203
|
readonly #adapter: SqlSessionStorageAdapter;
|
|
197
204
|
readonly #table: string;
|
|
198
|
-
readonly #q: DialectQueries;
|
|
199
|
-
readonly #mirror = new Map<string, MirrorEntry>();
|
|
200
|
-
readonly #writers = new Set<SqlSessionStorageWriter>();
|
|
201
|
-
#nextMtimeMs = 0;
|
|
202
|
-
#pendingTail: Promise<void> = Promise.resolve();
|
|
203
205
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
this.#adapter =
|
|
207
|
-
const table = options.table ?? DEFAULT_TABLE;
|
|
208
|
-
if (!IDENT_RE.test(table)) {
|
|
209
|
-
throw new Error(`SqlSessionStorage: table name must match ${IDENT_RE.source} (got ${JSON.stringify(table)})`);
|
|
210
|
-
}
|
|
206
|
+
constructor(backend: SessionStorageBackend, adapter: SqlSessionStorageAdapter, table: string) {
|
|
207
|
+
super(backend);
|
|
208
|
+
this.#adapter = adapter;
|
|
211
209
|
this.#table = table;
|
|
212
|
-
this.#q = buildQueries(this.#adapter, table);
|
|
213
210
|
}
|
|
214
211
|
|
|
215
212
|
/**
|
|
216
|
-
* Apply the dialect-correct DDL (unless `createTable: false` is set) and
|
|
217
|
-
*
|
|
218
|
-
*
|
|
213
|
+
* Apply the dialect-correct DDL (unless `createTable: false` is set) and warm
|
|
214
|
+
* the metadata index with every existing row. Must be awaited before passing
|
|
215
|
+
* the storage into `SessionManager.create()`.
|
|
219
216
|
*/
|
|
220
217
|
static async create(options: SqlSessionStorageOptions): Promise<SqlSessionStorage> {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
await storage.refresh();
|
|
218
|
+
const backend = new SqlSessionStorageBackend(options);
|
|
219
|
+
const storage = new SqlSessionStorage(backend, backend.adapter, backend.table);
|
|
220
|
+
await storage.initialize();
|
|
226
221
|
return storage;
|
|
227
222
|
}
|
|
228
223
|
|
|
@@ -233,333 +228,87 @@ export class SqlSessionStorage implements SessionStorage {
|
|
|
233
228
|
get table(): string {
|
|
234
229
|
return this.#table;
|
|
235
230
|
}
|
|
231
|
+
}
|
|
236
232
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
this.#mirror.clear();
|
|
244
|
-
const rows = (await this.#client.unsafe(this.#q.selectAll)) as DbRow[];
|
|
245
|
-
for (const row of rows) {
|
|
246
|
-
const mtimeMs = rowMtime(row.mtime_ms);
|
|
247
|
-
this.#mirror.set(row.path, { content: row.content, mtimeMs });
|
|
248
|
-
if (mtimeMs > this.#nextMtimeMs) this.#nextMtimeMs = mtimeMs;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Resolve once every pending background write (issued via `writeTextSync`
|
|
254
|
-
* or `writer.writeLineSync`) has been acknowledged by the database.
|
|
255
|
-
* Throws if any background write failed since the last drain. Call on
|
|
256
|
-
* graceful shutdown to avoid losing the last unflushed line.
|
|
257
|
-
*/
|
|
258
|
-
async drain(): Promise<void> {
|
|
259
|
-
// Take ownership of the current tail, then reset so subsequent
|
|
260
|
-
// operations start from a clean (resolved) chain. Without the reset,
|
|
261
|
-
// any failure observed here would also be re-thrown by every later
|
|
262
|
-
// write that piggybacks on the tail via `#trackPending`.
|
|
263
|
-
const tail = this.#pendingTail;
|
|
264
|
-
this.#pendingTail = Promise.resolve();
|
|
265
|
-
await tail;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Allocate a strictly monotonic mtime. Two writes within the same
|
|
270
|
-
* millisecond would otherwise yield identical `mtimeMs` values and break
|
|
271
|
-
* `getSortedSessions`' newest-first ordering.
|
|
272
|
-
*/
|
|
273
|
-
#allocMtimeMs(): number {
|
|
274
|
-
const now = Date.now();
|
|
275
|
-
const next = now > this.#nextMtimeMs ? now : this.#nextMtimeMs + 1;
|
|
276
|
-
this.#nextMtimeMs = next;
|
|
277
|
-
return next;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
#trackPending(promise: Promise<void>): void {
|
|
281
|
-
// `Promise.all` rejects when either input rejects, which is exactly
|
|
282
|
-
// what we want for `drain()`. The follow-up `.catch(() => {})` only
|
|
283
|
-
// silences the unhandled-rejection signal on the shared tail —
|
|
284
|
-
// `drain()` keeps its own handler chain and still observes the
|
|
285
|
-
// original error, because rejection delivery is per-handler-chain.
|
|
286
|
-
this.#pendingTail = Promise.all([this.#pendingTail, promise]).then(() => {});
|
|
287
|
-
this.#pendingTail.catch(() => {});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// --- sync surface ---------------------------------------------------------
|
|
291
|
-
|
|
292
|
-
ensureDirSync(_dir: string): void {
|
|
293
|
-
// SQL is flat: directories are derived from key prefixes.
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
existsSync(path: string): boolean {
|
|
297
|
-
return this.#mirror.has(path);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
writeTextSync(path: string, content: string): void {
|
|
301
|
-
const mtimeMs = this.#allocMtimeMs();
|
|
302
|
-
this.#mirror.set(path, { content, mtimeMs });
|
|
303
|
-
this.#trackPending(this.#upsertReplace(path, content, mtimeMs));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
readTextSync(path: string): string {
|
|
307
|
-
const entry = this.#mirror.get(path);
|
|
308
|
-
if (!entry) throw enoent(path);
|
|
309
|
-
return entry.content;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
statSync(path: string): SessionStorageStat {
|
|
313
|
-
const entry = this.#mirror.get(path);
|
|
314
|
-
if (!entry) throw enoent(path);
|
|
315
|
-
return {
|
|
316
|
-
size: Buffer.byteLength(entry.content, "utf-8"),
|
|
317
|
-
mtimeMs: entry.mtimeMs,
|
|
318
|
-
mtime: new Date(entry.mtimeMs),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
233
|
+
class SqlSessionStorageBackend implements SessionStorageBackend {
|
|
234
|
+
readonly #client: SqlSessionStorageClient;
|
|
235
|
+
readonly #adapter: SqlSessionStorageAdapter;
|
|
236
|
+
readonly #table: string;
|
|
237
|
+
readonly #q: DialectQueries;
|
|
238
|
+
readonly #createTable: boolean;
|
|
321
239
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (name.includes("/")) continue;
|
|
329
|
-
if (!matchesGlob(name, pattern)) continue;
|
|
330
|
-
out.push(path);
|
|
240
|
+
constructor(options: SqlSessionStorageOptions) {
|
|
241
|
+
this.#client = options.client;
|
|
242
|
+
this.#adapter = options.adapter ?? detectAdapter(options.client);
|
|
243
|
+
const table = options.table ?? DEFAULT_TABLE;
|
|
244
|
+
if (!IDENT_RE.test(table)) {
|
|
245
|
+
throw new Error(`SqlSessionStorage: table name must match ${IDENT_RE.source} (got ${JSON.stringify(table)})`);
|
|
331
246
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// --- async surface --------------------------------------------------------
|
|
336
|
-
|
|
337
|
-
async exists(path: string): Promise<boolean> {
|
|
338
|
-
return this.#mirror.has(path);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async readText(path: string): Promise<string> {
|
|
342
|
-
const entry = this.#mirror.get(path);
|
|
343
|
-
if (!entry) throw enoent(path);
|
|
344
|
-
return entry.content;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async readTextPrefix(path: string, maxBytes: number): Promise<string> {
|
|
348
|
-
const entry = this.#mirror.get(path);
|
|
349
|
-
if (!entry) throw enoent(path);
|
|
350
|
-
if (maxBytes <= 0) return "";
|
|
351
|
-
// `entry.content` is a JS string (UTF-16 code units); the prefix
|
|
352
|
-
// contract is byte-oriented. Encode to UTF-8, slice, then decode —
|
|
353
|
-
// matching `peekFile`'s behaviour for the file-backed storage.
|
|
354
|
-
const bytes = Buffer.from(entry.content, "utf-8");
|
|
355
|
-
const slice = bytes.subarray(0, Math.min(maxBytes, bytes.byteLength));
|
|
356
|
-
return slice.toString("utf-8");
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async writeText(path: string, content: string): Promise<void> {
|
|
360
|
-
const mtimeMs = this.#allocMtimeMs();
|
|
361
|
-
this.#mirror.set(path, { content, mtimeMs });
|
|
362
|
-
await this.#upsertReplace(path, content, mtimeMs);
|
|
247
|
+
this.#table = table;
|
|
248
|
+
this.#q = buildQueries(this.#adapter, table);
|
|
249
|
+
this.#createTable = options.createTable !== false;
|
|
363
250
|
}
|
|
364
251
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (!entry) throw enoent(src);
|
|
368
|
-
// Update the mirror first so a synchronous existsSync() right after
|
|
369
|
-
// the await resolves consistently. If the DB update fails the mirror
|
|
370
|
-
// is rolled back below.
|
|
371
|
-
const dstPrev = this.#mirror.get(dst);
|
|
372
|
-
this.#mirror.delete(src);
|
|
373
|
-
this.#mirror.set(dst, entry);
|
|
374
|
-
|
|
375
|
-
try {
|
|
376
|
-
// `fs.promises.rename` overwrites the destination when one
|
|
377
|
-
// exists; mirror that here so the JSONL atomic-rewrite flow
|
|
378
|
-
// (temp file → rename) keeps working unchanged.
|
|
379
|
-
if (dstPrev !== undefined) {
|
|
380
|
-
await this.#client.unsafe(this.#q.delete, [dst]);
|
|
381
|
-
}
|
|
382
|
-
await this.#client.unsafe(this.#q.rename, [dst, entry.mtimeMs, src]);
|
|
383
|
-
} catch (err) {
|
|
384
|
-
this.#mirror.delete(dst);
|
|
385
|
-
if (dstPrev !== undefined) this.#mirror.set(dst, dstPrev);
|
|
386
|
-
this.#mirror.set(src, entry);
|
|
387
|
-
throw toError(err);
|
|
388
|
-
}
|
|
252
|
+
get adapter(): SqlSessionStorageAdapter {
|
|
253
|
+
return this.#adapter;
|
|
389
254
|
}
|
|
390
255
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
await this.#client.unsafe(this.#q.delete, [path]);
|
|
394
|
-
if (!existed) {
|
|
395
|
-
throw enoent(path);
|
|
396
|
-
}
|
|
256
|
+
get table(): string {
|
|
257
|
+
return this.#table;
|
|
397
258
|
}
|
|
398
259
|
|
|
399
|
-
async
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// Tool artifact bytes don't live in SQL (the file-backed
|
|
403
|
-
// `ArtifactManager` keeps them on disk), but a draft sidecar may
|
|
404
|
-
// have been written through `writeText` under the artifacts
|
|
405
|
-
// directory prefix. Sweep those keys in one statement.
|
|
406
|
-
const artifactsDir = sessionPath.slice(0, -6);
|
|
407
|
-
const prefix = artifactsDir.endsWith("/") ? artifactsDir : `${artifactsDir}/`;
|
|
408
|
-
|
|
409
|
-
const victims: string[] = [];
|
|
410
|
-
for (const key of this.#mirror.keys()) {
|
|
411
|
-
if (key.startsWith(prefix)) victims.push(key);
|
|
412
|
-
}
|
|
413
|
-
if (victims.length === 0) return;
|
|
414
|
-
|
|
415
|
-
for (const key of victims) this.#mirror.delete(key);
|
|
416
|
-
const likePattern = `${escapeLikeLiteral(prefix)}%`;
|
|
417
|
-
try {
|
|
418
|
-
await this.#client.unsafe(this.#q.deletePrefix, [likePattern]);
|
|
419
|
-
} catch (err) {
|
|
420
|
-
logger.warn("SQL session storage artifact sweep failed", {
|
|
421
|
-
sessionPath,
|
|
422
|
-
prefix,
|
|
423
|
-
error: toError(err).message,
|
|
424
|
-
});
|
|
425
|
-
throw toError(err);
|
|
260
|
+
async init(): Promise<void> {
|
|
261
|
+
if (this.#createTable) {
|
|
262
|
+
await this.#client.unsafe(this.#q.createTable);
|
|
426
263
|
}
|
|
427
264
|
}
|
|
428
265
|
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
266
|
+
async loadIndex(): Promise<SessionStorageIndexEntry[]> {
|
|
267
|
+
const rows = (await this.#client.unsafe(this.#q.loadIndex)) as IndexRow[];
|
|
268
|
+
return rows.map(row => ({
|
|
269
|
+
path: row.path,
|
|
270
|
+
size: rowNumber(row.byte_len),
|
|
271
|
+
mtimeMs: rowNumber(row.mtime_ms),
|
|
272
|
+
}));
|
|
433
273
|
}
|
|
434
274
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
_mirrorAppend(path: string, line: string): { content: string; mtimeMs: number } {
|
|
442
|
-
const existing = this.#mirror.get(path);
|
|
443
|
-
const content = existing ? existing.content + line : line;
|
|
444
|
-
const mtimeMs = this.#allocMtimeMs();
|
|
445
|
-
this.#mirror.set(path, { content, mtimeMs });
|
|
446
|
-
return { content, mtimeMs };
|
|
275
|
+
async readFull(path: string): Promise<string | null> {
|
|
276
|
+
const rows = (await this.#client.unsafe(this.#q.readFull, [path])) as ContentRow[];
|
|
277
|
+
const row = rows[0];
|
|
278
|
+
return row ? row.content : null;
|
|
447
279
|
}
|
|
448
280
|
|
|
449
|
-
|
|
450
|
-
|
|
281
|
+
async readSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]> {
|
|
282
|
+
const values =
|
|
283
|
+
this.#adapter === "postgres"
|
|
284
|
+
? [prefixBytes, suffixBytes, path]
|
|
285
|
+
: [prefixBytes, suffixBytes, suffixBytes, path];
|
|
286
|
+
const rows = (await this.#client.unsafe(this.#q.readSlices, values)) as SliceRow[];
|
|
287
|
+
const row = rows[0];
|
|
288
|
+
if (!row) throw enoent(path);
|
|
289
|
+
return [decodeSqlBytes(row.head), decodeSqlBytes(row.tail)];
|
|
451
290
|
}
|
|
452
291
|
|
|
453
|
-
async
|
|
454
|
-
const entry = this.#mirror.get(path);
|
|
455
|
-
const mtimeMs = entry?.mtimeMs ?? this.#allocMtimeMs();
|
|
456
|
-
await this.#upsertReplace(path, "", mtimeMs);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Append a chunk to the row at `path`, inserting if the row doesn't
|
|
461
|
-
* exist yet. Single round-trip via the dialect-specific `upsertAppend`.
|
|
462
|
-
*/
|
|
463
|
-
async _remoteAppend(path: string, line: string, mtimeMs: number): Promise<void> {
|
|
464
|
-
await this.#client.unsafe(this.#q.upsertAppend, [path, line, mtimeMs]);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
_attachPending(promise: Promise<void>): void {
|
|
468
|
-
this.#trackPending(promise);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async #upsertReplace(path: string, content: string, mtimeMs: number): Promise<void> {
|
|
292
|
+
async writeFull(path: string, content: string, mtimeMs: number): Promise<void> {
|
|
472
293
|
await this.#client.unsafe(this.#q.upsertReplace, [path, content, mtimeMs]);
|
|
473
294
|
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
class SqlSessionStorageWriter implements SessionStorageWriter {
|
|
477
|
-
#storage: SqlSessionStorage;
|
|
478
|
-
#path: string;
|
|
479
|
-
#closed = false;
|
|
480
|
-
#error: Error | undefined;
|
|
481
|
-
#onError: ((err: Error) => void) | undefined;
|
|
482
|
-
#pendingChain: Promise<void> = Promise.resolve();
|
|
483
|
-
|
|
484
|
-
constructor(
|
|
485
|
-
storage: SqlSessionStorage,
|
|
486
|
-
path: string,
|
|
487
|
-
options?: { flags?: "a" | "w"; onError?: (err: Error) => void },
|
|
488
|
-
) {
|
|
489
|
-
this.#storage = storage;
|
|
490
|
-
this.#path = path;
|
|
491
|
-
this.#onError = options?.onError;
|
|
492
|
-
const flags = options?.flags ?? "a";
|
|
493
|
-
if (flags === "w") {
|
|
494
|
-
// Mirror `FileSessionStorageWriter`'s `flags: "w"` contract by
|
|
495
|
-
// truncating both the mirror and the underlying row immediately.
|
|
496
|
-
storage._mirrorTruncate(path);
|
|
497
|
-
this.#enqueueRaw(() => storage._remoteTruncate(path));
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
#recordError(err: unknown): Error {
|
|
502
|
-
const error = toError(err);
|
|
503
|
-
if (!this.#error) this.#error = error;
|
|
504
|
-
this.#onError?.(error);
|
|
505
|
-
return error;
|
|
506
|
-
}
|
|
507
295
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (this.#error) throw this.#error;
|
|
511
|
-
try {
|
|
512
|
-
await task();
|
|
513
|
-
} catch (err) {
|
|
514
|
-
throw this.#recordError(err);
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
this.#pendingChain = next.catch(() => {
|
|
518
|
-
// Errors are recorded on `this.#error`; subsequent enqueues
|
|
519
|
-
// throw from inside the wrapper above. The outer chain swallows
|
|
520
|
-
// to avoid surfacing as an unhandled promise rejection.
|
|
521
|
-
});
|
|
522
|
-
this.#storage._attachPending(next);
|
|
523
|
-
return next;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
writeLineSync(line: string): void {
|
|
527
|
-
if (this.#closed) throw new Error("Writer closed");
|
|
528
|
-
if (this.#error) throw this.#error;
|
|
529
|
-
const { mtimeMs } = this.#storage._mirrorAppend(this.#path, line);
|
|
530
|
-
this.#enqueueRaw(() => this.#storage._remoteAppend(this.#path, line, mtimeMs));
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
async writeLine(line: string): Promise<void> {
|
|
534
|
-
if (this.#closed) throw new Error("Writer closed");
|
|
535
|
-
if (this.#error) throw this.#error;
|
|
536
|
-
const { mtimeMs } = this.#storage._mirrorAppend(this.#path, line);
|
|
537
|
-
await this.#enqueueRaw(() => this.#storage._remoteAppend(this.#path, line, mtimeMs));
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
async flush(): Promise<void> {
|
|
541
|
-
if (this.#error) throw this.#error;
|
|
542
|
-
await this.#enqueueRaw(async () => {});
|
|
543
|
-
if (this.#error) throw this.#error;
|
|
296
|
+
async append(path: string, line: string, mtimeMs: number): Promise<void> {
|
|
297
|
+
await this.#client.unsafe(this.#q.upsertAppend, [path, line, mtimeMs]);
|
|
544
298
|
}
|
|
545
299
|
|
|
546
|
-
async
|
|
547
|
-
|
|
548
|
-
// flush() already drains every queued statement.
|
|
549
|
-
await this.flush();
|
|
300
|
+
async truncate(path: string, mtimeMs: number): Promise<void> {
|
|
301
|
+
await this.writeFull(path, "", mtimeMs);
|
|
550
302
|
}
|
|
551
303
|
|
|
552
|
-
async
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
try {
|
|
556
|
-
await this.flush();
|
|
557
|
-
} finally {
|
|
558
|
-
this.#storage._writerClosed(this);
|
|
304
|
+
async remove(paths: string[]): Promise<void> {
|
|
305
|
+
for (const path of paths) {
|
|
306
|
+
await this.#client.unsafe(this.#q.delete, [path]);
|
|
559
307
|
}
|
|
560
308
|
}
|
|
561
309
|
|
|
562
|
-
|
|
563
|
-
|
|
310
|
+
async move(src: string, dst: string, mtimeMs: number): Promise<void> {
|
|
311
|
+
await this.#client.unsafe(this.#q.delete, [dst]);
|
|
312
|
+
await this.#client.unsafe(this.#q.rename, [dst, mtimeMs, src]);
|
|
564
313
|
}
|
|
565
314
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import type { TodoPhase } from "../../tools/todo
|
|
2
|
+
import type { TodoPhase } from "../../tools/todo";
|
|
3
3
|
import {
|
|
4
4
|
applyOpsToPhases,
|
|
5
5
|
getLatestTodoPhasesFromEntries,
|
|
6
6
|
markdownToPhases,
|
|
7
7
|
phasesToMarkdown,
|
|
8
8
|
USER_TODO_EDIT_CUSTOM_TYPE,
|
|
9
|
-
} from "../../tools/todo
|
|
9
|
+
} from "../../tools/todo";
|
|
10
10
|
import type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime } from "../types";
|
|
11
11
|
import { commandConsumed, parseSubcommand, usage } from "./parse";
|
|
12
12
|
|
package/src/task/executor.ts
CHANGED
|
@@ -147,6 +147,12 @@ export interface ExecutorOptions {
|
|
|
147
147
|
task: string;
|
|
148
148
|
assignment?: string;
|
|
149
149
|
context?: string;
|
|
150
|
+
/**
|
|
151
|
+
* The session's active overall plan, handed off so subagents spawned during
|
|
152
|
+
* plan execution share the same plan context as the main agent. Omitted when
|
|
153
|
+
* the session did not start with a plan (or while plan mode is still active).
|
|
154
|
+
*/
|
|
155
|
+
planReference?: { path: string; content: string };
|
|
150
156
|
description?: string;
|
|
151
157
|
index: number;
|
|
152
158
|
id: string;
|
|
@@ -1230,6 +1236,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1230
1236
|
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
|
|
1231
1237
|
agent: agent.systemPrompt,
|
|
1232
1238
|
context: options.context?.trim() ?? "",
|
|
1239
|
+
planReference: options.planReference?.content ?? "",
|
|
1240
|
+
planReferencePath: options.planReference?.path ?? "",
|
|
1233
1241
|
worktree: worktree ?? "",
|
|
1234
1242
|
outputSchema: normalizedOutputSchema,
|
|
1235
1243
|
contextFile: contextFileForPrompt,
|
|
@@ -1276,7 +1284,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1276
1284
|
}
|
|
1277
1285
|
|
|
1278
1286
|
const subagentToolNames = session.getActiveToolNames();
|
|
1279
|
-
const parentOwnedToolNames = new Set(["
|
|
1287
|
+
const parentOwnedToolNames = new Set(["todo"]);
|
|
1280
1288
|
const filteredSubagentTools = subagentToolNames.filter(name => !parentOwnedToolNames.has(name));
|
|
1281
1289
|
if (filteredSubagentTools.length !== subagentToolNames.length) {
|
|
1282
1290
|
await awaitAbortable(session.setActiveToolsByName(filteredSubagentTools));
|