@oh-my-pi/pi-coding-agent 17.1.2 → 17.1.4

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 (110) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/cli.js +3806 -3798
  3. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +1 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
  5. package/dist/types/cli/update-cli.d.ts +41 -0
  6. package/dist/types/cli/usage-cli.d.ts +4 -2
  7. package/dist/types/commands/update.d.ts +1 -0
  8. package/dist/types/config/model-registry.d.ts +19 -0
  9. package/dist/types/config/settings-schema.d.ts +30 -7
  10. package/dist/types/cursor.d.ts +47 -1
  11. package/dist/types/eval/js/process-entry.d.ts +2 -1
  12. package/dist/types/eval/js/worker-core.d.ts +4 -1
  13. package/dist/types/extensibility/extensions/runner.d.ts +1 -0
  14. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
  15. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +4 -0
  16. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +15 -7
  17. package/dist/types/extensibility/shared-events.d.ts +2 -0
  18. package/dist/types/modes/components/assistant-message.d.ts +9 -0
  19. package/dist/types/modes/components/custom-editor.d.ts +12 -8
  20. package/dist/types/plan-mode/approved-plan.d.ts +3 -2
  21. package/dist/types/secrets/obfuscator.d.ts +7 -31
  22. package/dist/types/session/agent-session.d.ts +19 -4
  23. package/dist/types/session/prewalk.d.ts +2 -1
  24. package/dist/types/session/session-advisors.d.ts +8 -0
  25. package/dist/types/session/session-metadata.d.ts +29 -0
  26. package/dist/types/session/turn-recovery.d.ts +5 -5
  27. package/dist/types/stt/sherpa-runtime.d.ts +38 -0
  28. package/dist/types/thinking.d.ts +24 -0
  29. package/dist/types/tools/auto-generated-guard.d.ts +5 -2
  30. package/dist/types/tools/bash.d.ts +5 -4
  31. package/dist/types/tools/computer/exposure.d.ts +8 -0
  32. package/dist/types/tools/computer/supervisor.d.ts +1 -1
  33. package/dist/types/tools/computer/worker-entry.d.ts +1 -1
  34. package/dist/types/tools/computer/worker.d.ts +1 -1
  35. package/dist/types/tools/computer.d.ts +6 -0
  36. package/dist/types/tools/memory-render.d.ts +1 -4
  37. package/dist/types/tools/shell-tokenize.d.ts +14 -0
  38. package/dist/types/tools/todo.d.ts +7 -1
  39. package/package.json +12 -12
  40. package/src/advisor/__tests__/advisor.test.ts +80 -10
  41. package/src/advisor/runtime.ts +27 -1
  42. package/src/cli/__tests__/auth-gateway-catalog.test.ts +111 -0
  43. package/src/cli/auth-gateway-cli.ts +63 -16
  44. package/src/cli/config-cli.ts +25 -7
  45. package/src/cli/update-cli.ts +229 -29
  46. package/src/cli/usage-cli.ts +144 -15
  47. package/src/cli.ts +21 -15
  48. package/src/commands/update.ts +6 -0
  49. package/src/config/__tests__/model-registry.test.ts +42 -7
  50. package/src/config/model-registry.ts +67 -4
  51. package/src/config/settings-schema.ts +39 -8
  52. package/src/config/settings.ts +24 -2
  53. package/src/cursor.ts +153 -0
  54. package/src/dap/session.ts +70 -11
  55. package/src/edit/hashline/filesystem.ts +1 -1
  56. package/src/edit/modes/patch.ts +1 -1
  57. package/src/eval/__tests__/js-context-manager.test.ts +9 -1
  58. package/src/eval/__tests__/process-entry-import.test.ts +111 -1
  59. package/src/eval/js/process-entry.ts +9 -5
  60. package/src/eval/js/worker-core.ts +6 -2
  61. package/src/exec/bash-executor.ts +4 -2
  62. package/src/extensibility/extensions/runner.ts +23 -8
  63. package/src/extensibility/legacy-pi-ai-shim.ts +9 -0
  64. package/src/extensibility/legacy-pi-coding-agent-shim.ts +14 -2
  65. package/src/extensibility/legacy-pi-tui-shim.ts +33 -0
  66. package/src/extensibility/shared-events.ts +2 -0
  67. package/src/main.ts +22 -1
  68. package/src/modes/acp/acp-agent.ts +6 -0
  69. package/src/modes/components/assistant-message.ts +88 -11
  70. package/src/modes/components/chat-transcript-builder.ts +2 -0
  71. package/src/modes/components/custom-editor.test.ts +170 -0
  72. package/src/modes/components/custom-editor.ts +79 -29
  73. package/src/modes/components/settings-defs.ts +5 -1
  74. package/src/modes/controllers/event-controller.ts +96 -4
  75. package/src/modes/controllers/selector-controller.ts +14 -0
  76. package/src/modes/interactive-mode.ts +34 -8
  77. package/src/modes/utils/context-usage.ts +19 -2
  78. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  79. package/src/plan-mode/approved-plan.ts +18 -10
  80. package/src/prompts/system/custom-system-prompt.md +1 -1
  81. package/src/prompts/system/system-prompt.md +10 -1
  82. package/src/prompts/tools/ast-edit.md +1 -1
  83. package/src/sdk.ts +4 -0
  84. package/src/secrets/obfuscator.ts +36 -126
  85. package/src/session/agent-session.ts +69 -60
  86. package/src/session/prewalk.ts +8 -3
  87. package/src/session/session-advisors.ts +67 -3
  88. package/src/session/session-metadata.ts +53 -0
  89. package/src/session/session-provider-boundary.ts +0 -1
  90. package/src/session/session-tools.ts +35 -1
  91. package/src/session/stream-guards.ts +1 -1
  92. package/src/session/turn-recovery.ts +36 -19
  93. package/src/slash-commands/builtin-registry.ts +49 -7
  94. package/src/stt/asr-worker.ts +2 -37
  95. package/src/stt/sherpa-runtime.ts +71 -0
  96. package/src/task/executor.ts +6 -5
  97. package/src/thinking.ts +39 -0
  98. package/src/tools/auto-generated-guard.ts +18 -5
  99. package/src/tools/bash.ts +43 -15
  100. package/src/tools/computer/exposure.ts +38 -0
  101. package/src/tools/computer/supervisor.ts +61 -13
  102. package/src/tools/computer/worker-entry.ts +28 -19
  103. package/src/tools/computer/worker.ts +3 -7
  104. package/src/tools/computer.ts +65 -10
  105. package/src/tools/gh-cache-invalidation.ts +2 -82
  106. package/src/tools/memory-render.ts +11 -2
  107. package/src/tools/render-utils.ts +8 -3
  108. package/src/tools/shell-tokenize.ts +83 -0
  109. package/src/tools/todo.ts +44 -17
  110. package/src/tools/write.ts +1 -1
