@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cli.js +2648 -2662
  3. package/dist/types/advisor/runtime.d.ts +15 -1
  4. package/dist/types/config/model-roles.d.ts +1 -1
  5. package/dist/types/config/settings-schema.d.ts +32 -12
  6. package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
  7. package/dist/types/edit/index.d.ts +18 -0
  8. package/dist/types/edit/streaming.d.ts +30 -0
  9. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  10. package/dist/types/extensibility/shared-events.d.ts +1 -0
  11. package/dist/types/mcp/oauth-discovery.d.ts +0 -11
  12. package/dist/types/modes/components/status-line/component.d.ts +0 -2
  13. package/dist/types/sdk.d.ts +1 -1
  14. package/dist/types/session/agent-session.d.ts +26 -2
  15. package/dist/types/session/messages.d.ts +6 -7
  16. package/dist/types/session/settings-stream-fn.d.ts +21 -0
  17. package/dist/types/session/turn-persistence.d.ts +88 -0
  18. package/package.json +12 -12
  19. package/src/advisor/__tests__/advisor.test.ts +196 -0
  20. package/src/advisor/runtime.ts +65 -2
  21. package/src/auto-thinking/classifier.ts +2 -2
  22. package/src/config/model-resolver.ts +5 -1
  23. package/src/config/model-roles.ts +3 -3
  24. package/src/config/settings-schema.ts +30 -8
  25. package/src/discovery/omp-extension-roots.ts +38 -13
  26. package/src/edit/index.ts +21 -0
  27. package/src/edit/streaming.ts +170 -0
  28. package/src/extensibility/custom-tools/types.ts +1 -0
  29. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
  30. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
  31. package/src/extensibility/plugins/manager.ts +74 -4
  32. package/src/extensibility/shared-events.ts +1 -0
  33. package/src/internal-urls/docs-index.generated.txt +1 -1
  34. package/src/mcp/oauth-discovery.ts +5 -29
  35. package/src/mcp/transports/http.ts +3 -1
  36. package/src/mnemopi/backend.ts +2 -2
  37. package/src/modes/acp/acp-agent.ts +1 -1
  38. package/src/modes/components/assistant-message.ts +5 -5
  39. package/src/modes/components/status-line/component.ts +1 -9
  40. package/src/modes/components/status-line/segments.ts +1 -1
  41. package/src/modes/controllers/event-controller.ts +8 -11
  42. package/src/modes/interactive-mode.ts +0 -5
  43. package/src/modes/print-mode.ts +1 -1
  44. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  45. package/src/modes/utils/ui-helpers.ts +5 -4
  46. package/src/sdk.ts +12 -26
  47. package/src/session/agent-session.ts +319 -219
  48. package/src/session/messages.ts +20 -12
  49. package/src/session/session-persistence.ts +1 -2
  50. package/src/session/settings-stream-fn.ts +49 -0
  51. package/src/session/turn-persistence.ts +142 -0
  52. package/src/session/unexpected-stop-classifier.ts +2 -2
  53. package/src/slash-commands/helpers/mcp.ts +2 -1
  54. package/src/tiny/models.ts +8 -6
  55. package/src/tiny/text.ts +14 -7
  56. package/src/tools/image-gen.ts +2 -1
  57. package/src/tools/tts.ts +2 -1
  58. package/src/utils/title-generator.ts +1 -1
  59. package/src/web/search/providers/tavily.ts +36 -19
@@ -1,12 +1,24 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import { type SecretObfuscator } from "../secrets/obfuscator";
3
- /** Minimal slice of `Agent` the runtime drives — satisfied by pi-agent-core `Agent`. */
3
+ /**
4
+ * Minimal slice of `Agent` the runtime drives — satisfied by pi-agent-core
5
+ * `Agent`. `state.error` mirrors `Agent.state.error`: provider/stream failures
6
+ * the loop catches internally never reject `prompt()`, so the runtime reads
7
+ * this field after every prompt to detect a failed turn.
8
+ */
4
9
  export interface AdvisorAgent {
5
10
  prompt(input: string): Promise<void>;
6
11
  abort(reason?: unknown): void;
7
12
  reset(): void;
13
+ /**
14
+ * Drop messages appended past `count`. Called after a failed `prompt()` so a
15
+ * retry doesn't replay the failed user batch + synthetic assistant-error
16
+ * turn `Agent.#runLoop` records on its internal state.
17
+ */
18
+ rollbackTo?(count: number): void;
8
19
  readonly state: {
9
20
  messages: AgentMessage[];
21
+ error?: string;
10
22
  };
11
23
  }
