@oh-my-pi/pi-coding-agent 15.11.4 → 15.11.6

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 (58) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/cli.js +450 -424
  3. package/dist/types/cli/usage-cli.d.ts +10 -1
  4. package/dist/types/commands/usage.d.ts +9 -0
  5. package/dist/types/config/settings-schema.d.ts +53 -3
  6. package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
  7. package/dist/types/modes/components/session-selector.d.ts +1 -1
  8. package/dist/types/modes/components/tool-execution.d.ts +14 -0
  9. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  10. package/dist/types/modes/interactive-mode.d.ts +10 -0
  11. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  12. package/dist/types/modes/types.d.ts +2 -0
  13. package/dist/types/modes/utils/context-usage.d.ts +6 -1
  14. package/dist/types/session/agent-session.d.ts +14 -1
  15. package/dist/types/session/auth-storage.d.ts +1 -1
  16. package/dist/types/session/codex-auto-reset.d.ts +107 -0
  17. package/dist/types/session/snapcompact-inline.d.ts +105 -4
  18. package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
  19. package/dist/types/task/render.d.ts +1 -0
  20. package/dist/types/tools/todo.d.ts +0 -11
  21. package/package.json +11 -11
  22. package/src/cli/usage-cli.ts +187 -16
  23. package/src/commands/usage.ts +8 -0
  24. package/src/config/settings-schema.ts +56 -3
  25. package/src/config/settings.ts +9 -0
  26. package/src/internal-urls/docs-index.generated.ts +1 -1
  27. package/src/modes/components/reset-usage-selector.ts +161 -0
  28. package/src/modes/components/session-selector.ts +8 -2
  29. package/src/modes/components/settings-selector.ts +62 -47
  30. package/src/modes/components/tool-execution.ts +18 -0
  31. package/src/modes/components/transcript-container.ts +23 -1
  32. package/src/modes/controllers/command-controller.ts +24 -1
  33. package/src/modes/controllers/selector-controller.ts +68 -0
  34. package/src/modes/interactive-mode.ts +59 -0
  35. package/src/modes/session-observer-registry.ts +61 -3
  36. package/src/modes/theme/theme.ts +2 -2
  37. package/src/modes/types.ts +2 -0
  38. package/src/modes/utils/context-usage.ts +75 -1
  39. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  40. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  41. package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
  42. package/src/prompts/tools/browser.md +33 -43
  43. package/src/prompts/tools/eval.md +27 -50
  44. package/src/prompts/tools/irc.md +29 -31
  45. package/src/prompts/tools/read.md +31 -37
  46. package/src/prompts/tools/todo.md +1 -2
  47. package/src/sdk.ts +3 -2
  48. package/src/session/agent-session.ts +131 -6
  49. package/src/session/auth-storage.ts +3 -0
  50. package/src/session/codex-auto-reset.ts +190 -0
  51. package/src/session/snapcompact-inline.ts +396 -59
  52. package/src/slash-commands/builtin-registry.ts +145 -8
  53. package/src/slash-commands/helpers/context-report.ts +28 -1
  54. package/src/slash-commands/helpers/reset-usage.ts +66 -0
  55. package/src/slash-commands/helpers/usage-report.ts +12 -0
  56. package/src/task/index.ts +30 -7
  57. package/src/task/render.ts +34 -19
  58. package/src/tools/todo.ts +8 -128
@@ -7,11 +7,15 @@
7
7
  * credentials produced no usage report are listed too, so the output
8
8
  * always covers the full credential pool.
9
9
  */
10
- import type { UsageReport } from "@oh-my-pi/pi-ai";
10
+ import { type UsageHistoryEntry, type UsageReport } from "@oh-my-pi/pi-ai";
11
11
  export interface UsageCommandArgs {
12
12
  json?: boolean;
13
13
  provider?: string;
14
14
  redact?: boolean;
15
+ /** Show recorded usage-limit history instead of a live snapshot. */
16
+ history?: boolean;
17
+ /** History window in days (with `history`). */
18
+ days?: number;
15
19
  }
16
20
  /** Identity slice of a stored credential, for "every account" coverage. */
