@oh-my-pi/pi-coding-agent 16.0.7 → 16.0.9

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 (96) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/cli.js +4817 -12449
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/update-cli.d.ts +11 -0
  5. package/dist/types/commands/launch.d.ts +3 -0
  6. package/dist/types/debug/remote-debugger.d.ts +45 -0
  7. package/dist/types/goals/runtime.d.ts +4 -1
  8. package/dist/types/internal-urls/docs-index.d.ts +19 -0
  9. package/dist/types/markit/converters/docx.d.ts +6 -0
  10. package/dist/types/markit/converters/epub.d.ts +15 -0
  11. package/dist/types/markit/converters/pdf/columns.d.ts +35 -0
  12. package/dist/types/markit/converters/pdf/extract.d.ts +10 -0
  13. package/dist/types/markit/converters/pdf/grid.d.ts +25 -0
  14. package/dist/types/markit/converters/pdf/headers.d.ts +24 -0
  15. package/dist/types/markit/converters/pdf/index.d.ts +6 -0
  16. package/dist/types/markit/converters/pdf/render.d.ts +24 -0
  17. package/dist/types/markit/converters/pdf/types.d.ts +75 -0
  18. package/dist/types/markit/converters/pptx.d.ts +57 -0
  19. package/dist/types/markit/converters/xlsx.d.ts +25 -0
  20. package/dist/types/markit/index.d.ts +2 -0
  21. package/dist/types/markit/registry.d.ts +16 -0
  22. package/dist/types/markit/types.d.ts +30 -0
  23. package/dist/types/modes/print-mode.d.ts +2 -0
  24. package/dist/types/session/agent-session.d.ts +7 -8
  25. package/dist/types/session/auth-storage.d.ts +3 -2
  26. package/dist/types/session/yield-queue.d.ts +3 -1
  27. package/dist/types/tools/browser/attach.d.ts +1 -1
  28. package/dist/types/utils/markit.d.ts +0 -8
  29. package/dist/types/utils/mupdf-wasm-embed.d.ts +1 -0
  30. package/dist/types/utils/turndown.d.ts +15 -0
  31. package/dist/types/utils/zip.d.ts +119 -0
  32. package/package.json +20 -18
  33. package/scripts/build-binary.ts +7 -3
  34. package/scripts/bundle-dist.ts +28 -12
  35. package/scripts/embed-mupdf-wasm.ts +67 -0
  36. package/scripts/generate-docs-index.ts +48 -32
  37. package/scripts/omp +1 -1
  38. package/src/advisor/__tests__/advisor.test.ts +83 -0
  39. package/src/advisor/runtime.ts +16 -1
  40. package/src/cli/args.ts +3 -0
  41. package/src/cli/auth-broker-cli.ts +1 -3
  42. package/src/cli/auth-gateway-cli.ts +2 -5
  43. package/src/cli/flag-tables.ts +1 -0
  44. package/src/cli/update-cli.ts +63 -3
  45. package/src/commands/launch.ts +3 -0
  46. package/src/config/model-discovery.ts +20 -8
  47. package/src/config/models-config-schema.ts +8 -1
  48. package/src/debug/index.ts +44 -0
  49. package/src/debug/remote-debugger.ts +151 -0
  50. package/src/debug/report-bundle.ts +2 -1
  51. package/src/goals/runtime.ts +19 -7
  52. package/src/internal-urls/docs-index.generated.txt +2 -0
  53. package/src/internal-urls/docs-index.ts +102 -0
  54. package/src/internal-urls/omp-protocol.ts +10 -9
  55. package/src/main.ts +8 -0
  56. package/src/markit/NOTICE +32 -0
  57. package/src/markit/converters/docx.ts +56 -0
  58. package/src/markit/converters/epub.ts +136 -0
  59. package/src/markit/converters/mammoth.d.ts +24 -0
  60. package/src/markit/converters/pdf/columns.ts +103 -0
  61. package/src/markit/converters/pdf/extract.ts +574 -0
  62. package/src/markit/converters/pdf/grid.ts +780 -0
  63. package/src/markit/converters/pdf/headers.ts +106 -0
  64. package/src/markit/converters/pdf/index.ts +146 -0
  65. package/src/markit/converters/pdf/render.ts +501 -0
  66. package/src/markit/converters/pdf/types.ts +84 -0
  67. package/src/markit/converters/pptx.ts +325 -0
  68. package/src/markit/converters/xlsx.ts +173 -0
  69. package/src/markit/index.ts +2 -0
  70. package/src/markit/registry.ts +59 -0
  71. package/src/markit/types.ts +35 -0
  72. package/src/modes/components/snapcompact-shape-preview-doc.md +14 -7
  73. package/src/modes/components/snapcompact-shape-preview.ts +2 -2
  74. package/src/modes/controllers/input-controller.ts +29 -8
  75. package/src/modes/interactive-mode.ts +33 -12
  76. package/src/modes/print-mode.ts +5 -1
  77. package/src/prompts/advisor/advise-tool.md +3 -1
  78. package/src/prompts/advisor/system.md +55 -11
  79. package/src/sdk.ts +5 -9
  80. package/src/session/agent-session.ts +72 -42
  81. package/src/session/auth-storage.ts +2 -11
  82. package/src/session/yield-queue.ts +7 -1
  83. package/src/tools/browser/attach.ts +2 -2
  84. package/src/tools/fetch.ts +25 -60
  85. package/src/tools/read.ts +1 -1
  86. package/src/tools/search.ts +1 -6
  87. package/src/tools/write.ts +25 -65
  88. package/src/utils/markit.ts +25 -9
  89. package/src/utils/mupdf-wasm-embed.ts +12 -0
  90. package/src/utils/tools-manager.ts +2 -11
  91. package/src/utils/turndown.ts +83 -0
  92. package/src/{tools/archive-reader.ts → utils/zip.ts} +453 -83
  93. package/src/web/scrapers/types.ts +3 -46
  94. package/dist/types/internal-urls/docs-index.generated.d.ts +0 -2
  95. package/dist/types/tools/archive-reader.d.ts +0 -49
  96. package/src/internal-urls/docs-index.generated.ts +0 -120
