@oh-my-pi/pi-coding-agent 13.9.16 → 13.10.1

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.10.1] - 2026-03-10
6
+ ### Added
7
+
8
+ - Exported `submitInteractiveInput()` function for programmatic submission of user input in interactive mode
9
+ - Added proactive OAuth token refresh for MCP server connections with 5-minute expiry buffer
10
+ - Added reactive 401/403 retry with automatic token refresh on HTTP MCP transports
11
+ - Added `refreshMCPOAuthToken()` for standard OAuth 2.0 refresh_token grants
12
+ - Persisted `tokenUrl`, `clientId`, and `clientSecret` in MCP auth config for cross-session token refresh
13
+
14
+ ## [13.10.0] - 2026-03-10
15
+ ### Fixed
16
+
17
+ - Preserved text signature metadata (id and phase) when building OpenAI native history during session compaction
18
+
5
19
  ## [13.9.16] - 2026-03-10
6
20
  ### Breaking Changes
7
21
 
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": "13.9.16",
4
+ "version": "13.10.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.9.16",
45
- "@oh-my-pi/pi-agent-core": "13.9.16",
46
- "@oh-my-pi/pi-ai": "13.9.16",
47
- "@oh-my-pi/pi-natives": "13.9.16",
48
- "@oh-my-pi/pi-tui": "13.9.16",
49
- "@oh-my-pi/pi-utils": "13.9.16",
44
+ "@oh-my-pi/omp-stats": "13.10.1",
45
+ "@oh-my-pi/pi-agent-core": "13.10.1",
46
+ "@oh-my-pi/pi-ai": "13.10.1",
47
+ "@oh-my-pi/pi-natives": "13.10.1",
48
+ "@oh-my-pi/pi-tui": "13.10.1",
49
+ "@oh-my-pi/pi-utils": "13.10.1",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
package/src/main.ts CHANGED
@@ -26,6 +26,7 @@ import { exportFromFile } from "./export/html";
26
26
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
27
27
  import { InteractiveMode, runPrintMode, runRpcMode } from "./modes";
28
28
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
29
+ import type { SubmittedUserInput } from "./modes/types";
29
30
  import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage } from "./sdk";
30
31
  import type { AgentSession } from "./session/agent-session";
31
32
  import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
@@ -66,6 +67,29 @@ export interface InteractiveModeNotify {
66
67
  message: string;
67
68
  }
68
69
 