17
21
  export interface UsageAccountIdentity {
@@ -69,4 +73,9 @@ export declare function computeProviderWindowStats(reports: UsageReport[]): Prov
69
73
  * each provider section as "no usage data" rows.
70
74
  */
71
75
  export declare function formatUsageBreakdown(reports: UsageReport[], accounts: UsageAccountIdentity[], nowMs: number, redaction?: Map<string, string>): string;
76
+ /**
77
+ * Render recorded usage-limit history: per provider, per account, one
78
+ * peak-per-bucket sparkline per limit window plus latest/peak percentages.
79
+ */
80
+ export declare function formatUsageHistory(entries: UsageHistoryEntry[], sinceMs: number, nowMs: number, redaction?: Map<string, string>): string;
72
81
  export declare function runUsageCommand(cmd: UsageCommandArgs): Promise<void>;
@@ -19,6 +19,15 @@ export default class Usage extends Command {
19
19
  description: string;
20
20
  default: boolean;
21
21
  };
22
+ history: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"boolean"> & {
23
+ description: string;
24
+ default: boolean;
25
+ };
26
+ days: import("@oh-my-pi/pi-utils/cli").FlagDescriptor<"integer"> & {
27
+ char: string;
28
+ description: string;
29
+ default: number;
30
+ };
22
31
  };
23
32
  static examples: string[];
24
33
  run(): Promise<void>;
@@ -1738,13 +1738,27 @@ export declare const SETTINGS_SCHEMA: {
1738
1738
  };
1739
1739
  };
1740
1740
  readonly "snapcompact.systemPrompt": {
1741
- readonly type: "boolean";
1742
- readonly default: false;
1741
+ readonly type: "enum";
1742
+ readonly values: readonly ["none", "agents-md", "all"];
1743
+ readonly default: "none";
1743
1744
  readonly ui: {
1744
1745
  readonly tab: "context";
1745
1746
  readonly group: "Experimental";
1746
1747
  readonly label: "Snapcompact System Prompt";
1747
- readonly description: "Experimental: render the system prompt as dense PNG image(s) and attach to the first user message (vision models only). Saves tokens; loses system-prompt prompt caching.";
1748
+ readonly description: "Experimental: render selected system prompt text as dense PNG image(s) and attach to the first user message (vision models only). Saves tokens; loses prompt caching for imaged text.";
1749
+ readonly options: readonly [{
1750
+ readonly value: "none";
1751
+ readonly label: "None";
1752
+ readonly description: "Keep the system prompt as text.";
1753
+ }, {
1754
+ readonly value: "agents-md";
1755
+ readonly label: "AGENTS.md";
1756
+ readonly description: "Only move loaded context-file instructions to images, when that saves tokens.";
1757
+ }, {
1758
+ readonly value: "all";
1759
+ readonly label: "All";
1760
+ readonly description: "Move the full system prompt to images, when that saves tokens.";
1761
+ }];
1748
1762
  };
1749
1763
  };
1750
1764
  readonly "snapcompact.toolResults": {
@@ -4102,6 +4116,36 @@ export declare const SETTINGS_SCHEMA: {
4102
4116
  }];
4103
4117
  };
4104
4118
  };