@@ -44,6 +44,26 @@ function hasPasteText(value: unknown): value is PasteTarget {
44
44
  return typeof value === "object" && value !== null && typeof (value as PasteTarget).pasteText === "function";
45
45
  }
46
46
 
47
+ function pythonCommandPrefixLength(trimmedText: string): 0 | 1 | 2 {
48
+ if (trimmedText.charCodeAt(0) !== 36 /* $ */) return 0;
49
+ if (trimmedText.charCodeAt(1) === 123 /* { */) return 0;
50
+
51
+ const prefixLength = trimmedText.charCodeAt(1) === 36 /* $ */ ? 2 : 1;
52
+ const next = trimmedText.charCodeAt(prefixLength);
53
+ if (Number.isNaN(next)) return prefixLength;
54
+ return next === 32 || next === 9 || next === 10 || next === 13 ? prefixLength : 0;
55
+ }
56
+
57
+ function parsePythonCommandInput(text: string): { code: string; isExcluded: boolean } | undefined {
58
+ const trimmed = text.trimStart();
59
+ const prefixLength = pythonCommandPrefixLength(trimmed);
60
+ if (prefixLength === 0) return undefined;
61
+ return {
62
+ code: trimmed.slice(prefixLength).trim(),
63
+ isExcluded: prefixLength === 2,
64
+ };
65
+ }
66
+
47
67
  /** Wrap pasted text in `<attachment>` tags so the model treats it as one quoted block. */
