@oh-my-pi/pi-coding-agent 17.2.1 → 17.2.3

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.
Files changed (121) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/{CHANGELOG-dj46zzrm.md → CHANGELOG-c5hpqt9r.md} +61 -0
  3. package/dist/cli.js +3856 -3850
  4. package/dist/types/config/keybindings.d.ts +6 -1
  5. package/dist/types/config/settings-schema.d.ts +16 -5
  6. package/dist/types/edit/hashline/block-resolver.d.ts +5 -5
  7. package/dist/types/edit/hashline/diff.d.ts +4 -4
  8. package/dist/types/eval/executor-base.d.ts +27 -0
  9. package/dist/types/eval/js/context-manager.d.ts +7 -0
  10. package/dist/types/eval/js/executor.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/runner.d.ts +64 -5
  12. package/dist/types/extensibility/extensions/types.d.ts +15 -0
  13. package/dist/types/extensibility/extensions/wrapper.d.ts +1 -1
  14. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  15. package/dist/types/live/visualizer.d.ts +3 -1
  16. package/dist/types/modes/acp/acp-agent.d.ts +3 -3
  17. package/dist/types/modes/components/ask-dialog.d.ts +0 -1
  18. package/dist/types/modes/components/keybinding-hints.d.ts +1 -1
  19. package/dist/types/modes/controllers/mcp-command-controller.d.ts +13 -0
  20. package/dist/types/modes/interactive-mode.d.ts +1 -0
  21. package/dist/types/modes/theme/theme.d.ts +1 -1
  22. package/dist/types/modes/types.d.ts +2 -0
  23. package/dist/types/secrets/index.d.ts +22 -0
  24. package/dist/types/secrets/obfuscator.d.ts +1 -1
  25. package/dist/types/session/agent-session-types.d.ts +4 -0
  26. package/dist/types/session/agent-session.d.ts +9 -0
  27. package/dist/types/session/codex-auto-reset.d.ts +182 -64
  28. package/dist/types/session/session-storage.d.ts +5 -2
  29. package/dist/types/session/session-tools.d.ts +2 -1
  30. package/dist/types/session/turn-recovery.d.ts +7 -1
  31. package/dist/types/system-prompt.d.ts +8 -0
  32. package/dist/types/task/executor.d.ts +27 -0
  33. package/dist/types/task/persisted-revive.d.ts +7 -0
  34. package/dist/types/tools/bash-interceptor.d.ts +1 -1
  35. package/dist/types/tools/browser/attach.d.ts +9 -1
  36. package/dist/types/tools/browser/launch.d.ts +50 -1
  37. package/dist/types/tools/browser/registry.d.ts +7 -0
  38. package/dist/types/tools/browser/shared-daemon.d.ts +26 -0
  39. package/dist/types/tools/browser/tab-protocol.d.ts +8 -0
  40. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -0
  41. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  42. package/dist/types/tools/shell-tokenize.d.ts +11 -0
  43. package/dist/types/utils/image-loading.d.ts +2 -0
  44. package/dist/types/web/search/providers/duckduckgo.d.ts +9 -0
  45. package/dist/types/web/search/types.d.ts +2 -2
  46. package/package.json +12 -12
  47. package/src/commit/agentic/index.ts +2 -1
  48. package/src/commit/pipeline.ts +3 -2
  49. package/src/config/keybindings.ts +6 -1
  50. package/src/config/settings-schema.ts +16 -3
  51. package/src/edit/hashline/block-resolver.ts +5 -5
  52. package/src/edit/hashline/diff.ts +22 -15
  53. package/src/edit/hashline/execute.ts +14 -13
  54. package/src/edit/renderer.ts +3 -3
  55. package/src/eval/executor-base.ts +39 -0
  56. package/src/eval/jl/executor.ts +13 -2
  57. package/src/eval/js/context-manager.ts +97 -14
  58. package/src/eval/js/executor.ts +3 -0
  59. package/src/eval/js/index.ts +1 -0
  60. package/src/eval/py/executor.ts +55 -15
  61. package/src/eval/rb/executor.ts +8 -1
  62. package/src/extensibility/extensions/runner.ts +121 -4
  63. package/src/extensibility/extensions/types.ts +15 -0
  64. package/src/extensibility/extensions/wrapper.ts +19 -2
  65. package/src/extensibility/legacy-pi-coding-agent-shim.ts +5 -5
  66. package/src/launch/broker.ts +59 -41
  67. package/src/live/visualizer.ts +8 -1
  68. package/src/lsp/index.ts +51 -11
  69. package/src/main.ts +3 -0
  70. package/src/modes/acp/acp-agent.ts +16 -5
  71. package/src/modes/components/ask-dialog.ts +65 -23
  72. package/src/modes/components/assistant-message.ts +5 -10
  73. package/src/modes/components/custom-editor.ts +1 -1
  74. package/src/modes/components/keybinding-hints.ts +4 -13
  75. package/src/modes/components/status-line/segments.ts +4 -5
  76. package/src/modes/components/tool-execution.ts +4 -5
  77. package/src/modes/controllers/input-controller.ts +18 -40
  78. package/src/modes/controllers/live-command-controller.ts +1 -0
  79. package/src/modes/controllers/mcp-command-controller.ts +28 -11
  80. package/src/modes/interactive-mode.ts +59 -2
  81. package/src/modes/theme/theme.ts +13 -6
  82. package/src/modes/types.ts +2 -0
  83. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  84. package/src/prompts/system/plan-mode-active.md +5 -5
  85. package/src/sdk.ts +47 -16
  86. package/src/secrets/index.ts +83 -3
  87. package/src/secrets/obfuscator.ts +70 -15
  88. package/src/session/agent-session-types.ts +7 -1
  89. package/src/session/agent-session.ts +394 -113
  90. package/src/session/codex-auto-reset.ts +594 -123
  91. package/src/session/eval-runner.ts +2 -0
  92. package/src/session/messages.ts +1 -1
  93. package/src/session/session-maintenance.ts +50 -3
  94. package/src/session/session-manager.ts +4 -3
  95. package/src/session/session-stats.ts +14 -3
  96. package/src/session/session-storage.ts +47 -15
  97. package/src/session/session-tools.ts +30 -2
  98. package/src/session/turn-recovery.ts +15 -2
  99. package/src/slash-commands/builtin-registry.ts +21 -15
  100. package/src/slash-commands/helpers/reset-usage.ts +2 -0
  101. package/src/system-prompt.ts +13 -1
  102. package/src/task/executor.ts +168 -15
  103. package/src/task/persisted-revive.ts +26 -1
  104. package/src/telemetry-export.ts +8 -4
  105. package/src/tools/bash-interceptor.ts +85 -8
  106. package/src/tools/bash.ts +12 -6
  107. package/src/tools/browser/attach.ts +35 -10
  108. package/src/tools/browser/launch.ts +110 -16
  109. package/src/tools/browser/registry.ts +140 -31
  110. package/src/tools/browser/shared-daemon.ts +190 -0
  111. package/src/tools/browser/tab-protocol.ts +8 -0
  112. package/src/tools/browser/tab-supervisor.ts +31 -4
  113. package/src/tools/browser/tab-worker.ts +32 -2
  114. package/src/tools/browser.ts +1 -1
  115. package/src/tools/output-meta.ts +16 -1
  116. package/src/tools/shell-tokenize.ts +101 -0
  117. package/src/utils/image-loading.ts +8 -3
  118. package/src/utils/shell-snapshot.ts +31 -21
  119. package/src/web/search/providers/codex.ts +31 -6
  120. package/src/web/search/providers/duckduckgo.ts +183 -15
  121. package/src/web/search/types.ts +6 -1