70
+ export async function submitInteractiveInput(
71
+ mode: Pick<InteractiveMode, "markPendingSubmissionStarted" | "finishPendingSubmission" | "showError">,
72
+ session: Pick<AgentSession, "prompt">,
73
+ input: SubmittedUserInput,
74
+ ): Promise<void> {
75
+ if (input.cancelled) {
76
+ return;
77
+ }
78
+
79
+ try {
80
+ // Continue shortcuts submit an already-started empty prompt with no optimistic user message.
81
+ if (!input.started && !mode.markPendingSubmissionStarted(input)) {
82
+ return;
83
+ }
84
+ await session.prompt(input.text, { images: input.images });
85
+ } catch (error: unknown) {
86
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
87
+ mode.showError(errorMessage);
88
+ } finally {
89
+ mode.finishPendingSubmission(input);
90
+ }
91
+ }
92
+
69
93
  async function runInteractiveMode(
70
94
  session: AgentSession,
71
95
  version: string,
@@ -126,20 +150,7 @@ async function runInteractiveMode(
126
150
 
127
151
  while (true) {
128
152
  const input = await mode.getUserInput();
129
- if (input.cancelled) {
130
- continue;
131
- }
132
- try {
133
- if (!mode.markPendingSubmissionStarted(input)) {
134
- continue;
135
- }
136
- await session.prompt(input.text, { images: input.images });
137
- } catch (error: unknown) {
138
- const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
139
- mode.showError(errorMessage);
140
- } finally {
141
- mode.finishPendingSubmission(input);
142
- }
153
+ await submitInteractiveInput(mode, session, input);
143
154
  }
144
155
  }
145
156
 
@@ -25,9 +25,11 @@ import {
25
25
  unsubscribeFromResources,
26
26
  } from "./client";
27
27
  import { loadAllMCPConfigs, validateServerConfig } from "./config";
28
+ import { refreshMCPOAuthToken } from "./oauth-flow";
28
29
  import type { MCPToolDetails } from "./tool-bridge";
29
30
  import { DeferredMCPTool, MCPTool } from "./tool-bridge";
30
31
  import type { MCPToolCache } from "./tool-cache";
32
+ import { HttpTransport } from "./transports/http";
31
33
  import type {
32
34
  MCPGetPromptResult,
33
35
  MCPPrompt,
@@ -315,6 +317,18 @@ export class MCPManager {
315
317
  this.#pendingConnections.delete(name);
316
318
  this.#connections.set(name, connection);
317
319
  }
320
+
321
+ // Wire auth refresh for HTTP transports so 401s trigger token refresh
322
+ if (connection.transport instanceof HttpTransport && config.auth?.type === "oauth") {
323
+ connection.transport.onAuthError = async () => {
324
+ const refreshed = await this.#resolveAuthConfig(config, true);
325
+ if (refreshed.type === "http" || refreshed.type === "sse") {
326
+ return refreshed.headers ?? null;
327
+ }
328
+ return null;
329
+ };
330
+ }
331
+
318
332
  return connection;
319
333
  },
320
334
  error => {
@@ -814,15 +828,39 @@ export class MCPManager {
814
828
  /**
815
829
  * Resolve OAuth credentials and shell commands in config.
816
830
  */
817
- async #resolveAuthConfig(config: MCPServerConfig): Promise<MCPServerConfig> {
831
+ async #resolveAuthConfig(config: MCPServerConfig, forceRefresh = false): Promise<MCPServerConfig> {
818
832
  let resolved: MCPServerConfig = { ...config };
819
833
 
820
834
  const auth = config.auth;
821
835
  if (auth?.type === "oauth" && auth.credentialId && this.#authStorage) {
822
836
  const credentialId = auth.credentialId;
823
837
  try {
824
- const credential = this.#authStorage.get(credentialId);
838
+ let credential = this.#authStorage.get(credentialId);
825
839
  if (credential?.type === "oauth") {
840
+ // Proactive refresh: 5-minute buffer before expiry
841
+ // Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
842
+ const REFRESH_BUFFER_MS = 5 * 60_000;
843
+ const shouldRefresh =
844
+ forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
845
+ if (shouldRefresh && credential.refresh && auth.tokenUrl) {
846
+ try {
847
+ const refreshed = await refreshMCPOAuthToken(
848
+ auth.tokenUrl,
849
+ credential.refresh,
850
+ auth.clientId,
851
+ auth.clientSecret,
852
+ );
853
+ const refreshedCredential = { type: "oauth" as const, ...refreshed };
854
+ await this.#authStorage.set(credentialId, refreshedCredential);
855
+ credential = refreshedCredential;
856
+ } catch (refreshError) {
857
+ logger.warn("MCP OAuth refresh failed, using existing token", {
858
+ credentialId,
859
+ error: refreshError,
860
+ });
861
+ }
862
+ }
863
+
826
864
  if (resolved.type === "http" || resolved.type === "sse") {
827
865
  resolved = {
828
866
  ...resolved,
@@ -254,3 +254,44 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
254
254
  }
255
255
  }
256
256
  }
257
+
258
+ /**
259
+ * Refresh an MCP OAuth token using the standard refresh_token grant.
260
+ * Returns updated credentials; preserves the old refresh token if the server doesn't rotate it.
261
+ */
262
+ export async function refreshMCPOAuthToken(
263
+ tokenUrl: string,
264
+ refreshToken: string,
265
+ clientId?: string,
266
+ clientSecret?: string,
267
+ ): Promise<OAuthCredentials> {
268
+ const params = new URLSearchParams({
269
+ grant_type: "refresh_token",
270
+ refresh_token: refreshToken,
271
+ });
272
+ if (clientId) params.set("client_id", clientId);
273
+ if (clientSecret) params.set("client_secret", clientSecret);
274
+
275
+ const response = await fetch(tokenUrl, {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
278
+ body: params.toString(),
279
+ });
280
+
281
+ if (!response.ok) {
282
+ const text = await response.text();
283
+ throw new Error(`MCP OAuth refresh failed: ${response.status} ${text}`);
284
+ }
285
+
286
+ const data = (await response.json()) as {
287
+ access_token: string;
288
+ refresh_token?: string;
289
+ expires_in?: number;
290
+ };
291
+ const expiresIn = data.expires_in ?? 3600;
292
+ return {
293
+ access: data.access_token,
294
+ refresh: data.refresh_token ?? refreshToken,
295
+ expires: Date.now() + expiresIn * 1000,
296
+ };
297
+ }
@@ -26,6 +26,8 @@ export class HttpTransport implements MCPTransport {
26
26
  onClose?: () => void;
27
27
  onError?: (error: Error) => void;
28
28
  onNotification?: (method: string, params: unknown) => void;
29
+ /** Called on 401/403 to attempt token refresh. Returns updated headers or null. */
30
+ onAuthError?: () => Promise<Record<string, string> | null>;
29
31
 
30
32
  constructor(private config: MCPHttpServerConfig | MCPSseServerConfig) {}
31
33
 
@@ -102,6 +104,27 @@ export class HttpTransport implements MCPTransport {
102
104
  method: string,
103
105
  params?: Record<string, unknown>,
104
106
  options?: MCPRequestOptions,
107
+ ): Promise<T> {
108
+ try {
109
+ return await this.#executeRequest<T>(method, params, options);
110
+ } catch (error) {
111
+ // Retry once on auth failure if onAuthError is wired
112
+ if (this.onAuthError && error instanceof Error && /^HTTP (401|403):/.test(error.message)) {
113
+ const newHeaders = await this.onAuthError();
114
+ if (newHeaders) {
115
+ // Persist refreshed headers so subsequent requests use them directly
116
+ this.config = { ...this.config, headers: newHeaders };
117
+ return this.#executeRequest<T>(method, params, options);
118
+ }
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ async #executeRequest<T>(
125
+ method: string,
126
+ params: Record<string, unknown> | undefined,
127
+ options: MCPRequestOptions | undefined,
105
128
  ): Promise<T> {
106
129
  if (!this.#connected) {
107
130
  throw new Error("Transport not connected");
package/src/mcp/types.ts CHANGED
@@ -49,6 +49,12 @@ export interface MCPAuthConfig {
49
49
  type: "oauth" | "apikey";
50
50
  /** Credential ID for OAuth (references agent.db) */
51
51
  credentialId?: string;
52
+ /** Token endpoint URL — persisted for proactive token refresh */
53
+ tokenUrl?: string;
54
+ /** Client ID — persisted for token refresh */
55
+ clientId?: string;
56
+ /** Client secret — persisted for token refresh */
57
+ clientSecret?: string;
52
58
  }
53
59
 
54
60
  /** Base server config with shared options */
@@ -1030,6 +1030,9 @@ export class MCPAddWizard extends Container {
1030
1030
  config.auth = {
1031
1031
  type: "oauth",
1032
1032
  credentialId: this.#state.oauthCredentialId,
1033
+ tokenUrl: this.#state.oauthTokenUrl || undefined,
1034
+ clientId: this.#state.oauthClientId || undefined,
1035
+ clientSecret: this.#state.oauthClientSecret || undefined,
1033
1036
  };
1034
1037
  }
1035
1038
 
@@ -1054,6 +1057,9 @@ export class MCPAddWizard extends Container {
1054
1057
  config.auth = {
1055
1058
  type: "oauth",
1056
1059
  credentialId: this.#state.oauthCredentialId,
1060
+ tokenUrl: this.#state.oauthTokenUrl || undefined,
1061
+ clientId: this.#state.oauthClientId || undefined,
1062
+ clientSecret: this.#state.oauthClientSecret || undefined,
1057
1063
  };
1058
1064
  }
1059
1065
 
@@ -1250,6 +1256,9 @@ export class MCPAddWizard extends Container {
1250
1256
  config.auth = {
1251
1257
  type: "oauth",
1252
1258
  credentialId: this.#state.oauthCredentialId,
1259
+ tokenUrl: this.#state.oauthTokenUrl || undefined,
1260
+ clientId: this.#state.oauthClientId || undefined,
1261
+ clientSecret: this.#state.oauthClientSecret || undefined,
1253
1262
  };
1254
1263
  }
1255
1264
 
@@ -1275,6 +1284,9 @@ export class MCPAddWizard extends Container {
1275
1284
  config.auth = {
1276
1285
  type: "oauth",
1277
1286
  credentialId: this.#state.oauthCredentialId,
1287
+ tokenUrl: this.#state.oauthTokenUrl || undefined,
1288
+ clientId: this.#state.oauthClientId || undefined,
1289
+ clientSecret: this.#state.oauthClientSecret || undefined,
1278
1290
  };
1279
1291
  }
1280
1292
 
@@ -226,7 +226,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
226
226
  label: "Auto",
227
227
  description: "Preferred web-search provider",
228
228
  },
229
- { value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
229
+ { value: "exa", label: "Exa", description: "Uses Exa API when EXA_API_KEY is set; falls back to Exa MCP" },
230
230
  { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
231
231
  { value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
232
232
  { value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
@@ -413,6 +413,9 @@ export class MCPCommandController {
413
413
  auth: {
414
414
  type: "oauth",
415
415
  credentialId,
416
+ tokenUrl: oauth.tokenUrl,
417
+ clientId: oauth.clientId ?? finalConfig.oauth?.clientId,
418
+ clientSecret: undefined,
416
419
  },
417
420
  };
418
421
  } catch (oauthError) {
@@ -1296,7 +1299,9 @@ export class MCPCommandController {
1296
1299
  }
1297
1300
 
1298
1301
  const currentAuth = (
1299
- found.config as MCPServerConfig & { auth?: { type: "oauth" | "apikey"; credentialId?: string } }
1302
+ found.config as MCPServerConfig & {
1303
+ auth?: { type: "oauth" | "apikey"; credentialId?: string; clientSecret?: string };
1304
+ }
1300
1305
  ).auth;
1301
1306
  if (currentAuth?.type === "oauth") {
1302
1307
  await this.#removeManagedOAuthCredential(currentAuth.credentialId);
@@ -1321,6 +1326,9 @@ export class MCPCommandController {
1321
1326
  auth: {
1322
1327
  type: "oauth",
1323
1328
  credentialId,
1329
+ tokenUrl: oauth.tokenUrl,
1330
+ clientId: oauth.clientId ?? found.config.oauth?.clientId,
1331
+ clientSecret: currentAuth?.clientSecret,
1324
1332
  },
1325
1333
  };
1326
1334
  await updateMCPServer(found.filePath, name, updated);
@@ -1,14 +1,5 @@
1
1
  Applies precise, surgical file edits by referencing `LINE#ID` tags from `read` output. Each tag uniquely identifies a line, so edits remain stable even when lines shift.
2
2
 
3
- <critical>
4
- - Never anchor insertions on blank lines or lone closing delimiters like `}`, `]`, `)`, `};`, or `),` — they are mechanically valid tags but semantically unstable edit boundaries.
5
- - For `append`/`prepend`, `lines` **MUST** contain only the newly introduced content. Do not re-emit surrounding braces, brackets, parentheses, or sibling declarations that already exist in the file.
6
- - `append`/`prepend` are for self-contained new content only: sibling declarations, new object/list members, new test cases, or similar additions whose surrounding structure stays unchanged.
7
- - When changing existing code near a block tail or closing delimiter, default to `replace` over the owned span instead of inserting around the boundary.
8
- - When adding a sibling declaration, default to `prepend` on the next sibling declaration instead of `append` on the previous block's closing brace.
9
- - If any inserted line is just a closing delimiter, stop and re-check the edit shape. A closing line is only valid when it belongs to newly introduced structure; if it belongs to surrounding existing structure, your edit should be a `replace` that consumes the old boundary.
10
- </critical>
11
-
12
3
  <workflow>
13
4
  Follow these steps in order for every edit:
14
5
  1. You **SHOULD** issue a `read` call before editing to get fresh `LINE#ID` tags. Editing without current tags causes mismatches because other edits or external changes may have shifted line numbers since your last read.
@@ -21,15 +12,14 @@ Before choosing the payload, answer these questions in order:
21
12
  1. **Am I replacing existing lines or inserting new ones?** If any existing line changes, use `replace` for the full changed span.
22
13
  2. **What declaration or block owns this anchor line?** Prefer declaration/header lines over blank lines or delimiters.
23
14
  3. **Am I inserting self-contained new content, or changing an existing block?** Use `append`/`prepend` only for self-contained additions. If surrounding code, indentation, or closers also change, use `replace`.
24
- 4. **Am I editing near a block tail or closing delimiter?** If yes, expand the edit to own that tail instead of patching just the last line or two.
25
- 5. **Does `lines` contain only new content?** For `append`/`prepend`, do not include existing closing braces or other surrounding syntax from the file.
26
- 6. **Would the replacement duplicate the line immediately after `end`?** If yes, extend the range to consume the old boundary.
15
+ 4. **Am I editing near a block tail or closing delimiter?** If yes, use shape (a) or (b) from the block-boundaries rule: either stay entirely inside the body, or own the full block including header and closer. Never set `end` at a closer without re-emitting it, and never re-emit a closer without including it in `end`.
27
16
  </checklist>
28
17
 
29
18
  <operations>
30
19
  **`path`** — the path to the file to edit.
31
20
  **`move`** — if set, move the file to the given path.
32
21
  **`delete`** — if true, delete the file.
22
+
33
23
  **`edits[n].pos`** — the anchor line. Meaning depends on `op`:
34
24
  - if `replace`: line to rewrite
35
25
  - if `prepend`: line to insert new lines **before**; omit for beginning of file
@@ -40,24 +30,16 @@ Before choosing the payload, answer these questions in order:
40
30
  - `[""]` — blank line
41
31
  - `null` or `[]` — delete if replace, no-op if append or prepend
42
32
 
43
- Tags are applied bottom-up: later edits (by position) are applied first, so earlier tags remain valid even when subsequent ops add or remove lines. Tags **MUST** be referenced from the most recent `read` output.
33
+ Ops are applied bottom-up. Tags **MUST** be referenced from the most recent `read` output.
44
34
  </operations>
45
35
 
46
36
  <rules>
47
- 1. **Anchor on unique declaration or header lines, not delimiters.** Safe anchors are lines like `function beta() {`, `if (…) {`, `const value =`, or other unique structural headers. Blank lines and lone closers like `}` are never good insertion anchors.
48
- 2. **Use `prepend`/`append` only for self-contained additions whose surrounding structure stays unchanged.** If you are adding a sibling declaration, prefer `prepend` on the next sibling declaration instead of `append` on the previous block closer.
49
- 3. **If the change touches existing code near a block tail, use range `replace` over the owned span.** Do not patch just the final line(s) before a closing delimiter when the surrounding structure, indentation, or control flow is also changing.
50
- 4. **Match surrounding indentation for new lines.** When inserting via `prepend`/`append`, look at the anchor line and its neighbors in the `read` output. New `lines` entries **MUST** carry the same leading whitespace. If the context uses tabs at depth 1 (`\t`), your inserted declarations need `\t` and bodies need `\t\t`. Inserting at indent level 0 inside an indented block is always wrong.
51
- 5. **Consume the old closing boundary when your replacement emits one.** If the replacement's final line is a closing delimiter like `}`, `]`, or `)`, the `end` line **MUST** include the original matching closer that would otherwise remain in the file. Before submitting, compare the replacement's last line with the line immediately after `end`; if they would be the same boundary, extend the range so the old closer is removed.
52
- 6. **If you expect a second tiny cleanup edit for `}`, `};`, indentation, or a duplicated boundary, your first edit shape is wrong.** Expand the first `replace` so it owns the structural tail in one shot.
37
+ 1. **Use `prepend`/`append` only for self-contained additions whose surrounding structure stays unchanged.** If you are adding a sibling declaration, prefer `prepend` on the next sibling declaration instead of `append` on the previous block closer.
38
+ 2. **If the change touches existing code near a block tail, use range `replace` over the owned span.** Do not patch just the final line(s) before a closing delimiter when the surrounding structure, indentation, or control flow is also changing.
39
+ 3. **Match surrounding indentation for new lines.** When inserting via `prepend`/`append`, look at the anchor line and its neighbors in the `read` output. New `lines` entries **MUST** carry the same leading whitespace. If the context uses tabs at depth 1 (`\t`), your inserted declarations need `\t` and bodies need `\t\t`. Inserting at indent level 0 inside an indented block is always wrong.
40
+ 4. **Block boundaries travel together never split them.** See the block-boundaries rule in `<critical>`. The two valid shapes are: replace only the body (leave header and closer untouched), or replace the whole block (header through closer, re-emit all in `lines`). Do not set `end` to a closer and omit it from `lines` (deletes it). Do not emit a closer in `lines` without including it in `end` (duplicates it).
53
41
  </rules>
54
42
 
55
- <recovery>
56
- Edits can fail in two ways. Here is exactly what to do for each:
57
- 1. **Tag mismatch (`>>>`):** The file changed since your last read, so the tag no longer matches. You **MUST** retry using the fresh tags from the error snippet. If the snippet lacks enough context, or if you fail repeatedly, you **MUST** re-read the entire file and submit a simpler, single-op edit.
58
- 2. **No-op (`identical`):** Your replacement is identical to the existing content — nothing changed. You **MUST NOT** resubmit the same edit. Re-read the target lines to understand what is actually there, then adjust your edit.
59
- </recovery>
60
-
61
43
  <examples>
62
44
  All examples below reference the same file, `util.ts`:
63
45
  ```ts
@@ -135,63 +117,61 @@ Blank out a line without removing it:
135
117
  ```
136
118
  </example>
137
119
 
138
- <example name="rewrite a block">
139
- Replace the catch body with smarter error handling:
120
+ <example name="rewrite a block body — shape (a)">
121
+ Replace the catch body with smarter error handling. Shape (a): `pos` is the first body line, `end` is the last body line. The catch header (line 14) and its closer (line 17) are outside the range and stay untouched.
140
122
  ```
141
123
  {
142
124
  path: "util.ts",
143
125
  edits: [{
144
126
  op: "replace",
145
127
  pos: {{hlineref 15 "\t\tconsole.error(err);"}},
146
- end: {{hlineref 17 "\t}"}},
128
+ end: {{hlineref 16 "\t\treturn null;"}},
147
129
  lines: [
148
130
  "\t\tif (isEnoent(err)) return null;",
149
- "\t\tthrow err;",
150
- "\t}"
131
+ "\t\tthrow err;"
151
132
  ]
152
133
  }]
153
134
  }
154
135
  ```
155
136
  </example>
156
137
 
157
- <example name="own the block tail instead of patching around it">
158
- When changing the tail of an existing block, replace the owned span instead of appending just before the closer.
138
+ <example name="span the full body, not a single line">
139
+ When changing body content, replace the entire body span not just one line inside it. Patching one line leaves the rest of the body stale.
159
140
 
160
- Bad — appending a new return before the existing closer leaves the old tail in place and often leads to a second cleanup edit:
141
+ Bad — appends after one body line, leaving the original `return null` in place:
161
142
  ```
162
143
  {
163
144
  path: "util.ts",
164
145
  edits: [{
165
146
  op: "append",
166
- pos: {{hlineref 16 "\t\treturn null;"}},
147
+ pos: {{hlineref 15 "\t\tconsole.error(err);"}},
167
148
  lines: [
168
149
  "\t\treturn fallback;"
169
150
  ]
170
151
  }]
171
152
  }
172
153
  ```
173
- Good — replace the block tail so the new logic and the closing boundary are owned by one edit:
154
+ Good — shape (a): replace the full body span. Header and closer stay untouched:
174
155
  ```
175
156
  {
176
157
  path: "util.ts",
177
158
  edits: [{
178
159
  op: "replace",
179
160
  pos: {{hlineref 15 "\t\tconsole.error(err);"}},
180
- end: {{hlineref 17 "\t}"}},
161
+ end: {{hlineref 16 "\t\treturn null;"}},
181
162
  lines: [
182
163
  "\t\tif (isEnoent(err)) return null;",
183
- "\t\treturn fallback;",
184
- "\t}"
164
+ "\t\treturn fallback;"
185
165
  ]
186
166
  }]
187
167
  }
188
168
  ```
189
169
  </example>
190
170
 
191
- <example name="inclusive end avoids duplicate boundary">
192
- Simplify `beta()` to a one-liner. `end` must include the original closing `}` when the replacement also ends with `}`.
171
+ <example name="replace whole block shape (b)">
172
+ Simplify `beta()` to a one-liner. Shape (b): `pos`=header, `end`=closer, re-emit all in `lines`.
193
173
 
194
- Bad — `end` stops at line 17 (`\t}`), so the replacement adds `}` and the original function closer on line 18 survives. Result: two consecutive `}` lines.
174
+ Bad — `end` stops at the inner `\t}` on line 17, so the outer `}` on line 18 survives. Result: two consecutive `}` lines.
195
175
  ```
196
176
  {
197
177
  path: "util.ts",
@@ -207,7 +187,7 @@ Bad — `end` stops at line 17 (`\t}`), so the replacement adds `}` and the orig
207
187
  }]
208
188
  }
209
189
  ```
210
- Good — include the function's own `}` on line 18 in the range, so the old closing boundary is consumed:
190
+ Good — `end` includes the function's own `}` on line 18, so the old closer is consumed:
211
191
  ```
212
192
  {
213
193
  path: "util.ts",
@@ -284,11 +264,12 @@ Good — prepend before the next declaration so the new sibling is anchored on a
284
264
  </examples>
285
265
 
286
266
  <critical>
287
- - Edit payload: `{ path, edits[] }`. Each entry: `op`, `lines`, optional `pos`/`end`. No extra keys.
267
+ - You **MUST NOT** use this tool to reformat, reindent, or adjust whitespace run the project's formatter instead.
288
268
  - Every tag **MUST** be copied exactly from your most recent `read` output as `N#ID`. Stale or mistyped tags cause mismatches.
289
- - You **MUST** re-read the file after each edit call before issuing another on the same file. Tags shift after every edit, so reusing old tags produces mismatches.
290
- - You **MUST NOT** use this tool to reformat, reindent, or adjust whitespace — run the project's formatter instead. If the only difference is whitespace, it is formatting; leave it alone.
291
- - `lines` entries **MUST** be literal file content with indentation copied exactly from the `read` output. If the file uses tabs, use `\t` in JSON (a real tab character). Using `\\t` (backslash + t) writes the literal two-character string `\t` into the file.
292
- - For `append`/`prepend`, `lines` **MUST NOT** repeat surrounding delimiters or existing sibling code. Insert only the new content.
293
- - Before any range `replace`, you **MUST** check whether the replacement's last line duplicates the original line immediately after `end` (most often a closing `}`, `]`, or `)`). If it does, extend the range to consume that old boundary instead of leaving two closers behind.
269
+ - Edit payload: `{ path, edits[] }`. Each entry: `op`, `lines`, optional `pos`/`end`. No extra keys.
270
+ - For `append`/`prepend`, `lines` **MUST** contain only the newly introduced content. Do not re-emit surrounding content, or terminators that already exist.
271
+ - When changing existing code near a block tail or closing delimiter, default to `replace` over the owned span instead of inserting around the boundary.
272
+ - When adding a sibling declaration, default to `prepend` on the next sibling declaration instead of `append` on the previous block's closing brace.
273
+ - **Block boundaries travel together.** For a block `{ header / body / closer }`, there are exactly two valid replace shapes: (a) replace only the body — `pos`=first body line, `end`=last body line, leave the header and closer untouched; or (b) replace the whole block — `pos`=header, `end`=closer, re-emit all three in `lines`. Never split them: do not set `end` to the closer while omitting it from `lines` (deletes it), and do not emit the closer in `lines` without including it in `end` (duplicates it). This applies to every block terminator: `}`, `continue`, `break`, `return`, `throw`.
274
+ - `lines` entries **MUST** be literal file content with indentation copied exactly from the `read` output. If the file uses tabs, use a real tab character.
294
275
  </critical>
@@ -12,6 +12,7 @@ import {
12
12
  OPENAI_HEADER_VALUES,
13
13
  OPENAI_HEADERS,
14
14
  } from "@oh-my-pi/pi-ai/providers/openai-codex/constants";
15
+ import { parseTextSignature } from "@oh-my-pi/pi-ai/providers/openai-responses-shared";
15
16
  import { transformMessages } from "@oh-my-pi/pi-ai/providers/transform-messages";
16
17
  import {
17
18
  getOpenAIResponsesHistoryItems,
@@ -739,7 +740,8 @@ function buildOpenAiNativeHistory(
739
740
 
740
741
  if (block.type === "text") {
741
742
  if (!block.text || block.text.trim().length === 0) continue;
742
- let msgId = block.textSignature;
743
+ const parsedSignature = parseTextSignature(block.textSignature);
744
+ let msgId = parsedSignature?.id;
743
745
  if (!msgId) {
744
746
  msgId = `msg_${msgIndex}`;
745
747
  } else if (msgId.length > 64) {
@@ -751,6 +753,7 @@ function buildOpenAiNativeHistory(
751
753
  content: [{ type: "output_text", text: block.text.toWellFormed(), annotations: [] }],
752
754
  status: "completed",
753
755
  id: msgId,
756
+ phase: parsedSignature?.phase,
754
757
  });
755
758
  continue;
756
759
  }
@@ -629,15 +629,11 @@ function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
629
629
  const terminalId = getTerminalId();
630
630
  if (!terminalId) return;
631
631
 
632
- try {
633
- const breadcrumbDir = path.join(getDefaultAgentDir(), TERMINAL_SESSIONS_DIR);
634
- const breadcrumbFile = path.join(breadcrumbDir, terminalId);
635
- const content = `${cwd}\n${sessionFile}\n`;
636
- // Bun.write auto-creates parent dirs
637
- void Bun.write(breadcrumbFile, content);
638
- } catch {
639
- // Best-effort — don't break session creation if breadcrumb fails
640
- }
632
+ const breadcrumbDir = path.join(getDefaultAgentDir(), TERMINAL_SESSIONS_DIR);
633
+ const breadcrumbFile = path.join(breadcrumbDir, terminalId);
634
+ const content = `${cwd}\n${sessionFile}\n`;
635
+ // Best-effort don't break session creation if breadcrumb fails
636
+ Bun.write(breadcrumbFile, content).catch(() => {});
641
637
  }
642
638
 
643
639
  /**
@@ -14,7 +14,7 @@ export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
14
14
  export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
15
15
 
16
16
  /** Keys injected by the harness that should not be displayed to users */
17
- const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD]);
17
+ const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD, "__partialJson"]);
18
18
 
19
19
  /** Strip harness-internal keys from tool args for display */
20
20
  export function stripInternalArgs(args: Record<string, unknown>): Record<string, unknown> {