@@ -342,10 +342,7 @@ export declare class AgentSession {
342
342
  * Transcript for TUI display. Full history is kept for export/resume-style
343
343
  * callers; live chat can collapse compacted history to keep the hot render
344
344
  * surface bounded. Display-only — NEVER feed the result to
345
- * `agent.replaceMessages` or a provider. Because it is never re-obfuscated,
346
- * it opts into legacy index-derived alias restoration so pre-keyed sessions
347
- * still render their secrets; the agent-feeding paths
348
- * (`buildDisplaySessionContext`) keep the keyed-only default.
345
+ * `agent.replaceMessages` or a provider.
349
346
  */
350
347
  buildTranscriptSessionContext(options?: Pick<BuildSessionContextOptions, "collapseCompactedHistory" | "keepDanglingToolCalls">): SessionContext;
351
348
  /** Convert session messages using the same pre-LLM pipeline as the active session. */
@@ -830,7 +827,25 @@ export declare class AgentSession {
830
827
  toolCallId: string;
831
828
  questions: AskToolInput["questions"];
832
829
  };
830
+ /**
831
+ * `true` when this call committed a new sibling answer for an `ask`
832
+ * re-answer (`reanswerAskResult` was applied). The interactive caller
833
+ * resumes the agent via {@link resumeAfterAskReanswer} *after* rebuilding
834
+ * its transcript, so the resumed turn never renders against the stale
835
+ * pre-rebuild UI (issue #6483).
836
+ */
837
+ askReanswerCommitted?: boolean;
833
838
  }>;
839
+ /**
840
+ * Resume the agent after the interactive `/tree` caller has committed an
841
+ * `ask` re-answer (`navigateTree` returned `askReanswerCommitted`) and
842
+ * rebuilt its transcript. Mirrors how a live `ask` completion drives a
843
+ * follow-up turn, but is deferred to the caller so the resumed turn renders
844
+ * against the rebuilt UI rather than the stale pre-navigation transcript
845
+ * (issue #6483). The scheduled continue honors the same disposed/compacting
846
+ * guards as every other post-prompt continuation.
847
+ */
848
+ resumeAfterAskReanswer(): void;
834
849
  /**
835
850
  * Build a standalone `AgentToolContext` for running `AskTool.execute()`
836
851
  * outside a normal agent turn, for `/tree` `ask` re-answer (issue #5642).
@@ -2,7 +2,7 @@ import type { Agent, AgentMessage, AgentTurnEndContext } from "@oh-my-pi/pi-agen
2
2
  import type { Model } from "@oh-my-pi/pi-ai";
3
3
  import type { LocalProtocolOptions } from "../internal-urls/index.js";
4
4
  import type { PlanModeState } from "../plan-mode/state.js";
5
- import type { ConfiguredThinkingLevel } from "../thinking.js";
5
+ import { type ConfiguredThinkingLevel } from "../thinking.js";
6
6
  import type { PlanProposalHandler } from "../tools/resolve.js";
7
7
  import type { PlanYolo, Prewalk } from "./agent-session-types.js";
8
8
  import type { SessionManager } from "./session-manager.js";
@@ -11,6 +11,7 @@ export interface PrewalkCoordinatorHost {
11
11
  agent: Agent;
12
12
  sessionManager: SessionManager;
13
13
  model(): Model | undefined;
14
+ configuredThinkingLevel(): ConfiguredThinkingLevel | undefined;
14
15
  emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
15
16
  setModelTemporary(model: Model, thinkingLevel?: ConfiguredThinkingLevel, options?: {
16
17
  ephemeral?: boolean;
@@ -122,6 +122,14 @@ export declare class SessionAdvisors {
122
122
  detachAndCloseRecorders(): Promise<void>;
123
123
  /** Re-primes advisor transcript views across a conversation boundary. */