12
24
  export interface AdvisorRuntimeHost {
@@ -33,6 +45,8 @@ export interface AdvisorRuntimeHost {
33
45
  * one that routes `advise()` results back to the primary.
34
46
  */
35
47
  beginAdvisorUpdate?(): void;
48
+ /** Surface a non-recovering advisor failure to the host UI without adding model-visible context. */
49
+ notifyFailure?(error: unknown): void;
36
50
  }
37
51
  export declare class AdvisorRuntime {
38
52
  #private;
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { type ThemeColor } from "../modes/theme/theme";
5
5
  import type { Settings } from "./settings";
6
- export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "designer" | "commit" | "title" | "task" | "advisor";
6
+ export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "designer" | "commit" | "tiny" | "task" | "advisor";
7
7
  export interface ModelRoleInfo {
8
8
  tag?: string;
9
9
  name: string;
@@ -1198,6 +1198,30 @@ export declare const SETTINGS_SCHEMA: {
1198
1198
  }];
1199
1199
  };
1200
1200
  };
1201
+ readonly textVerbosity: {
1202
+ readonly type: "enum";
1203
+ readonly values: readonly ["low", "medium", "high"];
1204
+ readonly default: "high";
1205
+ readonly ui: {
1206
+ readonly tab: "model";
1207
+ readonly group: "Sampling";
1208
+ readonly label: "Text Verbosity";
1209
+ readonly description: "OpenAI Responses and Codex response verbosity (low, medium, or high)";
1210
+ readonly options: readonly [{
1211
+ readonly value: "low";
1212
+ readonly label: "Low";
1213
+ readonly description: "Prefer concise responses";
1214
+ }, {
1215
+ readonly value: "medium";
1216
+ readonly label: "Medium";
1217
+ readonly description: "Balance brevity and detail";
1218
+ }, {
1219
+ readonly value: "high";
1220
+ readonly label: "High";
1221
+ readonly description: "Prefer detailed responses (default)";
1222
+ }];
1223
+ };
1224
+ };
1201
1225
  readonly serviceTier: {
1202
1226
  readonly type: "enum";
1203
1227
  readonly values: readonly ["none", "auto", "default", "flex", "scale", "priority", "openai-only", "claude-only"];
@@ -2080,7 +2104,7 @@ export declare const SETTINGS_SCHEMA: {
2080
2104
  };
2081
2105
  readonly "tools.format": {
2082
2106
  readonly type: "enum";
2083
- readonly values: readonly ["auto", "native", "glm", "hermes", "kimi", "xml", "anthropic", "deepseek", "harmony", "pi", "qwen3", "gemini", "gemma", "minimax"];
2107
+ readonly values: readonly ["auto", "native", "glm", "hermes", "kimi", "xml", "anthropic", "deepseek", "harmony", "qwen3", "gemini", "gemma", "minimax"];
2084
2108
  readonly default: "auto";
2085
2109
  readonly ui: {
2086
2110
  readonly tab: "context";
@@ -2123,10 +2147,6 @@ export declare const SETTINGS_SCHEMA: {
2123
2147
  readonly value: "harmony";
2124
2148
  readonly label: "Harmony";
2125
2149
  readonly description: "Use Harmony-style in-band tool calls.";
2126
- }, {
2127
- readonly value: "pi";
2128
- readonly label: "Pi";
2129
- readonly description: "Use the Pi owned dialect (compact sigil-delimited tool calls).";
2130
2150
  }, {
2131
2151
  readonly value: "qwen3";
2132
2152
  readonly label: "Qwen3";
@@ -2532,7 +2552,7 @@ export declare const SETTINGS_SCHEMA: {
2532
2552
  readonly tab: "memory";
2533
2553
  readonly group: "Mnemopi";
2534
2554
  readonly label: "Mnemopi LLM Mode";
2535
- readonly description: "Use no LLM, the configured smol model, or a remote OpenAI-compatible endpoint";
2555
+ readonly description: "Use no LLM, the online tiny model (the TINY role from /models, else pi/smol), or a remote OpenAI-compatible endpoint";
2536
2556
  readonly condition: "mnemopiActive";
2537
2557
  readonly options: readonly [{
2538
2558
  readonly value: "none";
@@ -2540,8 +2560,8 @@ export declare const SETTINGS_SCHEMA: {
2540
2560
  readonly description: "Disable Mnemopi LLM-backed extraction";
2541
2561
  }, {
2542
2562
  readonly value: "smol";
2543
- readonly label: "Smol";
2544
- readonly description: "Use the configured pi-ai smol model";
2563
+ readonly label: "Online (tiny)";
2564
+ readonly description: "Use the online tiny model (the TINY role from /models, else pi/smol)";
2545
2565
  }, {
2546
2566
  readonly value: "remote";
2547
2567
  readonly label: "Remote";
@@ -4607,7 +4627,7 @@ export declare const SETTINGS_SCHEMA: {
4607
4627
  readonly tab: "providers";
4608
4628
  readonly group: "Tiny Model";
4609
4629
  readonly label: "Tiny Model";
4610
- readonly description: "Session-title model: online pi/smol by default, or a local on-device model";
4630
+ readonly description: "Session-title model: online (the TINY role from /models, else pi/smol) by default, or a local on-device model";
4611
4631
  readonly options: ({
4612
4632
  value: "online";
4613
4633
  label: string;
@@ -4763,7 +4783,7 @@ export declare const SETTINGS_SCHEMA: {
4763
4783
  readonly tab: "memory";
4764
4784
  readonly group: "General";
4765
4785
  readonly label: "Memory Model";
4766
- readonly description: "Mnemopi LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model";
4786
+ readonly description: "Mnemopi LLM for fact extraction + consolidation: online (the TINY role from /models, else smol/remote) by default, or a local on-device model";
4767
4787
  readonly condition: "mnemopiActive";
4768
4788
  readonly options: ({
4769
4789
  value: "online";
@@ -4784,7 +4804,7 @@ export declare const SETTINGS_SCHEMA: {
4784
4804
  readonly tab: "model";
4785
4805
  readonly group: "Thinking";
4786
4806
  readonly label: "Auto Thinking Model";
4787
- readonly description: "Difficulty classifier for the `auto` thinking level: online smol by default, or a local on-device model";
4807
+ readonly description: "Difficulty classifier for the `auto` thinking level: online (the TINY role from /models, else smol) by default, or a local on-device model";
4788
4808
  readonly condition: "autoThinkingActive";
4789
4809
  readonly options: ({
4790
4810
  value: "online";
@@ -4815,7 +4835,7 @@ export declare const SETTINGS_SCHEMA: {
4815
4835
  readonly tab: "providers";
4816
4836
  readonly group: "Tiny Model";
4817
4837
  readonly label: "Unexpected Stop Model";
4818
- readonly description: "Classifier for unexpected-stop detection: online smol by default, or a local on-device model.";
4838
+ readonly description: "Classifier for unexpected-stop detection: online (the TINY role from /models, else smol) by default, or a local on-device model.";
4819
4839
  readonly condition: "unexpectedStopDetection";
4820
4840
  readonly options: ({
4821
4841
  value: "online";
@@ -31,9 +31,9 @@ export declare function getInjectedOmpExtensionCliRoots(): readonly OmpExtension
31
31
  * 1. CLI roots injected via {@link injectOmpExtensionCliRoots}
32
32
  * 2. Project `<cwd>/.omp/settings.json#extensions`
33
33
  * 3. User `~/.omp/agent/settings.json#extensions`
34
- * 4. Enabled plugins installed under `<plugins>/node_modules/` (e.g. via
35
- * `omp install <pkg>` / `omp plugin install` / `omp plugin link`)
36
- *
34
+ * 4. Enabled npm/link plugins installed under `<plugins>/node_modules/` (for
35
+ * `omp install <pkg>` / `omp plugin install` / `omp plugin link`). Marketplace
36
+ * installs are loaded by the `claude-plugins` provider and are excluded here.
37
37
  * Only entries that resolve to a directory on disk are returned; file
38
38
  * entrypoints contribute zero sub-discovery surface and are filtered out.
39
39
  * Installed-plugin enumeration failures (missing lockfile, unreadable
@@ -60,5 +60,23 @@ export declare class EditTool implements AgentTool<TInput> {
60
60
  * mode-specific patch grammar.
61
61
  */
62
62
  matcherDigest(args: unknown): string | undefined;
63
+ /**
64
+ * Project the streamed args onto their target file paths so path-scoped
65
+ * stream matchers (e.g. TTSR `tool:edit(*.ts)` globs) match hashline and
66
+ * apply_patch edits even though the path lives in the wire payload (a
67
+ * section header / envelope marker) rather than a top-level argument.
68
+ */
69
+ matcherPaths(args: unknown): readonly string[] | undefined;
70
+ /**
71
+ * Per-file projection of the streamed args, splitting multi-section
72
+ * hashline / multi-hunk apply_patch payloads into one (path, digest) entry
73
+ * per touched file. Path-scoped stream matchers (TTSR) then evaluate each
74
+ * file in isolation, so a `tool:edit(*.ts)` rule never fires on text that
75
+ * actually belongs to a sibling Markdown hunk.
76
+ */
77
+ matcherEntries(args: unknown): readonly {
78
+ path: string;
79
+ digest: string;
80
+ }[] | undefined;
63
81
  execute(_toolCallId: string, params: EditParams, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>, context?: AgentToolContext): Promise<AgentToolResult<EditToolDetails, TInput>>;
64
82
  }
@@ -35,6 +35,16 @@ export interface StreamingDiffContext {
35
35
  */
36
36
  isStreaming?: boolean;
37
37
  }
38
+ /**
39
+ * Per-file projection of a streamed edit payload. Pairs one target file path
40
+ * with the digest of only the lines added to that file, so path-scoped stream
41
+ * matchers (TTSR) evaluate each file in isolation — a `tool:edit(*.ts)` rule
42
+ * never fires on text that actually belongs to a sibling `README.md` hunk.
43
+ */
44
+ export interface EditMatcherEntry {
45
+ readonly path: string;
46
+ readonly digest: string;
47
+ }
38
48
  export interface EditStreamingStrategy<Args = unknown> {
39
49
  /**
40
50
  * Return the args restricted to edits that are "complete enough" to
@@ -60,6 +70,26 @@ export interface EditStreamingStrategy<Args = unknown> {
60
70
  * args don't yet carry any content.
61
71
  */
62
72
  matcherDigest(args: Args): string | undefined;
73
+ /**
74
+ * Surface the target file paths a (potentially partial) call would touch,
75
+ * so path-scoped stream matchers (e.g. TTSR `tool:edit(*.ts)` globs) match
76
+ * even when the path is not a top-level argument but lives inside the wire
77
+ * payload — `hashline` section headers, `apply_patch` envelope markers.
78
+ * Returns `undefined` (or an empty list) when no paths are recoverable.
79
+ */
80
+ matcherPaths(args: Args): readonly string[] | undefined;
81
+ /**
82
+ * Per-file projection of the (potentially partial) args: one entry per
83
+ * touched file pairing the path with the digest of only the lines added to
84
+ * that file. Multi-file payloads (multi-section hashline / multi-hunk
85
+ * apply_patch) MUST split here so callers can evaluate each file under its
86
+ * own path scope instead of leaking added lines from one file into the
87
+ * other's match context. Same-path sections / hunks are merged into one
88
+ * entry. Returns `undefined` (or empty) when no per-file split is
89
+ * recoverable yet — the caller falls back to {@link matcherDigest} +
90
+ * {@link matcherPaths}.
91
+ */
92
+ matcherEntries(args: Args): readonly EditMatcherEntry[] | undefined;
63
93
  }
64
94
  /**
65
95
  * Given an edits array parsed from partial JSON, drop the last entry when the
@@ -109,6 +109,7 @@ export type CustomToolSessionEvent = {
109
109
  maxAttempts: number;
110
110
  delayMs: number;
111
111
  errorMessage: string;
112
+ errorId?: number;
112
113
  } | {
113
114
  reason: "auto_retry_end";
114
115
  success: boolean;
@@ -190,6 +190,7 @@ export interface AutoRetryStartEvent {
190
190
  maxAttempts: number;
191
191
  delayMs: number;
192
192
  errorMessage: string;
193
+ errorId?: number;
193
194
  }
194
195
  /** Fired when auto-retry ends */
195
196
  export interface AutoRetryEndEvent {
@@ -1,9 +1,3 @@
1
- /**
2
- * MCP OAuth Auto-Discovery
3
- *
4
- * Automatically detects OAuth requirements from MCP server responses
5
- * and extracts authentication endpoints.
6
- */
7
1
  import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
8
2
  export interface OAuthEndpoints {
9
3
  authorizationUrl: string;
@@ -21,11 +15,6 @@ export interface AuthDetectionResult {
21
15
  message?: string;
22
16
  }
23
17
  export declare function extractMcpAuthServerUrl(error: Error, serverUrl?: string): string | undefined;
24
- /**
25
- * Detect if an error indicates authentication is required.
26
- * Checks for common auth error patterns.
27
- */
28
- export declare function detectAuthError(error: Error): boolean;
29
18
  /**
30
19
  * Extract OAuth endpoints from error response.
31
20
  * Looks for WWW-Authenticate header format or JSON error bodies.
@@ -14,8 +14,6 @@ export declare class StatusLineComponent implements Component {
14
14
  getEffectiveSettingsForTest(): EffectiveStatusLineSettings;
15
15
  setAutoCompactEnabled(enabled: boolean): void;
16
16
  setSubagentCount(count: number): void;
17
- /** Hub key label shown in the forced running-subagents badge. */
18
- setSubagentHubHint(hint: string | undefined): void;
19
17
  /** Active subagent count as currently displayed (collab state mirroring). */
20
18
  get subagentCount(): number;
21
19
  setSessionStartTime(time: number): void;
@@ -1,5 +1,5 @@
1
1
  import { type AgentTelemetryConfig, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import { type Model } from "@oh-my-pi/pi-ai";
2
+ import type { Model } from "@oh-my-pi/pi-ai";
3
3
  import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
4
4
  import { type Rule } from "./capability/rule";
5
5
  import { ModelRegistry } from "./config/model-registry";
@@ -13,9 +13,9 @@
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
15
  import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
16
- import { Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, ThinkingLevel, type ToolChoiceDirective } from "@oh-my-pi/pi-agent-core";
16
+ import { Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, type StreamFn, ThinkingLevel, type ToolChoiceDirective } from "@oh-my-pi/pi-agent-core";
17
17
  import { type CompactionResult, type ShakeConfig } from "@oh-my-pi/pi-agent-core/compaction";
18
- import type { AssistantMessage, ImageContent, Message, MessageAttribution, Model, ProviderSessionState, ResetCreditAccountStatus, ResetCreditRedeemOutcome, ResetCreditTarget, ServiceTier, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@oh-my-pi/pi-ai";
18
+ import type { AssistantMessage, Context, ImageContent, Message, MessageAttribution, Model, ProviderSessionState, ResetCreditAccountStatus, ResetCreditRedeemOutcome, ResetCreditTarget, ServiceTier, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@oh-my-pi/pi-ai";
19
19
  import { Effort } from "@oh-my-pi/pi-ai";
20
20
  import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
21
21
  import type { Rule } from "../capability/rule";
@@ -72,6 +72,7 @@ export type AgentSessionEvent = AgentEvent | {
72
72
  maxAttempts: number;
73
73
  delayMs: number;
74
74
  errorMessage: string;
75
+ errorId?: number;
75
76
  } | {
76
77
  type: "auto_retry_end";
77
78
  success: boolean;
@@ -160,6 +161,21 @@ export interface AgentSessionConfig {
160
161
  builtInToolNames?: Iterable<string>;
161
162
  /** Current session pre-LLM message transform pipeline */
162
163
  transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
164
+ /**
165
+ * Per-request transform applied after `convertToLlm` and before the
166
+ * provider call. Used for snapcompact, secret obfuscation, and image
167
+ * clamping. When supplied via {@link createAgentSession}, the advisor agent
168
+ * inherits this so its requests undergo the same shaping as the main turn.
169
+ */
170
+ transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
171
+ /**
172
+ * Stream wrapper passed to the advisor agent so its requests apply the
173
+ * session's `providers.openrouterVariant`, `providers.antigravityEndpoint`,
174
+ * `providers.maxInFlightRequests`, and `model.loopGuard.*` settings —
175
+ * keeping OpenRouter sticky-routing / response caching consistent with the
176
+ * main agent. Defaults to plain `streamSimple` when omitted.
177
+ */
178
+ advisorStreamFn?: StreamFn;
163
179
  /** Provider payload hook used by the active session request path */
164
180
  onPayload?: SimpleStreamOptions["onPayload"];
165
181
  /** Provider response hook used by the active session request path */
@@ -1253,6 +1269,14 @@ export declare class AgentSession {
1253
1269
  * not merely the setting. Drives the status-line badge and `/dump advisor`.
1254
1270
  */
1255
1271
  isAdvisorActive(): boolean;
1272
+ /**
1273
+ * The live advisor `Agent`, or `undefined` when no advisor runtime is
1274
+ * attached. Surfaced for diagnostics (`/dump advisor` already serializes
1275
+ * its transcript via {@link formatAdvisorHistoryAsText}) and so callers can
1276
+ * verify the advisor inherits the session's provider-shaping options
1277
+ * (`streamFn`, `promptCacheKey`, `providerSessionState`, ...).
1278
+ */
1279
+ getAdvisorAgent(): Agent | undefined;
1256
1280
  /**
1257
1281
  * Return structured advisor stats for the status command and TUI panel.
1258
1282
  */
@@ -43,18 +43,17 @@ export interface SkillPromptDetails {
43
43
  * #buildTranscriptLines`, `runPrintMode`, and `AcpAgent#replayAssistantMessage`
44
44
  * (fallback error emission) read it via `isSilentAbort`. */
45
45
  export declare const SILENT_ABORT_MARKER = "__omp.silent_abort__";
46
- /** Type-guard for `SILENT_ABORT_MARKER`. Renderers MUST branch on this rather
47
- * than string-comparing inline so refactors to the marker constant (e.g.,
48
- * namespacing changes) propagate through every consumer in lockstep. */
49
- export declare function isSilentAbort(errorMessage: string | undefined): boolean;
46
+ /** Type-guard for silent aborts. Renderers MUST call this helper so structured
47
+ * `errorId` and legacy persisted marker messages stay in lockstep. */
48
+ export declare function isSilentAbort(message: Pick<AssistantMessage, "errorId" | "errorMessage">): boolean;
50
49
  /** Reason threaded through `AbortController.abort(reason)` when the user aborts
51
50
  * the turn with Esc (see `AgentSession.abort`). The agent keeps it on the
52
51
  * aborted assistant message's `errorMessage` so queued follow-ups/tool-result
53
52
  * placeholders can distinguish a deliberate interrupt from a bare lifecycle
54
53
  * abort, but interactive renderers suppress this redundant transcript line. */
55
54
  export declare const USER_INTERRUPT_LABEL = "Interrupted by user";
56
- export declare function isUserInterruptAbort(errorMessage: string | undefined): boolean;
57
- export declare function shouldRenderAbortReason(errorMessage: string | undefined): boolean;
55
+ export declare function isUserInterruptAbort(message: Pick<AssistantMessage, "errorId" | "errorMessage">): boolean;
56
+ export declare function shouldRenderAbortReason(message: Pick<AssistantMessage, "errorId" | "errorMessage">): boolean;
58
57
  /** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
59
58
  * reason (bare `abort()`). Renderers treat it as "no specific reason given". */
60
59
  export declare const GENERIC_ABORT_SENTINEL = "Request was aborted";
@@ -63,7 +62,7 @@ export declare const GENERIC_ABORT_SENTINEL = "Request was aborted";
63
62
  * no threaded reason fall back to the retry-aware generic label. Call
64
63
  * `shouldRenderAbortReason` before rendering when user interrupts should stay
65
64
  * visually quiet. */
66
- export declare function resolveAbortLabel(errorMessage: string | undefined, retryAttempt?: number): string;
65
+ export declare function resolveAbortLabel(message: Pick<AssistantMessage, "errorId" | "errorMessage">, retryAttempt?: number): string;
67
66
  /** Extract the optional `__queueChipText` field from a CustomMessage's
68
67
  * `details` blob. Safe over `unknown`; returns undefined when the field is
69
68
  * absent or non-string. */
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Settings-aware stream wrapper shared by the main agent (sdk.ts) and the
3
+ * advisor agent (AgentSession.#buildAdvisorRuntime).
4
+ *
5
+ * Reads OpenRouter / Antigravity routing variants, Responses-family text
6
+ * verbosity, per-provider in-flight caps, and the loop guard out of `Settings`
7
+ * per request, layering them onto whatever options the caller passed. Before
8
+ * this helper existed, advisor turns called bare `streamSimple` while the main
9
+ * turn went through an inline closure that read these settings — so an advisor on
10
+ * OpenRouter never saw `providers.openrouterVariant`, breaking sticky routing
11
+ * and OpenRouter response-cache hits across advisor calls.
12
+ */
13
+ import type { StreamFn } from "@oh-my-pi/pi-agent-core";
14
+ import { type Settings } from "../config/settings";
15
+ /**
16
+ * Build a {@link StreamFn} that reads provider routing/guard settings from
17
+ * `settings` per call and forwards to `base` (defaults to `streamSimple`).
18
+ *
19
+ * Caller-supplied `streamOptions` always win — the helper only fills holes.
20
+ */
21
+ export declare function createSettingsAwareStreamFn(settings: Settings, base?: StreamFn): StreamFn;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Helpers that share one cheap, structural identity for messages — both during
3
+ * incremental persistence and for the mid-run-compaction ordering check.
4
+ *
5
+ * Previously `AgentSession` carried two near-duplicate routines
6
+ * (`#sessionMessagesReferToSameTurn` + `#messageValueSignature`) that
7
+ * reconstructed the branch path on every check (O(n²) `unshift`) and
8
+ * `JSON.stringify`-compared the full message content on every pairwise hit.
9
+ * Long-running sessions with many subagents fired this thousands of times per
10
+ * minute and froze the TUI loop (see issue #3629). The persistence key already
11
+ * encodes a stable logical identity — timestamp + role-specific discriminators
12
+ * — so the structural compare is now the rare collision tiebreaker (e.g. two
13
+ * provider responses at the same millisecond with `undefined` responseId),
14
+ * not the load-bearing check.
15
+ *
16
+ * The helpers here keep that identity in one place and expose the planner so
17
+ * the persistence-ordering logic is unit-testable without standing up an
18
+ * `AgentSession`.
19
+ */
20
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
21
+ /**
22
+ * Stable identity for messages that pass through {@link AgentSession}'s
23
+ * incremental persistence path.
24
+ *
25
+ * The discriminators chosen per role are precisely the fields that uniquely
26
+ * identify a single logical message instance:
27
+ *
28
+ * - `assistant` — timestamp + provider + model + responseId + stopReason
29
+ * (responseId is the canonical provider-side id when available; the rest
30
+ * disambiguate when it is not, e.g. local/dev models).
31
+ * - `toolResult` — timestamp + toolCallId + toolName (toolCallId is unique
32
+ * per execution; toolName guards against synthetic reuse).
33
+ * - `user` / `developer` — timestamp + attribution (attribution distinguishes
34
+ * user-typed vs hook-injected at the same wall-clock millisecond).
35
+ * - `fileMention` — timestamp.
36
+ *
37
+ * Returns `undefined` for message roles that are not persisted through this
38
+ * path (e.g. `hookMessage`, `custom`, `bashExecution`) — those follow other
39
+ * append paths in `SessionManager`.
40
+ */
41
+ export declare function sessionMessagePersistenceKey(message: AgentMessage): string | undefined;
42
+ /**
43
+ * Slow-path content equality check used when two messages collide on
44
+ * {@link sessionMessagePersistenceKey}. Only the role's content fields are
45
+ * compared (no timestamps, no metadata) because the key already pinned all of
46
+ * those down.
47
+ *
48
+ * Most calls into the persistence path never reach this — keys are unique
49
+ * enough in production that the snapshot lookup short-circuits at the key
50
+ * level. Restoring the structural compare here preserves the pre-#3629
51
+ * contract that two messages with the same metadata BUT different content are
52
+ * distinct (e.g. two assistant turns with `undefined` responseId emitted in
53
+ * the same wall-clock millisecond, which is exactly how the in-memory test
54
+ * harness crafts streamed responses).
55
+ */
56
+ export declare function sameMessageContent(left: AgentMessage, right: AgentMessage): boolean;
57
+ /**
58
+ * Outcome of {@link planTurnPersistence}.
59
+ *
60
+ * `ok` lists the turn-message indices that still need to be appended (in
61
+ * order). `out-of-order` reports the first message whose later sibling is
62
+ * already persisted — the caller bails so it does not silently splice a
63
+ * stale message between newer entries on the live branch.
64
+ */
65
+ export type TurnPersistencePlan = {
66
+ kind: "ok";
67
+ toPersist: readonly number[];
68
+ } | {
69
+ kind: "out-of-order";
70
+ messageIndex: number;
71
+ };
72
+ /**
73
+ * Decide what to do with a turn's messages relative to what's already on the
74
+ * branch, in a single pass over the pre-computed keys.
75
+ *
76
+ * @param turnKeys persistence keys for each turn message, in the order the
77
+ * agent loop emitted them. `undefined` slots represent messages with no
78
+ * persistence key (skipped silently).
79
+ * @param persistedKeys the snapshot of persistence keys currently on the
80
+ * branch (built once per call from {@link sessionMessagePersistenceKey} for
81
+ * each persisted message entry).
82
+ *
83
+ * The check is O(n²) over turn messages — but `n` here is the size of one
84
+ * turn (a handful of tool results), not the size of the branch. That's the
85
+ * point of this refactor: the expensive O(branch) work happens exactly once,
86
+ * inside the caller's snapshot loop, not per-comparison.
87
+ */
88
+ export declare function planTurnPersistence(turnKeys: readonly (string | undefined)[], persistedKeys: ReadonlySet<string>): TurnPersistencePlan;
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": "16.2.1",
4
+ "version": "16.2.2",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -55,17 +55,17 @@
55
55
  "@agentclientprotocol/sdk": "0.25.0",
56
56
  "@babel/parser": "^7.29.7",
57
57
  "@mozilla/readability": "^0.6.0",
58
- "@oh-my-pi/hashline": "16.2.1",
59
- "@oh-my-pi/omp-stats": "16.2.1",
60
- "@oh-my-pi/pi-agent-core": "16.2.1",
61
- "@oh-my-pi/pi-ai": "16.2.1",
62
- "@oh-my-pi/pi-catalog": "16.2.1",
63
- "@oh-my-pi/pi-mnemopi": "16.2.1",
64
- "@oh-my-pi/pi-natives": "16.2.1",
65
- "@oh-my-pi/pi-tui": "16.2.1",
66
- "@oh-my-pi/pi-utils": "16.2.1",
67
- "@oh-my-pi/pi-wire": "16.2.1",
68
- "@oh-my-pi/snapcompact": "16.2.1",
58
+ "@oh-my-pi/hashline": "16.2.2",
59
+ "@oh-my-pi/omp-stats": "16.2.2",
60
+ "@oh-my-pi/pi-agent-core": "16.2.2",
61
+ "@oh-my-pi/pi-ai": "16.2.2",
62
+ "@oh-my-pi/pi-catalog": "16.2.2",
63
+ "@oh-my-pi/pi-mnemopi": "16.2.2",
64
+ "@oh-my-pi/pi-natives": "16.2.2",
65
+ "@oh-my-pi/pi-tui": "16.2.2",
66
+ "@oh-my-pi/pi-utils": "16.2.2",
67
+ "@oh-my-pi/pi-wire": "16.2.2",
68
+ "@oh-my-pi/snapcompact": "16.2.2",
69
69
  "@opentelemetry/api": "^1.9.1",
70
70
  "@opentelemetry/context-async-hooks": "^2.7.1",
71
71
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",