4119
+ readonly "codexResets.autoRedeem": {
4120
+ readonly type: "boolean";
4121
+ readonly default: false;
4122
+ readonly ui: {
4123
+ readonly tab: "providers";
4124
+ readonly group: "Services";
4125
+ readonly label: "Codex Auto-Redeem Saved Resets";
4126
+ readonly description: "When a turn is blocked by the Codex weekly limit on the active account and no other account is available, automatically spend one saved rate-limit reset (ChatGPT 'save rate limit resets'). Conservative: never fires for 5-hour-only or Spark limits, near a natural reset, or twice for the same block. Requires retries enabled.";
4127
+ };
4128
+ };
4129
+ readonly "codexResets.minBlockedMinutes": {
4130
+ readonly type: "number";
4131
+ readonly default: 60;
4132
+ readonly ui: {
4133
+ readonly tab: "providers";
4134
+ readonly group: "Services";
4135
+ readonly label: "Codex Auto-Redeem Min Block";
4136
+ readonly description: "Only auto-redeem when the natural weekly reset is at least this many minutes away (don't spend a ~30-day credit to save a short wait).";
4137
+ };
4138
+ };
4139
+ readonly "codexResets.keepCredits": {
4140
+ readonly type: "number";
4141
+ readonly default: 0;
4142
+ readonly ui: {
4143
+ readonly tab: "providers";
4144
+ readonly group: "Services";
4145
+ readonly label: "Codex Auto-Redeem Reserve";
4146
+ readonly description: "Never auto-spend below this many saved resets (0 = the last credit may be spent automatically).";
4147
+ };
4148
+ };
4105
4149
  readonly "provider.appendOnlyContext": {
4106
4150
  readonly type: "enum";
4107
4151
  readonly values: readonly ["auto", "on", "off"];
@@ -4451,6 +4495,11 @@ export interface ShellMinimizerSettings {
4451
4495
  sourceOutlineLevel: "default" | "aggressive";
4452
4496
  legacyFilters: boolean | undefined;
4453
4497
  }
4498
+ export interface CodexResetsSettings {
4499
+ autoRedeem: boolean;
4500
+ minBlockedMinutes: number;
4501
+ keepCredits: number;
4502
+ }
4454
4503
  /** Map group prefix -> typed settings interface */
4455
4504
  export interface GroupTypeMap {
4456
4505
  compaction: CompactionSettings;
@@ -4469,6 +4518,7 @@ export interface GroupTypeMap {
4469
4518
  modelTags: ModelTagsSettings;
4470
4519
  cycleOrder: string[];
4471
4520
  shellMinimizer: ShellMinimizerSettings;
4521
+ codexResets: CodexResetsSettings;
4472
4522
  }
4473
4523
  export type GroupPrefix = keyof GroupTypeMap;
4474
4524
  export {};
@@ -0,0 +1,12 @@
1
+ import { Container } from "@oh-my-pi/pi-tui";
2
+ import type { ResetUsageAccount } from "../../slash-commands/helpers/reset-usage";
3
+ /**
4
+ * Account picker for `/usage reset`. Lists Codex accounts with their saved
5
+ * rate-limit reset counts; selecting one redeems a reset. Because a reset is a
6
+ * scarce, irreversible credit, Enter requires a second press to confirm.
7
+ */
8
+ export declare class ResetUsageSelectorComponent extends Container {
9
+ #private;
10
+ constructor(accounts: ResetUsageAccount[], onSelect: (account: ResetUsageAccount) => void, onCancel: () => void);
11
+ handleInput(keyData: string): void;
12
+ }
@@ -8,7 +8,7 @@ export type SessionHistoryMatcher = (query: string) => string[];
8
8
  * Resume search narrows a recency-sorted list: once every query token appears
9
9
  * as a literal substring, newer sessions should beat a slightly better fuzzy
10
10
  * position match. Pure fuzzy/acronym matches still sort by fuzzy score after
11
- * literal matches.
11
+ * literal matches, but weak pure fuzzy tokens are dropped as noise.
12
12
  */
13
13
  export declare function rankSessionSearchMatches(allSessions: SessionInfo[], query: string): SessionInfo[];
14
14
  /**
@@ -72,6 +72,20 @@ export declare class ToolExecutionComponent extends Container {
72
72
  * past, or an explicit {@link seal} flips it to `true`.
73
73
  */
74
74
  isTranscriptBlockFinalized(): boolean;
75
+ /**
76
+ * Whether this still-live block's settled rows may enter native scrollback
77
+ * (see `FinalizableBlock.isTranscriptBlockCommitStable`). A pending
78
+ * collapsed preview is provisional: the tail-window streaming views
79
+ * (edit/bash/eval caps) are re-anchored top-first by the result render, so
80
+ * promoting their visually static head — e.g. an edit preview idling on
81
+ * its last frame while the apply + LSP pass runs — would strand a stale
82
+ * copy of the call box above the final block the moment the result lands.
83
+ * Expanded pending blocks stream top-anchored append-shaped content whose
84
+ * rows the result render preserves byte-stable (the over-tall write/eval
85
+ * scrollback contract), so they stay commit-eligible. Displaceable waiting
86
+ * polls are removed wholesale by the next poll and must never commit.
87
+ */
88
+ isTranscriptBlockCommitStable(): boolean;
75
89
  /**
76
90
  * Mark the tool terminal even though no result arrived (the turn aborted or
77
91
  * abandoned it) and stop animating, so it can freeze and stops pinning the
@@ -41,6 +41,7 @@ export declare class SelectorController {
41
41
  handleResumeSession(sessionPath: string): Promise<void>;
42
42
  handleSessionDeleteCommand(): Promise<void>;
43
43
  showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
44
+ showResetUsageSelector(): Promise<void>;
44
45
  showDebugSelector(): Promise<void>;
45
46
  showAgentHub(observers: SessionObserverRegistry): void;
46
47
  }
@@ -27,6 +27,7 @@ import type { ToolExecutionHandle } from "./components/tool-execution";
27
27
  import { TranscriptContainer } from "./components/transcript-container";
28
28
  import { type LoopLimitRuntime } from "./loop-limit";
29
29
  import { OAuthManualInputManager } from "./oauth-manual-input";
30
+ import type { ObservableSession } from "./session-observer-registry";
30
31
  import type { Theme } from "./theme/theme";
31
32
  import type { CompactionQueuedMessage, InteractiveModeContext, InteractiveModeInitOptions, InteractiveSelectorDialogOptions, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
32
33
  /** Options for creating an InteractiveMode instance (for future API use) */
@@ -42,6 +43,13 @@ export interface InteractiveModeOptions {
42
43
  /** Additional initial messages to queue */
43
44
  initialMessages?: string[];
44
45
  }
46
+ /**
47
+ * Build the anchored subagent HUD block: a bold accent "Subagents" header plus
48
+ * one hooked row per running agent in the same `Id: description` shape the
49
+ * inline task rows use (muted task preview when no description was given).
50
+ * Returns an empty array when nothing is running so the container can clear.
51
+ */
52
+ export declare function renderSubagentHudLines(sessions: ObservableSession[], columns: number): string[];
45
53
  export declare class InteractiveMode implements InteractiveModeContext {
46
54
  #private;
47
55
  session: AgentSession;
@@ -56,6 +64,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
56
64
  pendingMessagesContainer: Container;
57
65
  statusContainer: Container;
58
66
  todoContainer: Container;
67
+ subagentContainer: Container;
59
68
  btwContainer: Container;
60
69
  omfgContainer: Container;
61
70
  errorBannerContainer: Container;
@@ -265,6 +274,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
265
274
  handleResumeSession(sessionPath: string): Promise<void>;
266
275
  handleSessionDeleteCommand(): Promise<void>;
267
276
  showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
277
+ showResetUsageSelector(): Promise<void>;
268
278
  showProviderSetup(): Promise<void>;
269
279
  showHookConfirm(title: string, message: string): Promise<boolean>;
270
280
  handleCtrlC(): void;
@@ -8,6 +8,8 @@ export interface ObservableSession {
8
8
  description?: string;
9
9
  status: "active" | "completed" | "failed" | "aborted";
10
10
  sessionFile?: string;
11
+ parentToolCallId?: string;
12
+ index?: number;
11
13
  lastUpdate: number;
12
14
  /** Latest progress snapshot from the subagent executor */
13
15
  progress?: AgentProgress;
@@ -66,6 +66,7 @@ export interface InteractiveModeContext {
66
66
  pendingMessagesContainer: Container;
67
67
  statusContainer: Container;
68
68
  todoContainer: Container;
69
+ subagentContainer: Container;
69
70
  btwContainer: Container;
70
71
  omfgContainer: Container;
71
72
  errorBannerContainer: Container;
@@ -264,6 +265,7 @@ export interface InteractiveModeContext {
264
265
  handleResumeSession(sessionPath: string): Promise<void>;
265
266
  handleSessionDeleteCommand(): Promise<void>;
266
267
  showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void>;
268
+ showResetUsageSelector(): Promise<void>;
267
269
  showProviderSetup(): Promise<void>;
268
270
  showHookConfirm(title: string, message: string): Promise<boolean>;
269
271
  showDebugSelector(): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import type { Model } from "@oh-my-pi/pi-ai";
2
2
  import type { Skill } from "../../extensibility/skills";
3
3
  import type { AgentSession } from "../../session/agent-session";
4
+ import { type SnapcompactSavingsEstimate } from "../../session/snapcompact-inline";
4
5
  import type { Tool } from "../../tools";
5
6
  import type { theme as Theme } from "../theme/theme";
6
7
  type CategoryId = "systemPrompt" | "systemContext" | "systemTools" | "skills" | "messages";
@@ -18,6 +19,8 @@ export interface ContextBreakdown {
18
19
  usedTokens: number;
19
20
  autoCompactBufferTokens: number;
20
21
  freeTokens: number;
22
+ /** Estimated snapcompact wire savings; set when requested and a snapcompact.* setting is enabled. */
23
+ snapcompact?: SnapcompactSavingsEstimate;
21
24
  }
22
25
  export declare function estimateSkillsTokens(skills: readonly Skill[]): number;
23
26
  export declare function estimateToolSchemaTokens(tools: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>): number;
@@ -38,7 +41,9 @@ export declare function computeNonMessageTokens(session: AgentSession): number;
38
41
  * Compute a breakdown of estimated context usage by category for the active
39
42
  * session and model.
40
43
  */
41
- export declare function computeContextBreakdown(session: AgentSession): ContextBreakdown;
44
+ export declare function computeContextBreakdown(session: AgentSession, options?: {
45
+ snapcompactSavings?: boolean;
46
+ }): ContextBreakdown;
42
47
  /**
43
48
  * Render a colorful context-usage panel as ANSI text. Output is a series of
44
49
  * lines pairing the grid (left) with the legend (right).
@@ -15,7 +15,7 @@
15
15
  import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
16
16
  import { type Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, ThinkingLevel } 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, ServiceTier, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@oh-my-pi/pi-ai";
18
+ import type { AssistantMessage, 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";
@@ -1052,6 +1052,19 @@ export declare class AgentSession {
1052
1052
  contextWindow?: number;
1053
1053
  }): ContextUsage | undefined;
1054
1054
  fetchUsageReports(signal?: AbortSignal): Promise<UsageReport[] | null>;
1055
+ /**
1056
+ * Redeem one saved Codex rate-limit reset for a specific account, injecting
1057
+ * the provider base URL like {@link AgentSession.fetchUsageReports}. Powers
1058
+ * the `/usage reset` command and auto-redeem. Never throws for business
1059
+ * outcomes — inspect the returned `code`.
1060
+ */
1061
+ redeemResetCredit(target: ResetCreditTarget, signal?: AbortSignal): Promise<ResetCreditRedeemOutcome>;
1062
+ /**
1063
+ * List saved Codex rate-limit resets per stored account, fetched live from
1064
+ * the dedicated credits endpoint (bypasses the usage cache). Powers the
1065
+ * `/usage reset` account selector.
1066
+ */
1067
+ listResetCredits(signal?: AbortSignal): Promise<ResetCreditAccountStatus[]>;
1055
1068
  /**
1056
1069
  * Export session to HTML.
1057
1070
  * @param outputPath Optional output path (defaults to session directory)
@@ -2,5 +2,5 @@
2
2
  * Re-exports from @oh-my-pi/pi-ai.
3
3
  * All credential storage types and the AuthStorage class now live in the ai package.
4
4
  */
5
- export type { ApiKeyCredential, AuthCredential, AuthCredentialEntry, AuthCredentialStore, AuthStorageData, AuthStorageOptions, CredentialOrigin, CredentialOriginKind, OAuthAccountIdentity, OAuthCredential, SerializedAuthStorage, SnapshotResponse, StoredAuthCredential, } from "@oh-my-pi/pi-ai";
5
+ export type { ApiKeyCredential, AuthCredential, AuthCredentialEntry, AuthCredentialStore, AuthStorageData, AuthStorageOptions, CredentialOrigin, CredentialOriginKind, OAuthAccountIdentity, OAuthCredential, ResetCreditAccountStatus, ResetCreditRedeemOutcome, ResetCreditTarget, SerializedAuthStorage, SnapshotResponse, StoredAuthCredential, } from "@oh-my-pi/pi-ai";
6
6
  export { AuthBrokerClient, AuthStorage, DEFAULT_SNAPSHOT_CACHE_TTL_MS, REMOTE_REFRESH_SENTINEL, RemoteAuthCredentialStore, readAuthBrokerSnapshotCache, SqliteAuthCredentialStore, writeAuthBrokerSnapshotCache, } from "@oh-my-pi/pi-ai";
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Pure decision predicate for auto-redeeming a saved OpenAI Codex rate-limit
3
+ * reset, plus the process-wide coordinator that serializes attempts.
4
+ *
5
+ * WHY THIS IS REACTIVE-ONLY (never proactive):
6
+ * The only trustworthy "blocked right now" signal is a live 429 /
7
+ * `usage_limit_reached` from a request authenticated as the session's active
8
+ * Codex credential. The session hook calls this predicate from the usage-limit
9
+ * branch of the retry pipeline, *after* free remedies (sibling-account switch)
10
+ * fail and *before* model fallback. A proactive surface (the status-line usage
11
+ * poll) cannot be used: at `used_percent < 100` the account is not actually
12
+ * limited, so redeeming would be a credit-wasting no-op; at exactly 100 the
13
+ * user may be idle, so the freshly-reset weekly window would tick away with
14
+ * nobody working. Saved resets are a scarce, ~monthly, effectively
15
+ * irreversible resource — every gate here is biased to precision over recall:
16
+ * we would rather miss a redeem than waste a credit.
17
+ *
18
+ * THE DECISION-2 TRAP (status MUST NOT be used to find the blocker):
19
+ * `openai-codex.ts` applies the top-level `rate_limit.limit_reached` flag to
20
+ * BOTH the primary (5h) and secondary (weekly) `buildUsageLimit` calls, so when
21
+ * an account is blocked, *both* limit entries carry `status: "exhausted"`
22
+ * regardless of which window is actually at 100%. Only `amount.usedFraction`
23
+ * disambiguates which window is the real blocker. This module therefore keys
24
+ * eligibility off exact limit ids (`openai-codex:primary` /
25
+ * `openai-codex:secondary`) and `usedFraction`, never off `status`.
26
+ *
27
+ * ANTI-WASTE GATES (in evaluation order): the policy must be OFF unless opted
28
+ * in; the active model must be Codex (not Spark — a Spark block lives on a
29
+ * separate meter and it is unknown whether a credit even resets it); a fresh
30
+ * usage report for the active account must confirm `limitReached`; the WEEKLY
31
+ * (secondary) window must be genuinely exhausted — a 5h-only block self-heals
32
+ * within the hour, so a credit spent there buys nothing; the natural reset must be far
33
+ * enough away to justify spending a ~30-day credit yet within one plausible
34
+ * window length; a credit must be verifiably available above the reserve; and
35
+ * the same block episode must not have been attempted already (debounce +
36
+ * per-account cooldown). All of this is pure — no fetches, no IO. The only
37
+ * stateful piece is the {@link CodexAutoRedeemCoordinator} container, whose
38
+ * read-only views are passed in so the predicate itself stays deterministic.
39
+ */
40
+ import type { OAuthAccountIdentity, ResetCreditTarget, UsageReport } from "@oh-my-pi/pi-ai";
41
+ /** Weekly window counts as exhausted at `usedFraction >= 0.999` (used_percent >= 99.9). */
42
+ export declare const WEEKLY_EXHAUSTED_MIN_FRACTION = 0.999;
43
+ /** A weekly reset can never be more than one window length (7d) away; +1h slack for skew. */
44
+ export declare const MAX_PLAUSIBLE_REMAINING_MS: number;
45
+ /** Report must be no older than the 5-min usage cache TTL plus slack. */
46
+ export declare const REPORT_FRESHNESS_MS: number;
47
+ /** Per-account cooldown that catches blockKey drift across a minute boundary. */
48
+ export declare const ATTEMPT_COOLDOWN_MS = 60000;
49
+ /** Minute bucket for blockKey, absorbing `reset_after_seconds`-derived jitter. */
50
+ export declare const DEBOUNCE_BUCKET_MS = 60000;
51
+ export type CodexAutoRedeemSkipReason = "disabled" | "wrong-provider" | "spark-model" | "no-identity" | "no-report" | "stale-report" | "not-limit-reached" | "weekly-not-exhausted" | "no-reset-time" | "reset-too-soon" | "reset-implausible" | "credits-unknown" | "reserve" | "already-attempted" | "cooldown";
52
+ export interface CodexAutoRedeemInput {
53
+ nowMs: number;
54
+ /** `this.model.provider`. */
55
+ provider: string;
56
+ /** `this.model.id`. */
57
+ modelId: string;
58
+ settings: {
59
+ autoRedeem: boolean;
60
+ minBlockedMinutes: number;
61
+ keepCredits: number;
62
+ };
63
+ /** `getOAuthAccountIdentity("openai-codex", sessionId)`, captured at hook entry before any await. */
64
+ identity: OAuthAccountIdentity | undefined;
65
+ /** `session.fetchUsageReports()` (≤5-min cache). */
66
+ reports: UsageReport[] | null;
67
+ attemptedBlockKeys: ReadonlySet<string>;
68
+ lastAttemptAtByAccount: ReadonlyMap<string, number>;
69
+ }
70
+ export type CodexAutoRedeemDecision = {
71
+ redeem: true;
72
+ target: ResetCreditTarget;
73
+ accountKey: string;
74
+ blockKey: string;
75
+ weeklyResetAtMs: number;
76
+ remainingMs: number;
77
+ availableCount: number;
78
+ } | {
79
+ redeem: false;
80
+ reason: CodexAutoRedeemSkipReason;
81
+ };
82
+ /**
83
+ * Decide whether to auto-redeem a saved Codex reset for the active account.
84
+ *
85
+ * Pure: every gate below is a function of the snapshot inputs only. Order
86
+ * matters — cheapest / most-decisive gates first so the common "not eligible"
87
+ * paths short-circuit before any account/report matching.
88
+ */
89
+ export declare function evaluateCodexAutoRedeem(input: CodexAutoRedeemInput): CodexAutoRedeemDecision;
90
+ /**
91
+ * Process-wide (NOT per-session) coordinator state. Parallel subagent sessions
92
+ * share the same Codex accounts and must not race a double-spend, so this is a
93
+ * single shared container, not a per-session field.
94
+ *
95
+ * - `attemptedBlockKeys`: one attempt EVER per block episode, regardless of
96
+ * outcome — recorded before calling the consume so exceptions can't re-enter.
97
+ * - `lastAttemptAtByAccount`: per-account cooldown timestamps (epoch ms),
98
+ * catching blockKey drift across a minute boundary.
99
+ * - `inFlightByAccount`: serializes per account — a second session for the same
100
+ * account adopts the in-flight promise instead of starting a second consume.
101
+ */
102
+ export interface CodexAutoRedeemCoordinator {
103
+ attemptedBlockKeys: Set<string>;
104
+ lastAttemptAtByAccount: Map<string, number>;
105
+ inFlightByAccount: Map<string, Promise<boolean>>;
106
+ }
107
+ export declare const defaultCodexAutoRedeemCoordinator: CodexAutoRedeemCoordinator;
@@ -1,20 +1,121 @@
1
1
  /**
2
2
  * Snapcompact inline imaging: per-request transform that swaps the system
3
- * prompt and/or large historical tool results for dense PNG frames on
4
- * vision-capable models.
5
- *
3
+ * prompt, loaded context-file instructions, and/or large historical tool
4
+ * results for dense PNG frames on vision-capable models.
6
5
  * Runs inside the agent loop's `transformProviderContext` hook — after the
7
6
  * persisted history is converted to the outgoing `Context`, before the
8
7
  * provider stream call. It only ever builds NEW message objects/arrays; the
9
8
  * input context shares `content` array references with the persisted
10
9
  * `SessionMessageEntry` messages, so mutation would leak rendered images
11
10
  * into session.jsonl.
11
+ *
12
+ * The swap policy (budget, savings gate, skip rules) lives in
13
+ * `planInlineSwaps`, shared by the transform and the `/context` savings
14
+ * estimate (`estimateInlineSavings`) so the two can never disagree.
12
15
  */
13
16
  import type { Context, Model } from "@oh-my-pi/pi-ai";
17
+ import * as snapcompact from "@oh-my-pi/snapcompact";
18
+ export type SnapcompactSystemPromptMode = "none" | "agents-md" | "all";
14
19
  export interface SnapcompactInlineOptions {
15
- renderSystemPrompt: boolean;
20
+ renderSystemPrompt: SnapcompactSystemPromptMode;
16
21
  renderToolResults: boolean;
17
22
  }
23
+ /** Tool-result swap candidate, in context order. */
24
+ export interface InlineToolResultCandidate {
25
+ /** toolCallId — stable identity for render caching and application. */
26
+ id: string;
27
+ /** Token count of the joined text blocks (0 when empty or image-carrying). */
28
+ textTokens: number;
29
+ /** Frames needed to render the text (0 = empty or below the token floor). */
30
+ frames: number;
31
+ /** Already carries an image (screenshot etc.) — never re-imaged. */
32
+ hasImage: boolean;
33
+ }
34
+ export interface InlineSystemPromptCandidate {
35
+ textTokens: number;
36
+ frames: number;
37
+ }
38
+ export interface InlinePlanInput {
39
+ options: SnapcompactInlineOptions;
40
+ shape: snapcompact.Shape;
41
+ /** Provider image-count budget minus images already present in the context. */
42
+ budget: number;
43
+ /** All tool results in context order, INCLUDING the most recent one. */
44
+ toolResults: readonly InlineToolResultCandidate[];
45
+ /** Selected prompt text; undefined when system-prompt imaging is off or empty. */
46
+ systemPrompt: InlineSystemPromptCandidate | undefined;
47
+ /** Whether a user message exists to carry the prompt frames. */
48
+ hasUserMessage: boolean;
49
+ }
50
+ export interface InlineSwapPlan {
51
+ /** Tool results to swap, oldest first. */
52
+ toolResults: Array<{
53
+ id: string;
54
+ textTokens: number;
55
+ frames: number;
56
+ }>;
57
+ /** Set when the system prompt should swap to frames (uses leftover budget). */
58
+ systemPrompt: InlineSystemPromptCandidate | undefined;
59
+ }
60
+ /**
61
+ * Decide which content gets swapped for frames. Pure — the same rules drive
62
+ * the provider-request transform and the /context savings estimate.
63
+ */
64
+ export declare function planInlineSwaps(input: InlinePlanInput): InlineSwapPlan;
65
+ /**
66
+ * Minimal structural view of a history message — both pi-ai `Message`s (the
67
+ * outgoing context) and agent-core `AgentMessage`s (the live session) satisfy
68
+ * it, so the estimator can read session state without conversion.
69
+ */
70
+ export interface InlineMessageView {
71
+ role: string;
72
+ toolCallId?: string;
73
+ content?: unknown;
74
+ }
75
+ export interface SnapcompactSavingsEstimate {
76
+ /** Frames only ship on models that accept image input. */
77
+ visionCapable: boolean;
78
+ /** Present iff system-prompt imaging is enabled. */
79
+ systemPrompt?: {
80
+ applied: boolean;
81
+ /** Why the prompt stays text when `applied` is false. */
82
+ reason?: "empty" | "margin" | "budget";
83
+ textTokens: number;
84
+ frames: number;
85
+ /** Estimated billed tokens for the frames (0 when there are none). */
86
+ imageTokens: number;
87
+ savedTokens: number;
88
+ scope: Exclude<SnapcompactSystemPromptMode, "none">;
89
+ };
90
+ /** Present iff tool-result imaging is enabled. */
91
+ toolResults?: {
92
+ /** Tool results currently in history. */
93
+ total: number;
94
+ swapped: number;
95
+ /** Text tokens of the swapped results only. */
96
+ textTokens: number;
97
+ frames: number;
98
+ imageTokens: number;
99
+ savedTokens: number;
100
+ };
101
+ /** Net estimated wire savings for the next request. */
102
+ savedTokens: number;
103
+ }
104
+ /**
105
+ * Estimate what `SnapcompactInlineTransformer.transform` would save on the
106
+ * NEXT request, given the session's live system prompt and message history.
107
+ *
108
+ * Mirrors the transform exactly via `planInlineSwaps`, with one deliberate
109
+ * difference: `hasUserMessage` is assumed true, because the request being
110
+ * estimated is always triggered by a user prompt — even when the current
111
+ * history is still empty.
112
+ */
113
+ export declare function estimateInlineSavings(input: {
114
+ options: SnapcompactInlineOptions;
115
+ model: Model | undefined;
116
+ systemPrompt: readonly string[];
117
+ messages: readonly InlineMessageView[];
118
+ }): SnapcompactSavingsEstimate;
18
119
  /**
19
120
  * Stateless with respect to the model (passed per call, so mid-session model
20
121
  * switches re-resolve shape and budget); stateful only for the render caches,
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared helpers for the `/usage reset` command (TUI selector + ACP): turn the
3
+ * live per-account reset-credit status into selector rows, and map a redeem
4
+ * outcome code to a human message.
5
+ */
6
+ import type { ResetCreditAccountStatus, ResetCreditRedeemOutcome, ResetCreditTarget } from "../../session/auth-storage";
7
+ export declare const CODEX_PROVIDER_ID = "openai-codex";
8
+ /** One Codex account row for the reset-usage selector. */
9
+ export interface ResetUsageAccount {
10
+ /** Display label (email, else account id). */
11
+ label: string;
12
+ /** Saved resets redeemable for this account right now. */
13
+ availableCount: number;
14
+ /** Identifies the account when redeeming. */
15
+ target: ResetCreditTarget;
16
+ /** Whether this is the session's active Codex account. */
17
+ active: boolean;
18
+ /** Set when this account could not be reached (token/list failure). */
19
+ error?: string;
20
+ }
21
+ /**
22
+ * Map live per-account reset status to selector rows. Sorted with the active
23
+ * account first, then most-credits, then label.
24
+ */
25
+ export declare function toResetUsageAccounts(statuses: ResetCreditAccountStatus[]): ResetUsageAccount[];
26
+ /** Human-facing summary of a redeem outcome for status lines and ACP output. */
27
+ export declare function describeRedeemOutcome(outcome: ResetCreditRedeemOutcome, label: string): string;
@@ -15,6 +15,7 @@ interface TaskRenderContext {
15
15
  type TaskRenderOptions = RenderResultOptions & {
16
16
  renderContext?: TaskRenderContext;
17
17
  };
18
+ export declare function formatTaskId(id: string): string;
18
19
  /**
19
20
  * Render the tool call arguments.
20
21
  */
@@ -9,13 +9,6 @@ export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
9
9
  export interface TodoItem {
10
10
  content: string;
11
11
  status: TodoStatus;
12
- /**
13
- * Append-only list of freeform notes attached by `op: "note"`.
14
- * Each element is one note and may itself be multi-line.
15
- * Rendered as text only when the task is in_progress; otherwise shown as a
16
- * dim marker indicating the task has notes.
17
- */
18
- notes?: string[];
19
12
  }
20
13
  export interface TodoPhase {
21
14
  name: string;
@@ -37,7 +30,6 @@ declare const todoSchema: z.ZodObject<{
37
30
  done: "done";
38
31
  drop: "drop";
39
32
  init: "init";
40
- note: "note";
41
33
  rm: "rm";
42
34
  start: "start";
43
35
  view: "view";
@@ -49,7 +41,6 @@ declare const todoSchema: z.ZodObject<{
49
41
  task: z.ZodOptional<z.ZodString>;
50
42
  phase: z.ZodOptional<z.ZodString>;
51
43
  items: z.ZodOptional<z.ZodArray<z.ZodString>>;
52
- text: z.ZodOptional<z.ZodString>;
53
44
  }, z.core.$strip>>;
54
45
  }, z.core.$strip>;
55
46
  type TodoParams = z.infer<typeof todoSchema>;
@@ -111,7 +102,6 @@ export declare class TodoTool implements AgentTool<typeof todoSchema, TodoToolDe
111
102
  done: "done";
112
103
  drop: "drop";
113
104
  init: "init";
114
- note: "note";
115
105
  rm: "rm";
116
106
  start: "start";
117
107
  view: "view";
@@ -123,7 +113,6 @@ export declare class TodoTool implements AgentTool<typeof todoSchema, TodoToolDe
123
113
  task: z.ZodOptional<z.ZodString>;
124
114
  phase: z.ZodOptional<z.ZodString>;
125
115
  items: z.ZodOptional<z.ZodArray<z.ZodString>>;
126
- text: z.ZodOptional<z.ZodString>;
127
116
  }, z.core.$strip>>;
128
117
  }, z.core.$strip>;
129
118
  readonly concurrency = "exclusive";