@@ -2,6 +2,15 @@ import type { AuthStorage } from "@oh-my-pi/pi-ai";
2
2
  import type { SearchResponse } from "../../../web/search/types.js";
3
3
  import type { SearchParams } from "./base.js";
4
4
  import { SearchProvider } from "./base.js";
5
+ /**
6
+ * Map a parsed `lang:` locale onto DuckDuckGo's documented `kl` values.
7
+ *
8
+ * Shared queries use `language-region` order while DDG generally uses
9
+ * `region-language`. Provider-specific exceptions resolve through
10
+ * {@link DDG_LOCALE_ALIASES}; all other values must survive the documented
11
+ * allowlist after swapping or the caller keeps its default region.
12
+ */
13
+ export declare function localeToKl(lang: string | undefined): string | undefined;
5
14
  /** Execute a DuckDuckGo web search via the no-JS HTML frontend. */
6
15
  export declare function searchDuckDuckGo(params: SearchParams): Promise<SearchResponse>;
7
16
  /** Search provider for DuckDuckGo (no API key required). */
@@ -26,7 +26,7 @@ export declare const SEARCH_PROVIDER_OPTIONS: readonly [{
26
26
  }, {
27
27
  readonly value: "xai";
28
28
  readonly label: "xAI";
29
- readonly description: "Grok web search via xAI Responses API (requires XAI_API_KEY)";
29
+ readonly description: "Grok web search via xAI Responses API (uses SuperGrok/X Premium+ OAuth via /login xai-oauth, or XAI_API_KEY)";
30
30
  }, {
31
31
  readonly value: "zai";
32
32
  readonly label: "Z.AI";
@@ -128,7 +128,7 @@ export declare const SEARCH_PROVIDER_CHOICES: ({
128
128
  } | {
129
129
  readonly value: "xai";
130
130
  readonly label: "xAI";
131
- readonly description: "Grok web search via xAI Responses API (requires XAI_API_KEY)";
131
+ readonly description: "Grok web search via xAI Responses API (uses SuperGrok/X Premium+ OAuth via /login xai-oauth, or XAI_API_KEY)";
132
132
  } | {
133
133
  readonly value: "zai";
134
134
  readonly label: "Z.AI";
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.1",
4
+ "version": "17.2.3",
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.1",
56
- "@oh-my-pi/omp-stats": "17.2.1",
57
- "@oh-my-pi/pi-agent-core": "17.2.1",
58
- "@oh-my-pi/pi-ai": "17.2.1",
59
- "@oh-my-pi/pi-catalog": "17.2.1",
60
- "@oh-my-pi/pi-mnemopi": "17.2.1",
61
- "@oh-my-pi/pi-natives": "17.2.1",
62
- "@oh-my-pi/pi-tui": "17.2.1",
63
- "@oh-my-pi/pi-utils": "17.2.1",
64
- "@oh-my-pi/pi-wire": "17.2.1",
65
- "@oh-my-pi/snapcompact": "17.2.1",
55
+ "@oh-my-pi/hashline": "17.2.3",
56
+ "@oh-my-pi/omp-stats": "17.2.3",
57
+ "@oh-my-pi/pi-agent-core": "17.2.3",
58
+ "@oh-my-pi/pi-ai": "17.2.3",
59
+ "@oh-my-pi/pi-catalog": "17.2.3",
60
+ "@oh-my-pi/pi-mnemopi": "17.2.3",
61
+ "@oh-my-pi/pi-natives": "17.2.3",
62
+ "@oh-my-pi/pi-tui": "17.2.3",
63
+ "@oh-my-pi/pi-utils": "17.2.3",
64
+ "@oh-my-pi/pi-wire": "17.2.3",
65
+ "@oh-my-pi/snapcompact": "17.2.3",
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) {
@@ -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: "ctrl+l",
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
- "When a turn is blocked by the Codex weekly limit on the active account and no other account is available, run the conservative saved-reset check. unset asks before spending the first eligible reset, yes spends eligible resets without prompting, and no disables the check entirely. Requires retries enabled.",
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 weekly reset is at least this many minutes away (don't spend a ~30-day credit to save a short wait).",
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: "Never auto-spend below this many saved resets (0 = the last credit may be spent automatically).",
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 block replace
3
- * operator. Bridges the pure hashline seam to the native `blockRangeAt`
4
- * primitive in `@oh-my-pi/pi-natives`, which infers the language from the file
5
- * path and returns the 1-indexed line span of the syntactic block beginning on
6
- * the requested line (or `null` when none can be resolved).
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 `PASTE` in a later one, so the preview
57
- * matches apply. Multi-section previews MUST thread one register through
58
- * sections in patch order; omitted, each section gets a
59
- * private register (same-file cut/paste still previews correctly).
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 === "block") return true;
158
- if (edit.kind === "cut") return true;
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 clipboard ops so a `PASTE` previews the moved rows. Resolve into
283
- // a scratch register and commit it back only on success: a mid-typed
284
- // capture (out-of-range while the range digits are still streaming,
285
- // transient sequencing violations) throws AFTER earlier captures may have
286
- // landed, and leaking those into the shared register would let a later
287
- // section preview a paste that never happened. On failure, fall back to
288
- // filtering the clipboard edits out of this section's frame.
289
- const scratch: Clipboard = { ...clipboard };
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
- // `|literal` body rows matched the file content at the targeted lines
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: "SWAP.BLK",
107
- insert_after: "INS.BLK.POST",
108
- cut: "CUT.BLK",
109
- paste_after: "PASTE.BLK.POST",
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} ${resolution.anchorLine} → resolved ${span} (${lines} line${lines === 1 ? "" : "s"})${suffix}`;
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
- // The clipboard register is session-persistent: `CUT` in one edit call can
233
- // `PASTE` in a later one. Each batch works on a fork and publishes it back
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 = forkClipboard(sessionClipboard);
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 clipboard register spans the batch,
258
- // so `CUT` in one section feeds `PASTE` in a later one.
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
@@ -140,7 +140,7 @@ interface HashlineInputEntry {
140
140
  path: string;
141
141
  op?: Operation;
142
142
  rename?: string;
143
- /** A SWAP/CUT/INS line-editing op precedes the file op — keeps a move framed. */
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 (SWAP/CUT/INS family), distinct from file-level
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 = /^(?:SWAP|CUT|INS)\b/;
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
  // ---------------------------------------------------------------------------
@@ -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 { attachSessionOwner, createCancelledKernelResult, executeWithKernelBase } from "../executor-base";
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 = buildSessionKey(sessionId, cwd, options.interpreter);
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
  }