@oh-my-pi/pi-coding-agent 17.3.4 → 17.3.7
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 +88 -0
- package/dist/{CHANGELOG-trcc215s.md → CHANGELOG-1hmwwt45.md} +88 -0
- package/dist/cli.js +3156 -3163
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/cli/stats-cli.d.ts +1 -6
- package/dist/types/commands/stats.d.ts +4 -0
- package/dist/types/config/model-discovery.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +7 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +24 -6
- package/dist/types/extensibility/extensions/types.d.ts +60 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +22 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
- package/dist/types/mcp/config.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +3 -1
- package/dist/types/registry/agent-lifecycle.d.ts +11 -0
- package/dist/types/registry/agent-registry.d.ts +4 -0
- package/dist/types/session/agent-session-types.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/date-cwd-reminder.d.ts +22 -0
- package/dist/types/session/irc-bridge.d.ts +2 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-loader.d.ts +18 -11
- package/dist/types/session/session-maintenance.d.ts +6 -2
- package/dist/types/session/session-manager.d.ts +19 -3
- package/dist/types/session/session-tools.d.ts +0 -1
- package/dist/types/session/turn-recovery.d.ts +7 -7
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +1 -0
- package/dist/types/tools/browser/attach.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +2 -2
- package/dist/types/tools/browser.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/file-write-fallback.d.ts +124 -0
- package/dist/types/tools/hub/jobs.d.ts +6 -0
- package/dist/types/tools/hub/types.d.ts +6 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +23 -0
- package/dist/types/vibe/runtime.d.ts +3 -3
- package/package.json +13 -13
- package/src/auto-thinking/classifier.ts +37 -23
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/stats-cli.ts +6 -72
- package/src/commands/stats.ts +7 -4
- package/src/commit/analysis/conventional.ts +15 -9
- package/src/commit/analysis/summary.ts +15 -9
- package/src/commit/changelog/generate.ts +15 -9
- package/src/commit/map-reduce/map-phase.ts +3 -19
- package/src/commit/map-reduce/reduce-phase.ts +15 -9
- package/src/config/model-discovery.ts +3 -3
- package/src/config/model-registry.ts +29 -16
- package/src/config/settings-schema.ts +13 -0
- package/src/debug/report-bundle.ts +25 -59
- package/src/discovery/claude-plugins.ts +2 -1
- package/src/discovery/claude.ts +4 -2
- package/src/discovery/helpers.ts +5 -0
- package/src/edit/hashline/filesystem.ts +9 -3
- package/src/edit/modes/patch.ts +31 -5
- package/src/edit/renderer.ts +48 -19
- package/src/extensibility/extensions/loader.ts +20 -5
- package/src/extensibility/extensions/runner.ts +239 -24
- package/src/extensibility/extensions/types.ts +62 -0
- package/src/extensibility/extensions/wrapper.ts +26 -11
- package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
- package/src/extensibility/plugins/marketplace/manager.ts +15 -18
- package/src/irc/bus.ts +1 -1
- package/src/launch/broker.ts +8 -1
- package/src/launch/presence.ts +77 -1
- package/src/lsp/client.ts +35 -3
- package/src/lsp/clients/biome-client.ts +47 -93
- package/src/lsp/servers.ts +17 -11
- package/src/lsp/tool.ts +2 -4
- package/src/lsp/workspace-diagnostics.ts +24 -2
- package/src/lsp/writethrough.ts +20 -10
- package/src/mcp/config.ts +50 -6
- package/src/memories/index.ts +29 -25
- package/src/mnemopi/backend.ts +13 -11
- package/src/modes/components/ask-dialog.ts +25 -5
- package/src/modes/components/tool-execution.ts +10 -6
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +52 -32
- package/src/modes/controllers/selector-controller.ts +6 -0
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +49 -10
- package/src/modes/theme/defaults/birch.json +2 -2
- package/src/prompts/system/checkpoint-active-notice.md +5 -0
- package/src/prompts/system/date-cwd-reminder.md +3 -0
- package/src/prompts/system/project-prompt.md +0 -1
- package/src/prompts/system/rewind-report.md +1 -3
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/rewind.md +1 -13
- package/src/registry/agent-lifecycle.ts +34 -0
- package/src/registry/agent-registry.ts +27 -2
- package/src/registry/persisted-agents.ts +40 -20
- package/src/sdk.ts +32 -2
- package/src/session/agent-session-types.ts +0 -2
- package/src/session/agent-session.ts +178 -46
- package/src/session/date-cwd-reminder.ts +77 -0
- package/src/session/irc-bridge.ts +5 -0
- package/src/session/messages.ts +5 -1
- package/src/session/session-loader.ts +106 -64
- package/src/session/session-maintenance.ts +189 -115
- package/src/session/session-manager.ts +142 -35
- package/src/session/session-persistence.ts +4 -4
- package/src/session/session-storage.ts +18 -5
- package/src/session/session-tools.ts +5 -10
- package/src/session/turn-recovery.ts +102 -20
- package/src/session/unexpected-stop-classifier.ts +33 -21
- package/src/slash-commands/builtin-session.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +23 -9
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +4 -15
- package/src/task/persisted-revive.ts +3 -6
- package/src/tools/ask.ts +10 -3
- package/src/tools/browser/attach.ts +80 -25
- package/src/tools/browser/launch.ts +44 -15
- package/src/tools/browser/relay/daemon.ts +3 -7
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/shared-daemon.ts +3 -7
- package/src/tools/browser.ts +5 -5
- package/src/tools/builtin-names.ts +7 -3
- package/src/tools/checkpoint.ts +1 -7
- package/src/tools/file-write-fallback.ts +467 -0
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +20 -3
- package/src/tools/hub/types.ts +6 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/path-utils.ts +79 -0
- package/src/tools/render-utils.ts +53 -21
- package/src/tools/think.ts +15 -3
- package/src/tts/speech-enhancer.ts +19 -14
- package/src/utils/commit-message-generator.ts +14 -12
- package/src/utils/title-generator.ts +23 -19
- package/src/vibe/runtime.ts +32 -17
|
@@ -129,8 +129,8 @@ export declare class TurnRecovery {
|
|
|
129
129
|
handleEmptyAssistantStop(message: AssistantMessage): Promise<"continue" | "terminal" | undefined>;
|
|
130
130
|
/** Classifies suspicious terminal stops and schedules bounded recovery. */
|
|
131
131
|
handleUnexpectedAssistantStop(message: AssistantMessage): Promise<boolean>;
|
|
132
|
-
/** Removes a persisted failed assistant turn after its persistence slot settles. */
|
|
133
|
-
dropPersistedAssistantTurn(message: AssistantMessage): Promise<
|
|
132
|
+
/** Removes a persisted failed assistant turn after its persistence slot settles; returns the dropped branch entry id. */
|
|
133
|
+
dropPersistedAssistantTurn(message: AssistantMessage): Promise<string | undefined>;
|
|
134
134
|
/** Runs recovery compaction and restores the failed turn when no rewrite occurs. */
|
|
135
135
|
runRecoveryCompactionWithRollback(reason: "overflow" | "incomplete", message: AssistantMessage, allowDefer: boolean, options: {
|
|
136
136
|
autoContinue: boolean;
|
|
@@ -174,7 +174,7 @@ export declare class TurnRecovery {
|
|
|
174
174
|
* the Gemini header-runaway interrupt, which must not replay a partial,
|
|
175
175
|
* loop-fueling thinking block.
|
|
176
176
|
*/
|
|
177
|
-
discardAssistantTurn(assistantMessage: AssistantMessage):
|
|
177
|
+
discardAssistantTurn(assistantMessage: AssistantMessage): string | undefined;
|
|
178
178
|
/**
|
|
179
179
|
* Retry an empty, reason-less provider abort: a turn with no content that
|
|
180
180
|
* carries the generic sentinel (bare `abort()`), whether the provider
|
|
@@ -198,10 +198,10 @@ export declare class TurnRecovery {
|
|
|
198
198
|
*/
|
|
199
199
|
isRetryableError(message: AssistantMessage): boolean;
|
|
200
200
|
/**
|
|
201
|
-
* Classify a reasonless abort
|
|
202
|
-
* have results. The failed assistant/tool-result pair
|
|
203
|
-
* continuation cannot replay completed side effects;
|
|
204
|
-
* the next turn that an unexecuted call must be reissued.
|
|
201
|
+
* Classify a reasonless abort, idle stream stall, or HTTP/2 stream reset whose
|
|
202
|
+
* emitted tool calls all have results. The failed assistant/tool-result pair
|
|
203
|
+
* stays in context so continuation cannot replay completed side effects;
|
|
204
|
+
* synthetic results tell the next turn that an unexecuted call must be reissued.
|
|
205
205
|
*/
|
|
206
206
|
classifyResolvedInterruptedToolTurn(message: AssistantMessage): "reasonless-abort" | "stream-stall" | undefined;
|
|
207
207
|
/** Checks whether a provider error represents a classifier refusal. */
|
|
@@ -5,6 +5,22 @@ import type { Browser, Page } from "puppeteer-core";
|
|
|
5
5
|
* subsequent bind in the launched app, but Chromium's listener will retry.
|
|
6
6
|
*/
|
|
7
7
|
export declare function findFreeCdpPort(): Promise<number>;
|
|
8
|
+
/**
|
|
9
|
+
* Loopback HTTP/1.1 GET that never routes through a proxy, resolving to the
|
|
10
|
+
* response status code (or null when the endpoint is unreachable, aborted,
|
|
11
|
+
* malformed, or slow past `timeoutMs`).
|
|
12
|
+
*
|
|
13
|
+
* Chrome's DevTools endpoint listens on loopback and speaks plain HTTP/1.1.
|
|
14
|
+
* Both `fetch` and Bun's `node:http` honor `HTTP_PROXY`/`HTTPS_PROXY` and
|
|
15
|
+
* forward even `127.0.0.1` requests to the proxy unless `NO_PROXY` covers them,
|
|
16
|
+
* so a local proxy that 502s internal addresses makes a healthy daemon look
|
|
17
|
+
* dead and the CDP readiness checks tear it down (issue #8567). Talking to the
|
|
18
|
+
* socket over raw TCP sidesteps proxy env entirely.
|
|
19
|
+
*/
|
|
20
|
+
export declare function probeCdpStatus(url: string, opts: {
|
|
21
|
+
timeoutMs: number;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}): Promise<number | null>;
|
|
8
24
|
/** Poll `${cdpUrl}/json/version` until it responds with 200, with abort + timeout support. */
|
|
9
25
|
export declare function waitForCdp(cdpUrl: string, timeoutMs: number, signal?: AbortSignal): Promise<void>;
|
|
10
26
|
/**
|
|
@@ -56,8 +56,8 @@ export interface SharedBrowserLaunchSpec {
|
|
|
56
56
|
* Resolve the executable and complete argv for a shared Chromium the daemon
|
|
57
57
|
* broker spawns directly (no puppeteer inside the broker). Mirrors
|
|
58
58
|
* `launchHeadlessBrowser` flag assembly — puppeteer's default args minus the
|
|
59
|
-
* stealth-suppressed set —
|
|
60
|
-
*
|
|
59
|
+
* stealth-suppressed set — suppresses Puppeteer's unowned startup window, and
|
|
60
|
+
* exposes CDP on an ephemeral port. Returns null when no executable resolves;
|
|
61
61
|
* callers fall back to a process-local launch.
|
|
62
62
|
*/
|
|
63
63
|
export declare function resolveSharedBrowserLaunchSpec(opts: {
|
|
@@ -76,7 +76,7 @@ export interface BrowserToolDetails {
|
|
|
76
76
|
/**
|
|
77
77
|
* Browser tool: stateful, multi-tab. Three actions:
|
|
78
78
|
* - `open` → acquire/create a named tab on a browser kind (headless | spawned | connected) and optionally goto a url.
|
|
79
|
-
* - `close` → release a named tab (or all
|
|
79
|
+
* - `close` → release a named tab handle (or all handles); attached/relay pages remain open, and spawned pages remain unless killed.
|
|
80
80
|
* - `run` → execute JS code against an existing tab with `page`/`browser`/`tab` helpers in scope.
|
|
81
81
|
*/
|
|
82
82
|
export declare class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolDetails> {
|
|
@@ -2,7 +2,7 @@ export declare const BUILTIN_TOOL_NAMES: readonly ["read", "bash", "edit", "ast_
|
|
|
2
2
|
export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
|
|
3
3
|
export declare const HIDDEN_TOOL_NAMES: readonly ["yield", "goal", "think"];
|
|
4
4
|
export type HiddenToolName = (typeof HIDDEN_TOOL_NAMES)[number];
|
|
5
|
-
/**
|
|
5
|
+
/** Canonicalize built-in IDs and legacy aliases. Leave plugin names unchanged. */
|
|
6
6
|
export declare function normalizeToolName(name: string): string;
|
|
7
7
|
/** Normalize and deduplicate tool names while preserving first-seen order. */
|
|
8
8
|
export declare function normalizeToolNames(names: Iterable<string>): string[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { BunFile } from "bun";
|
|
2
|
+
import type { ExtensionContext } from "../extensibility/extensions/types.js";
|
|
3
|
+
/** A denied write, captured for a registered fallback to retry through a privileged channel. */
|
|
4
|
+
export interface FileWriteFallbackRequest {
|
|
5
|
+
/**
|
|
6
|
+
* Absolute, symlink-resolved path to write the bytes to.
|
|
7
|
+
*
|
|
8
|
+
* This is where the failed in-process write would itself have landed, which is
|
|
9
|
+
* not necessarily the path the tool was given: `open` follows every component,
|
|
10
|
+
* so a link anywhere in that path redirects the bytes. Resolving it here is
|
|
11
|
+
* what lets a handler's allowlist see the real destination instead of a
|
|
12
|
+
* lexically innocent path, so a handler MUST treat this as authoritative and
|
|
13
|
+
* MUST NOT re-derive the target from anything else.
|
|
14
|
+
*/
|
|
15
|
+
dst: string;
|
|
16
|
+
/**
|
|
17
|
+
* Session the denied write was issued from, or `undefined` when the mutation
|
|
18
|
+
* did not happen inside a tool call (an external `applyPatch` caller, a test).
|
|
19
|
+
*
|
|
20
|
+
* The registry is process-wide, so a handler can be consulted for a write from
|
|
21
|
+
* a session other than the one whose extension registered it. Compare this with
|
|
22
|
+
* `ctx.sessionManager.getSessionId()` to tell the two apart — a handler that
|
|
23
|
+
* prompts through `ctx.ui` needs to, since that UI belongs to ITS session and
|
|
24
|
+
* not necessarily to the one being asked about.
|
|
25
|
+
*/
|
|
26
|
+
sessionId: string | undefined;
|
|
27
|
+
/** The exact bytes the tool intended to write. */
|
|
28
|
+
content: string;
|
|
29
|
+
/**
|
|
30
|
+
* The error that proves the write hit a permission boundary. Usually the write's
|
|
31
|
+
* own `EPERM`/`EACCES`/`EROFS`; for a write into a directory the host may not
|
|
32
|
+
* create, the denial raised by creating that directory, in which case `dst`'s
|
|
33
|
+
* parent may not exist yet and the handler is responsible for creating it.
|
|
34
|
+
*/
|
|
35
|
+
cause: unknown;
|
|
36
|
+
}
|
|
37
|
+
/** Extension-authored handler. Return `true` once `content` is durably on disk at `dst`. */
|
|
38
|
+
export type FileWriteFallbackHandler = (req: FileWriteFallbackRequest, ctx: ExtensionContext) => Promise<boolean>;
|
|
39
|
+
/** A handler already bound to its owning extension's live context. */
|
|
40
|
+
type BoundFileWriteFallbackHandler = (req: FileWriteFallbackRequest) => Promise<boolean>;
|
|
41
|
+
/** A denied unlink, captured for a registered fallback to perform through a privileged channel. */
|
|
42
|
+
export interface FileDeleteFallbackRequest {
|
|
43
|
+
/**
|
|
44
|
+
* Absolute, symlink-resolved path the unlink was denied for.
|
|
45
|
+
*
|
|
46
|
+
* Every component ABOVE the last is resolved, so a handler cannot be walked
|
|
47
|
+
* outside its allowed roots through a link in the path. The last component is
|
|
48
|
+
* deliberately NOT resolved, because `unlink` removes a link itself rather
|
|
49
|
+
* than its target — which is also why this may still name a symlink.
|
|
50
|
+
*/
|
|
51
|
+
dst: string;
|
|
52
|
+
/** The `EPERM`/`EACCES`/`EROFS` that proves the unlink hit a permission boundary. */
|
|
53
|
+
cause: unknown;
|
|
54
|
+
/**
|
|
55
|
+
* Whether `dst` was confirmed to be a plain regular file before diverting.
|
|
56
|
+
*
|
|
57
|
+
* `false` means the seam could not establish that, either because the target's
|
|
58
|
+
* own metadata is behind the same boundary that denied the unlink — the common
|
|
59
|
+
* sandbox case, since `unlink` on a directory also reports `EPERM` on Darwin —
|
|
60
|
+
* or because `dst` is a symlink.
|
|
61
|
+
*
|
|
62
|
+
* A handler MUST remove `dst` with a plain unlink. It MUST NOT remove it
|
|
63
|
+
* recursively, and MUST NOT resolve the path first: when this is `false` the
|
|
64
|
+
* target may be a directory, and resolving a symlink would delete whatever it
|
|
65
|
+
* points at instead of the link.
|
|
66
|
+
*/
|
|
67
|
+
confirmedFile: boolean;
|
|
68
|
+
/** See {@link FileWriteFallbackRequest.sessionId}. */
|
|
69
|
+
sessionId: string | undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Extension-authored handler. Return `true` once `dst` is gone from disk. */
|
|
72
|
+
export type FileDeleteFallbackHandler = (req: FileDeleteFallbackRequest, ctx: ExtensionContext) => Promise<boolean>;
|
|
73
|
+
/** A handler already bound to its owning extension's live context. */
|
|
74
|
+
type BoundFileDeleteFallbackHandler = (req: FileDeleteFallbackRequest) => Promise<boolean>;
|
|
75
|
+
/** True for `EPERM`, `EACCES`, and `EROFS` — the sandbox-boundary write failures this seam exists for. */
|
|
76
|
+
export declare function isPermissionDeniedError(error: unknown): boolean;
|
|
77
|
+
/** Whether any fallback is registered. Lets a caller skip work that only this seam needs. */
|
|
78
|
+
export declare function hasFileWriteFallback(): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Append a fallback writer, consulted in registration order when a direct write is
|
|
81
|
+
* permission-denied. Returns a disposer that removes this exact registration; the
|
|
82
|
+
* runner calls it on session shutdown so no handler outlives its session.
|
|
83
|
+
*/
|
|
84
|
+
export declare function addFileWriteFallback(handler: BoundFileWriteFallbackHandler): () => void;
|
|
85
|
+
/** Whether any delete fallback is registered. */
|
|
86
|
+
export declare function hasFileDeleteFallback(): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Append a fallback deleter, consulted in registration order when a direct unlink is
|
|
89
|
+
* permission-denied. Deliberately a separate registry from
|
|
90
|
+
* {@link addFileWriteFallback}: a write handler brokers `content` to `dst`, and
|
|
91
|
+
* handing it a request with no content would let it "broker" an empty write and
|
|
92
|
+
* truncate the file it was asked to remove. Opting in is explicit for that reason.
|
|
93
|
+
*/
|
|
94
|
+
export declare function addFileDeleteFallback(handler: BoundFileDeleteFallbackHandler): () => void;
|
|
95
|
+
/**
|
|
96
|
+
* Name the session whose tool call is about to run, so a denied mutation inside it
|
|
97
|
+
* can tell a handler where the request came from.
|
|
98
|
+
*
|
|
99
|
+
* Entered once per tool call by `ExtensionToolWrapper` (`extensibility/extensions/
|
|
100
|
+
* wrapper.ts`), which `sdk.ts` puts around the whole tool registry whenever an
|
|
101
|
+
* `ExtensionRunner` exists — so the component that owns the handlers is the one
|
|
102
|
+
* naming its own session, and no caller has to thread an `AgentToolContext`
|
|
103
|
+
* through for attribution to work.
|
|
104
|
+
*
|
|
105
|
+
* That covers the deferred LSP write batch too: a batch id belongs to one
|
|
106
|
+
* assistant turn of one session, and its flush is awaited inside a tool call of
|
|
107
|
+
* that same session, so a write performed during a later call of the group is
|
|
108
|
+
* still attributed to the session that issued it.
|
|
109
|
+
*
|
|
110
|
+
* Deliberately NOT a general "current session" accessor: nothing else enters this
|
|
111
|
+
* scope, so outside a tool call it is empty by design — an external `applyPatch`
|
|
112
|
+
* caller reports `undefined` rather than borrowing someone else's identity.
|
|
113
|
+
*/
|
|
114
|
+
export declare function withFileMutationSession<T>(sessionId: string | undefined, fn: () => T): T;
|
|
115
|
+
/**
|
|
116
|
+
* Remove a file, consulting registered delete fallbacks when the unlink is denied.
|
|
117
|
+
*
|
|
118
|
+
* Unlike the write path there is no masked-`ENOENT` case to see through: nothing is
|
|
119
|
+
* created on the way, so an `ENOENT` here means the file genuinely is not there and
|
|
120
|
+
* must propagate — `edit`'s `REM` turns it into a `NotFoundError`.
|
|
121
|
+
*/
|
|
122
|
+
export declare function deleteFileWithFallback(dst: string, file?: BunFile): Promise<void>;
|
|
123
|
+
export declare function writeFileWithFallback(dst: string, content: string, file?: BunFile): Promise<void>;
|
|
124
|
+
export {};
|
|
@@ -36,6 +36,12 @@ export declare function visibleJobs(manager: AsyncJobManager, ids: string[], own
|
|
|
36
36
|
* that activity instead of implying the system is quiet. Existence is
|
|
37
37
|
* already public via the peer roster, so listing ids here leaks nothing new;
|
|
38
38
|
* job *control* stays owner-scoped.
|
|
39
|
+
*
|
|
40
|
+
* Reporting deliberately uses the claimed `status`, not the session-corroborated
|
|
41
|
+
* `registry.isRunning` used by the wait-sustaining gates: a ref that claims
|
|
42
|
+
* `running` with no live turn is exactly the stale entry an operator must see
|
|
43
|
+
* here to cancel it (#8634). Hiding it would match the badge count to nothing
|
|
44
|
+
* and remove the only discovery path for the id.
|
|
39
45
|
*/
|
|
40
46
|
export declare function runningAgentsOutsideJobs(session: ToolSession): AgentActivitySnapshot[];
|
|
41
47
|
interface TrackedJobLike {
|
|
@@ -55,6 +55,12 @@ export interface AgentActivitySnapshot {
|
|
|
55
55
|
activity?: string;
|
|
56
56
|
/** Time since the agent was registered. */
|
|
57
57
|
ageMs: number;
|
|
58
|
+
/**
|
|
59
|
+
* Whether an attached session corroborates the `running` claim. False marks
|
|
60
|
+
* a ref that says `running` with no turn in flight — either a spawn still
|
|
61
|
+
* wiring up or a stale registration that `hub cancel <id>` clears (#8634).
|
|
62
|
+
*/
|
|
63
|
+
live: boolean;
|
|
58
64
|
}
|
|
59
65
|
/** Result details for messaging and job ops; fields are disjoint per op. */
|
|
60
66
|
export interface CoordinationDetails {
|
|
@@ -50,6 +50,7 @@ export * from "./debug.js";
|
|
|
50
50
|
export * from "./essential-tools.js";
|
|
51
51
|
export * from "./eval.js";
|
|
52
52
|
export * from "./eval-backends.js";
|
|
53
|
+
export * from "./file-write-fallback.js";
|
|
53
54
|
export * from "./gh.js";
|
|
54
55
|
export * from "./glob.js";
|
|
55
56
|
export * from "./grep.js";
|
|
@@ -155,6 +155,29 @@ export declare function resolveToCwd(filePath: string, cwd: string): string;
|
|
|
155
155
|
* The cwd itself is rejected: a download names a file, never the directory.
|
|
156
156
|
*/
|
|
157
157
|
export declare function confineToWorkspace(filePath: string, cwd: string): string | null;
|
|
158
|
+
/**
|
|
159
|
+
* Resolve the path a syscall on `filePath` would really act on, or `null` when
|
|
160
|
+
* that cannot be established.
|
|
161
|
+
*
|
|
162
|
+
* A lexical path is not a destination. The kernel follows every component above
|
|
163
|
+
* the last, so `ws/link/file` under a `ws/link -> /elsewhere` link lands outside
|
|
164
|
+
* `ws` while still looking relative and `..`-free. Handing such a path to a
|
|
165
|
+
* privileged helper defeats the defence a helper author reaches for first — a
|
|
166
|
+
* prefix allowlist passes, because the link sits inside the allowed root while
|
|
167
|
+
* its target does not. Callers that hand a path to something more privileged
|
|
168
|
+
* than the syscall that just failed resolve it here first.
|
|
169
|
+
*
|
|
170
|
+
* Rejecting symlinked components outright is not an option: `/var` and `/tmp`
|
|
171
|
+
* are links on macOS, so every path under `os.tmpdir()` traverses one. They are
|
|
172
|
+
* resolved instead, and only a path whose real destination cannot be established
|
|
173
|
+
* is refused, because "where would this land" then has no answer to hand over.
|
|
174
|
+
* {@link confineToWorkspace} refuses an unresolvable link for the same reason.
|
|
175
|
+
*
|
|
176
|
+
* @param followFinal `true` for a syscall that follows a link at the final
|
|
177
|
+
* component (`open`, so every write), `false` for one that acts on the link
|
|
178
|
+
* itself (`unlink`) and therefore needs it left alone.
|
|
179
|
+
*/
|
|
180
|
+
export declare function resolveSyscallTarget(filePath: string, followFinal: boolean): Promise<string | null>;
|
|
158
181
|
export declare function formatPathRelativeToCwd(filePath: string, cwd: string, options?: {
|
|
159
182
|
trailingSlash?: boolean;
|
|
160
183
|
}): string;
|
|
@@ -102,15 +102,15 @@ export declare class VibeSessionRegistry {
|
|
|
102
102
|
/** Reset the global registry. Test-only. */
|
|
103
103
|
static resetGlobalForTests(): void;
|
|
104
104
|
/**
|
|
105
|
-
* Insert a bare worker record without the spawn
|
|
106
|
-
* lets
|
|
107
|
-
* fake roster + AgentRegistry session without driving a real turn.
|
|
105
|
+
* Insert a bare worker record without the spawn machinery. Test-only —
|
|
106
|
+
* lets focused runtime tests attach an optional synthetic in-flight job.
|
|
108
107
|
*/
|
|
109
108
|
registerRecordForTests(record: {
|
|
110
109
|
id: string;
|
|
111
110
|
cli?: VibeCli;
|
|
112
111
|
ownerId: string;
|
|
113
112
|
state?: VibeSessionState;
|
|
113
|
+
jobId?: string;
|
|
114
114
|
}): void;
|
|
115
115
|
/** Override the teardown grace period for deterministic lifecycle tests. */
|
|
116
116
|
setTeardownGraceForTesting(timeoutMs: number): void;
|
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": "17.3.
|
|
4
|
+
"version": "17.3.7",
|
|
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",
|
|
@@ -48,18 +48,18 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@babel/parser": "^7.29.7",
|
|
51
|
-
"@oh-my-pi/hashline": "17.3.
|
|
52
|
-
"@oh-my-pi/omp-stats": "17.3.
|
|
53
|
-
"@oh-my-pi/omptype": "17.3.
|
|
54
|
-
"@oh-my-pi/pi-agent-core": "17.3.
|
|
55
|
-
"@oh-my-pi/pi-ai": "17.3.
|
|
56
|
-
"@oh-my-pi/pi-catalog": "17.3.
|
|
57
|
-
"@oh-my-pi/pi-mnemopi": "17.3.
|
|
58
|
-
"@oh-my-pi/pi-natives": "17.3.
|
|
59
|
-
"@oh-my-pi/pi-tui": "17.3.
|
|
60
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
61
|
-
"@oh-my-pi/pi-wire": "17.3.
|
|
62
|
-
"@oh-my-pi/snapcompact": "17.3.
|
|
51
|
+
"@oh-my-pi/hashline": "17.3.7",
|
|
52
|
+
"@oh-my-pi/omp-stats": "17.3.7",
|
|
53
|
+
"@oh-my-pi/omptype": "17.3.7",
|
|
54
|
+
"@oh-my-pi/pi-agent-core": "17.3.7",
|
|
55
|
+
"@oh-my-pi/pi-ai": "17.3.7",
|
|
56
|
+
"@oh-my-pi/pi-catalog": "17.3.7",
|
|
57
|
+
"@oh-my-pi/pi-mnemopi": "17.3.7",
|
|
58
|
+
"@oh-my-pi/pi-natives": "17.3.7",
|
|
59
|
+
"@oh-my-pi/pi-tui": "17.3.7",
|
|
60
|
+
"@oh-my-pi/pi-utils": "17.3.7",
|
|
61
|
+
"@oh-my-pi/pi-wire": "17.3.7",
|
|
62
|
+
"@oh-my-pi/snapcompact": "17.3.7",
|
|
63
63
|
"@opentelemetry/api": "^1.9.1",
|
|
64
64
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
65
65
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Throws on any failure (no model, no key, unparseable output, abort/timeout);
|
|
15
15
|
* the caller falls back to a concrete level and continues the turn.
|
|
16
16
|
*/
|
|
17
|
-
import { type AssistantMessage, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
|
|
17
|
+
import { type AssistantMessage, completeSimple, Effort, type Model, retryTransientCompletion } from "@oh-my-pi/pi-ai";
|
|
18
18
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
19
19
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
20
20
|
|
|
@@ -60,14 +60,24 @@ function difficultySystemPromptFor(ceiling: Effort): string {
|
|
|
60
60
|
|
|
61
61
|
/** Local classifiers occasionally need more room for chat-template boilerplate. */
|
|
62
62
|
const LOCAL_ANSWER_MAX_TOKENS = 16;
|
|
63
|
+
/** On-device reasoning classifiers need room for the bucket keyword after the `<think>` preamble. */
|
|
64
|
+
const LOCAL_REASONING_MAX_TOKENS = 1024;
|
|
63
65
|
/**
|
|
64
|
-
* Online classifier budget. Sized
|
|
65
|
-
* `disableReasoning`
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* Online classifier budget. Sized against two independent constraints:
|
|
67
|
+
* - Backends that ignore `disableReasoning` still emit a thinking preamble
|
|
68
|
+
* (e.g. Qwen3 via llama.cpp catalogued `reasoning: false` but still thinking;
|
|
69
|
+
* Anthropic via LiteLLM/Vertex, whose `openai-completions` route downgrades a
|
|
70
|
+
* disabled request to the lowest reasoning effort instead of turning thinking
|
|
71
|
+
* off). The classifier keyword must have room to land after that preamble
|
|
72
|
+
* (issue #4355).
|
|
73
|
+
* - Anthropic-dialect proxies reject `max_tokens <= thinking.budget_tokens`. The
|
|
74
|
+
* pinned lowest effort maps to at least Anthropic's 1024-token minimum budget,
|
|
75
|
+
* so the cap MUST comfortably exceed 1024 or every classifier call 400s with
|
|
76
|
+
* `max_tokens must be greater than thinking.budget_tokens` (issue #8610).
|
|
77
|
+
* `maxTokens` is a hard cap — non-thinking completions still return in a handful
|
|
78
|
+
* of tokens.
|
|
69
79
|
*/
|
|
70
|
-
const
|
|
80
|
+
const ONLINE_REASONING_SAFE_MAX_TOKENS = 4096;
|
|
71
81
|
|
|
72
82
|
export interface ClassifyDifficultyDeps {
|
|
73
83
|
settings: Settings;
|
|
@@ -113,21 +123,25 @@ async function classifyOnline(input: string, deps: ClassifyDifficultyDeps, ceili
|
|
|
113
123
|
}
|
|
114
124
|
// Resolve metadata after getApiKey so the session-sticky credential is recorded first.
|
|
115
125
|
const metadata = deps.metadataResolver?.(model.provider);
|
|
116
|
-
const maxTokens =
|
|
117
|
-
|
|
118
|
-
const response = await
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
const maxTokens = ONLINE_REASONING_SAFE_MAX_TOKENS;
|
|
127
|
+
|
|
128
|
+
const response = await retryTransientCompletion(
|
|
129
|
+
() =>
|
|
130
|
+
completeSimple(
|
|
131
|
+
model,
|
|
132
|
+
{
|
|
133
|
+
systemPrompt: [difficultySystemPromptFor(ceiling)],
|
|
134
|
+
messages: [{ role: "user", content: input, timestamp: Date.now() }],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
apiKey: deps.registry.resolver(model, deps.sessionId),
|
|
138
|
+
maxTokens,
|
|
139
|
+
disableReasoning: true,
|
|
140
|
+
metadata,
|
|
141
|
+
signal: deps.signal,
|
|
142
|
+
},
|
|
143
|
+
),
|
|
144
|
+
{ signal: deps.signal },
|
|
131
145
|
);
|
|
132
146
|
|
|
133
147
|
if (response.stopReason === "error") {
|
|
@@ -147,7 +161,7 @@ async function classifyLocal(input: string, modelKey: string, deps: ClassifyDiff
|
|
|
147
161
|
throw new Error(`auto-thinking: unsupported local classifier model: ${modelKey}`);
|
|
148
162
|
}
|
|
149
163
|
const maxTokens = isTinyMemoryReasoningModelKey(modelKey)
|
|
150
|
-
? Math.max(LOCAL_ANSWER_MAX_TOKENS,
|
|
164
|
+
? Math.max(LOCAL_ANSWER_MAX_TOKENS, LOCAL_REASONING_MAX_TOKENS)
|
|
151
165
|
: LOCAL_ANSWER_MAX_TOKENS;
|
|
152
166
|
const builtPrompt = prompt.render(difficultyLocalPrompt, { prompt: input });
|
|
153
167
|
const text = await tinyModelClient.complete(modelKey, builtPrompt, {
|
package/src/cli/models-cli.ts
CHANGED
|
@@ -304,7 +304,7 @@ export async function runModelsListing(options: RunModelsListingOptions): Promis
|
|
|
304
304
|
cwd,
|
|
305
305
|
eventBus,
|
|
306
306
|
disableExtensionDiscovery ? undefined : disabledExtensionIds,
|
|
307
|
-
{ ambient: !disableExtensionDiscovery },
|
|
307
|
+
{ ambient: !disableExtensionDiscovery, includeAmbientHooks: false },
|
|
308
308
|
);
|
|
309
309
|
const extensionRunner =
|
|
310
310
|
extensionsResult.extensions.length > 0
|
package/src/cli/stats-cli.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { truncateToWidth } from "@oh-my-pi/pi-tui/utils";
|
|
8
|
-
import {
|
|
8
|
+
import { formatDuration, formatNumber, formatPercent } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
10
10
|
import { openPath } from "../utils/open";
|
|
11
11
|
|
|
@@ -57,45 +57,11 @@ function shortenSessionFile(p: string): string {
|
|
|
57
57
|
|
|
58
58
|
export interface StatsCommandArgs {
|
|
59
59
|
port: number;
|
|
60
|
+
host: string;
|
|
60
61
|
json: boolean;
|
|
61
62
|
summary: boolean;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
// =============================================================================
|
|
65
|
-
// Argument Parser
|
|
66
|
-
// =============================================================================
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Parse stats subcommand arguments.
|
|
70
|
-
* Returns undefined if not a stats command.
|
|
71
|
-
*/
|
|
72
|
-
export function parseStatsArgs(args: string[]): StatsCommandArgs | undefined {
|
|
73
|
-
if (args.length === 0 || args[0] !== "stats") {
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const result: StatsCommandArgs = {
|
|
78
|
-
port: 3847,
|
|
79
|
-
json: false,
|
|
80
|
-
summary: false,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
for (let i = 1; i < args.length; i++) {
|
|
84
|
-
const arg = args[i];
|
|
85
|
-
if (arg === "--json" || arg === "-j") {
|
|
86
|
-
result.json = true;
|
|
87
|
-
} else if (arg === "--summary" || arg === "-s") {
|
|
88
|
-
result.summary = true;
|
|
89
|
-
} else if ((arg === "--port" || arg === "-p") && i + 1 < args.length) {
|
|
90
|
-
result.port = parseInt(args[++i], 10);
|
|
91
|
-
} else if (arg.startsWith("--port=")) {
|
|
92
|
-
result.port = parseInt(arg.split("=")[1], 10);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
65
|
function formatCost(n: number): string {
|
|
100
66
|
if (n < 0.01) return `$${n.toFixed(4)}`;
|
|
101
67
|
if (n < 1) return `$${n.toFixed(3)}`;
|
|
@@ -112,9 +78,8 @@ function normalizePremiumRequests(n: number): number {
|
|
|
112
78
|
|
|
113
79
|
export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
|
|
114
80
|
// Lazy import to avoid loading stats module when not needed
|
|
115
|
-
const {
|
|
116
|
-
"@oh-my-pi/omp-stats"
|
|
117
|
-
);
|
|
81
|
+
const { closeDb, formatStatsDashboardUrl, getDashboardStats, getTotalMessageCount, startServer, syncAllSessions } =
|
|
82
|
+
await import("@oh-my-pi/omp-stats");
|
|
118
83
|
|
|
119
84
|
// Sync session files first
|
|
120
85
|
const progress = createSyncProgressReporter();
|
|
@@ -136,8 +101,8 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
|
|
|
136
101
|
}
|
|
137
102
|
|
|
138
103
|
// Start the dashboard server
|
|
139
|
-
const { hostname, port } = await startServer(cmd.port);
|
|
140
|
-
const url =
|
|
104
|
+
const { hostname, port } = await startServer(cmd.port, cmd.host);
|
|
105
|
+
const url = formatStatsDashboardUrl(hostname, port);
|
|
141
106
|
console.log(chalk.green(`Dashboard available at: ${url}`));
|
|
142
107
|
|
|
143
108
|
// Open browser
|
|
@@ -197,34 +162,3 @@ async function printStatsSummary(): Promise<void> {
|
|
|
197
162
|
|
|
198
163
|
console.log("");
|
|
199
164
|
}
|
|
200
|
-
|
|
201
|
-
// =============================================================================
|
|
202
|
-
// Help
|
|
203
|
-
// =============================================================================
|
|
204
|
-
|
|
205
|
-
export function printStatsHelp(): void {
|
|
206
|
-
console.log(`${chalk.bold(`${APP_NAME} stats`)} - AI Usage Statistics Dashboard
|
|
207
|
-
|
|
208
|
-
${chalk.bold("Usage:")}
|
|
209
|
-
${APP_NAME} stats [options]
|
|
210
|
-
|
|
211
|
-
${chalk.bold("Options:")}
|
|
212
|
-
-p, --port <port> Port for the dashboard server (default: 3847)
|
|
213
|
-
-j, --json Output stats as JSON and exit
|
|
214
|
-
-s, --summary Print summary to console and exit
|
|
215
|
-
-h, --help Show this help message
|
|
216
|
-
|
|
217
|
-
${chalk.bold("Examples:")}
|
|
218
|
-
${APP_NAME} stats # Start dashboard server
|
|
219
|
-
${APP_NAME} stats --json # Print stats as JSON
|
|
220
|
-
${APP_NAME} stats --summary # Print summary to console
|
|
221
|
-
${APP_NAME} stats --port 8080 # Start on custom port
|
|
222
|
-
|
|
223
|
-
${chalk.bold("Metrics:")}
|
|
224
|
-
- Total requests and error rate
|
|
225
|
-
- Token usage (input, output, cache)
|
|
226
|
-
- Cost breakdown
|
|
227
|
-
- Average duration and time to first token (TTFT)
|
|
228
|
-
- Tokens per second throughput
|
|
229
|
-
`);
|
|
230
|
-
}
|
package/src/commands/stats.ts
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
6
6
|
import { statsHelp as commandHelp } from "../cli/command-help";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
7
|
+
import type { StatsCommandArgs } from "../cli/stats-cli";
|
|
8
|
+
import * as statsCli from "../cli/stats-cli";
|
|
9
|
+
import * as theme from "../modes/theme/theme";
|
|
9
10
|
|
|
10
11
|
export default class Stats extends Command {
|
|
11
12
|
static description = commandHelp.description;
|
|
12
13
|
static flags = {
|
|
13
14
|
port: Flags.integer({ char: "p", description: "Port for the dashboard server", default: 3847 }),
|
|
15
|
+
host: Flags.string({ description: "Host to bind", default: "127.0.0.1" }),
|
|
14
16
|
json: Flags.boolean({ char: "j", description: "Output stats as JSON", default: false }),
|
|
15
17
|
summary: Flags.boolean({ char: "s", description: "Print summary to console", default: false }),
|
|
16
18
|
};
|
|
@@ -20,11 +22,12 @@ export default class Stats extends Command {
|
|
|
20
22
|
|
|
21
23
|
const cmd: StatsCommandArgs = {
|
|
22
24
|
port: flags.port,
|
|
25
|
+
host: flags.host ?? "127.0.0.1",
|
|
23
26
|
json: flags.json,
|
|
24
27
|
summary: flags.summary,
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
await initTheme();
|
|
28
|
-
await runStatsCommand(cmd);
|
|
30
|
+
await theme.initTheme();
|
|
31
|
+
await statsCli.runStatsCommand(cmd);
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
|
|
3
|
-
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { completeSimple, retryTransientCompletion } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
|
|
6
6
|
import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
|
|
@@ -50,15 +50,21 @@ export async function generateConventionalAnalysis({
|
|
|
50
50
|
diff,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
const response = await
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
const response = await retryTransientCompletion(() =>
|
|
54
|
+
completeSimple(
|
|
55
|
+
model,
|
|
56
|
+
{
|
|
57
|
+
systemPrompt: [prompt.render(analysisSystemPrompt)],
|
|
58
|
+
messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
|
|
59
|
+
tools: [ConventionalAnalysisTool],
|
|
60
|
+
},
|
|
61
|
+
{ apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
|
|
62
|
+
),
|
|
61
63
|
);
|
|
62
64
|
|
|
65
|
+
if (response.stopReason === "error") {
|
|
66
|
+
throw new Error(response.errorMessage ?? "provider error");
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
return parseConventionalAnalysisResponse(response, ConventionalAnalysisTool);
|
|
64
70
|
}
|