@oh-my-pi/pi-coding-agent 17.2.1 → 17.2.2
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 +47 -0
- package/dist/{CHANGELOG-dj46zzrm.md → CHANGELOG-xfpkakrn.md} +47 -0
- package/dist/cli.js +3891 -3885
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +16 -5
- package/dist/types/edit/hashline/block-resolver.d.ts +5 -5
- package/dist/types/edit/hashline/diff.d.ts +4 -4
- package/dist/types/eval/executor-base.d.ts +27 -0
- package/dist/types/eval/js/context-manager.d.ts +7 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +64 -5
- package/dist/types/extensibility/extensions/types.d.ts +15 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/live/visualizer.d.ts +3 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -3
- package/dist/types/modes/components/keybinding-hints.d.ts +1 -1
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +13 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/secrets/index.d.ts +22 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/codex-auto-reset.d.ts +182 -64
- package/dist/types/session/session-storage.d.ts +5 -2
- package/dist/types/session/session-tools.d.ts +2 -1
- package/dist/types/session/turn-recovery.d.ts +7 -1
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +27 -0
- package/dist/types/task/persisted-revive.d.ts +7 -0
- package/dist/types/tools/bash-interceptor.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +9 -1
- package/dist/types/tools/browser/launch.d.ts +20 -1
- package/dist/types/tools/browser/registry.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +8 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/shell-tokenize.d.ts +11 -0
- package/dist/types/utils/image-loading.d.ts +2 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/src/commit/agentic/index.ts +2 -1
- package/src/commit/pipeline.ts +3 -2
- package/src/config/keybindings.ts +6 -1
- package/src/config/settings-schema.ts +16 -3
- package/src/edit/hashline/block-resolver.ts +5 -5
- package/src/edit/hashline/diff.ts +22 -15
- package/src/edit/hashline/execute.ts +14 -13
- package/src/edit/renderer.ts +3 -3
- package/src/eval/executor-base.ts +39 -0
- package/src/eval/jl/executor.ts +13 -2
- package/src/eval/js/context-manager.ts +97 -14
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/py/executor.ts +55 -15
- package/src/eval/rb/executor.ts +8 -1
- package/src/extensibility/extensions/runner.ts +121 -4
- package/src/extensibility/extensions/types.ts +15 -0
- package/src/extensibility/extensions/wrapper.ts +19 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +5 -5
- package/src/live/visualizer.ts +8 -1
- package/src/lsp/index.ts +51 -11
- package/src/main.ts +3 -0
- package/src/modes/acp/acp-agent.ts +16 -5
- package/src/modes/components/assistant-message.ts +5 -10
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/keybinding-hints.ts +4 -13
- package/src/modes/components/status-line/segments.ts +4 -5
- package/src/modes/components/tool-execution.ts +4 -5
- package/src/modes/controllers/input-controller.ts +18 -40
- package/src/modes/controllers/live-command-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +28 -11
- package/src/modes/interactive-mode.ts +59 -2
- package/src/modes/theme/theme.ts +13 -6
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/sdk.ts +47 -16
- package/src/secrets/index.ts +83 -3
- package/src/secrets/obfuscator.ts +70 -15
- package/src/session/agent-session-types.ts +7 -1
- package/src/session/agent-session.ts +393 -112
- package/src/session/codex-auto-reset.ts +594 -123
- package/src/session/eval-runner.ts +2 -0
- package/src/session/messages.ts +1 -1
- package/src/session/session-maintenance.ts +48 -1
- package/src/session/session-manager.ts +4 -3
- package/src/session/session-stats.ts +14 -3
- package/src/session/session-storage.ts +47 -15
- package/src/session/session-tools.ts +30 -2
- package/src/session/turn-recovery.ts +15 -2
- package/src/slash-commands/builtin-registry.ts +21 -15
- package/src/slash-commands/helpers/reset-usage.ts +2 -0
- package/src/system-prompt.ts +13 -1
- package/src/task/executor.ts +168 -15
- package/src/task/persisted-revive.ts +26 -1
- package/src/telemetry-export.ts +8 -4
- package/src/tools/bash-interceptor.ts +85 -8
- package/src/tools/bash.ts +1 -1
- package/src/tools/browser/attach.ts +35 -10
- package/src/tools/browser/launch.ts +60 -11
- package/src/tools/browser/registry.ts +18 -2
- package/src/tools/browser/tab-protocol.ts +8 -0
- package/src/tools/browser/tab-supervisor.ts +31 -4
- package/src/tools/browser/tab-worker.ts +32 -2
- package/src/tools/output-meta.ts +16 -1
- package/src/tools/shell-tokenize.ts +101 -0
- package/src/utils/image-loading.ts +8 -3
- package/src/utils/shell-snapshot.ts +31 -21
- package/src/web/search/providers/duckduckgo.ts +183 -15
- package/src/web/search/types.ts +6 -1
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.2.
|
|
4
|
+
"version": "17.2.2",
|
|
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",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "17.2.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.2.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.2.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.2.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.2.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.2.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.2.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.2.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.2.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.2.
|
|
55
|
+
"@oh-my-pi/hashline": "17.2.2",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.2.2",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.2.2",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.2.2",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.2.2",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.2.2",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.2.2",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.2.2",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.2.2",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.2.2",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.2.2",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
68
68
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -9,7 +9,7 @@ import { resolvePrimaryModel, resolveSmolModel } from "../../commit/model-select
|
|
|
9
9
|
import type { CommitCommandArgs, ConventionalAnalysis, NumstatEntry } from "../../commit/types";
|
|
10
10
|
import { ModelRegistry } from "../../config/model-registry";
|
|
11
11
|
import { Settings } from "../../config/settings";
|
|
12
|
-
import { discoverAuthStorage, discoverContextFiles } from "../../sdk";
|
|
12
|
+
import { discoverAuthStorage, discoverContextFiles, loadCliExtensionProviders } from "../../sdk";
|
|
13
13
|
import * as git from "../../utils/git";
|
|
14
14
|
import { type ExistingChangelogEntries, runCommitAgentSession } from "./agent";
|
|
15
15
|
import { generateFallbackProposal } from "./fallback";
|
|
@@ -32,6 +32,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
32
32
|
process.stdout.write("● Resolving model...\n");
|
|
33
33
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
34
34
|
await modelRegistry.refresh();
|
|
35
|
+
await loadCliExtensionProviders(modelRegistry, settings, cwd);
|
|
35
36
|
const stagedFilesPromise = (async () => {
|
|
36
37
|
let stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
|
|
37
38
|
if (stagedFiles.length === 0) {
|
package/src/commit/pipeline.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
|
|
|
4
4
|
import { getProjectDir, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { ModelRegistry } from "../config/model-registry";
|
|
6
6
|
import { Settings } from "../config/settings";
|
|
7
|
-
import { discoverAuthStorage } from "../sdk";
|
|
7
|
+
import { discoverAuthStorage, loadCliExtensionProviders } from "../sdk";
|
|
8
8
|
import { loadProjectContextFiles } from "../system-prompt";
|
|
9
9
|
import * as git from "../utils/git";
|
|
10
10
|
import { runAgenticCommit } from "./agentic";
|
|
@@ -40,11 +40,12 @@ export async function runCommitCommand(args: CommitCommandArgs): Promise<void> {
|
|
|
40
40
|
|
|
41
41
|
async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
|
|
42
42
|
const cwd = getProjectDir();
|
|
43
|
-
const settings = await Settings.init();
|
|
43
|
+
const settings = await Settings.init({ cwd });
|
|
44
44
|
const commitSettings = settings.getGroup("commit");
|
|
45
45
|
const authStorage = await discoverAuthStorage();
|
|
46
46
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
47
47
|
await modelRegistry.refresh();
|
|
48
|
+
await loadCliExtensionProviders(modelRegistry, settings, cwd);
|
|
48
49
|
|
|
49
50
|
const {
|
|
50
51
|
model: primaryModel,
|
|
@@ -53,6 +53,7 @@ interface AppKeybindings {
|
|
|
53
53
|
"app.plan.toggle": true;
|
|
54
54
|
"app.history.search": true;
|
|
55
55
|
"app.stt.toggle": true;
|
|
56
|
+
"app.live.toggle": true;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export type AppKeybinding = keyof AppKeybindings;
|
|
@@ -92,7 +93,7 @@ export const KEYBINDINGS = {
|
|
|
92
93
|
description: "Suspend application",
|
|
93
94
|
},
|
|
94
95
|
"app.display.reset": {
|
|
95
|
-
defaultKeys: "
|
|
96
|
+
defaultKeys: "alt+l",
|
|
96
97
|
description: "Reset terminal display",
|
|
97
98
|
},
|
|
98
99
|
"app.thinking.cycle": {
|
|
@@ -222,6 +223,10 @@ export const KEYBINDINGS = {
|
|
|
222
223
|
defaultKeys: [],
|
|
223
224
|
description: "Toggle speech-to-text (default gesture: hold Space)",
|
|
224
225
|
},
|
|
226
|
+
"app.live.toggle": {
|
|
227
|
+
defaultKeys: "ctrl+l",
|
|
228
|
+
description: "Start or stop live voice mode (/live)",
|
|
229
|
+
},
|
|
225
230
|
} as const satisfies KeybindingDefinitions;
|
|
226
231
|
|
|
227
232
|
/**
|
|
@@ -5289,7 +5289,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
5289
5289
|
group: "Services",
|
|
5290
5290
|
label: "Codex Auto-Redeem Saved Resets",
|
|
5291
5291
|
description:
|
|
5292
|
-
"
|
|
5292
|
+
"Spend saved Codex rate-limit resets automatically: restore an account blocked by an exhausted 5h or weekly window when a turn is stuck and no other account can take over, and salvage credits that are about to expire. unset asks before the first spend, yes spends without prompting, and no disables both checks.",
|
|
5293
5293
|
options: [
|
|
5294
5294
|
{
|
|
5295
5295
|
value: "unset",
|
|
@@ -5309,7 +5309,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
5309
5309
|
group: "Services",
|
|
5310
5310
|
label: "Codex Auto-Redeem Min Block",
|
|
5311
5311
|
description:
|
|
5312
|
-
"Only auto-redeem when the natural
|
|
5312
|
+
"Only auto-redeem when the natural unblock — the latest reset among the exhausted 5h/weekly windows — is at least this many minutes away (don't spend a scarce credit to save a short wait). Raise it (e.g. 360) to ignore 5h-only blocks.",
|
|
5313
5313
|
},
|
|
5314
5314
|
},
|
|
5315
5315
|
"codexResets.keepCredits": {
|
|
@@ -5319,7 +5319,19 @@ export const SETTINGS_SCHEMA = {
|
|
|
5319
5319
|
tab: "providers",
|
|
5320
5320
|
group: "Services",
|
|
5321
5321
|
label: "Codex Auto-Redeem Reserve",
|
|
5322
|
-
description:
|
|
5322
|
+
description:
|
|
5323
|
+
"Never auto-spend below this many saved resets (0 = the last credit may be spent automatically). Credits about to expire are exempt — a reserved credit that expires preserves nothing.",
|
|
5324
|
+
},
|
|
5325
|
+
},
|
|
5326
|
+
"codexResets.salvageHorizonHours": {
|
|
5327
|
+
type: "number",
|
|
5328
|
+
default: 12,
|
|
5329
|
+
ui: {
|
|
5330
|
+
tab: "providers",
|
|
5331
|
+
group: "Services",
|
|
5332
|
+
label: "Codex Reset Salvage Horizon",
|
|
5333
|
+
description:
|
|
5334
|
+
"Spend a saved Codex reset automatically when it would otherwise expire within this many hours and either chat window (5h or weekly) has meaningful usage to restore (0 disables expiry salvage).",
|
|
5323
5335
|
},
|
|
5324
5336
|
},
|
|
5325
5337
|
"provider.appendOnlyContext": {
|
|
@@ -5784,6 +5796,7 @@ export interface CodexResetsSettings {
|
|
|
5784
5796
|
autoRedeem: CodexAutoRedeemMode;
|
|
5785
5797
|
minBlockedMinutes: number;
|
|
5786
5798
|
keepCredits: number;
|
|
5799
|
+
salvageHorizonHours: number;
|
|
5787
5800
|
}
|
|
5788
5801
|
|
|
5789
5802
|
export interface GcSettings {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tree-sitter-backed {@link BlockResolver} for the hashline
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Tree-sitter-backed {@link BlockResolver} for the hashline `N*` locator.
|
|
3
|
+
* Bridges the pure hashline seam to the native `blockRangeAt` primitive in
|
|
4
|
+
* `@oh-my-pi/pi-natives`, which infers the language from the file path and
|
|
5
|
+
* returns the 1-indexed line span of the syntactic block beginning on the
|
|
6
|
+
* requested line (or `null` when none can be resolved).
|
|
7
7
|
*/
|
|
8
8
|
import type { BlockResolver } from "@oh-my-pi/hashline";
|
|
9
9
|
import { blockRangeAt } from "@oh-my-pi/pi-natives";
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type Cursor,
|
|
18
18
|
computeFileHash,
|
|
19
19
|
type Edit,
|
|
20
|
+
forkClipboard,
|
|
20
21
|
Patch as HashlinePatch,
|
|
21
22
|
hasBlockEdit,
|
|
22
23
|
MismatchError,
|
|
@@ -53,10 +54,10 @@ export interface HashlineDiffOptions {
|
|
|
53
54
|
skipHashValidation?: boolean;
|
|
54
55
|
/**
|
|
55
56
|
* Clipboard register shared across the sections of one patch preview.
|
|
56
|
-
* `CUT` in an earlier section feeds `
|
|
57
|
-
* matches apply. Multi-section previews MUST thread one
|
|
58
|
-
* sections in patch order; omitted, each section gets a
|
|
59
|
-
* private register (same-file cut/
|
|
57
|
+
* `CUT` in an earlier section feeds a register-backed `PUT` in a later one,
|
|
58
|
+
* so the preview matches apply. Multi-section previews MUST thread one
|
|
59
|
+
* register through sections in patch order; omitted, each section gets a
|
|
60
|
+
* private register (same-file cut/put still previews correctly).
|
|
60
61
|
*/
|
|
61
62
|
clipboard?: Clipboard;
|
|
62
63
|
}
|
|
@@ -153,9 +154,11 @@ async function readSectionForPreview(
|
|
|
153
154
|
|
|
154
155
|
function hasAnchorScopedEdit(edits: readonly Edit[]): boolean {
|
|
155
156
|
return edits.some(edit => {
|
|
156
|
-
if (edit.kind === "delete") return true;
|
|
157
|
-
if (edit.kind === "
|
|
158
|
-
|
|
157
|
+
if (edit.kind === "delete" || edit.kind === "block" || edit.kind === "cut") return true;
|
|
158
|
+
if (edit.kind === "paste") {
|
|
159
|
+
if (edit.at.kind === "span") return true;
|
|
160
|
+
return edit.at.cursor.kind === "before_anchor" || edit.at.cursor.kind === "after_anchor";
|
|
161
|
+
}
|
|
159
162
|
return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor";
|
|
160
163
|
});
|
|
161
164
|
}
|
|
@@ -279,19 +282,23 @@ function buildStreamingSectionDiff(
|
|
|
279
282
|
onUnresolved: "drop",
|
|
280
283
|
});
|
|
281
284
|
const fileLines = normalized.split("\n");
|
|
282
|
-
// Expand
|
|
283
|
-
// a
|
|
284
|
-
// capture (out-of-range while the range digits are still
|
|
285
|
-
// transient sequencing violations) throws AFTER earlier
|
|
286
|
-
// landed, and leaking those into the shared register
|
|
287
|
-
// section preview a
|
|
288
|
-
//
|
|
289
|
-
const scratch
|
|
285
|
+
// Expand register-backed PUT ops so moved rows appear in the preview.
|
|
286
|
+
// Resolve into a transactional register and publish it only on success: a
|
|
287
|
+
// mid-typed capture (out-of-range while the range digits are still
|
|
288
|
+
// streaming, transient sequencing violations) throws AFTER earlier
|
|
289
|
+
// captures may have landed, and leaking those into the shared register
|
|
290
|
+
// would let a later section preview a PUT that never happened. On failure,
|
|
291
|
+
// filter the clipboard edits out of this section's frame.
|
|
292
|
+
const scratch = forkClipboard(clipboard);
|
|
290
293
|
let resolved: readonly Edit[];
|
|
291
294
|
try {
|
|
292
295
|
resolved = resolveClipboardEdits(blockResolved, fileLines, scratch, { onEmptyPaste: "drop" });
|
|
293
296
|
if (scratch.lines === undefined) delete clipboard.lines;
|
|
294
297
|
else clipboard.lines = scratch.lines;
|
|
298
|
+
if (scratch.named === undefined) delete clipboard.named;
|
|
299
|
+
else clipboard.named = scratch.named;
|
|
300
|
+
if (scratch.pendingAnonCuts === undefined) delete clipboard.pendingAnonCuts;
|
|
301
|
+
else clipboard.pendingAnonCuts = scratch.pendingAnonCuts;
|
|
295
302
|
} catch {
|
|
296
303
|
resolved = blockResolved.filter(edit => edit.kind !== "cut" && edit.kind !== "paste");
|
|
297
304
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
Patcher,
|
|
22
22
|
type PatchSectionResult,
|
|
23
23
|
type PreparedSection,
|
|
24
|
+
startClipboardBatch,
|
|
24
25
|
} from "@oh-my-pi/hashline";
|
|
25
26
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
26
27
|
import type { FileDiagnosticsResult, WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
@@ -48,7 +49,7 @@ export interface ExecuteHashlineSingleOptions {
|
|
|
48
49
|
|
|
49
50
|
function noChangeDiagnostic(path: string): string {
|
|
50
51
|
// The patch parsed and applied cleanly but produced no change — the
|
|
51
|
-
//
|
|
52
|
+
// `+TEXT` body rows matched the file content at the targeted lines
|
|
52
53
|
// byte-for-byte. The model usually misreads this as "wrong anchor, try
|
|
53
54
|
// again with a bigger payload" and starts duplicating content; the
|
|
54
55
|
// message below names the cause directly so the next turn can re-read
|
|
@@ -103,14 +104,14 @@ interface RenderedSection {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
const BLOCK_OP_LABELS: Record<BlockResolution["op"], string> = {
|
|
106
|
-
replace: "
|
|
107
|
-
insert_after: "
|
|
108
|
-
cut: "CUT
|
|
109
|
-
paste_after: "
|
|
107
|
+
replace: "PUT N*:",
|
|
108
|
+
insert_after: "PUT >N*:",
|
|
109
|
+
cut: "CUT N*",
|
|
110
|
+
paste_after: "PUT >N*",
|
|
110
111
|
};
|
|
111
112
|
|
|
112
113
|
function formatBlockResolution(resolution: BlockResolution): string {
|
|
113
|
-
const op = BLOCK_OP_LABELS[resolution.op];
|
|
114
|
+
const op = BLOCK_OP_LABELS[resolution.op].replace("N", String(resolution.anchorLine));
|
|
114
115
|
const lines = resolution.end - resolution.start + 1;
|
|
115
116
|
const span =
|
|
116
117
|
resolution.start === resolution.end ? `line ${resolution.start}` : `lines ${resolution.start}-${resolution.end}`;
|
|
@@ -120,7 +121,7 @@ function formatBlockResolution(resolution: BlockResolution): string {
|
|
|
120
121
|
: resolution.op === "paste_after"
|
|
121
122
|
? `; clipboard lands after line ${resolution.end}`
|
|
122
123
|
: "";
|
|
123
|
-
return `${op}
|
|
124
|
+
return `${op} → resolved ${span} (${lines} line${lines === 1 ? "" : "s"})${suffix}`;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
function renderSection(
|
|
@@ -229,11 +230,11 @@ export async function executeHashlineSingle(
|
|
|
229
230
|
const enforceSeenLines = options.session.settings.get("edit.enforceSeenLines");
|
|
230
231
|
const patcher = new Patcher({ fs, snapshots, blockResolver: nativeBlockResolver, enforceSeenLines });
|
|
231
232
|
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// only after writes land.
|
|
233
|
+
// Named registers persist across edit calls; the anonymous register is
|
|
234
|
+
// batch-local. Each batch starts without anonymous state and publishes
|
|
235
|
+
// named registers only after writes land.
|
|
235
236
|
const sessionClipboard = getEditClipboard(options.session);
|
|
236
|
-
const clipboard =
|
|
237
|
+
const clipboard = startClipboardBatch(sessionClipboard);
|
|
237
238
|
|
|
238
239
|
// Single-section fast path: prepare, commit, render.
|
|
239
240
|
const inputHash = hashPatchInput(options.input);
|
|
@@ -254,8 +255,8 @@ export async function executeHashlineSingle(
|
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
// Multi-section: prepare every section up front so we fail fast before
|
|
257
|
-
// any write hits the filesystem. One
|
|
258
|
-
// so `CUT` in one section feeds `
|
|
258
|
+
// any write hits the filesystem. One batch-local register spans the batch,
|
|
259
|
+
// so `CUT` in one section feeds a register-backed `PUT` in a later one.
|
|
259
260
|
const prepared: PreparedSection[] = [];
|
|
260
261
|
// Register state after each section's prepare. Commits are non-atomic: a
|
|
261
262
|
// mid-batch write failure leaves earlier sections on disk, so the session
|
package/src/edit/renderer.ts
CHANGED
|
@@ -140,7 +140,7 @@ interface HashlineInputEntry {
|
|
|
140
140
|
path: string;
|
|
141
141
|
op?: Operation;
|
|
142
142
|
rename?: string;
|
|
143
|
-
/** A
|
|
143
|
+
/** A PUT/CUT line edit precedes the file op — keeps a move framed. */
|
|
144
144
|
hasLineEdits?: boolean;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -568,9 +568,9 @@ function parseHashlineInputPreviewHeader(line: string): string | null {
|
|
|
568
568
|
return previewPath.length > 0 ? previewPath : null;
|
|
569
569
|
}
|
|
570
570
|
|
|
571
|
-
// Line-editing op headers (
|
|
571
|
+
// Line-editing op headers (PUT/CUT family), distinct from file-level
|
|
572
572
|
// REM/MV ops. Body rows are `+TEXT`, so this only matches real headers.
|
|
573
|
-
const HL_LINE_OP_HEADER = /^(?:
|
|
573
|
+
const HL_LINE_OP_HEADER = /^(?:PUT|CUT)\b/;
|
|
574
574
|
|
|
575
575
|
/**
|
|
576
576
|
* Walk a (possibly mid-stream) hashline payload into per-section descriptors:
|
|
@@ -313,6 +313,45 @@ export function attachSessionOwner(
|
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
/** Owner registry shared by every language's live/starting session records. */
|
|
317
|
+
export interface SessionOwners {
|
|
318
|
+
ownerIds: Set<string>;
|
|
319
|
+
hasFallbackOwner: boolean;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Resolve the session key an owner's eval cell runs on, forking `reset` away
|
|
324
|
+
* from shared kernels.
|
|
325
|
+
*
|
|
326
|
+
* Eval sessions are shared across agents by design (subagents inherit the
|
|
327
|
+
* parent's eval session id), so honoring `reset` on a co-owned kernel would
|
|
328
|
+
* destroy every other agent's state — including cells executing at that
|
|
329
|
+
* moment. When the requester does not exclusively own the live base session,
|
|
330
|
+
* its reset resolves to a deterministic per-owner fork key: the requester
|
|
331
|
+
* starts a fresh private kernel while co-owners keep the shared one. Once
|
|
332
|
+
* forked, the owner keeps resolving to its fork, and per-owner dispose reaps
|
|
333
|
+
* the fork since the requester is its only registered owner.
|
|
334
|
+
*/
|
|
335
|
+
export function resolveOwnerScopedSessionKey(options: {
|
|
336
|
+
baseKey: string;
|
|
337
|
+
ownerId: string | undefined;
|
|
338
|
+
reset: boolean;
|
|
339
|
+
/** True when a live or starting session exists under `key`. */
|
|
340
|
+
hasSession: (key: string) => boolean;
|
|
341
|
+
/** Owner registry for the session under `key`, when inspectable. */
|
|
342
|
+
getOwners: (key: string) => SessionOwners | undefined;
|
|
343
|
+
}): string {
|
|
344
|
+
const { baseKey, ownerId } = options;
|
|
345
|
+
if (ownerId === undefined) return baseKey;
|
|
346
|
+
const forkKey = `${baseKey}\0fork\0${ownerId}`;
|
|
347
|
+
if (options.hasSession(forkKey)) return forkKey;
|
|
348
|
+
if (!options.reset) return baseKey;
|
|
349
|
+
const base = options.getOwners(baseKey);
|
|
350
|
+
if (!base) return baseKey;
|
|
351
|
+
const exclusive = !base.hasFallbackOwner && base.ownerIds.size === 1 && base.ownerIds.has(ownerId);
|
|
352
|
+
return exclusive ? baseKey : forkKey;
|
|
353
|
+
}
|
|
354
|
+
|
|
316
355
|
// ---------------------------------------------------------------------------
|
|
317
356
|
// Base executor implementation
|
|
318
357
|
// ---------------------------------------------------------------------------
|
package/src/eval/jl/executor.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { ToolSession } from "../../tools";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
attachSessionOwner,
|
|
6
|
+
createCancelledKernelResult,
|
|
7
|
+
executeWithKernelBase,
|
|
8
|
+
resolveOwnerScopedSessionKey,
|
|
9
|
+
} from "../executor-base";
|
|
5
10
|
import { ensurePyToolBridge, type PyToolBridgeInfo } from "../py/tool-bridge";
|
|
6
11
|
import type { EvalDisplayOutput, EvalStatusEvent } from "../types";
|
|
7
12
|
import {
|
|
@@ -451,7 +456,13 @@ async function ensureToolBridge(options: JuliaExecutorOptions): Promise<void> {
|
|
|
451
456
|
|
|
452
457
|
async function executeOnSession(code: string, cwd: string, options: JuliaExecutorOptions): Promise<JuliaResult> {
|
|
453
458
|
const sessionId = options.sessionId ?? `session:${cwd}`;
|
|
454
|
-
const sessionKey =
|
|
459
|
+
const sessionKey = resolveOwnerScopedSessionKey({
|
|
460
|
+
baseKey: buildSessionKey(sessionId, cwd, options.interpreter),
|
|
461
|
+
ownerId: options.kernelOwnerId,
|
|
462
|
+
reset: options.reset === true,
|
|
463
|
+
hasSession: key => sessions.has(key) || startingSessions.has(key),
|
|
464
|
+
getOwners: key => sessions.get(key) ?? startingSessions.get(key),
|
|
465
|
+
});
|
|
455
466
|
if (options.bridge && !options.bridgeSessionId) {
|
|
456
467
|
options.bridgeSessionId = sessionId;
|
|
457
468
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import type { ToolSession } from "../../tools";
|
|
9
9
|
import { ToolAbortError, ToolError } from "../../tools/tool-errors";
|
|
10
10
|
import { safeSend as safeSendIpc } from "../../utils/ipc";
|
|
11
|
+
import { attachSessionOwner, resolveOwnerScopedSessionKey, type SessionOwners } from "../executor-base";
|
|
11
12
|
import { shouldDetachKernel } from "../py/spawn-options";
|
|
12
13
|
import { callSessionTool, type JsStatusEvent } from "./tool-bridge";
|
|
13
14
|
import { WorkerCore } from "./worker-core";
|
|
@@ -57,10 +58,16 @@ interface JsSession {
|
|
|
57
58
|
worker: WorkerHandle;
|
|
58
59
|
state: "alive" | "dead";
|
|
59
60
|
pending: Map<string, PendingRun>;
|
|
61
|
+
ownerIds: Set<string>;
|
|
62
|
+
hasFallbackOwner: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface StartingJsSession extends SessionOwners {
|
|
66
|
+
promise: Promise<JsSession>;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
const sessions = new Map<string, JsSession>();
|
|
63
|
-
const startingSessions = new Map<string,
|
|
70
|
+
const startingSessions = new Map<string, StartingJsSession>();
|
|
64
71
|
const resettingSessions = new Map<string, Promise<void>>();
|
|
65
72
|
// Worker startup (module-graph import + WorkerCore construction) is infrastructure
|
|
66
73
|
// cost, not user compute. Floor it independently of Bun's 5s default per-test timeout
|
|
@@ -99,6 +106,8 @@ export function setJsEvalWorkerThreadForTests(enabled: boolean): boolean {
|
|
|
99
106
|
export async function executeInVmContext(options: {
|
|
100
107
|
sessionKey: string;
|
|
101
108
|
sessionId: string;
|
|
109
|
+
/** Logical owner identifier; scopes `reset` on shared contexts and retained-worker cleanup. */
|
|
110
|
+
ownerId?: string;
|
|
102
111
|
cwd: string;
|
|
103
112
|
session: ToolSession;
|
|
104
113
|
localRoots?: Record<string, string>;
|
|
@@ -108,47 +117,55 @@ export async function executeInVmContext(options: {
|
|
|
108
117
|
timeoutMs?: number;
|
|
109
118
|
runState: VmRunState;
|
|
110
119
|
}): Promise<{ value: unknown }> {
|
|
120
|
+
const sessionKey = resolveOwnerScopedSessionKey({
|
|
121
|
+
baseKey: options.sessionKey,
|
|
122
|
+
ownerId: options.ownerId,
|
|
123
|
+
reset: options.reset === true,
|
|
124
|
+
hasSession: key => sessions.has(key) || startingSessions.has(key),
|
|
125
|
+
getOwners: key => sessions.get(key) ?? startingSessions.get(key),
|
|
126
|
+
});
|
|
111
127
|
if (options.reset) {
|
|
112
128
|
// Coalesce concurrent resets: an existing in-flight reset already
|
|
113
129
|
// produces a fresh context, so a follow-up `reset: true` cell should
|
|
114
130
|
// just wait for it rather than failing the user-visible call.
|
|
115
|
-
const inFlight = resettingSessions.get(
|
|
131
|
+
const inFlight = resettingSessions.get(sessionKey);
|
|
116
132
|
if (inFlight) await inFlight.catch(() => undefined);
|
|
117
133
|
else {
|
|
118
|
-
const resetPromise = resetVmContext(
|
|
134
|
+
const resetPromise = resetVmContext(sessionKey);
|
|
119
135
|
resettingSessions.set(
|
|
120
|
-
|
|
136
|
+
sessionKey,
|
|
121
137
|
resetPromise.then(() => undefined),
|
|
122
138
|
);
|
|
123
139
|
try {
|
|
124
140
|
await resetPromise;
|
|
125
141
|
} finally {
|
|
126
|
-
resettingSessions.delete(
|
|
142
|
+
resettingSessions.delete(sessionKey);
|
|
127
143
|
}
|
|
128
144
|
}
|
|
129
145
|
} else {
|
|
130
146
|
// Internal coordination: wait for any in-flight reset to settle and
|
|
131
147
|
// then run on the freshly-rebuilt context.
|
|
132
|
-
const inFlight = resettingSessions.get(
|
|
148
|
+
const inFlight = resettingSessions.get(sessionKey);
|
|
133
149
|
if (inFlight) await inFlight.catch(() => undefined);
|
|
134
150
|
}
|
|
135
151
|
const session = await acquireSession(
|
|
136
|
-
|
|
152
|
+
sessionKey,
|
|
137
153
|
{ cwd: options.cwd, sessionId: options.sessionId, localRoots: options.localRoots },
|
|
138
154
|
options.timeoutMs,
|
|
155
|
+
options.ownerId,
|
|
139
156
|
);
|
|
140
157
|
return await runOnce(session, options);
|
|
141
158
|
}
|
|
142
159
|
|
|
143
160
|
export async function resetVmContext(sessionKey: string): Promise<void> {
|
|
144
|
-
const session = sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.catch(() => undefined));
|
|
161
|
+
const session = sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.promise.catch(() => undefined));
|
|
145
162
|
if (!session) return;
|
|
146
163
|
sessions.delete(sessionKey);
|
|
147
164
|
await killSession(session, new ToolError("JS context reset"), { force: false });
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
export async function disposeAllVmContexts(): Promise<void> {
|
|
151
|
-
const pending = [...startingSessions.values()];
|
|
168
|
+
const pending = [...startingSessions.values()].map(starting => starting.promise);
|
|
152
169
|
startingSessions.clear();
|
|
153
170
|
const started = await Promise.allSettled(pending);
|
|
154
171
|
const all = [...sessions.values()];
|
|
@@ -160,6 +177,45 @@ export async function disposeAllVmContexts(): Promise<void> {
|
|
|
160
177
|
await Promise.all(all.map(session => killSession(session, new ToolError("JS context disposed"), { force: false })));
|
|
161
178
|
}
|
|
162
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Shut down retained JS contexts owned solely by `ownerId` (e.g. a subagent's
|
|
182
|
+
* private fork); shared contexts just drop the owner registration.
|
|
183
|
+
*/
|
|
184
|
+
export async function disposeVmContextsByOwner(ownerId: string): Promise<void> {
|
|
185
|
+
const toKill: JsSession[] = [];
|
|
186
|
+
for (const session of [...sessions.values()]) {
|
|
187
|
+
if (!session.ownerIds.has(ownerId)) continue;
|
|
188
|
+
if (session.ownerIds.size === 1) {
|
|
189
|
+
toKill.push(session);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
session.ownerIds.delete(ownerId);
|
|
193
|
+
}
|
|
194
|
+
const startingToKill: StartingJsSession[] = [];
|
|
195
|
+
for (const [sessionKey, starting] of [...startingSessions.entries()]) {
|
|
196
|
+
if (sessions.has(sessionKey) || !starting.ownerIds.has(ownerId)) continue;
|
|
197
|
+
if (starting.ownerIds.size === 1) {
|
|
198
|
+
startingSessions.delete(sessionKey);
|
|
199
|
+
startingToKill.push(starting);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
starting.ownerIds.delete(ownerId);
|
|
203
|
+
}
|
|
204
|
+
for (const session of toKill) {
|
|
205
|
+
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
206
|
+
}
|
|
207
|
+
const started = await Promise.allSettled(startingToKill.map(starting => starting.promise));
|
|
208
|
+
for (const result of started) {
|
|
209
|
+
if (result.status !== "fulfilled") continue;
|
|
210
|
+
const session = result.value;
|
|
211
|
+
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
212
|
+
toKill.push(session);
|
|
213
|
+
}
|
|
214
|
+
await Promise.all(
|
|
215
|
+
toKill.map(session => killSession(session, new ToolError("JS context disposed"), { force: false })),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
163
219
|
/**
|
|
164
220
|
* Smoke probe: spawn the JS evaluator through the worker-host entry and prove
|
|
165
221
|
* it answers the `init` handshake in a real isolated subprocess (not the inline
|
|
@@ -177,6 +233,8 @@ export async function smokeTestJsEvalWorker(): Promise<void> {
|
|
|
177
233
|
worker,
|
|
178
234
|
state: "alive",
|
|
179
235
|
pending: new Map(),
|
|
236
|
+
ownerIds: new Set(),
|
|
237
|
+
hasFallbackOwner: false,
|
|
180
238
|
};
|
|
181
239
|
try {
|
|
182
240
|
await initWorker(session, { cwd: process.cwd(), sessionId: "smoke" }, WORKER_INIT_TIMEOUT_MS);
|
|
@@ -243,15 +301,25 @@ async function runOnce(
|
|
|
243
301
|
}
|
|
244
302
|
}
|
|
245
303
|
|
|
246
|
-
async function acquireSession(
|
|
304
|
+
async function acquireSession(
|
|
305
|
+
sessionKey: string,
|
|
306
|
+
snapshot: SessionSnapshot,
|
|
307
|
+
timeoutMs?: number,
|
|
308
|
+
ownerId?: string,
|
|
309
|
+
): Promise<JsSession> {
|
|
247
310
|
const existing = sessions.get(sessionKey);
|
|
248
311
|
if (existing && existing.state === "alive") {
|
|
249
312
|
existing.sessionId = snapshot.sessionId;
|
|
250
313
|
existing.cwd = snapshot.cwd;
|
|
314
|
+
attachSessionOwner(existing, snapshot.sessionId, ownerId);
|
|
251
315
|
return existing;
|
|
252
316
|
}
|
|
253
317
|
const starting = startingSessions.get(sessionKey);
|
|
254
|
-
if (starting)
|
|
318
|
+
if (starting) {
|
|
319
|
+
attachSessionOwner(starting, snapshot.sessionId, ownerId);
|
|
320
|
+
return await starting.promise;
|
|
321
|
+
}
|
|
322
|
+
let startingSession!: StartingJsSession;
|
|
255
323
|
|
|
256
324
|
const startup = (async (): Promise<JsSession> => {
|
|
257
325
|
// Attach the message listener before sending init. Both Bun Worker messages
|
|
@@ -264,6 +332,8 @@ async function acquireSession(sessionKey: string, snapshot: SessionSnapshot, tim
|
|
|
264
332
|
worker,
|
|
265
333
|
state: "alive",
|
|
266
334
|
pending: new Map(),
|
|
335
|
+
ownerIds: new Set(),
|
|
336
|
+
hasFallbackOwner: false,
|
|
267
337
|
};
|
|
268
338
|
// Init headroom is the fixed infrastructure floor; the caller's per-cell timeout
|
|
269
339
|
// dominates when larger so users can grant more by raising `timeout` on a cell.
|
|
@@ -293,14 +363,27 @@ async function acquireSession(sessionKey: string, snapshot: SessionSnapshot, tim
|
|
|
293
363
|
session.state = "alive";
|
|
294
364
|
}
|
|
295
365
|
}
|
|
296
|
-
|
|
366
|
+
session.ownerIds = new Set(startingSession.ownerIds);
|
|
367
|
+
session.hasFallbackOwner = startingSession.hasFallbackOwner;
|
|
368
|
+
// Publish only while this startup still owns the key: owner disposal or
|
|
369
|
+
// a concurrent dispose-all may have already reaped the starting record,
|
|
370
|
+
// and publishing here would resurrect a context that was just torn down.
|
|
371
|
+
if (startingSessions.get(sessionKey) === startingSession) {
|
|
372
|
+
sessions.set(sessionKey, session);
|
|
373
|
+
}
|
|
297
374
|
return session;
|
|
298
375
|
})();
|
|
299
|
-
|
|
376
|
+
startingSession = {
|
|
377
|
+
ownerIds: new Set(),
|
|
378
|
+
hasFallbackOwner: false,
|
|
379
|
+
promise: startup,
|
|
380
|
+
};
|
|
381
|
+
attachSessionOwner(startingSession, snapshot.sessionId, ownerId);
|
|
382
|
+
startingSessions.set(sessionKey, startingSession);
|
|
300
383
|
try {
|
|
301
384
|
return await startup;
|
|
302
385
|
} finally {
|
|
303
|
-
if (startingSessions.get(sessionKey) ===
|
|
386
|
+
if (startingSessions.get(sessionKey) === startingSession) startingSessions.delete(sessionKey);
|
|
304
387
|
}
|
|
305
388
|
}
|
|
306
389
|
|
package/src/eval/js/executor.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface JsExecutorOptions {
|
|
|
19
19
|
onStatus?: (event: JsStatusEvent) => void;
|
|
20
20
|
signal?: AbortSignal;
|
|
21
21
|
sessionId: string;
|
|
22
|
+
/** Logical owner identifier; scopes `reset` on shared contexts and retained-worker cleanup. */
|
|
23
|
+
kernelOwnerId?: string;
|
|
22
24
|
reset?: boolean;
|
|
23
25
|
sessionFile?: string;
|
|
24
26
|
artifactPath?: string;
|
|
@@ -100,6 +102,7 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
|
|
|
100
102
|
await executeInVmContext({
|
|
101
103
|
sessionKey: options.sessionId,
|
|
102
104
|
sessionId: options.sessionId,
|
|
105
|
+
ownerId: options.kernelOwnerId,
|
|
103
106
|
cwd: options.cwd ?? options.session.cwd,
|
|
104
107
|
session: options.session,
|
|
105
108
|
localRoots: options.localRoots,
|
package/src/eval/js/index.ts
CHANGED