@oh-my-pi/pi-coding-agent 15.12.3 → 15.12.4
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 +43 -1
- package/dist/cli.js +1120 -870
- package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
- package/dist/types/cli/args.d.ts +0 -1
- package/dist/types/cli/models-cli.d.ts +49 -0
- package/dist/types/commands/launch.d.ts +0 -3
- package/dist/types/commands/models.d.ts +33 -0
- package/dist/types/commands/token.d.ts +25 -0
- package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
- package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
- package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
- package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
- package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/models-config-schema.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/edit/hashline/params.d.ts +1 -1
- package/dist/types/edit/modes/apply-patch.d.ts +1 -1
- package/dist/types/edit/modes/patch.d.ts +1 -1
- package/dist/types/edit/modes/replace.d.ts +1 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +2 -2
- package/dist/types/extensibility/hooks/types.d.ts +2 -2
- package/dist/types/goals/tools/goal-tool.d.ts +1 -1
- package/dist/types/lsp/types.d.ts +1 -1
- package/dist/types/mcp/manager.d.ts +8 -0
- package/dist/types/mnemopi/config.d.ts +28 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -2
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
- package/dist/types/modes/components/status-line/component.d.ts +9 -5
- package/dist/types/modes/components/status-line/types.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +0 -17
- package/dist/types/modes/interactive-mode.d.ts +0 -3
- package/dist/types/modes/types.d.ts +0 -5
- package/dist/types/session/agent-session.d.ts +14 -33
- package/dist/types/session/agent-storage.d.ts +2 -1
- package/dist/types/session/indexed-session-storage.d.ts +1 -0
- package/dist/types/session/messages.d.ts +8 -10
- package/dist/types/session/session-manager.d.ts +15 -0
- package/dist/types/session/session-storage.d.ts +5 -0
- package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
- package/dist/types/task/types.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +1 -1
- package/dist/types/tools/ast-edit.d.ts +1 -1
- package/dist/types/tools/ast-grep.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
- package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
- package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
- package/dist/types/tools/browser/registry.d.ts +16 -3
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/checkpoint.d.ts +1 -1
- package/dist/types/tools/debug.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/gh.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/inspect-image.d.ts +1 -1
- package/dist/types/tools/irc.d.ts +1 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/dist/types/tools/memory-recall.d.ts +1 -1
- package/dist/types/tools/memory-reflect.d.ts +1 -1
- package/dist/types/tools/memory-retain.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/render-mermaid.d.ts +1 -1
- package/dist/types/tools/resolve.d.ts +1 -1
- package/dist/types/tools/review.d.ts +1 -1
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +1 -1
- package/dist/types/tools/tts.d.ts +1 -1
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/clipboard.d.ts +4 -3
- package/dist/types/utils/image-loading.d.ts +18 -1
- package/dist/types/utils/thinking-display.d.ts +17 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/package.json +14 -14
- package/src/autoresearch/storage.ts +2 -1
- package/src/autoresearch/tools/init-experiment.ts +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +1 -1
- package/src/cli/args.ts +0 -8
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/models-cli.ts +427 -0
- package/src/cli-commands.ts +2 -0
- package/src/collab/host.ts +9 -12
- package/src/commands/launch.ts +0 -3
- package/src/commands/models.ts +61 -0
- package/src/commands/token.ts +89 -0
- package/src/commit/agentic/tools/analyze-file.ts +1 -1
- package/src/commit/agentic/tools/git-file-diff.ts +1 -1
- package/src/commit/agentic/tools/git-hunk.ts +1 -1
- package/src/commit/agentic/tools/git-overview.ts +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +1 -1
- package/src/commit/agentic/tools/propose-commit.ts +1 -1
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -1
- package/src/commit/agentic/tools/split-commit.ts +1 -1
- package/src/commit/analysis/summary.ts +1 -1
- package/src/commit/changelog/generate.ts +1 -1
- package/src/commit/shared-llm.ts +1 -1
- package/src/config/model-registry.ts +15 -12
- package/src/config/model-resolver.ts +2 -2
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +18 -0
- package/src/edit/hashline/params.ts +1 -1
- package/src/edit/modes/apply-patch.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/edit/modes/replace.ts +1 -1
- package/src/eval/agent-bridge.ts +1 -1
- package/src/eval/completion-bridge.ts +1 -1
- package/src/export/html/template.js +24 -2
- package/src/export/html/tool-views.generated.js +2 -2
- package/src/extensibility/custom-commands/loader.ts +1 -1
- package/src/extensibility/custom-commands/types.ts +2 -2
- package/src/extensibility/custom-tools/loader.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +2 -2
- package/src/extensibility/extensions/types.ts +2 -2
- package/src/extensibility/hooks/loader.ts +1 -1
- package/src/extensibility/hooks/types.ts +2 -2
- package/src/extensibility/skills.ts +18 -3
- package/src/goals/tools/goal-tool.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +5 -2
- package/src/lsp/types.ts +1 -1
- package/src/main.ts +0 -25
- package/src/mcp/config-writer.ts +7 -3
- package/src/mcp/manager.ts +11 -0
- package/src/memories/index.ts +3 -1
- package/src/memories/storage.ts +2 -1
- package/src/mnemopi/config.ts +95 -11
- package/src/modes/acp/acp-agent.ts +5 -48
- package/src/modes/acp/acp-event-mapper.ts +5 -1
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/assistant-message.ts +8 -7
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/logout-account-selector.ts +130 -0
- package/src/modes/components/mcp-add-wizard.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/status-line/component.ts +54 -157
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +2 -1
- package/src/modes/controllers/command-controller.ts +0 -12
- package/src/modes/controllers/event-controller.ts +23 -62
- package/src/modes/controllers/input-controller.ts +53 -30
- package/src/modes/controllers/mcp-command-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +56 -10
- package/src/modes/controllers/streaming-reveal.ts +4 -3
- package/src/modes/interactive-mode.ts +2 -8
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +0 -5
- package/src/modes/utils/ui-helpers.ts +2 -1
- package/src/prompts/system/empty-stop-retry.md +4 -6
- package/src/sdk.ts +15 -19
- package/src/session/agent-session.ts +125 -234
- package/src/session/agent-storage.ts +18 -9
- package/src/session/history-storage.ts +2 -1
- package/src/session/indexed-session-storage.ts +7 -0
- package/src/session/messages.ts +9 -11
- package/src/session/session-dump-format.ts +4 -2
- package/src/session/session-manager.ts +116 -0
- package/src/session/session-storage.ts +20 -0
- package/src/slash-commands/builtin-registry.ts +15 -1
- package/src/slash-commands/helpers/logout.ts +88 -0
- package/src/task/types.ts +1 -1
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +1 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
- package/src/tools/browser/cmux/rpc.ts +156 -0
- package/src/tools/browser/cmux/socket-client.ts +309 -0
- package/src/tools/browser/registry.ts +37 -3
- package/src/tools/browser/render.ts +6 -1
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +189 -18
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/browser.ts +16 -1
- package/src/tools/checkpoint.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +11 -6
- package/src/tools/fetch.ts +13 -2
- package/src/tools/find.ts +1 -1
- package/src/tools/gh.ts +1 -1
- package/src/tools/github-cache.ts +2 -1
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/inspect-image.ts +3 -1
- package/src/tools/irc.ts +1 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/memory-retain.ts +1 -1
- package/src/tools/read.ts +8 -2
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +1 -1
- package/src/tools/search-tool-bm25.ts +1 -1
- package/src/tools/search.ts +1 -1
- package/src/tools/ssh.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/tools/tts.ts +1 -1
- package/src/tools/write.ts +1 -1
- package/src/utils/clipboard.ts +35 -18
- package/src/utils/image-loading.ts +35 -4
- package/src/utils/thinking-display.ts +37 -0
- package/src/web/search/index.ts +1 -1
- package/dist/types/cli/list-models.d.ts +0 -30
- package/src/cli/list-models.ts +0 -194
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import {
|
|
5
5
|
type AuthCredential,
|
|
6
6
|
type AuthCredentialStore,
|
|
7
|
+
isSqliteBusyError,
|
|
7
8
|
SqliteAuthCredentialStore,
|
|
8
9
|
type StoredAuthCredential,
|
|
9
10
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -78,10 +79,14 @@ export class AgentStorage {
|
|
|
78
79
|
* AuthCredentialStore handles auth_credentials and cache tables.
|
|
79
80
|
*/
|
|
80
81
|
#initializeSchema(): void {
|
|
82
|
+
// Install the busy handler BEFORE any lock-taking statement (incl.
|
|
83
|
+
// `PRAGMA journal_mode=WAL`, which acquires an exclusive lock during WAL
|
|
84
|
+
// recovery). Without this, concurrent omp startups can crash here with
|
|
85
|
+
// `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY`. See issue #2421.
|
|
86
|
+
this.#db.run("PRAGMA busy_timeout = 5000");
|
|
81
87
|
this.#db.run(`
|
|
82
88
|
PRAGMA journal_mode=WAL;
|
|
83
89
|
PRAGMA synchronous=NORMAL;
|
|
84
|
-
PRAGMA busy_timeout=5000;
|
|
85
90
|
|
|
86
91
|
CREATE TABLE IF NOT EXISTS model_usage (
|
|
87
92
|
model_key TEXT PRIMARY KEY,
|
|
@@ -208,7 +213,8 @@ FROM model_usage_legacy
|
|
|
208
213
|
|
|
209
214
|
/**
|
|
210
215
|
* Returns singleton instance for the given database path, creating if needed.
|
|
211
|
-
* Retries on SQLITE_BUSY
|
|
216
|
+
* Retries on the `SQLITE_BUSY` family (including `SQLITE_BUSY_RECOVERY`) with
|
|
217
|
+
* exponential backoff. See issue #2421.
|
|
212
218
|
* @param dbPath - Path to the SQLite database file (defaults to config path)
|
|
213
219
|
* @returns AgentStorage instance for the given path
|
|
214
220
|
*/
|
|
@@ -216,7 +222,7 @@ FROM model_usage_legacy
|
|
|
216
222
|
const existing = instances.get(dbPath);
|
|
217
223
|
if (existing) return existing;
|
|
218
224
|
|
|
219
|
-
const maxRetries =
|
|
225
|
+
const maxRetries = 4;
|
|
220
226
|
const baseDelayMs = 100;
|
|
221
227
|
let lastError: Error | undefined;
|
|
222
228
|
|
|
@@ -226,17 +232,20 @@ FROM model_usage_legacy
|
|
|
226
232
|
instances.set(dbPath, storage);
|
|
227
233
|
return storage;
|
|
228
234
|
} catch (err) {
|
|
229
|
-
|
|
230
|
-
if (!isSqliteBusy) {
|
|
235
|
+
if (!isSqliteBusyError(err)) {
|
|
231
236
|
throw err;
|
|
232
237
|
}
|
|
233
|
-
lastError = err
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
239
|
+
if (attempt < maxRetries - 1) {
|
|
240
|
+
await Bun.sleep(baseDelayMs * 2 ** attempt);
|
|
241
|
+
}
|
|
236
242
|
}
|
|
237
243
|
}
|
|
238
244
|
|
|
239
|
-
throw
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Failed to open agent database at '${dbPath}' after ${maxRetries} attempts: ${lastError?.message}`,
|
|
247
|
+
{ cause: lastError },
|
|
248
|
+
);
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
/**
|
|
@@ -86,10 +86,11 @@ export class HistoryStorage {
|
|
|
86
86
|
|
|
87
87
|
const hasFts = this.#db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
|
|
88
88
|
|
|
89
|
+
// Install the busy handler BEFORE any lock-taking statement. See #2421.
|
|
90
|
+
this.#db.run("PRAGMA busy_timeout = 5000");
|
|
89
91
|
this.#db.run(`
|
|
90
92
|
PRAGMA journal_mode=WAL;
|
|
91
93
|
PRAGMA synchronous=NORMAL;
|
|
92
|
-
PRAGMA busy_timeout=5000;
|
|
93
94
|
|
|
94
95
|
CREATE TABLE IF NOT EXISTS history (
|
|
95
96
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -414,6 +414,13 @@ class IndexedSessionStorageWriter implements SessionStorageWriter {
|
|
|
414
414
|
await this.flush();
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
fsyncSync(): void {
|
|
418
|
+
// Indexed storage has no real fd to fsync; drain the pending chain
|
|
419
|
+
// synchronously is not possible, so this is a no-op. The async flush()
|
|
420
|
+
// above already ensures durability for the indexed backend.
|
|
421
|
+
if (this.#error) throw this.#error;
|
|
422
|
+
}
|
|
423
|
+
|
|
417
424
|
async close(): Promise<void> {
|
|
418
425
|
if (this.#closed) return;
|
|
419
426
|
this.#closed = true;
|
package/src/session/messages.ts
CHANGED
|
@@ -40,13 +40,11 @@ export interface SkillPromptDetails {
|
|
|
40
40
|
path: string;
|
|
41
41
|
args?: string;
|
|
42
42
|
lineCount: number;
|
|
43
|
-
/** Internal:
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
* via the `INTERNAL_DETAILS_FIELDS` allowlist below. */
|
|
49
|
-
__pendingDisplayTag?: string;
|
|
43
|
+
/** Internal: compact label shown for a queued custom message. Optional —
|
|
44
|
+
* non-streaming skill prompts never set it. Stripped from persisted
|
|
45
|
+
* `details` by `SessionManager.appendCustomMessageEntry` via the
|
|
46
|
+
* `INTERNAL_DETAILS_FIELDS` allowlist below. */
|
|
47
|
+
__queueChipText?: string;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
/** Sentinel value for `AssistantMessage.errorMessage` indicating that the abort
|
|
@@ -104,12 +102,12 @@ export function resolveAbortLabel(errorMessage: string | undefined, retryAttempt
|
|
|
104
102
|
return "Operation aborted";
|
|
105
103
|
}
|
|
106
104
|
|
|
107
|
-
/** Extract the optional `
|
|
105
|
+
/** Extract the optional `__queueChipText` field from a CustomMessage's
|
|
108
106
|
* `details` blob. Safe over `unknown`; returns undefined when the field is
|
|
109
107
|
* absent or non-string. */
|
|
110
|
-
export function
|
|
108
|
+
export function readQueueChipText(details: unknown): string | undefined {
|
|
111
109
|
if (typeof details !== "object" || details === null) return undefined;
|
|
112
|
-
const candidate = (details as {
|
|
110
|
+
const candidate = (details as { __queueChipText?: unknown }).__queueChipText;
|
|
113
111
|
return typeof candidate === "string" ? candidate : undefined;
|
|
114
112
|
}
|
|
115
113
|
|
|
@@ -118,7 +116,7 @@ export function readPendingDisplayTag(details: unknown): string | undefined {
|
|
|
118
116
|
* the CustomMessageEntry to disk. Scoped intentionally narrow: only fields
|
|
119
117
|
* declared here are stripped. Adding a new entry is a deliberate, reviewed
|
|
120
118
|
* change — unrelated future payload fields are never silently dropped. */
|
|
121
|
-
export const INTERNAL_DETAILS_FIELDS = ["
|
|
119
|
+
export const INTERNAL_DETAILS_FIELDS = ["__queueChipText"] as const;
|
|
122
120
|
|
|
123
121
|
/** Return a `details` copy with every key in `INTERNAL_DETAILS_FIELDS`
|
|
124
122
|
* removed. Returns the input unchanged when there is nothing to strip
|
|
@@ -5,6 +5,7 @@ import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
|
5
5
|
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
|
6
6
|
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
8
|
+
import { getVisibleThinkingText } from "../utils/thinking-display";
|
|
8
9
|
import {
|
|
9
10
|
type BashExecutionMessage,
|
|
10
11
|
type BranchSummaryMessage,
|
|
@@ -126,9 +127,10 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
|
|
|
126
127
|
if (c.type === "text") {
|
|
127
128
|
lines.push(c.text);
|
|
128
129
|
} else if (c.type === "thinking") {
|
|
129
|
-
|
|
130
|
+
const thinking = getVisibleThinkingText(c);
|
|
131
|
+
if (thinking.length === 0) continue;
|
|
130
132
|
lines.push("<thinking>");
|
|
131
|
-
lines.push(
|
|
133
|
+
lines.push(thinking);
|
|
132
134
|
lines.push("</thinking>\n");
|
|
133
135
|
} else if (c.type === "toolCall") {
|
|
134
136
|
lines.push(`<invoke name="${c.name}">`);
|
|
@@ -1527,6 +1527,17 @@ class NdjsonFileWriter {
|
|
|
1527
1527
|
}
|
|
1528
1528
|
}
|
|
1529
1529
|
|
|
1530
|
+
/** Synchronously fsync the underlying file descriptor to physical disk. */
|
|
1531
|
+
fsyncSync(): void {
|
|
1532
|
+
if (this.#closed) return;
|
|
1533
|
+
if (this.#error) throw this.#error;
|
|
1534
|
+
try {
|
|
1535
|
+
this.#writer.fsyncSync();
|
|
1536
|
+
} catch (err) {
|
|
1537
|
+
throw this.#recordError(err);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1530
1541
|
/** Close the writer, flushing all data. */
|
|
1531
1542
|
async close(): Promise<void> {
|
|
1532
1543
|
if (this.#closed || this.#closing) return;
|
|
@@ -2592,6 +2603,111 @@ export class SessionManager {
|
|
|
2592
2603
|
if (this.#persistError) throw this.#persistError;
|
|
2593
2604
|
}
|
|
2594
2605
|
|
|
2606
|
+
/**
|
|
2607
|
+
* Synchronously flush all in-memory entries to disk and fsync.
|
|
2608
|
+
* Use when the process may exit before an async flush settles (e.g. Ctrl+C
|
|
2609
|
+
* in the TUI, where raw mode consumes the keystroke so postmortem's SIGINT
|
|
2610
|
+
* handler never fires).
|
|
2611
|
+
*
|
|
2612
|
+
* Hot path: the persist writer is open and flushed, so a single fsyncSync
|
|
2613
|
+
* pushes the page-cache data to physical disk.
|
|
2614
|
+
*
|
|
2615
|
+
* Cold path: entries are only in memory (session just started, or a rewrite
|
|
2616
|
+
* is pending). Writes all entries to a temp file, fsyncs, and atomically
|
|
2617
|
+
* renames over the session file — then re-opens an append writer so the
|
|
2618
|
+
* hot path resumes on subsequent `_persist` calls.
|
|
2619
|
+
*/
|
|
2620
|
+
flushSync(): void {
|
|
2621
|
+
if (!this.persist || !this.#sessionFile) return;
|
|
2622
|
+
if (this.#persistError) throw this.#persistError;
|
|
2623
|
+
|
|
2624
|
+
// Hot path: writer is open and all entries have been written via writeSync.
|
|
2625
|
+
// Just fsync the fd — the data is already in the kernel page cache.
|
|
2626
|
+
if (this.#persistWriter?.isOpen() && this.#flushed && !this.#needsFullRewriteOnNextPersist) {
|
|
2627
|
+
this.#persistWriter.fsyncSync();
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// Cold path: write all in-memory entries to a temp file and atomically
|
|
2632
|
+
// replace the session file. This is safe to run even when an async
|
|
2633
|
+
// rewrite is queued on #persistChain: the async task won't progress
|
|
2634
|
+
// while we're on the sync call stack, and the file we produce is a
|
|
2635
|
+
// superset of whatever the async rewrite would write.
|
|
2636
|
+
const dir = path.resolve(this.#sessionFile, "..");
|
|
2637
|
+
const tempPath = path.join(dir, `.${path.basename(this.#sessionFile)}.${Snowflake.next()}.tmp`);
|
|
2638
|
+
const fd = fs.openSync(tempPath, "w");
|
|
2639
|
+
try {
|
|
2640
|
+
for (const entry of this.#fileEntries) {
|
|
2641
|
+
const persisted = prepareEntryForPersistenceSync(entry, this.#blobStore);
|
|
2642
|
+
const line = `${JSON.stringify(persisted)}\n`;
|
|
2643
|
+
fs.writeSync(fd, line);
|
|
2644
|
+
}
|
|
2645
|
+
fs.fsyncSync(fd);
|
|
2646
|
+
} finally {
|
|
2647
|
+
fs.closeSync(fd);
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// Atomic replace (with EPERM retry for Windows)
|
|
2651
|
+
try {
|
|
2652
|
+
fs.renameSync(tempPath, this.#sessionFile);
|
|
2653
|
+
} catch (err) {
|
|
2654
|
+
if (!hasFsCode(err, "EPERM")) {
|
|
2655
|
+
try {
|
|
2656
|
+
fs.unlinkSync(tempPath);
|
|
2657
|
+
} catch {
|
|
2658
|
+
/* best effort */
|
|
2659
|
+
}
|
|
2660
|
+
throw toError(err);
|
|
2661
|
+
}
|
|
2662
|
+
// Windows: move the old file aside, then rename
|
|
2663
|
+
const backupPath = path.join(dir, `${path.basename(this.#sessionFile)}.${Snowflake.next()}.bak`);
|
|
2664
|
+
try {
|
|
2665
|
+
fs.renameSync(this.#sessionFile, backupPath);
|
|
2666
|
+
} catch (moveAsideErr) {
|
|
2667
|
+
if (isEnoent(moveAsideErr)) {
|
|
2668
|
+
fs.renameSync(tempPath, this.#sessionFile);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
try {
|
|
2672
|
+
fs.unlinkSync(tempPath);
|
|
2673
|
+
} catch {
|
|
2674
|
+
/* best effort */
|
|
2675
|
+
}
|
|
2676
|
+
throw toError(err);
|
|
2677
|
+
}
|
|
2678
|
+
try {
|
|
2679
|
+
fs.renameSync(tempPath, this.#sessionFile);
|
|
2680
|
+
} catch (replaceErr) {
|
|
2681
|
+
// Roll back
|
|
2682
|
+
try {
|
|
2683
|
+
fs.renameSync(backupPath, this.#sessionFile);
|
|
2684
|
+
} catch {
|
|
2685
|
+
/* best effort */
|
|
2686
|
+
}
|
|
2687
|
+
throw toError(replaceErr);
|
|
2688
|
+
}
|
|
2689
|
+
try {
|
|
2690
|
+
fs.unlinkSync(backupPath);
|
|
2691
|
+
} catch {
|
|
2692
|
+
/* best effort */
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
// Re-open the persist writer in append mode so the hot path resumes.
|
|
2697
|
+
if (this.#persistWriter) {
|
|
2698
|
+
// The old writer is stale (pointed at the pre-rewrite file or was
|
|
2699
|
+
// mid-close). Close it asynchronously — it's a no-op if already
|
|
2700
|
+
// closed, and we don't want to block on draining its queue.
|
|
2701
|
+
void this.#persistWriter.close().catch(() => {});
|
|
2702
|
+
}
|
|
2703
|
+
this.#persistWriter = new NdjsonFileWriter(this.storage, this.#sessionFile, {
|
|
2704
|
+
onError: err => this.#recordPersistError(err),
|
|
2705
|
+
});
|
|
2706
|
+
this.#persistWriterPath = this.#sessionFile;
|
|
2707
|
+
this.#flushed = true;
|
|
2708
|
+
this.#needsFullRewriteOnNextPersist = false;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2595
2711
|
/** Close the persistent writer after flushing all pending data. */
|
|
2596
2712
|
async close(): Promise<void> {
|
|
2597
2713
|
if (!this.#persistWriter) return;
|
|
@@ -23,6 +23,11 @@ export interface SessionStorageWriter {
|
|
|
23
23
|
writeLineSync(line: string): void;
|
|
24
24
|
flush(): Promise<void>;
|
|
25
25
|
fsync(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Synchronously fsync the underlying file descriptor. Returns once the data
|
|
28
|
+
* is on the physical disk. Throws synchronously on I/O error.
|
|
29
|
+
*/
|
|
30
|
+
fsyncSync(): void;
|
|
26
31
|
close(): Promise<void>;
|
|
27
32
|
getError(): Error | undefined;
|
|
28
33
|
}
|
|
@@ -118,6 +123,16 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
118
123
|
}
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
fsyncSync(): void {
|
|
127
|
+
if (this.#closed) throw new Error("Writer closed");
|
|
128
|
+
if (this.#error) throw this.#error;
|
|
129
|
+
try {
|
|
130
|
+
fs.fsyncSync(this.#fd);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw this.#recordError(err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
async close(): Promise<void> {
|
|
122
137
|
if (this.#closed) return;
|
|
123
138
|
this.#closed = true;
|
|
@@ -291,6 +306,11 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
291
306
|
if (this.#error) throw this.#error;
|
|
292
307
|
}
|
|
293
308
|
|
|
309
|
+
fsyncSync(): void {
|
|
310
|
+
// No-op for in-memory storage
|
|
311
|
+
if (this.#error) throw this.#error;
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
async close(): Promise<void> {
|
|
295
315
|
if (this.#closed) return;
|
|
296
316
|
this.#closed = true;
|
|
@@ -1005,7 +1005,21 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1005
1005
|
{
|
|
1006
1006
|
name: "logout",
|
|
1007
1007
|
description: "Logout from OAuth provider",
|
|
1008
|
-
|
|
1008
|
+
inlineHint: "[provider]",
|
|
1009
|
+
allowArgs: true,
|
|
1010
|
+
handleTui: (command, runtime) => {
|
|
1011
|
+
const providerId = command.args.trim();
|
|
1012
|
+
if (providerId) {
|
|
1013
|
+
const matchedProvider = getOAuthProviders().find(provider => provider.id === providerId);
|
|
1014
|
+
if (!matchedProvider) {
|
|
1015
|
+
runtime.ctx.showWarning(`Unknown OAuth provider: ${providerId}`);
|
|
1016
|
+
runtime.ctx.editor.setText("");
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
void runtime.ctx.showOAuthSelector("logout", matchedProvider.id);
|
|
1020
|
+
runtime.ctx.editor.setText("");
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1009
1023
|
void runtime.ctx.showOAuthSelector("logout");
|
|
1010
1024
|
runtime.ctx.editor.setText("");
|
|
1011
1025
|
},
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { OAuthAccountIdentity, StoredAuthCredential } from "../../session/auth-storage";
|
|
2
|
+
|
|
3
|
+
export interface LogoutAccount {
|
|
4
|
+
credentialId: number;
|
|
5
|
+
provider: string;
|
|
6
|
+
label: string;
|
|
7
|
+
detail: string;
|
|
8
|
+
type: "api_key" | "oauth";
|
|
9
|
+
active: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface LogoutAccountOptions {
|
|
13
|
+
activeIdentity?: OAuthAccountIdentity;
|
|
14
|
+
activeApiKey?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function nonEmpty(value: string | undefined): string | undefined {
|
|
18
|
+
const trimmed = value?.trim();
|
|
19
|
+
return trimmed && trimmed.length > 0 ? trimmed : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function oauthLabel(row: StoredAuthCredential): string {
|
|
23
|
+
const credential = row.credential;
|
|
24
|
+
if (credential.type !== "oauth") return `API key #${row.id}`;
|
|
25
|
+
return (
|
|
26
|
+
nonEmpty(credential.email) ??
|
|
27
|
+
nonEmpty(credential.accountId) ??
|
|
28
|
+
nonEmpty(credential.projectId) ??
|
|
29
|
+
nonEmpty(credential.enterpriseUrl) ??
|
|
30
|
+
`OAuth credential #${row.id}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function oauthDetail(row: StoredAuthCredential, label: string): string {
|
|
35
|
+
const credential = row.credential;
|
|
36
|
+
if (credential.type === "api_key") return `stored API key #${row.id}`;
|
|
37
|
+
const parts: string[] = [];
|
|
38
|
+
const email = nonEmpty(credential.email);
|
|
39
|
+
const accountId = nonEmpty(credential.accountId);
|
|
40
|
+
const projectId = nonEmpty(credential.projectId);
|
|
41
|
+
const enterpriseUrl = nonEmpty(credential.enterpriseUrl);
|
|
42
|
+
if (email && email !== label) parts.push(email);
|
|
43
|
+
if (accountId && accountId !== label) parts.push(`account ${accountId}`);
|
|
44
|
+
if (projectId && projectId !== label) parts.push(`project ${projectId}`);
|
|
45
|
+
if (enterpriseUrl && enterpriseUrl !== label) parts.push(enterpriseUrl);
|
|
46
|
+
parts.push(`oauth #${row.id}`);
|
|
47
|
+
return parts.join(" · ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function oauthMatchesActiveIdentity(
|
|
51
|
+
row: StoredAuthCredential,
|
|
52
|
+
activeIdentity: OAuthAccountIdentity | undefined,
|
|
53
|
+
): boolean {
|
|
54
|
+
if (!activeIdentity || row.credential.type !== "oauth") return false;
|
|
55
|
+
const credential = row.credential;
|
|
56
|
+
return (
|
|
57
|
+
(activeIdentity.accountId !== undefined && credential.accountId === activeIdentity.accountId) ||
|
|
58
|
+
(activeIdentity.email !== undefined && credential.email === activeIdentity.email) ||
|
|
59
|
+
(activeIdentity.projectId !== undefined && credential.projectId === activeIdentity.projectId)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function toLogoutAccounts(
|
|
64
|
+
provider: string,
|
|
65
|
+
credentials: StoredAuthCredential[],
|
|
66
|
+
options: LogoutAccountOptions = {},
|
|
67
|
+
): LogoutAccount[] {
|
|
68
|
+
return credentials
|
|
69
|
+
.map(row => {
|
|
70
|
+
const label = oauthLabel(row);
|
|
71
|
+
const active =
|
|
72
|
+
row.credential.type === "oauth"
|
|
73
|
+
? oauthMatchesActiveIdentity(row, options.activeIdentity)
|
|
74
|
+
: options.activeApiKey === true;
|
|
75
|
+
return {
|
|
76
|
+
credentialId: row.id,
|
|
77
|
+
provider,
|
|
78
|
+
label,
|
|
79
|
+
detail: oauthDetail(row, label),
|
|
80
|
+
type: row.credential.type,
|
|
81
|
+
active,
|
|
82
|
+
} satisfies LogoutAccount;
|
|
83
|
+
})
|
|
84
|
+
.sort((left, right) => {
|
|
85
|
+
if (left.active !== right.active) return left.active ? -1 : 1;
|
|
86
|
+
return left.label.localeCompare(right.label) || left.credentialId - right.credentialId;
|
|
87
|
+
});
|
|
88
|
+
}
|
package/src/task/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import
|
|
4
|
+
import { z } from "zod/v4";
|
|
5
5
|
import type { AgentSessionEvent } from "../session/agent-session";
|
|
6
6
|
import type { NestedRepoPatch } from "./worktree";
|
|
7
7
|
|
package/src/tools/ask.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import { type Component, Markdown, type MarkdownTheme, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
20
20
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
21
|
-
import
|
|
21
|
+
import { z } from "zod/v4";
|
|
22
22
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
23
23
|
import type { ExtensionUISelectItem } from "../extensibility/extensions";
|
|
24
24
|
import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-m
|
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { replaceTabs, Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
|
-
import
|
|
8
|
+
import { z } from "zod/v4";
|
|
9
9
|
import { canonicalSnapshotKey, getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
10
10
|
import { normalizeToLF } from "../edit/normalize";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
|
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
|
-
import
|
|
8
|
+
import { z } from "zod/v4";
|
|
9
9
|
import { recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import type { Theme } from "../modes/theme/theme";
|
package/src/tools/bash.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import { ImageProtocol, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
11
11
|
import { getProjectDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
12
|
-
import
|
|
12
|
+
import { z } from "zod/v4";
|
|
13
13
|
import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
14
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
15
15
|
import { InternalUrlRouter } from "../internal-urls";
|