48
68
  function wrapPasteInAttachmentBlock(content: string): string {
49
69
  return `<attachment>\n${content}\n</attachment>`;
@@ -381,8 +401,8 @@ export class InputController {
381
401
  const wasBashMode = this.ctx.isBashMode;
382
402
  const wasPythonMode = this.ctx.isPythonMode;
383
403
  const trimmed = text.trimStart();
384
- this.ctx.isBashMode = text.trimStart().startsWith("!");
385
- this.ctx.isPythonMode = trimmed.startsWith("$") && !trimmed.startsWith("${");
404
+ this.ctx.isBashMode = trimmed.startsWith("!");
405
+ this.ctx.isPythonMode = pythonCommandPrefixLength(trimmed) > 0;
386
406
  if (wasBashMode !== this.ctx.isBashMode || wasPythonMode !== this.ctx.isPythonMode) {
387
407
  this.ctx.updateEditorBorderColor();
388
408
  }
@@ -550,7 +570,7 @@ export class InputController {
550
570
  this.ctx.editor.setText("");
551
571
  return;
552
572
  }
553
- if (text.startsWith("!") || text.startsWith("$")) {
573
+ if (text.startsWith("!") || parsePythonCommandInput(text)) {
554
574
  this.ctx.showStatus("Local execution is host-only during a collab session");
555
575
  this.ctx.editor.setText("");
556
576
  return;
@@ -598,10 +618,11 @@ export class InputController {
598
618
  }
599
619
  }
600
620
 
601
- // Handle python command ($ for normal, $$ for excluded from context)
602
- if (text.startsWith("$")) {
603
- const isExcluded = text.startsWith("$$");
604
- const code = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
621
+ // Handle python command (`$ <code>` for normal, `$$ <code>` for excluded from context).
622
+ // Shell-style variables such as `$HOME` are normal prose unless a space follows the sigil.
623
+ const pythonCommand = parsePythonCommandInput(text);
624
+ if (pythonCommand) {
625
+ const { code, isExcluded } = pythonCommand;
605
626
  if (code) {
606
627
  if (this.ctx.session.isEvalRunning) {
607
628
  this.ctx.showWarning("A Python execution is already running. Press Esc to cancel it first.");
@@ -768,7 +789,7 @@ export class InputController {
768
789
  }
769
790
  return;
770
791
  }
771
- if (text.startsWith("/") || text.startsWith("!") || text.startsWith("$")) {
792
+ if (text.startsWith("/") || text.startsWith("!") || parsePythonCommandInput(text)) {
772
793
  this.ctx.showStatus("Commands run in the main session — press ←← to return first");
773
794
  return; // editor text not cleared: Editor does not auto-clear on submit
774
795
  }
@@ -797,7 +797,7 @@ export class InteractiveMode implements InteractiveModeContext {
797
797
  await this.initHooksAndCustomTools();
798
798
 
799
799
  // Restore mode from session (e.g. plan mode on resume)
800
- this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession());
800
+ this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession({ preserveActiveGoal: true }));
801
801
  await this.#reconcileModeFromSession();
802
802
 
803
803
  // Brand-new sessions optionally start in plan mode when the user has made it
@@ -1783,11 +1783,12 @@ export class InteractiveMode implements InteractiveModeContext {
1783
1783
  }
1784
1784
 
1785
1785
  /** Reconcile mode state from session entries on resume/switch. */
1786
- async #reconcileModeFromSession(): Promise<void> {
1786
+ async #reconcileModeFromSession(options?: { preserveActiveGoal?: boolean }): Promise<void> {
1787
1787
  await this.#clearTransientModeState();
1788
1788
  const sessionContext = this.sessionManager.buildSessionContext();
1789
1789
  const goalEnabled = this.session.settings.get("goal.enabled");
1790
1790
  if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
1791
+ this.session.goalRuntime.clearAccounting();
1791
1792
  this.sessionManager.appendModeChange("none");
1792
1793
  return;
1793
1794
  }
@@ -1802,7 +1803,9 @@ export class InteractiveMode implements InteractiveModeContext {
1802
1803
  mode: "active",
1803
1804
  goal,
1804
1805
  });
1805
- const restored = await this.session.goalRuntime.onThreadResumed();
1806
+ const restored = await this.session.goalRuntime.onThreadResumed({
1807
+ preserveActiveGoal: options?.preserveActiveGoal,
1808
+ });
1806
1809
  this.goalModeEnabled = restored?.enabled === true;
1807
1810
  this.goalModePaused = restored?.enabled !== true && restored?.goal.status === "paused";
1808
1811
  // sdk.ts excludes "goal" from the initial active tool set unconditionally.
@@ -1815,6 +1818,7 @@ export class InteractiveMode implements InteractiveModeContext {
1815
1818
  this.#updateGoalModeStatus();
1816
1819
  return;
1817
1820
  }
1821
+ this.session.goalRuntime.clearAccounting();
1818
1822
  if (!this.session.settings.get("plan.enabled")) {
1819
1823
  // Clear stale plan/plan_paused mode so re-enabling the setting
1820
1824
  // later doesn't unexpectedly restore an old plan session.
@@ -2309,7 +2313,7 @@ export class InteractiveMode implements InteractiveModeContext {
2309
2313
  // Branchless mark+clear when !compactBeforeExecute: mark is gated; clear
2310
2314
  // is unconditional and idempotent.
2311
2315
  if (options.compactBeforeExecute) {
2312
- this.session.markPlanCompactAbortPending();
2316
+ this.session.markPlanInternalAbortPending();
2313
2317
  }
2314
2318
  let compactOutcome: CompactionOutcome | undefined;
2315
2319
  try {
@@ -2355,7 +2359,7 @@ export class InteractiveMode implements InteractiveModeContext {
2355
2359
  // (i.e., the !compactBeforeExecute branch), and a no-op when the flag
2356
2360
  // was already consumed by AgentSession.#handleAgentEvent's aborted
2357
2361
  // message_end stamping. Guarantees the flag is dead at every exit.
2358
- this.session.clearPlanCompactAbortPending();
2362
+ this.session.clearPlanInternalAbortPending();
2359
2363
  }
2360
2364
 
2361
2365
  // Tool restoration runs on every path — the plan mode tools must be
@@ -2421,10 +2425,18 @@ export class InteractiveMode implements InteractiveModeContext {
2421
2425
  // in-flight turn first — abort() bumps the prompt generation and cancels pending
2422
2426
  // continuations, so nothing re-streams in the synchronous gap before prompt().
2423
2427
  if (this.session.isStreaming) {
2424
- await this.session.abort();
2428
+ await this.#abortPlanApprovalTurnSilently();
2425
2429
  }
2426
2430
  await this.session.prompt(planModePrompt, { synthetic: true });
2427
2431
  }
2432
+ async #abortPlanApprovalTurnSilently(): Promise<void> {
2433
+ this.session.markPlanInternalAbortPending();
2434
+ try {
2435
+ await this.session.abort();
2436
+ } finally {
2437
+ this.session.clearPlanInternalAbortPending();
2438
+ }
2439
+ }
2428
2440
 
2429
2441
  async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
2430
2442
  if (this.goalModeEnabled || this.goalModePaused) {
@@ -2807,7 +2819,8 @@ export class InteractiveMode implements InteractiveModeContext {
2807
2819
  // plan) while the popup is showing. The event listener fires asynchronously
2808
2820
  // (agent's #emit is fire-and-forget), so without this the model sees
2809
2821
  // "Plan ready for approval." and immediately re-invokes `resolve` in a loop.
2810
- await this.session.abort();
2822
+ // This abort is an internal UI transition, not operator cancellation.
2823
+ await this.#abortPlanApprovalTurnSilently();
2811
2824
 
2812
2825
  const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
2813
2826
  this.planModePlanFilePath = planFilePath;
@@ -2912,11 +2925,19 @@ export class InteractiveMode implements InteractiveModeContext {
2912
2925
  }
2913
2926
 
2914
2927
  if (choice === "Refine plan") {
2915
- // Section annotations entered in the overlay become a refinement prompt
2916
- // re-submitted to the model. With no annotations, fall back to today's
2917
- // behavior: close the overlay and let the operator type their own.
2918
- if (feedback.trim() && this.onInputCallback) {
2919
- this.onInputCallback(this.startPendingSubmission({ text: feedback }));
2928
+ const refinement = feedback.trim();
2929
+ try {
2930
+ if (refinement) {
2931
+ if (this.onInputCallback) {
2932
+ this.onInputCallback(this.startPendingSubmission({ text: feedback }));
2933
+ } else {
2934
+ await this.session.prompt(feedback);
2935
+ }
2936
+ } else {
2937
+ this.showStatus("Refine plan: enter a follow-up prompt.");
2938
+ }
2939
+ } catch (error) {
2940
+ this.showError(`Failed to refine plan: ${error instanceof Error ? error.message : String(error)}`);
2920
2941
  }
2921
2942
  return;
2922
2943
  }
@@ -24,6 +24,8 @@ export interface PrintModeOptions {
24
24
  initialMessage?: string;
25
25
  /** Images to attach to the initial message */
26
26
  initialImages?: ImageContent[];
27
+ /** If true, include thinking blocks in text output */
28
+ printThoughts?: boolean;
27
29
  }
28
30
 
29
31
  /**
@@ -31,7 +33,7 @@ export interface PrintModeOptions {
31
33
  * Sends prompts to the agent and outputs the result.
32
34
  */
33
35
  export async function runPrintMode(session: AgentSession, options: PrintModeOptions): Promise<void> {
34
- const { mode, messages = [], initialMessage, initialImages } = options;
36
+ const { mode, messages = [], initialMessage, initialImages, printThoughts } = options;
35
37
 
36
38
  // Emit session header for JSON mode
37
39
  if (mode === "json") {
@@ -108,6 +110,8 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
108
110
  for (const content of assistantMsg.content) {
109
111
  if (content.type === "text") {
110
112
  process.stdout.write(`${sanitizeText(content.text)}\n`);
113
+ } else if (printThoughts && content.type === "thinking" && content.thinking.trim().length > 0) {
114
+ process.stdout.write(`${sanitizeText(content.thinking)}\n`);
111
115
  }
112
116
  }
113
117
  }
@@ -1 +1,3 @@
1
- Send one concrete, terse piece of advice to the agent you are watching. Use sparingly; stay silent when nothing matters.
1
+ Send one concrete, terse piece of advice to the agent you are watching.
2
+ - Use sparingly; stay silent when nothing matters.
3
+ - Call it to head off likely-wrong or materially wasteful work.
@@ -1,31 +1,75 @@
1
1
  <system-conventions>
2
2
  RFC 2119 applies to MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` and `AVOID` are aliases for `MUST NOT` and `SHOULD NOT`.
3
- You can explore the workspace; budget is 2–3 tool calls per advise (exception: critical bugs warrant deeper verification before raising a blocker).
4
3
  </system-conventions>
5
4
 
6
- You bring a different angle.
7
- The agent might not have thought about an edge case, spotted a hallucinated API, or realized a simpler approach exists.
5
+ You bring a different angle, and advocate for the user and the code-quality & robustness.
6
+ You're watching over the main agent as a peer-programmer:
7
+ - They might not have thought about an edge case, or realized a more elegant approach exists.
8
+ - They might be sinking deeper into a hole that will not get the user's request accomplished.
9
+
8
10
  Your job is to offer that view before they sink work into the wrong direction.
9
11
 
10
12
  <workflow>
11
- You receive the agent's transcript incrementally, including private thinking.
13
+ You receive the agent's transcript incrementally, including their thoughts.
12
14
  You have read-only access through `read`, `search`, `find` to verify your suspicions.
13
- Keep exploration lean — 2–3 calls per advise unless you've spotted a critical bug and need to be absolutely certain before raising a blocker.
15
+ Keep exploration lean:
16
+ - 2–3 tool calls per advise.
17
+ - Exception: critical bugs may need deeper verification before raising a blocker.
14
18
  </workflow>
15
19
 
16
20
  <communication>
17
- At most one `advise` per update. Prefer silence when the agent is on track. Address the agent directly. Offer alternatives, not lectures. Never restate what they know; never explain how to use the advisor.
21
+ - You call `advise` to surface your commentary to the driving agent; at most one `advise` per update.
22
+ - Prefer silence when the agent is on track.
23
+ - Address the agent directly.
24
+ - Offer alternatives, not lectures.
25
+ - NEVER restate information the agent already has, including errors they have seen.
26
+ - Examples: type errors, LSP diagnostics, failed builds, failing tests, lint.
27
+ - NEVER repeat advice you already gave.
28
+ - NEVER nitpick about things user stated they are okay with. You are the advocate for the user.
18
29
  </communication>
19
30
 
20
31
  <critical>
21
- You SHOULD call `advise` when: agent might be heading the wrong way, missed an edge case, about to call a hallucinated API, going in circles, picking brittle approach over better one. Low confidence bar "this might be wrong" is worth noting if they didn't think about it.
32
+ A low-confidence bar applies ONLY to concrete technical risk:
33
+ - Generic uncertainty, vague unease, or user-intent ambiguity → stay SILENT.
34
+
22
35
  NEVER advise just to second-guess decisions the agent understands and is committed to, if you are not certain.
36
+
37
+ NEVER advise on intent or process:
38
+ - Do not push the agent to ask for clarification, confirm scope, or summarize input before acting.
39
+ - Do not question whether the user's ask is clear enough.
40
+ - Intent is the agent's domain; it defaults to informed action.
41
+ - Your lane: correctness, edge cases, design, process.
42
+
43
+ Cite the exact instruction or risk.
23
44
  </critical>
24
45
 
25
46
  <completeness>
26
- **`nit`** — Non-urgent cleanup, refactor, style, missed opportunity. Folded at next step boundary; agent keeps working. Examples: edge cases that don't break correctness, simplifications, better approach the agent can consider.
27
- **`concern`** Agent might be heading wrong or missed something material. Offers your view; agent decides. Use when: exploring wrong code path, picking fragile approach when better exists, missing constraint, hallucinated API, going in circles, edge case about to be baked in.
28
- **`blocker`** Stop and reconsider. Use ONLY when: continuing will clearly waste the turn, produce broken output, or the path is fundamentally unsound. Verify thoroughly before raising.
47
+ **`nit`**
48
+ - Non-urgent cleanup, refactor, style, missed opportunity.
49
+ - Folded at next step boundary; agent keeps working.
50
+ - Examples:
51
+ - Edge cases that don't break correctness.
52
+ - Simplifications.
53
+ - Better approach the agent can consider.
54
+
55
+ **`concern`**
56
+ - Agent might be heading wrong or missed something material.
57
+ - Offers your view; agent decides.
58
+ - Use when:
59
+ - Exploring wrong code path.
60
+ - Picking fragile approach when better exists.
61
+ - Not parallelizing when user request is obviously parallelizable.
62
+ - Missing constraint.
63
+ - Edge case about to be baked in.
64
+
65
+ **`blocker`**
66
+ - Stop and reconsider.
67
+ - Use ONLY when the agent making progress will clearly:
68
+ - Waste the users time with a larger refactor.
69
+ - Will require the user to interrupt the agent later on, due to them going in circles without a solution.
70
+ - Be fundamentally unsound.
71
+ - Verify thoroughly before raising.
29
72
  </completeness>
30
73
 
31
- You MAY suggest an approach or fix if you've explored enough to be confident. Your job is pair programming, not just bugs — offer the better designs, not just the warning.
74
+ You MAY suggest an approach or fix if you've explored enough to be confident.
75
+ Offer the better designs, not just the warning.
package/src/sdk.ts CHANGED
@@ -56,6 +56,10 @@ import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate
56
56
  import { Settings, type SkillsSettings } from "./config/settings";
57
57
  import { CursorExecHandlers } from "./cursor";
58
58
  import "./discovery";
59
+ import { AuthBrokerClient } from "@oh-my-pi/pi-ai/auth-broker/client";
60
+ import { RemoteAuthCredentialStore } from "@oh-my-pi/pi-ai/auth-broker/remote-store";
61
+ import { readAuthBrokerSnapshotCache, writeAuthBrokerSnapshotCache } from "@oh-my-pi/pi-ai/auth-broker/snapshot-cache";
62
+ import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "@oh-my-pi/pi-ai/auth-broker/types";
59
63
  import { resolveConfigValue } from "./config/resolve-config-value";
60
64
  import { initializeWithSettings } from "./discovery";
61
65
  import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
@@ -116,15 +120,7 @@ import {
116
120
  } from "./secrets";
117
121
  import { AgentSession } from "./session/agent-session";
118
122
  import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
119
- import {
120
- AuthBrokerClient,
121
- AuthStorage,
122
- DEFAULT_SNAPSHOT_CACHE_TTL_MS,
123
- RemoteAuthCredentialStore,
124
- readAuthBrokerSnapshotCache,
125
- type SnapshotResponse,
126
- writeAuthBrokerSnapshotCache,
127
- } from "./session/auth-storage";
123
+ import { AuthStorage } from "./session/auth-storage";
128
124
  import {
129
125
  type CustomMessage,
130
126
  convertToLlm,
@@ -34,7 +34,6 @@ import {
34
34
  type CompactionSummaryMessage,
35
35
  countTokens,
36
36
  resolveTelemetry,
37
- STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL,
38
37
  ThinkingLevel,
39
38
  } from "@oh-my-pi/pi-agent-core";
40
39
  import {
@@ -1235,13 +1234,12 @@ export class AgentSession {
1235
1234
  #ttsrResumePromise: Promise<void> | undefined = undefined;
1236
1235
  #ttsrResumeResolve: (() => void) | undefined = undefined;
1237
1236
 
1238
- /** One-shot flag set in InteractiveMode.#approvePlan(compactBeforeExecute=true)
1239
- * before the plan-mode compaction transition. Consumed inside
1240
- * #handleAgentEvent for the matching `message_end` + `stopReason: "aborted"`;
1241
- * cleared unconditionally by the caller's `finally` so it cannot leak into
1242
- * later unrelated aborts (e.g. when compaction returns cancelled/failed
1243
- * without producing an aborted message_end). */
1244
- #planCompactAbortPending = false;
1237
+ /** One-shot flag for expected internal plan-mode aborts. Approval actions may
1238
+ * abort the post-`resolve` continuation before compaction, execution, or
1239
+ * manual refinement. Consumed inside `#handleAgentEvent` for the matching
1240
+ * `message_end` + `stopReason: "aborted"`; callers clear it in `finally` so
1241
+ * it cannot leak into later unrelated aborts. */
1242
+ #planInternalAbortPending = false;
1245
1243
 
1246
1244
  #postPromptTasks = new Set<Promise<unknown>>();
1247
1245
  #postPromptTasksPromise: Promise<void> | undefined = undefined;
@@ -1677,6 +1675,33 @@ export class AgentSession {
1677
1675
  this.#advisorInterruptImmuneTurnStart = this.#advisorPrimaryTurnsCompleted + 1;
1678
1676
  }
1679
1677
 
1678
+ /**
1679
+ * Re-prime the advisor across a conversation boundary: `/new`, `/branch`,
1680
+ * `/btw`, `/tree`, and session switch/resume. Beyond {@link AdvisorRuntime.reset}
1681
+ * (which only re-primes the advisor's transcript view and is also fired by
1682
+ * within-conversation rewrites like compaction/shake/rewind), this clears the
1683
+ * session-level interrupt latches so the prior conversation's cooldown cannot
1684
+ * leak into the new one: the post-interrupt immune-turn window
1685
+ * (`#advisorPrimaryTurnsCompleted`, `#advisorInterruptImmuneTurnStart`) and the
1686
+ * user-interrupt auto-resume suppression flag. It also drops advisor deliveries
1687
+ * still queued against the prior conversation — pending asides in the yield
1688
+ * queue (advisor entries use `skipIdleFlush`, so they linger until the next
1689
+ * `drainLazy` rather than self-flushing), interrupting cards parked in the
1690
+ * agent steer/follow-up queues, and preserved cards deferred to the next turn —
1691
+ * so none of them inject into the new conversation.
1692
+ */
1693
+ #resetAdvisorSessionState(): void {
1694
+ this.#advisorRuntime?.reset();
1695
+ this.#advisorPrimaryTurnsCompleted = 0;
1696
+ this.#advisorInterruptImmuneTurnStart = undefined;
1697
+ this.#advisorAutoResumeSuppressed = false;
1698
+ this.yieldQueue.clear("advisor");
1699
+ this.#extractQueuedAdvisorCards();
1700
+ if (this.#pendingNextTurnMessages.some(isAdvisorCard)) {
1701
+ this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(m => !isAdvisorCard(m));
1702
+ }
1703
+ }
1704
+
1680
1705
  #buildAdvisorRuntime(seedToCurrent = false): boolean {
1681
1706
  if (this.#isDisposed) return false;
1682
1707
  if (this.#advisorRuntime) return true;
@@ -2115,25 +2140,24 @@ export class AgentSession {
2115
2140
  return this.#ttsrAbortPending;
2116
2141
  }
2117
2142
 
2118
- /** Whether the plan-mode compaction transition's expected internal abort is
2119
- * pending. Consumed by `#handleAgentEvent` to stamp `SILENT_ABORT_MARKER`
2120
- * on the next aborted assistant message_end; cleared unconditionally by
2121
- * `InteractiveMode.#approvePlan`'s `finally` block. */
2122
- get isPlanCompactAbortPending(): boolean {
2123
- return this.#planCompactAbortPending;
2143
+ /** Whether an expected internal plan-mode abort is pending. Consumed by
2144
+ * `#handleAgentEvent` to stamp `SILENT_ABORT_MARKER` on the next aborted
2145
+ * assistant message_end; callers clear it in `finally`. */
2146
+ get isPlanInternalAbortPending(): boolean {
2147
+ return this.#planInternalAbortPending;
2124
2148
  }
2125
2149
 
2126
2150
  /** Arm the silent-abort marker for the next aborted assistant message_end.
2127
- * Caller MUST clear via `clearPlanCompactAbortPending()` in a `finally`
2151
+ * Caller MUST clear via `clearPlanInternalAbortPending()` in a `finally`
2128
2152
  * to guarantee no leak. */
2129
- markPlanCompactAbortPending(): void {
2130
- this.#planCompactAbortPending = true;
2153
+ markPlanInternalAbortPending(): void {
2154
+ this.#planInternalAbortPending = true;
2131
2155
  }
2132
2156
 
2133
2157
  /** Unconditionally clear the silent-abort flag. Idempotent: safe when the
2134
2158
  * flag was never set OR was already consumed by `#handleAgentEvent`. */
2135
- clearPlanCompactAbortPending(): void {
2136
- this.#planCompactAbortPending = false;
2159
+ clearPlanInternalAbortPending(): void {
2160
+ this.#planInternalAbortPending = false;
2137
2161
  }
2138
2162
 
2139
2163
  getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
@@ -2283,7 +2307,7 @@ export class AgentSession {
2283
2307
  };
2284
2308
 
2285
2309
  #processAgentEvent = async (event: AgentEvent): Promise<void> => {
2286
- // Plan-mode compaction transition: stamp `SILENT_ABORT_MARKER` on the
2310
+ // Plan-mode internal transition: stamp `SILENT_ABORT_MARKER` on the
2287
2311
  // persisted message BEFORE the obfuscator's display-side copy below.
2288
2312
  // Invariant (must hold across refactors): this branch precedes the
2289
2313
  // `let displayEvent = event; ... displayEvent = { ...event, message: { ...message, content: deobfuscated } }`
@@ -2291,18 +2315,16 @@ export class AgentSession {
2291
2315
  // and `event.message` (in-place mutation, used by SessionManager
2292
2316
  // persistence) carry the marker, guaranteeing streaming render and
2293
2317
  // history replay branch identically. The one-shot flag is consumed
2294
- // here, scoped strictly to this aborted message_end; the caller's
2295
- // `finally` (in `InteractiveMode.#approvePlan`) clears it again on
2296
- // every terminal compaction outcome (`ok` / `cancelled` / `failed` /
2297
- // throw) so a leaked flag cannot silence a later unrelated abort.
2318
+ // here, scoped strictly to this aborted message_end; callers still clear it
2319
+ // in `finally` so a leaked flag cannot silence a later unrelated abort.
2298
2320
  if (
2299
2321
  event.type === "message_end" &&
2300
2322
  event.message.role === "assistant" &&
2301
2323
  event.message.stopReason === "aborted" &&
2302
- this.#planCompactAbortPending
2324
+ this.#planInternalAbortPending
2303
2325
  ) {
2304
2326
  (event.message as AssistantMessage).errorMessage = SILENT_ABORT_MARKER;
2305
- this.#planCompactAbortPending = false;
2327
+ this.#planInternalAbortPending = false;
2306
2328
  }
2307
2329
 
2308
2330
  // Deobfuscate assistant message content for display emission — the LLM echoes back
@@ -6552,7 +6574,7 @@ export class AgentSession {
6552
6574
  this.#todoReminderAwaitingProgress = false;
6553
6575
  this.#planReferenceSent = false;
6554
6576
  this.#planReferencePath = "local://PLAN.md";
6555
- this.#advisorRuntime?.reset();
6577
+ this.#resetAdvisorSessionState();
6556
6578
  this.#reconnectToAgent();
6557
6579
 
6558
6580
  // Emit session_switch event with reason "new" to hooks
@@ -7353,7 +7375,7 @@ export class AgentSession {
7353
7375
  throw new Error("Compaction already in progress");
7354
7376
  }
7355
7377
  this.#disconnectFromAgent();
7356
- await this.abort();
7378
+ await this.abort({ goalReason: "internal" });
7357
7379
  const compactionAbortController = new AbortController();
7358
7380
  this.#compactionAbortController = compactionAbortController;
7359
7381
 
@@ -7521,6 +7543,10 @@ export class AgentSession {
7521
7543
  const newEntries = this.sessionManager.getEntries();
7522
7544
  const sessionContext = this.buildDisplaySessionContext();
7523
7545
  this.agent.replaceMessages(sessionContext.messages);
7546
+ // Compaction discarded the conversation history that carried the approved
7547
+ // plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
7548
+ // the plan from disk and re-injects it on the next turn (issue #1246).
7549
+ this.#planReferenceSent = false;
7524
7550
  this.#advisorRuntime?.reset();
7525
7551
  this.#syncTodoPhasesFromBranch();
7526
7552
  this.#closeCodexProviderSessionsForHistoryRewrite();
@@ -9387,6 +9413,10 @@ export class AgentSession {
9387
9413
  const newEntries = this.sessionManager.getEntries();
9388
9414
  const sessionContext = this.buildDisplaySessionContext();
9389
9415
  this.agent.replaceMessages(sessionContext.messages);
9416
+ // Compaction discarded the conversation history that carried the approved
9417
+ // plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
9418
+ // the plan from disk and re-injects it on the next turn (issue #1246).
9419
+ this.#planReferenceSent = false;
9390
9420
  this.#advisorRuntime?.reset();
9391
9421
  this.#syncTodoPhasesFromBranch();
9392
9422
  this.#closeCodexProviderSessionsForHistoryRewrite();
@@ -9691,21 +9721,20 @@ export class AgentSession {
9691
9721
  if (this.#isClassifierRefusal(message)) return true;
9692
9722
  if (this.#isProviderErrorFinishReasonBeforeToolUse(message)) return true;
9693
9723
  if (this.#isMalformedFunctionCallError(message)) return true;
9694
- if (this.#streamInterruptedAfterObservableOutput(message)) return false;
9724
+ if (this.#hasReplayUnsafeToolOutput(message)) return false;
9695
9725
  if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
9696
9726
 
9697
9727
  const err = message.errorMessage;
9698
9728
  return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
9699
9729
  }
9700
- #streamInterruptedAfterObservableOutput(message: AssistantMessage): boolean {
9701
- if (message.stopDetails?.type === STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL) return true;
9702
- for (const block of message.content) {
9703
- if (block.type === "toolCall") return true;
9704
- if (block.type === "text" && block.text.length > 0) return true;
9705
- if (block.type === "thinking" && block.thinking.length > 0) return true;
9706
- if (block.type === "redactedThinking" && block.data.length > 0) return true;
9707
- }
9708
- return false;
9730
+ /**
9731
+ * Retried turns remove the failed assistant message from active context.
9732
+ * Text/thinking-only partials are safe to discard and replay. Retained
9733
+ * tool calls are not: a completed tool call may already have emitted its
9734
+ * tool result after this assistant message, so replaying can duplicate work.
9735
+ */
9736
+ #hasReplayUnsafeToolOutput(message: AssistantMessage): boolean {
9737
+ return message.content.some(block => block.type === "toolCall");
9709
9738
  }
9710
9739
 
9711
9740
  #isStaleOpenAIResponsesReplayError(message: AssistantMessage): boolean {
@@ -10939,7 +10968,7 @@ export class AgentSession {
10939
10968
  }
10940
10969
 
10941
10970
  this.#disconnectFromAgent();
10942
- await this.abort();
10971
+ await this.abort({ goalReason: "internal" });
10943
10972
 
10944
10973
  // Flush pending writes before switching so restore snapshots reflect committed state.
10945
10974
  await this.sessionManager.flush();
@@ -10997,6 +11026,7 @@ export class AgentSession {
10997
11026
  }
10998
11027
 
10999
11028
  this.agent.replaceMessages(sessionContext.messages);
11029
+ this.#resetAdvisorSessionState();
11000
11030
  this.#syncTodoPhasesFromBranch();
11001
11031
  if (switchingToDifferentSession) {
11002
11032
  this.#closeAllProviderSessions("session switch");
@@ -11202,7 +11232,7 @@ export class AgentSession {
11202
11232
 
11203
11233
  if (!skipConversationRestore) {
11204
11234
  this.agent.replaceMessages(sessionContext.messages);
11205
- this.#advisorRuntime?.reset();
11235
+ this.#resetAdvisorSessionState();
11206
11236
  this.#closeCodexProviderSessionsForHistoryRewrite();
11207
11237
  }
11208
11238
 
@@ -11291,7 +11321,7 @@ export class AgentSession {
11291
11321
  }
11292
11322
 
11293
11323
  this.agent.replaceMessages(sessionContext.messages);
11294
- this.#advisorRuntime?.reset();
11324
+ this.#resetAdvisorSessionState();
11295
11325
  this.#closeCodexProviderSessionsForHistoryRewrite();
11296
11326
 
11297
11327
  return { cancelled: false, sessionFile: this.sessionFile };
@@ -11457,7 +11487,7 @@ export class AgentSession {
11457
11487
  const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
11458
11488
  await this.#restoreMCPSelectionsForSessionContext(displayContext);
11459
11489
  this.agent.replaceMessages(displayContext.messages);
11460
- this.#advisorRuntime?.reset();
11490
+ this.#resetAdvisorSessionState();
11461
11491
  this.#syncTodoPhasesFromBranch();
11462
11492
  this.#closeCodexProviderSessionsForHistoryRewrite();
11463
11493
 
@@ -18,16 +18,7 @@ export type {
18
18
  ResetCreditRedeemOutcome,
19
19
  ResetCreditTarget,
20
20
  SerializedAuthStorage,
21
- SnapshotResponse,
22
21
  StoredAuthCredential,
23
22
  } from "@oh-my-pi/pi-ai";
24
- export {
25
- AuthBrokerClient,
26
- AuthStorage,
27
- DEFAULT_SNAPSHOT_CACHE_TTL_MS,
28
- REMOTE_REFRESH_SENTINEL,
29
- RemoteAuthCredentialStore,
30
- readAuthBrokerSnapshotCache,
31
- SqliteAuthCredentialStore,
32
- writeAuthBrokerSnapshotCache,
33
- } from "@oh-my-pi/pi-ai";
23
+ export { AuthStorage, REMOTE_REFRESH_SENTINEL, SqliteAuthCredentialStore } from "@oh-my-pi/pi-ai";
24
+ export type { SnapshotResponse } from "@oh-my-pi/pi-ai/auth-broker/types";
@@ -124,7 +124,13 @@ export class YieldQueue {
124
124
  return thunks;
125
125
  }
126
126
 
127
- clear(): void {
127
+ /** Drop queued entries. With `kind`, drop only that kind's entries (leaving
128
+ * any pending idle-flush for other kinds intact); otherwise drop everything. */
129
+ clear(kind?: string): void {
130
+ if (kind !== undefined) {
131
+ this.#entries.delete(kind);
132
+ return;
133
+ }
128
134
  this.#entries.clear();
129
135
  this.#idleFlushPending = false;
130
136
  }
@@ -1,6 +1,6 @@
1
1
  import * as net from "node:net";
2
2
  import { Process, ProcessStatus } from "@oh-my-pi/pi-natives";
3
- import { type Browser, type Page, TargetType } from "puppeteer-core";
3
+ import type { Browser, Page } from "puppeteer-core";
4
4
  import { ToolError, throwIfAborted } from "../tool-errors";
5
5
 
6
6
  const ATTACH_TARGET_SKIP_PATTERN =
@@ -126,7 +126,7 @@ export async function findReusableCdp(
126
126
  export async function pickElectronTarget(browser: Browser, matcher?: string): Promise<Page> {
127
127
  const discoveredPages = await Promise.all(
128
128
  browser.targets().map(async target => {
129
- if (target.type() !== TargetType.PAGE) return null;
129
+ if (String(target.type()) !== "page") return null;
130
130
  return await target.page().catch(() => null);
131
131
  }),
132
132
  );