124
124
  resetSessionState(): void;
125
+ /**
126
+ * Rebind every live advisor to the active primary conversation's provider
127
+ * identity (session id, prompt-cache key, credential + metadata resolvers,
128
+ * telemetry). Invoked on every provider-session change — including branch
129
+ * paths that skip conversation restore — so advisors never keep emitting the
130
+ * previous conversation's session id/metadata (issue #6625).
131
+ */
132
+ refreshProviderIdentity(): void;
125
133
  /** Re-primes advisor transcript views after an in-conversation history rewrite. */
126
134
  resetAllRuntimes(): void;
127
135
  /** Whether live runtimes still match the resolved advisor configuration. */
@@ -0,0 +1,29 @@
1
+ import type { AuthStorage } from "./auth-storage.js";
2
+ /**
3
+ * Build the per-request `metadata` payload for the Anthropic provider, shaped
4
+ * like real Claude Code's `getAPIMetadata` output (`{ session_id, account_uuid,
5
+ * device_id }`) so the backend buckets requests under one session and attributes
6
+ * them to the authenticated OAuth account when available. Resolved at request
7
+ * time so token refreshes and login/logout transitions don't strand a stale
8
+ * account UUID in memory. `account_uuid` and `device_id` are omitted for
9
+ * non-Anthropic providers to avoid leaking the user's Claude identity to
10
+ * third-party APIs (including Anthropic-format-compatible proxies such as
11
+ * cloudflare-ai-gateway or gitlab-duo).
12
+ *
13
+ * Installed via `Agent#setMetadataResolver` on the main `AgentSession`, each
14
+ * subagent session, and the separately constructed advisor `Agent` — each with
15
+ * its own provider session id — so Main, subagent, and Advisor requests each
16
+ * expose a distinct, stable provider-facing session identity.
17
+ *
18
+ * `provider` is the target provider string (e.g. `"anthropic"`) and gates the
19
+ * `account_uuid` and `device_id` lookups — only `"anthropic"` requests carry them.
20
+ *
21
+ * `sessionId` is forwarded to the auth-storage session-sticky lookup so that
22
+ * multi-credential setups attribute to the same OAuth account used for the
23
+ * actual API request rather than always picking the first credential.
24
+ *
25
+ * `authStorage` is treated as optional so test fixtures that stub `modelRegistry`
26
+ * without a real storage layer still work; the resolver simply skips the lookup
27
+ * and emits `{ session_id }` alone, matching the no-OAuth-credential path.
28
+ */
29
+ export declare function buildSessionMetadata(sessionId: string, provider: string, authStorage: AuthStorage | undefined): Record<string, unknown>;
@@ -137,12 +137,12 @@ export declare class TurnRecovery {
137
137
  */
138
138
  isRetryableError(message: AssistantMessage): boolean;
139
139
  /**
140
- * Resume a stalled turn after every emitted tool call has produced a result.
141
- * Cursor calls must also carry the server-execution marker. The failed
142
- * assistant/tool-result pair stays in context so completed side effects are
143
- * continued from rather than replayed.
140
+ * Classify a reasonless abort or stream stall whose emitted tool calls all
141
+ * have results. The failed assistant/tool-result pair stays in context so
142
+ * continuation cannot replay completed side effects; synthetic results tell
143
+ * the next turn that an unexecuted call must be reissued.
144
144
  */
145
- canResumeResolvedStreamStall(message: AssistantMessage): boolean;
145
+ classifyResolvedInterruptedToolTurn(message: AssistantMessage): "reasonless-abort" | "stream-stall" | undefined;
146
146
  /** Checks whether a provider error represents a classifier refusal. */
147
147
  isClassifierRefusal(message: AssistantMessage): boolean;
148
148
  /** Clears fallback ownership after an explicit model change. */
@@ -0,0 +1,38 @@
1
+ interface SherpaOfflineResult {
2
+ text?: string;
3
+ }
4
+ interface SherpaOfflineStream {
5
+ acceptWaveform(audio: {
6
+ samples: Float32Array;
7
+ sampleRate: number;
8
+ }): void;
9
+ }
10
+ interface SherpaOfflineConfig {
11
+ modelConfig: {
12
+ transducer: {
13
+ encoder: string;
14
+ decoder: string;
15
+ joiner: string;
16
+ };
17
+ tokens: string;
18
+ modelType: string;
19
+ numThreads: number;
20
+ provider: string;
21
+ debug: number;
22
+ };
23
+ decodingMethod: string;
24
+ }
25
+ /** A sherpa-onnx recognizer instance used by the STT worker. */
26
+ export interface SherpaOfflineRecognizer {
27
+ createStream(): SherpaOfflineStream;
28
+ decodeAsync(stream: SherpaOfflineStream): Promise<SherpaOfflineResult>;
29
+ }
30
+ /** The native sherpa-onnx module surface used by the STT worker. */
31
+ export interface SherpaRuntime {
32
+ OfflineRecognizer: {
33
+ createAsync(config: SherpaOfflineConfig): Promise<SherpaOfflineRecognizer>;
34
+ };
35
+ }
36
+ /** Loads the nearest working source-workspace sherpa wrapper, including hoisted fallbacks. */
37
+ export declare function loadSourceSherpaRuntime(sourceUrl: string): SherpaRuntime;
38
+ export {};
@@ -43,6 +43,30 @@ export declare const AUTO_THINKING: "auto";
43
43
  export type ConfiguredThinkingLevel = ThinkingLevel | typeof AUTO_THINKING;
44
44
  /** Maps the session-level `auto` sentinel to `undefined`; concrete levels pass through. */
45
45
  export declare function concreteThinkingLevel(level: ConfiguredThinkingLevel | undefined): ThinkingLevel | undefined;
46
+ /**
47
+ * True when a prewalk hand-off from `current`/`currentLevel` to
48
+ * `target`/`targetLevel` would change nothing observable: same model id, same
49
+ * auto/fixed mode, and the same model-clamped effective effort. Prewalk arms and
50
+ * switches only when this is false.
51
+ *
52
+ * An effort-only delta on the same model id is a legitimate cheapening hand-off
53
+ * — on a reasoning model the effort is the bulk of the cost — so it is NOT a
54
+ * no-op and must still switch. A `targetLevel` of `undefined` means the prewalk
55
+ * pattern carried no explicit `:level` suffix (no effort change requested),
56
+ * which on the same model is a no-op.
57
+ *
58
+ * `auto` mode is compared before efforts: `auto` and a fixed selector that both
59
+ * resolve to `undefined` effort (e.g. `:inherit`) are NOT interchangeable —
60
+ * applying the fixed selector clears per-turn classification, so switching
61
+ * auto↔fixed is always a real change even when the clamped efforts match.
62
+ *
63
+ * Efforts are otherwise compared AFTER model clamping, so a target the model
64
+ * cannot honor (e.g. `:xhigh` on a model capped at `high`) — which
65
+ * `setThinkingLevel` would clamp straight back to the active effort — is
66
+ * recognized as a no-op instead of triggering an ephemeral reset and the
67
+ * plan/checklist nudges for nothing.
68
+ */
69
+ export declare function prewalkWouldBeNoop(current: Model | undefined, currentLevel: ConfiguredThinkingLevel | undefined, target: Model, targetLevel: ConfiguredThinkingLevel | undefined): boolean;
46
70
  /** Metadata used to render the `auto` selector value alongside concrete levels. */
47
71
  export interface ConfiguredThinkingLevelMetadata {
48
72
  value: ConfiguredThinkingLevel;
@@ -1,11 +1,13 @@
1
+ import { type Settings } from "../config/settings.js";
1
2
  /**
2
3
  * Check if a file is auto-generated by examining its content.
3
4
  * Throws ToolError if the file appears to be auto-generated.
4
5
  *
5
6
  * @param absolutePath - Absolute path to the file
6
7
  * @param displayPath - Path to show in error messages (relative or as provided)
8
+ * @param activeSettings - Session settings; falls back to global settings or the schema default
7
9
  */
8
- export declare function assertEditableFile(absolutePath: string, displayPath?: string): Promise<void>;
10
+ export declare function assertEditableFile(absolutePath: string, displayPath?: string, activeSettings?: Settings): Promise<void>;
9
11
  /**
10
12
  * Check if file content is auto-generated.
11
13
  * Uses only the first CHECK_BYTE_COUNT characters of the content.
@@ -13,5 +15,6 @@ export declare function assertEditableFile(absolutePath: string, displayPath?: s
13
15
  *
14
16
  * @param content - File content to check (can be full content or prefix)
15
17
  * @param displayPath - Path to show in error messages
18
+ * @param activeSettings - Session settings; falls back to global settings or the schema default
16
19
  */
17
- export declare function assertEditableFileContent(content: string, displayPath: string): void;
20
+ export declare function assertEditableFileContent(content: string, displayPath: string, activeSettings?: Settings): void;
@@ -17,12 +17,13 @@ export declare const BASH_DEFAULT_PREVIEW_LINES = 10;
17
17
  * containing a space, pipe, `&&`, redirect, or `$(...)`.
18
18
  *
19
19
  * The wrap reuses the same shell binary + args the local `bash-executor` would
20
- * pick via `settings.getShellConfig()` — Git Bash / `bash.exe` on Windows,
20
+ * pick via `settings.getShellConfig()` — Git Bash / `bash.exe` on Windows
21
+ * (`cmd.exe /c` as the last-resort fallback when no bash exists on the host),
21
22
  * `$SHELL` (bash/zsh) with the `sh` fallback on POSIX — so the ACP path
22
23
  * preserves `bash` tool semantics (`$VAR`, `$(...)`, `source`, POSIX quoting,
23
- * `-l`) instead of dropping to `cmd.exe` on Windows. The agent host's shell
24
- * path is used as a proxy for the client's, matching the near-universal
25
- * ACP deployment shape of an editor spawning omp as a co-hosted subprocess.
24
+ * `-l`) wherever a POSIX shell is available. The agent host's shell path is
25
+ * used as a proxy for the client's, matching the near-universal ACP
26
+ * deployment shape of an editor spawning omp as a co-hosted subprocess.
26
27
  */
27
28
  export declare function wrapShellLineForClientTerminal(line: string, shellConfig: {
28
29
  shell: string;
@@ -0,0 +1,8 @@
1
+ import type { Model } from "@oh-my-pi/pi-ai";
2
+ export type ComputerExposureMode = "native" | "function" | "unavailable";
3
+ export interface ComputerExposureOptions {
4
+ azureBaseUrl?: string;
5
+ azureResourceName?: string;
6
+ }
7
+ /** Match the provider transport's effective Computer Use tool representation. */
8
+ export declare function computerExposureMode(model: Model | undefined, options?: ComputerExposureOptions): ComputerExposureMode;
@@ -29,4 +29,4 @@ export declare class ComputerSupervisor implements ComputerController {
29
29
  }
30
30
  export declare function registerComputerController(ownerId: string | undefined, controller: ComputerController): () => void;
31
31
  export declare function releaseComputerSessionsForOwner(ownerId: string | undefined): Promise<void>;
32
- export declare function smokeTestComputerWorker(timeoutMs?: number): Promise<void>;
32
+ export declare function smokeTestComputerWorker(timeoutMs?: number, createWorker?: ComputerWorkerFactory): Promise<void>;
@@ -1 +1 @@
1
- export {};
1
+ export declare function startComputerWorker(): void;
@@ -1,4 +1,4 @@
1
- import { type DesktopAction, type DesktopCapture, DesktopSession, type DesktopSessionOptions } from "@oh-my-pi/pi-natives";
1
+ import type { DesktopAction, DesktopCapture, DesktopSession, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
2
2
  import type { ComputerWorkerTransport } from "./protocol.js";
3
3
  export interface NativeDesktopSession {
4
4
  readonly capabilities: DesktopSession["capabilities"];
@@ -63,6 +63,12 @@ export declare class ComputerTool implements AgentTool<typeof computerSchema, Co
63
63
  readonly strict = false;
64
64
  readonly approval: typeof computerApproval;
65
65
  readonly formatApprovalDetails: (args: unknown) => string[];
66
+ /**
67
+ * Settings snapshot used to create the tool's current controller; refreshed
68
+ * when a model switch crosses the coordinate-safe sizing boundary. Surfaced
69
+ * by `/computer status`.
70
+ */
71
+ effectiveConfiguration: Readonly<DesktopSessionOptions>;
66
72
  constructor(session: ToolSession, createController?: ComputerControllerFactory);
67
73
  get description(): string;
68
74
  execute(_toolCallId: string, params: ComputerParams, signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<ComputerToolDetails>, context?: AgentToolContext): Promise<AgentToolResult<ComputerToolDetails>>;
@@ -13,10 +13,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
13
13
  import type { RenderResultOptions } from "../extensibility/custom-tools/types.js";
14
14
  import type { Theme } from "../modes/theme/theme.js";
15
15
  interface RetainRenderArgs {
16
- items?: Array<{
17
- content?: string;
18
- context?: string;
19
- }>;
16
+ items?: unknown;
20
17
  }
21
18
  interface QueryRenderArgs {
22
19
  query?: string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Conservative shell command tokenizer shared by the bash approval-pattern
3
+ * matcher and the gh-cache invalidator.
4
+ *
5
+ * Splits a bash command into independent command segments, each a list of word
6
+ * tokens. Handles single/double-quoted strings, backslash escapes, and the
7
+ * standard operators (`;`, `&&`, `||`, `|`, `&`, `(`, `)`, newlines) as segment
8
+ * boundaries so callers treat the pieces as independent command sequences.
9
+ *
10
+ * It is deliberately not a full POSIX parser — heredocs, command substitution,
11
+ * and arithmetic expansion are out of scope; callers fall through when they
12
+ * cannot find the structure they need.
13
+ */
14
+ export declare function tokenizeShellSegments(command: string): string[][];
@@ -144,7 +144,13 @@ type TodoRenderArgs = TodoRenderOp & {
144
144
  };
145
145
  /** One-based ASCII roman numeral for display (I, II, III, IV, …). */
146
146
  export declare function phaseRomanNumeral(oneBasedIndex: number): string;
147
- /** Display-only phase header: `I. Foundation`. State and prompts never see this. */
147
+ /**
148
+ * Display-only phase header: `I. Foundation`. State and prompts never see this.
149
+ *
150
+ * Sanitized for the same reason task labels are: this is a render boundary and
151
+ * the name may carry provider or session text holding control sequences. The
152
+ * raw `phase.name` stays the lookup key everywhere else.
153
+ */
148
154
  export declare function formatPhaseDisplayName(name: string, oneBasedIndex: number): string;
149
155
  export declare const TODO_STRIKE_HOLD_FRAMES = 2;
150
156
  export declare const TODO_STRIKE_REVEAL_FRAMES = 12;
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.1.2",
4
+ "version": "17.1.4",
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",
@@ -53,17 +53,17 @@
53
53
  "@babel/parser": "^7.29.7",
54
54
  "@babel/traverse": "^7.29.7",
55
55
  "@mozilla/readability": "^0.6.0",
56
- "@oh-my-pi/hashline": "17.1.2",
57
- "@oh-my-pi/omp-stats": "17.1.2",
58
- "@oh-my-pi/pi-agent-core": "17.1.2",
59
- "@oh-my-pi/pi-ai": "17.1.2",
60
- "@oh-my-pi/pi-catalog": "17.1.2",
61
- "@oh-my-pi/pi-mnemopi": "17.1.2",
62
- "@oh-my-pi/pi-natives": "17.1.2",
63
- "@oh-my-pi/pi-tui": "17.1.2",
64
- "@oh-my-pi/pi-utils": "17.1.2",
65
- "@oh-my-pi/pi-wire": "17.1.2",
66
- "@oh-my-pi/snapcompact": "17.1.2",
56
+ "@oh-my-pi/hashline": "17.1.4",
57
+ "@oh-my-pi/omp-stats": "17.1.4",
58
+ "@oh-my-pi/pi-agent-core": "17.1.4",
59
+ "@oh-my-pi/pi-ai": "17.1.4",
60
+ "@oh-my-pi/pi-catalog": "17.1.4",
61
+ "@oh-my-pi/pi-mnemopi": "17.1.4",
62
+ "@oh-my-pi/pi-natives": "17.1.4",
63
+ "@oh-my-pi/pi-tui": "17.1.4",
64
+ "@oh-my-pi/pi-utils": "17.1.4",
65
+ "@oh-my-pi/pi-wire": "17.1.4",
66
+ "@oh-my-pi/snapcompact": "17.1.4",
67
67
  "@opentelemetry/api": "^1.9.1",
68
68
  "@opentelemetry/api-logs": "^0.220.0",
69
69
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -1634,7 +1634,7 @@ describe("advisor", () => {
1634
1634
  await Promise.resolve();
1635
1635
 
1636
1636
  expect(promptInputs).toHaveLength(1);
1637
- expect(promptInputs[0]).toContain("#TOKABC123_");
1637
+ expect(promptInputs[0]).toContain("$$TOKABC123_");
1638
1638
  expect(promptInputs[0]).not.toContain("tok_abc123");
1639
1639
  });
1640
1640
  it("does not scan advisor-hidden successful tool-result bodies", async () => {
@@ -1666,7 +1666,7 @@ describe("advisor", () => {
1666
1666
  await Promise.resolve();
1667
1667
 
1668
1668
  expect(promptInputs).toHaveLength(1);
1669
- expect(promptInputs[0]).toContain("#TOKABC123_");
1669
+ expect(promptInputs[0]).toContain("$$TOKABC123_");
1670
1670
  expect(promptInputs[0]).not.toContain("tok_abc123");
1671
1671
  });
1672
1672
  it("does not scan tool-call arguments hidden by the primary-argument preview", async () => {
@@ -1693,7 +1693,7 @@ describe("advisor", () => {
1693
1693
  });
1694
1694
  runtime.onTurnEnd();
1695
1695
  await runtime.waitForCatchup(1000, 1);
1696
- expect(promptInputs[0]).toContain("#TOKABC123_");
1696
+ expect(promptInputs[0]).toContain("$$TOKABC123_");
1697
1697
  expect(promptInputs[0]).not.toContain("tok_abc123");
1698
1698
  });
1699
1699
 
@@ -1722,7 +1722,7 @@ describe("advisor", () => {
1722
1722
  });
1723
1723
  runtime.onTurnEnd();
1724
1724
  await runtime.waitForCatchup(1000, 1);
1725
- expect(promptInputs[0]).toContain("#TOKABC123_");
1725
+ expect(promptInputs[0]).toContain("$$TOKABC123_");
1726
1726
  expect(promptInputs[0]).not.toContain("tok_abc123");
1727
1727
  });
1728
1728
 
@@ -1761,7 +1761,7 @@ describe("advisor", () => {
1761
1761
  await Promise.resolve();
1762
1762
 
1763
1763
  expect(promptInputs).toHaveLength(1);
1764
- expect(promptInputs[0]).toContain("#TOKABC123_");
1764
+ expect(promptInputs[0]).toContain("$$TOKABC123_");
1765
1765
  expect(promptInputs[0]).not.toContain("tok_abc123");
1766
1766
  });
1767
1767
  it("does not scan execution source after the advisor preview cap", async () => {
@@ -1798,7 +1798,7 @@ describe("advisor", () => {
1798
1798
  await Promise.resolve();
1799
1799
 
1800
1800
  expect(promptInputs).toHaveLength(1);
1801
- expect(promptInputs[0]).toContain("#TOKABC123_");
1801
+ expect(promptInputs[0]).toContain("$$TOKABC123_");
1802
1802
  expect(promptInputs[0]).not.toContain("tok_abc123");
1803
1803
  });
1804
1804
 
@@ -1829,7 +1829,7 @@ describe("advisor", () => {
1829
1829
  await Promise.resolve();
1830
1830
 
1831
1831
  expect(promptInputs).toHaveLength(1);
1832
- expect(promptInputs[0]).toContain("#TOKABC123_");
1832
+ expect(promptInputs[0]).toContain("$$TOKABC123_");
1833
1833
  expect(promptInputs[0]).not.toContain("tok_abc123");
1834
1834
  expect(obfuscate).not.toHaveBeenCalledWith("tok_abc123", expect.anything());
1835
1835
  });
@@ -1864,7 +1864,7 @@ describe("advisor", () => {
1864
1864
  await Promise.resolve();
1865
1865
 
1866
1866
  expect(promptInputs).toHaveLength(1);
1867
- expect(promptInputs[0]).toContain("#TOKABC123_");
1867
+ expect(promptInputs[0]).toContain("$$TOKABC123_");
1868
1868
  expect(promptInputs[0]).not.toContain("tok_abc123");
1869
1869
  expect(obfuscate).not.toHaveBeenCalledWith("tok_abc123", expect.anything());
1870
1870
  });
@@ -1876,7 +1876,7 @@ describe("advisor", () => {
1876
1876
  // precomputation obfuscateMessages performs for the primary provider path
1877
1877
  // (see secrets-obfuscator.test.ts). Redacting message fields independently
1878
1878
  // would let the EARLIER user message's plain secret (OTHERSECRET) mint a
1879
- // friendly-prefixed placeholder ("#TOKABC123_<hash>#") before the SIBLING
1879
+ // friendly-prefixed placeholder ("$$TOKABC123_<hash>$$") before the SIBLING
1880
1880
  // toolResult's `details.diff` field, later in the same delta, reveals the
1881
1881
  // regex-protected value that friendly name normalizes to
1882
1882
  // (tok_abc123 -> TOKABC123) — baking a normalized rendering of that
@@ -2004,7 +2004,7 @@ describe("advisor", () => {
2004
2004
  // placeholder from a PRIOR thinking block: if thinking fell through
2005
2005
  // unredacted, the advisor prompt would receive both the raw secret AND,
2006
2006
  // had it been redacted without sharing the regex collision set, a
2007
- // normalized "#TOKABC123_<hash>#" rendering of the regex-protected value
2007
+ // normalized "$$TOKABC123_<hash>$$" rendering of the regex-protected value
2008
2008
  // (tok_abc123) only discovered later in the same delta.
2009
2009
  const obfuscator = new SecretObfuscator([
2010
2010
  { type: "plain", content: "OTHERSECRET", friendlyName: "TOKABC123" },
@@ -3938,6 +3938,76 @@ describe("advisor", () => {
3938
3938
  expect(promptInputs[1]).toContain("bbb");
3939
3939
  });
3940
3940
 
3941
+ it("notifies the host after the advisor persistently quarantines its output (issue #6661)", async () => {
3942
+ const state: { messages: AgentMessage[]; error?: string } = { messages: [] };
3943
+ let promptCalls = 0;
3944
+ let shouldQuarantine = true;
3945
+ const agent: AdvisorAgent = {
3946
+ prompt: async input => {
3947
+ promptCalls++;
3948
+ state.messages.push({ role: "user", content: input, timestamp: Date.now() } as AgentMessage);
3949
+ if (shouldQuarantine) {
3950
+ state.messages.push({
3951
+ role: "assistant",
3952
+ content: [
3953
+ { type: "text", text: "The agent skipped the required plan step." },
3954
+ { type: "toolCall", id: `tc-${promptCalls}`, name: "bash", arguments: { command: "ls" } },
3955
+ ],
3956
+ stopReason: "toolUse",
3957
+ timestamp: Date.now(),
3958
+ } as unknown as AgentMessage);
3959
+ throw new AdvisorOutputQuarantinedError(
3960
+ "Advisor response quarantined: requested unavailable tool bash",
3961
+ );
3962
+ }
3963
+ state.messages.push({
3964
+ role: "assistant",
3965
+ content: [{ type: "text", text: "ok" }],
3966
+ timestamp: Date.now(),
3967
+ } as unknown as AgentMessage);
3968
+ },
3969
+ abort: () => {},
3970
+ reset: () => {
3971
+ state.messages.length = 0;
3972
+ state.error = undefined;
3973
+ },
3974
+ rollbackTo: count => {
3975
+ if (count < state.messages.length) state.messages.length = count;
3976
+ state.error = undefined;
3977
+ },
3978
+ state,
3979
+ };
3980
+ const notifyFailures: string[] = [];
3981
+ const messages: AgentMessage[] = [{ role: "user", content: "aaa", timestamp: 1 } as AgentMessage];
3982
+ const host: AdvisorRuntimeHost = {
3983
+ snapshotMessages: () => messages,
3984
+ enqueueAdvice: () => {},
3985
+ notifyFailure: err => notifyFailures.push(err instanceof Error ? err.message : String(err)),
3986
+ };
3987
+ const runtime = new AdvisorRuntime(agent, host, 0);
3988
+
3989
+ // Every advisor turn calls an ungranted tool and is quarantined, so its
3990
+ // advice never reaches the primary. A persistently-quarantining advisor is
3991
+ // a supervision failure the user must see in the main UI, not an unbounded
3992
+ // silent re-prime loop.
3993
+ for (let i = 2; i <= 5; i++) {
3994
+ messages.push({ role: "user", content: `msg-${i}`, timestamp: i } as AgentMessage);
3995
+ runtime.onTurnEnd(messages);
3996
+ await settleUntil(() => runtime.backlog === 0);
3997
+ }
3998
+
3999
+ expect(promptCalls).toBeGreaterThanOrEqual(2);
4000
+ expect(notifyFailures).toEqual(["Advisor response quarantined: requested unavailable tool bash"]);
4001
+ expect(runtime.failureNotified).toBe(true);
4002
+
4003
+ shouldQuarantine = false;
4004
+ messages.push({ role: "user", content: "recovered", timestamp: 6 } as AgentMessage);
4005
+ runtime.onTurnEnd(messages);
4006
+ await settleUntil(() => runtime.backlog === 0);
4007
+
4008
+ expect(runtime.failureNotified).toBe(false);
4009
+ });
4010
+
3941
4011
  it("drops the in-flight batch when a reset aborts the advisor prompt", async () => {
3942
4012
  const promptInputs: string[] = [];
3943
4013
  const { promise: firstPromptStarted, resolve: startFirstPrompt } = Promise.withResolvers<void>();
@@ -218,6 +218,15 @@ export function buildAdvisorQuarantineSourceText(currentInput: string, messages:
218
218
  */
219
219
  const MAX_COALESCE_ROUNDS = 3;
220
220
 
221
+ /**
222
+ * Consecutive quarantined advisor turns tolerated before the failure is surfaced
223
+ * to the host UI. A quarantine discards the advisor's whole turn before dispatch,
224
+ * so its advice never reaches the primary; one silent re-prime is allowed to
225
+ * recover a one-off hallucination, but a persistent quarantine loop is a real
226
+ * supervision gap the user must see (issue #6661). Reset on any successful turn.
227
+ */
228
+ const MAX_QUARANTINE_RETRIES = 2;
229
+
221
230
  const ADVISOR_RENDER_OPTIONS = {
222
231
  includeThinking: true,
223
232
  includeToolIntent: true,
@@ -279,6 +288,8 @@ export class AdvisorRuntime {
279
288
  #backlog = 0;
280
289
  #consecutiveFailures = 0;
281
290
  #failureNotified = false;
291
+ /** Consecutive quarantined turns since the last success/reset (issue #6661). */
292
+ #consecutiveQuarantines = 0;
282
293
  /** Completed 3-failure backlog-drop cycles since the last success/reset. */
283
294
  #droppedBacklogs = 0;
284
295
  /**
@@ -450,7 +461,6 @@ export class AdvisorRuntime {
450
461
 
451
462
  #clearAdvisorContextAtCurrentCursor(): void {
452
463
  this.#consecutiveFailures = 0;
453
- this.#failureNotified = false;
454
464
  this.#clearSeenContext();
455
465
  try {
456
466
  this.agent.reset();
@@ -505,6 +515,8 @@ export class AdvisorRuntime {
505
515
  this.#halted = false;
506
516
  this.#failing = false;
507
517
  this.#droppedBacklogs = 0;
518
+ this.#consecutiveQuarantines = 0;
519
+ this.#failureNotified = false;
508
520
  this.#resetAdvisorContext(true, true);
509
521
  }
510
522
 
@@ -865,6 +877,7 @@ export class AdvisorRuntime {
865
877
  this.#consecutiveFailures = 0;
866
878
  this.#failureNotified = false;
867
879
  this.#droppedBacklogs = 0;
880
+ this.#consecutiveQuarantines = 0;
868
881
  if (this.host.onTurnSuccess) {
869
882
  try {
870
883
  await this.host.onTurnSuccess();
@@ -906,6 +919,19 @@ export class AdvisorRuntime {
906
919
  logger.debug("advisor onTurnError hook failed", { err: String(hookErr) });
907
920
  }
908
921
  if (err instanceof AdvisorOutputQuarantinedError) {
922
+ // A quarantine discards the advisor's whole turn before dispatch, so
923
+ // its advice never reaches the primary. One re-prime is allowed to
924
+ // recover a one-off hallucination silently; a persistent quarantine
925
+ // loop is a supervision gap the user must see in the main UI — not an
926
+ // unbounded silent retry. Surface it (deduped by #notifyFailureOnce)
927
+ // and drop the batch to break the loop (issue #6661).
928
+ this.#consecutiveQuarantines++;
929
+ if (this.#consecutiveQuarantines >= MAX_QUARANTINE_RETRIES) {
930
+ this.#notifyFailureOnce(err);
931
+ this.#consecutiveQuarantines = 0;
932
+ this.#resetAdvisorContext(true, true);
933
+ continue;
934
+ }
909
935
  const rePrime = this.#pending.length > 0 ? this.#latestMessages : undefined;
910
936
  // Wake catchup waiters only when nothing is re-primed; otherwise the
911
937
  // re-primed turn restores the backlog and waiters resolve on its completion.