@oh-my-pi/pi-coding-agent 15.11.3 → 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.
- package/CHANGELOG.md +107 -0
- package/dist/cli.js +692 -607
- package/dist/types/cli/usage-cli.d.ts +10 -1
- package/dist/types/commands/usage.d.ts +9 -0
- package/dist/types/config/api-key-resolver.d.ts +9 -3
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-discovery.d.ts +6 -4
- package/dist/types/config/model-registry.d.ts +7 -4
- package/dist/types/config/settings-schema.d.ts +508 -155
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/mnemopi/config.d.ts +3 -1
- package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/settings-defs.d.ts +9 -2
- package/dist/types/modes/components/settings-selector.d.ts +9 -4
- package/dist/types/modes/components/tool-execution.d.ts +26 -1
- package/dist/types/modes/components/transcript-container.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +9 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +23 -3
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/context-usage.d.ts +6 -1
- package/dist/types/session/agent-session.d.ts +28 -8
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/codex-auto-reset.d.ts +107 -0
- package/dist/types/session/snapcompact-inline.d.ts +129 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
- package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
- package/dist/types/system-prompt.d.ts +3 -1
- package/dist/types/task/render.d.ts +17 -6
- package/dist/types/tools/gh.d.ts +3 -0
- package/dist/types/tools/render-utils.d.ts +8 -16
- package/dist/types/tools/todo.d.ts +0 -11
- package/dist/types/utils/session-color.d.ts +15 -3
- package/dist/types/web/kagi.d.ts +1 -2
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +9 -6
- package/package.json +11 -11
- package/src/auto-thinking/classifier.ts +1 -5
- package/src/cli/usage-cli.ts +187 -16
- package/src/commands/usage.ts +8 -0
- package/src/commit/model-selection.ts +3 -6
- package/src/config/api-key-resolver.ts +10 -3
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-discovery.ts +60 -46
- package/src/config/model-registry.ts +21 -8
- package/src/config/model-resolver.ts +57 -3
- package/src/config/settings-schema.ts +654 -153
- package/src/config/settings.ts +9 -0
- package/src/eval/completion-bridge.ts +1 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +13 -6
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/issue-pr-protocol.ts +10 -4
- package/src/memories/index.ts +2 -10
- package/src/mnemopi/backend.ts +30 -8
- package/src/mnemopi/config.ts +6 -1
- package/src/mnemopi/state.ts +6 -0
- package/src/modes/components/extensions/inspector-panel.ts +6 -2
- package/src/modes/components/plan-review-overlay.ts +15 -17
- package/src/modes/components/plugin-settings.ts +22 -5
- package/src/modes/components/reset-usage-selector.ts +161 -0
- package/src/modes/components/session-selector.ts +8 -2
- package/src/modes/components/settings-defs.ts +19 -4
- package/src/modes/components/settings-selector.ts +510 -95
- package/src/modes/components/status-line/component.ts +3 -1
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tool-execution.ts +87 -12
- package/src/modes/components/transcript-container.ts +49 -1
- package/src/modes/components/tree-selector.ts +16 -6
- package/src/modes/controllers/command-controller.ts +61 -8
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +68 -6
- package/src/modes/controllers/selector-controller.ts +149 -61
- package/src/modes/interactive-mode.ts +63 -2
- package/src/modes/rpc/rpc-mode.ts +2 -1
- package/src/modes/session-observer-registry.ts +61 -3
- package/src/modes/shared.ts +2 -0
- package/src/modes/theme/theme.ts +102 -9
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/context-usage.ts +78 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +9 -5
- package/src/prompts/system/personalities/default.md +26 -0
- package/src/prompts/system/personalities/friendly.md +17 -0
- package/src/prompts/system/personalities/pragmatic.md +15 -0
- package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-context-stub.md +1 -0
- package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-system-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
- package/src/prompts/system/system-prompt.md +5 -22
- package/src/prompts/tools/browser.md +33 -43
- package/src/prompts/tools/eval.md +27 -50
- package/src/prompts/tools/irc.md +29 -31
- package/src/prompts/tools/read.md +31 -37
- package/src/prompts/tools/task.md +3 -3
- package/src/prompts/tools/todo.md +1 -2
- package/src/sdk.ts +23 -1
- package/src/session/agent-session.ts +221 -29
- package/src/session/auth-storage.ts +4 -0
- package/src/session/codex-auto-reset.ts +190 -0
- package/src/session/session-dump-format.ts +8 -1
- package/src/session/session-manager.ts +5 -5
- package/src/session/snapcompact-inline.ts +524 -0
- package/src/slash-commands/builtin-registry.ts +145 -8
- package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
- package/src/slash-commands/helpers/context-report.ts +28 -1
- package/src/slash-commands/helpers/reset-usage.ts +66 -0
- package/src/slash-commands/helpers/usage-report.ts +36 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/index.ts +30 -7
- package/src/task/render.ts +57 -32
- package/src/tool-discovery/tool-index.ts +2 -0
- package/src/tools/bash.ts +10 -3
- package/src/tools/eval-render.ts +13 -8
- package/src/tools/gh.ts +39 -1
- package/src/tools/image-gen.ts +114 -78
- package/src/tools/inspect-image.ts +1 -5
- package/src/tools/job.ts +25 -5
- package/src/tools/read.ts +1 -57
- package/src/tools/render-utils.ts +29 -31
- package/src/tools/ssh.ts +3 -3
- package/src/tools/todo.ts +8 -128
- package/src/tools/tts.ts +40 -20
- package/src/utils/clipboard.ts +56 -4
- package/src/utils/commit-message-generator.ts +1 -5
- package/src/utils/session-color.ts +83 -9
- package/src/utils/title-generator.ts +1 -1
- package/src/web/kagi.ts +26 -27
- package/src/web/search/providers/codex.ts +42 -40
- package/src/web/search/providers/gemini.ts +42 -22
- package/src/web/search/providers/perplexity.ts +22 -10
|
@@ -73,6 +73,9 @@ import type {
|
|
|
73
73
|
Model,
|
|
74
74
|
ProviderResponseMetadata,
|
|
75
75
|
ProviderSessionState,
|
|
76
|
+
ResetCreditAccountStatus,
|
|
77
|
+
ResetCreditRedeemOutcome,
|
|
78
|
+
ResetCreditTarget,
|
|
76
79
|
ServiceTier,
|
|
77
80
|
SimpleStreamOptions,
|
|
78
81
|
TextContent,
|
|
@@ -107,7 +110,7 @@ import {
|
|
|
107
110
|
relativePathWithinRoot,
|
|
108
111
|
Snowflake,
|
|
109
112
|
} from "@oh-my-pi/pi-utils";
|
|
110
|
-
import
|
|
113
|
+
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
111
114
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
112
115
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
113
116
|
import { reset as resetCapabilities } from "../capability";
|
|
@@ -237,6 +240,7 @@ import { normalizeModelContextImages } from "../utils/image-loading";
|
|
|
237
240
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
238
241
|
import type { AuthStorage } from "./auth-storage";
|
|
239
242
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
243
|
+
import { defaultCodexAutoRedeemCoordinator, evaluateCodexAutoRedeem } from "./codex-auto-reset";
|
|
240
244
|
import {
|
|
241
245
|
type BashExecutionMessage,
|
|
242
246
|
type CustomMessage,
|
|
@@ -855,8 +859,13 @@ function extractPermissionLocations(
|
|
|
855
859
|
* `tag` is set only by `enqueueCustomMessageDisplay` (used for skill-prompt
|
|
856
860
|
* custom messages queued during streaming) and is matched by the custom-role
|
|
857
861
|
* `message_start` dequeue branch; user-message pushes leave it undefined and
|
|
858
|
-
* rely on the existing text-equality match.
|
|
859
|
-
|
|
862
|
+
* rely on the existing text-equality match. `images` carries the original
|
|
863
|
+
* (pre-normalization) image blocks so queue restoration (Esc / Alt+Up) can
|
|
864
|
+
* hand them back to the editor instead of dropping them. */
|
|
865
|
+
type QueuedDisplayEntry = { text: string; tag?: string; images?: ImageContent[] };
|
|
866
|
+
|
|
867
|
+
/** Entry returned by {@link AgentSession.clearQueue} / {@link AgentSession.popLastQueuedMessage}. */
|
|
868
|
+
export type RestoredQueuedMessage = { text: string; images?: ImageContent[] };
|
|
860
869
|
|
|
861
870
|
export class AgentSession {
|
|
862
871
|
readonly agent: Agent;
|
|
@@ -5028,7 +5037,7 @@ export class AgentSession {
|
|
|
5028
5037
|
async #queueSteer(text: string, images?: ImageContent[]): Promise<void> {
|
|
5029
5038
|
const normalizedImages = await normalizeModelContextImages(images);
|
|
5030
5039
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
5031
|
-
this.#steeringMessages.push({ text: displayText });
|
|
5040
|
+
this.#steeringMessages.push({ text: displayText, images });
|
|
5032
5041
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
5033
5042
|
if (normalizedImages && normalizedImages.length > 0) {
|
|
5034
5043
|
content.push(...normalizedImages);
|
|
@@ -5040,6 +5049,16 @@ export class AgentSession {
|
|
|
5040
5049
|
attribution: "user",
|
|
5041
5050
|
timestamp: Date.now(),
|
|
5042
5051
|
});
|
|
5052
|
+
// A steer can land on an idle session: the caller checked isStreaming
|
|
5053
|
+
// before the (potentially slow) image normalization above, so the turn
|
|
5054
|
+
// may have ended in between. Without a drain the message would strand in
|
|
5055
|
+
// the queue until the next manual prompt — schedule an immediate continue,
|
|
5056
|
+
// mirroring #queueFollowUp's idle-path delivery.
|
|
5057
|
+
if (this.#canAutoContinueForFollowUp()) {
|
|
5058
|
+
this.#scheduleAgentContinue({
|
|
5059
|
+
shouldContinue: () => this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages(),
|
|
5060
|
+
});
|
|
5061
|
+
}
|
|
5043
5062
|
}
|
|
5044
5063
|
|
|
5045
5064
|
/**
|
|
@@ -5048,7 +5067,7 @@ export class AgentSession {
|
|
|
5048
5067
|
async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
|
|
5049
5068
|
const normalizedImages = await normalizeModelContextImages(images);
|
|
5050
5069
|
const displayText = text || (images && images.length > 0 ? "[Image]" : "");
|
|
5051
|
-
this.#followUpMessages.push({ text: displayText });
|
|
5070
|
+
this.#followUpMessages.push({ text: displayText, images });
|
|
5052
5071
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
5053
5072
|
if (normalizedImages && normalizedImages.length > 0) {
|
|
5054
5073
|
content.push(...normalizedImages);
|
|
@@ -5297,12 +5316,14 @@ export class AgentSession {
|
|
|
5297
5316
|
}
|
|
5298
5317
|
|
|
5299
5318
|
/**
|
|
5300
|
-
* Clear queued messages and return them.
|
|
5301
|
-
* Useful for restoring to editor when user aborts.
|
|
5319
|
+
* Clear queued messages and return them (text plus any attached images).
|
|
5320
|
+
* Useful for restoring to editor when user aborts. The internal entry
|
|
5321
|
+
* arrays are handed out as-is — a `tag` (if any) is inert once the record
|
|
5322
|
+
* leaves the queue.
|
|
5302
5323
|
*/
|
|
5303
|
-
clearQueue(): { steering:
|
|
5304
|
-
const steering = this.#steeringMessages
|
|
5305
|
-
const followUp = this.#followUpMessages
|
|
5324
|
+
clearQueue(): { steering: RestoredQueuedMessage[]; followUp: RestoredQueuedMessage[] } {
|
|
5325
|
+
const steering = this.#steeringMessages;
|
|
5326
|
+
const followUp = this.#followUpMessages;
|
|
5306
5327
|
this.#steeringMessages = [];
|
|
5307
5328
|
this.#followUpMessages = [];
|
|
5308
5329
|
this.agent.clearAllQueues();
|
|
@@ -5328,21 +5349,21 @@ export class AgentSession {
|
|
|
5328
5349
|
/**
|
|
5329
5350
|
* Pop the last queued message (steering first, then follow-up).
|
|
5330
5351
|
* Used by dequeue keybinding to restore messages to editor one at a time.
|
|
5331
|
-
* Returns the popped entry's
|
|
5332
|
-
* record — no orphan state can outlive the queue entry.
|
|
5352
|
+
* Returns the popped entry's text and images; the tag (if any) dies with
|
|
5353
|
+
* the record — no orphan state can outlive the queue entry.
|
|
5333
5354
|
*/
|
|
5334
|
-
popLastQueuedMessage():
|
|
5355
|
+
popLastQueuedMessage(): RestoredQueuedMessage | undefined {
|
|
5335
5356
|
// Pop from steering first (LIFO)
|
|
5336
5357
|
if (this.#steeringMessages.length > 0) {
|
|
5337
5358
|
const entry = this.#steeringMessages.pop();
|
|
5338
5359
|
this.agent.popLastSteer();
|
|
5339
|
-
return entry
|
|
5360
|
+
return entry;
|
|
5340
5361
|
}
|
|
5341
5362
|
// Then from follow-up
|
|
5342
5363
|
if (this.#followUpMessages.length > 0) {
|
|
5343
5364
|
const entry = this.#followUpMessages.pop();
|
|
5344
5365
|
this.agent.popLastFollowUp();
|
|
5345
|
-
return entry
|
|
5366
|
+
return entry;
|
|
5346
5367
|
}
|
|
5347
5368
|
return undefined;
|
|
5348
5369
|
}
|
|
@@ -5382,11 +5403,7 @@ export class AgentSession {
|
|
|
5382
5403
|
#cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
|
|
5383
5404
|
return phases.map(phase => ({
|
|
5384
5405
|
name: phase.name,
|
|
5385
|
-
tasks: phase.tasks.map(task => {
|
|
5386
|
-
const out: TodoItem = { content: task.content, status: task.status };
|
|
5387
|
-
if (task.notes && task.notes.length > 0) out.notes = [...task.notes];
|
|
5388
|
-
return out;
|
|
5389
|
-
}),
|
|
5406
|
+
tasks: phase.tasks.map(task => ({ content: task.content, status: task.status })),
|
|
5390
5407
|
}));
|
|
5391
5408
|
}
|
|
5392
5409
|
|
|
@@ -6368,7 +6385,10 @@ export class AgentSession {
|
|
|
6368
6385
|
details = compactionPrep.details;
|
|
6369
6386
|
preserveData = compactionPrep.preserveData;
|
|
6370
6387
|
} else if (snapcompactReady) {
|
|
6371
|
-
const snapcompactResult = await
|
|
6388
|
+
const snapcompactResult = await snapcompact.compact(preparation, {
|
|
6389
|
+
convertToLlm,
|
|
6390
|
+
model: this.model,
|
|
6391
|
+
});
|
|
6372
6392
|
summary = snapcompactResult.summary;
|
|
6373
6393
|
shortSummary = snapcompactResult.shortSummary;
|
|
6374
6394
|
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
@@ -6582,7 +6602,7 @@ export class AgentSession {
|
|
|
6582
6602
|
const rawHandoffText = await generateHandoff(
|
|
6583
6603
|
this.agent.state.messages,
|
|
6584
6604
|
model,
|
|
6585
|
-
|
|
6605
|
+
this.#modelRegistry.resolver(model, this.sessionId),
|
|
6586
6606
|
{
|
|
6587
6607
|
systemPrompt: this.#obfuscateForProvider(this.#baseSystemPrompt),
|
|
6588
6608
|
tools: obfuscateProviderTools(this.#obfuscator, this.agent.state.tools),
|
|
@@ -7252,6 +7272,22 @@ export class AgentSession {
|
|
|
7252
7272
|
this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
|
|
7253
7273
|
}
|
|
7254
7274
|
|
|
7275
|
+
#resetCurrentResponsesProviderSession(reason: string): void {
|
|
7276
|
+
const currentModel = this.model;
|
|
7277
|
+
if (currentModel?.api !== "openai-responses" && currentModel?.api !== "openai-codex-responses") {
|
|
7278
|
+
return;
|
|
7279
|
+
}
|
|
7280
|
+
|
|
7281
|
+
this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
|
|
7282
|
+
this.agent.appendOnlyContext?.invalidateForModelChange();
|
|
7283
|
+
logger.debug("Reset Responses provider session after stale replay error", {
|
|
7284
|
+
provider: currentModel.provider,
|
|
7285
|
+
model: currentModel.id,
|
|
7286
|
+
api: currentModel.api,
|
|
7287
|
+
reason,
|
|
7288
|
+
});
|
|
7289
|
+
}
|
|
7290
|
+
|
|
7255
7291
|
/**
|
|
7256
7292
|
* Re-evaluate append-only context mode, creating or destroying the
|
|
7257
7293
|
* manager as needed. Called on model switch AND setting change.
|
|
@@ -7577,7 +7613,7 @@ export class AgentSession {
|
|
|
7577
7613
|
return await compact(
|
|
7578
7614
|
this.#obfuscatePreparationForProvider(preparation),
|
|
7579
7615
|
candidate,
|
|
7580
|
-
|
|
7616
|
+
this.#modelRegistry.resolver(candidate, this.sessionId),
|
|
7581
7617
|
this.#obfuscateTextForProvider(customInstructions),
|
|
7582
7618
|
signal,
|
|
7583
7619
|
{
|
|
@@ -7882,7 +7918,10 @@ export class AgentSession {
|
|
|
7882
7918
|
} else if (action === "snapcompact") {
|
|
7883
7919
|
// Local, deterministic: render discarded history onto PNG frames.
|
|
7884
7920
|
// No model candidates, no API key, no retry loop.
|
|
7885
|
-
const snapcompactResult = await
|
|
7921
|
+
const snapcompactResult = await snapcompact.compact(preparation, {
|
|
7922
|
+
convertToLlm,
|
|
7923
|
+
model: this.model,
|
|
7924
|
+
});
|
|
7886
7925
|
summary = snapcompactResult.summary;
|
|
7887
7926
|
shortSummary = snapcompactResult.shortSummary;
|
|
7888
7927
|
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
@@ -7906,7 +7945,7 @@ export class AgentSession {
|
|
|
7906
7945
|
compactResult = await compact(
|
|
7907
7946
|
this.#obfuscatePreparationForProvider(preparation),
|
|
7908
7947
|
candidate,
|
|
7909
|
-
|
|
7948
|
+
this.#modelRegistry.resolver(candidate, this.sessionId),
|
|
7910
7949
|
undefined,
|
|
7911
7950
|
autoCompactionSignal,
|
|
7912
7951
|
{
|
|
@@ -8293,11 +8332,33 @@ export class AgentSession {
|
|
|
8293
8332
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
8294
8333
|
|
|
8295
8334
|
if (this.#isClassifierRefusal(message)) return true;
|
|
8335
|
+
if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
|
|
8296
8336
|
|
|
8297
8337
|
const err = message.errorMessage;
|
|
8298
8338
|
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
8299
8339
|
}
|
|
8300
8340
|
|
|
8341
|
+
#isStaleOpenAIResponsesReplayError(message: AssistantMessage): boolean {
|
|
8342
|
+
const currentApi = this.model?.api;
|
|
8343
|
+
if (
|
|
8344
|
+
message.api !== "openai-responses" &&
|
|
8345
|
+
message.api !== "openai-codex-responses" &&
|
|
8346
|
+
currentApi !== "openai-responses" &&
|
|
8347
|
+
currentApi !== "openai-codex-responses"
|
|
8348
|
+
) {
|
|
8349
|
+
return false;
|
|
8350
|
+
}
|
|
8351
|
+
|
|
8352
|
+
const errorMessage = message.errorMessage;
|
|
8353
|
+
if (!errorMessage) return false;
|
|
8354
|
+
|
|
8355
|
+
return (
|
|
8356
|
+
/\bItem with id ['"][^'"]+['"] not found\.?/i.test(errorMessage) ||
|
|
8357
|
+
(/previous[ _]?response/i.test(errorMessage) &&
|
|
8358
|
+
/not[ _]?found|invalid|expired|stale|zero[ _-]?data[ _-]?retention/i.test(errorMessage))
|
|
8359
|
+
);
|
|
8360
|
+
}
|
|
8361
|
+
|
|
8301
8362
|
#isClassifierRefusal(message: AssistantMessage): boolean {
|
|
8302
8363
|
if (message.stopReason !== "error") return false;
|
|
8303
8364
|
const stopType = message.stopDetails?.type;
|
|
@@ -8631,15 +8692,22 @@ export class AgentSession {
|
|
|
8631
8692
|
}
|
|
8632
8693
|
|
|
8633
8694
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
8695
|
+
const staleOpenAIResponsesReplayError = this.#isStaleOpenAIResponsesReplayError(message);
|
|
8634
8696
|
const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
|
|
8635
|
-
let delayMs =
|
|
8697
|
+
let delayMs = staleOpenAIResponsesReplayError
|
|
8698
|
+
? 0
|
|
8699
|
+
: calculateRetryBackoffDelayMs(retrySettings.baseDelayMs, this.#retryAttempt);
|
|
8636
8700
|
let switchedCredential = false;
|
|
8637
8701
|
let switchedModel = false;
|
|
8638
8702
|
// Set when a usage-limit error pinned the wait to credential
|
|
8639
8703
|
// availability — suppresses the generic retry-after bump below.
|
|
8640
8704
|
let usageLimitWaitMs: number | undefined;
|
|
8641
8705
|
|
|
8642
|
-
if (
|
|
8706
|
+
if (staleOpenAIResponsesReplayError) {
|
|
8707
|
+
this.#resetCurrentResponsesProviderSession("stale replay error");
|
|
8708
|
+
}
|
|
8709
|
+
|
|
8710
|
+
if (this.model && !staleOpenAIResponsesReplayError && isUsageLimitError(errorMessage)) {
|
|
8643
8711
|
const retryAfterMs = parsedRetryAfterMs ?? calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
8644
8712
|
const outcome = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
8645
8713
|
this.model.provider,
|
|
@@ -8653,6 +8721,13 @@ export class AgentSession {
|
|
|
8653
8721
|
if (outcome.switched) {
|
|
8654
8722
|
switchedCredential = true;
|
|
8655
8723
|
delayMs = 0;
|
|
8724
|
+
} else if (await this.#maybeAutoRedeemCodexReset()) {
|
|
8725
|
+
// A live usage-limit 429 on the active Codex account, with a banked
|
|
8726
|
+
// reset and the opt-in setting on: spend the reset and retry
|
|
8727
|
+
// immediately instead of waiting out the window. Runs after the
|
|
8728
|
+
// free sibling-switch above and before model fallback below.
|
|
8729
|
+
switchedCredential = true;
|
|
8730
|
+
delayMs = 0;
|
|
8656
8731
|
} else {
|
|
8657
8732
|
// No sibling credential is usable right now. Wait for whichever
|
|
8658
8733
|
// comes first: the provider's retry-after window for the current
|
|
@@ -8676,7 +8751,7 @@ export class AgentSession {
|
|
|
8676
8751
|
}
|
|
8677
8752
|
|
|
8678
8753
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
8679
|
-
if (!switchedCredential && currentSelector) {
|
|
8754
|
+
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
8680
8755
|
if (retrySettings.modelFallback) {
|
|
8681
8756
|
if (!classifierRefusal) {
|
|
8682
8757
|
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
@@ -9813,7 +9888,7 @@ export class AgentSession {
|
|
|
9813
9888
|
const branchSummarySettings = this.settings.getGroup("branchSummary");
|
|
9814
9889
|
const result = await generateBranchSummary(entriesToSummarize, {
|
|
9815
9890
|
model,
|
|
9816
|
-
apiKey,
|
|
9891
|
+
apiKey: this.#modelRegistry.resolver(model, this.sessionId),
|
|
9817
9892
|
signal: this.#branchSummaryAbortController.signal,
|
|
9818
9893
|
customInstructions: this.#obfuscateTextForProvider(options.customInstructions),
|
|
9819
9894
|
reserveTokens: branchSummarySettings.reserveTokens,
|
|
@@ -10070,6 +10145,123 @@ export class AgentSession {
|
|
|
10070
10145
|
});
|
|
10071
10146
|
}
|
|
10072
10147
|
|
|
10148
|
+
/**
|
|
10149
|
+
* Redeem one saved Codex rate-limit reset for a specific account, injecting
|
|
10150
|
+
* the provider base URL like {@link AgentSession.fetchUsageReports}. Powers
|
|
10151
|
+
* the `/usage reset` command and auto-redeem. Never throws for business
|
|
10152
|
+
* outcomes — inspect the returned `code`.
|
|
10153
|
+
*/
|
|
10154
|
+
async redeemResetCredit(target: ResetCreditTarget, signal?: AbortSignal): Promise<ResetCreditRedeemOutcome> {
|
|
10155
|
+
return this.#modelRegistry.authStorage.redeemResetCredit({
|
|
10156
|
+
target,
|
|
10157
|
+
baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
|
|
10158
|
+
signal,
|
|
10159
|
+
});
|
|
10160
|
+
}
|
|
10161
|
+
|
|
10162
|
+
/**
|
|
10163
|
+
* List saved Codex rate-limit resets per stored account, fetched live from
|
|
10164
|
+
* the dedicated credits endpoint (bypasses the usage cache). Powers the
|
|
10165
|
+
* `/usage reset` account selector.
|
|
10166
|
+
*/
|
|
10167
|
+
async listResetCredits(signal?: AbortSignal): Promise<ResetCreditAccountStatus[]> {
|
|
10168
|
+
return this.#modelRegistry.authStorage.listResetCredits({
|
|
10169
|
+
sessionId: this.sessionId,
|
|
10170
|
+
baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
|
|
10171
|
+
signal,
|
|
10172
|
+
});
|
|
10173
|
+
}
|
|
10174
|
+
|
|
10175
|
+
/**
|
|
10176
|
+
* Auto-redeem hook for {@link AgentSession.#handleRetryableError}'s
|
|
10177
|
+
* usage-limit branch. Returns `true` only when a saved Codex reset was
|
|
10178
|
+
* actually spent (so the caller retries immediately). Opt-in, reactive, and
|
|
10179
|
+
* heavily gated — see `./codex-auto-reset` and the design in
|
|
10180
|
+
* `local://autoreset-spec.md`. Per-account in-flight dedup lets concurrent
|
|
10181
|
+
* sessions adopt one redeem instead of double-spending.
|
|
10182
|
+
*/
|
|
10183
|
+
async #maybeAutoRedeemCodexReset(coordinator = defaultCodexAutoRedeemCoordinator): Promise<boolean> {
|
|
10184
|
+
const cfg = this.settings.getGroup("codexResets");
|
|
10185
|
+
const model = this.model;
|
|
10186
|
+
// Cheap exits before any IO.
|
|
10187
|
+
if (!cfg.autoRedeem || !model || model.provider !== "openai-codex") return false;
|
|
10188
|
+
const authStorage = this.#modelRegistry.authStorage;
|
|
10189
|
+
// Capture identity BEFORE awaits: markUsageLimitReached leaves the
|
|
10190
|
+
// usage-limit session credential sticky, so this names the blocked account.
|
|
10191
|
+
const identity = authStorage.getOAuthAccountIdentity("openai-codex", this.sessionId);
|
|
10192
|
+
const accountKey = (identity?.accountId ?? identity?.email)?.trim().toLowerCase();
|
|
10193
|
+
if (!accountKey) return false;
|
|
10194
|
+
const existing = coordinator.inFlightByAccount.get(accountKey);
|
|
10195
|
+
if (existing) return existing;
|
|
10196
|
+
|
|
10197
|
+
const run = (async (): Promise<boolean> => {
|
|
10198
|
+
const reports = await this.fetchUsageReports();
|
|
10199
|
+
const decision = evaluateCodexAutoRedeem({
|
|
10200
|
+
nowMs: Date.now(),
|
|
10201
|
+
provider: model.provider,
|
|
10202
|
+
modelId: model.id,
|
|
10203
|
+
settings: {
|
|
10204
|
+
autoRedeem: cfg.autoRedeem,
|
|
10205
|
+
minBlockedMinutes: Math.max(0, cfg.minBlockedMinutes),
|
|
10206
|
+
keepCredits: Math.max(0, Math.trunc(cfg.keepCredits)),
|
|
10207
|
+
},
|
|
10208
|
+
identity,
|
|
10209
|
+
reports,
|
|
10210
|
+
attemptedBlockKeys: coordinator.attemptedBlockKeys,
|
|
10211
|
+
lastAttemptAtByAccount: coordinator.lastAttemptAtByAccount,
|
|
10212
|
+
});
|
|
10213
|
+
if (!decision.redeem) {
|
|
10214
|
+
logger.debug("codex-auto-reset: skipped", { reason: decision.reason });
|
|
10215
|
+
return false;
|
|
10216
|
+
}
|
|
10217
|
+
// Commit the attempt BEFORE acting so this block can never re-enter.
|
|
10218
|
+
coordinator.attemptedBlockKeys.add(decision.blockKey);
|
|
10219
|
+
coordinator.lastAttemptAtByAccount.set(decision.accountKey, Date.now());
|
|
10220
|
+
const who = decision.target.email ?? decision.target.accountId ?? "the active account";
|
|
10221
|
+
const outcome = await authStorage.redeemResetCredit({
|
|
10222
|
+
target: decision.target,
|
|
10223
|
+
baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
|
|
10224
|
+
// Not tied to the retry abort controller: aborting a consume
|
|
10225
|
+
// mid-flight leaves credit state unknown.
|
|
10226
|
+
signal: AbortSignal.timeout(15_000),
|
|
10227
|
+
});
|
|
10228
|
+
switch (outcome.code) {
|
|
10229
|
+
case "reset": {
|
|
10230
|
+
const left = Math.max(0, decision.availableCount - 1);
|
|
10231
|
+
this.emitNotice(
|
|
10232
|
+
"info",
|
|
10233
|
+
`Auto-redeemed a saved Codex rate-limit reset for ${who} (${left} left); retrying now.`,
|
|
10234
|
+
"codex-auto-reset",
|
|
10235
|
+
);
|
|
10236
|
+
void this.fetchUsageReports();
|
|
10237
|
+
return true;
|
|
10238
|
+
}
|
|
10239
|
+
case "already_redeemed":
|
|
10240
|
+
this.emitNotice(
|
|
10241
|
+
"warning",
|
|
10242
|
+
"A saved Codex reset was already redeemed elsewhere; waiting for the window.",
|
|
10243
|
+
"codex-auto-reset",
|
|
10244
|
+
);
|
|
10245
|
+
return false;
|
|
10246
|
+
case "no_credit":
|
|
10247
|
+
logger.debug("codex-auto-reset: no_credit (snapshot/live mismatch)", { account: accountKey });
|
|
10248
|
+
return false;
|
|
10249
|
+
case "nothing_to_reset":
|
|
10250
|
+
this.emitNotice(
|
|
10251
|
+
"warning",
|
|
10252
|
+
"Codex reset reported nothing to reset; auto-redeem suppressed for this window.",
|
|
10253
|
+
"codex-auto-reset",
|
|
10254
|
+
);
|
|
10255
|
+
return false;
|
|
10256
|
+
default:
|
|
10257
|
+
this.emitNotice("warning", `Codex auto-redeem failed (${outcome.code}).`, "codex-auto-reset");
|
|
10258
|
+
return false;
|
|
10259
|
+
}
|
|
10260
|
+
})().finally(() => coordinator.inFlightByAccount.delete(accountKey));
|
|
10261
|
+
coordinator.inFlightByAccount.set(accountKey, run);
|
|
10262
|
+
return run;
|
|
10263
|
+
}
|
|
10264
|
+
|
|
10073
10265
|
/**
|
|
10074
10266
|
* Estimate context tokens from messages, using the last assistant usage when available.
|
|
10075
10267
|
*/
|
|
@@ -12,7 +12,11 @@ export type {
|
|
|
12
12
|
AuthStorageOptions,
|
|
13
13
|
CredentialOrigin,
|
|
14
14
|
CredentialOriginKind,
|
|
15
|
+
OAuthAccountIdentity,
|
|
15
16
|
OAuthCredential,
|
|
17
|
+
ResetCreditAccountStatus,
|
|
18
|
+
ResetCreditRedeemOutcome,
|
|
19
|
+
ResetCreditTarget,
|
|
16
20
|
SerializedAuthStorage,
|
|
17
21
|
SnapshotResponse,
|
|
18
22
|
StoredAuthCredential,
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
import { reportMatchesActiveAccount } from "../slash-commands/helpers/active-oauth-account";
|
|
42
|
+
|
|
43
|
+
/** Weekly window counts as exhausted at `usedFraction >= 0.999` (used_percent >= 99.9). */
|
|
44
|
+
export const WEEKLY_EXHAUSTED_MIN_FRACTION = 0.999;
|
|
45
|
+
/** A weekly reset can never be more than one window length (7d) away; +1h slack for skew. */
|
|
46
|
+
export const MAX_PLAUSIBLE_REMAINING_MS = 7 * 24 * 3_600_000 + 60 * 60_000;
|
|
47
|
+
/** Report must be no older than the 5-min usage cache TTL plus slack. */
|
|
48
|
+
export const REPORT_FRESHNESS_MS = 10 * 60_000;
|
|
49
|
+
/** Per-account cooldown that catches blockKey drift across a minute boundary. */
|
|
50
|
+
export const ATTEMPT_COOLDOWN_MS = 60_000;
|
|
51
|
+
/** Minute bucket for blockKey, absorbing `reset_after_seconds`-derived jitter. */
|
|
52
|
+
export const DEBOUNCE_BUCKET_MS = 60_000;
|
|
53
|
+
|
|
54
|
+
export type CodexAutoRedeemSkipReason =
|
|
55
|
+
| "disabled"
|
|
56
|
+
| "wrong-provider"
|
|
57
|
+
| "spark-model"
|
|
58
|
+
| "no-identity"
|
|
59
|
+
| "no-report"
|
|
60
|
+
| "stale-report"
|
|
61
|
+
| "not-limit-reached"
|
|
62
|
+
| "weekly-not-exhausted"
|
|
63
|
+
| "no-reset-time"
|
|
64
|
+
| "reset-too-soon"
|
|
65
|
+
| "reset-implausible"
|
|
66
|
+
| "credits-unknown"
|
|
67
|
+
| "reserve"
|
|
68
|
+
| "already-attempted"
|
|
69
|
+
| "cooldown";
|
|
70
|
+
|
|
71
|
+
export interface CodexAutoRedeemInput {
|
|
72
|
+
nowMs: number;
|
|
73
|
+
/** `this.model.provider`. */
|
|
74
|
+
provider: string;
|
|
75
|
+
/** `this.model.id`. */
|
|
76
|
+
modelId: string;
|
|
77
|
+
settings: { autoRedeem: boolean; minBlockedMinutes: number; keepCredits: number };
|
|
78
|
+
/** `getOAuthAccountIdentity("openai-codex", sessionId)`, captured at hook entry before any await. */
|
|
79
|
+
identity: OAuthAccountIdentity | undefined;
|
|
80
|
+
/** `session.fetchUsageReports()` (≤5-min cache). */
|
|
81
|
+
reports: UsageReport[] | null;
|
|
82
|
+
attemptedBlockKeys: ReadonlySet<string>;
|
|
83
|
+
lastAttemptAtByAccount: ReadonlyMap<string, number>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type CodexAutoRedeemDecision =
|
|
87
|
+
| {
|
|
88
|
+
redeem: true;
|
|
89
|
+
target: ResetCreditTarget;
|
|
90
|
+
accountKey: string;
|
|
91
|
+
blockKey: string;
|
|
92
|
+
weeklyResetAtMs: number;
|
|
93
|
+
remainingMs: number;
|
|
94
|
+
availableCount: number;
|
|
95
|
+
}
|
|
96
|
+
| { redeem: false; reason: CodexAutoRedeemSkipReason };
|
|
97
|
+
|
|
98
|
+
/** Trimmed lowercase, or undefined when blank. Mirrors `normalizeIdentityValue` in active-oauth-account.ts. */
|
|
99
|
+
function normalize(value: unknown): string | undefined {
|
|
100
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decide whether to auto-redeem a saved Codex reset for the active account.
|
|
105
|
+
*
|
|
106
|
+
* Pure: every gate below is a function of the snapshot inputs only. Order
|
|
107
|
+
* matters — cheapest / most-decisive gates first so the common "not eligible"
|
|
108
|
+
* paths short-circuit before any account/report matching.
|
|
109
|
+
*/
|
|
110
|
+
export function evaluateCodexAutoRedeem(input: CodexAutoRedeemInput): CodexAutoRedeemDecision {
|
|
111
|
+
const { nowMs, settings } = input;
|
|
112
|
+
if (!settings.autoRedeem) return { redeem: false, reason: "disabled" };
|
|
113
|
+
if (input.provider !== "openai-codex") return { redeem: false, reason: "wrong-provider" };
|
|
114
|
+
// Unknown #1: it is unknown whether a credit resets the separate Spark meter.
|
|
115
|
+
if (input.modelId.includes("-spark")) return { redeem: false, reason: "spark-model" };
|
|
116
|
+
|
|
117
|
+
const accountKey = normalize(input.identity?.accountId) ?? normalize(input.identity?.email);
|
|
118
|
+
if (!accountKey) return { redeem: false, reason: "no-identity" };
|
|
119
|
+
|
|
120
|
+
const report = input.reports?.find(
|
|
121
|
+
r => r.provider === "openai-codex" && reportMatchesActiveAccount(r, input.identity),
|
|
122
|
+
);
|
|
123
|
+
if (!report) return { redeem: false, reason: "no-report" };
|
|
124
|
+
if (nowMs - report.fetchedAt > REPORT_FRESHNESS_MS) return { redeem: false, reason: "stale-report" };
|
|
125
|
+
// The wire's own blocked flag must confirm the 429.
|
|
126
|
+
if (report.metadata?.limitReached !== true) return { redeem: false, reason: "not-limit-reached" };
|
|
127
|
+
|
|
128
|
+
// EXACT ids — never `status` (see the Decision-2 trap in the module docs).
|
|
129
|
+
// The saved reset applies to the WEEKLY window, so that is the blocker we act
|
|
130
|
+
// on. A 5h-only block (weekly still has headroom) self-heals within the hour,
|
|
131
|
+
// so spending a scarce ~monthly credit there would be wasted.
|
|
132
|
+
const weekly = report.limits.find(l => l.id === "openai-codex:secondary");
|
|
133
|
+
const wUsed = weekly?.amount.usedFraction;
|
|
134
|
+
if (!weekly || wUsed === undefined || wUsed < WEEKLY_EXHAUSTED_MIN_FRACTION) {
|
|
135
|
+
return { redeem: false, reason: "weekly-not-exhausted" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const resetsAt = weekly.window?.resetsAt;
|
|
139
|
+
if (resetsAt === undefined) return { redeem: false, reason: "no-reset-time" };
|
|
140
|
+
const remainingMs = resetsAt - nowMs;
|
|
141
|
+
// anti-waste: too close to the natural reset — let it roll over instead of spending a credit.
|
|
142
|
+
if (remainingMs < settings.minBlockedMinutes * 60_000) return { redeem: false, reason: "reset-too-soon" };
|
|
143
|
+
if (remainingMs > MAX_PLAUSIBLE_REMAINING_MS) return { redeem: false, reason: "reset-implausible" };
|
|
144
|
+
|
|
145
|
+
const available = report.resetCredits?.availableCount;
|
|
146
|
+
// can't verify availability from the snapshot → don't spend (precision over recall).
|
|
147
|
+
if (available === undefined) return { redeem: false, reason: "credits-unknown" };
|
|
148
|
+
if (available - Math.max(0, Math.trunc(settings.keepCredits)) < 1) {
|
|
149
|
+
return { redeem: false, reason: "reserve" };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const blockKey = `${accountKey}|${Math.round(resetsAt / DEBOUNCE_BUCKET_MS)}`;
|
|
153
|
+
if (input.attemptedBlockKeys.has(blockKey)) return { redeem: false, reason: "already-attempted" };
|
|
154
|
+
const lastAt = input.lastAttemptAtByAccount.get(accountKey);
|
|
155
|
+
if (lastAt !== undefined && nowMs - lastAt < ATTEMPT_COOLDOWN_MS) return { redeem: false, reason: "cooldown" };
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
redeem: true,
|
|
159
|
+
target: { accountId: input.identity?.accountId, email: input.identity?.email },
|
|
160
|
+
accountKey,
|
|
161
|
+
blockKey,
|
|
162
|
+
weeklyResetAtMs: resetsAt,
|
|
163
|
+
remainingMs,
|
|
164
|
+
availableCount: available,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Process-wide (NOT per-session) coordinator state. Parallel subagent sessions
|
|
170
|
+
* share the same Codex accounts and must not race a double-spend, so this is a
|
|
171
|
+
* single shared container, not a per-session field.
|
|
172
|
+
*
|
|
173
|
+
* - `attemptedBlockKeys`: one attempt EVER per block episode, regardless of
|
|
174
|
+
* outcome — recorded before calling the consume so exceptions can't re-enter.
|
|
175
|
+
* - `lastAttemptAtByAccount`: per-account cooldown timestamps (epoch ms),
|
|
176
|
+
* catching blockKey drift across a minute boundary.
|
|
177
|
+
* - `inFlightByAccount`: serializes per account — a second session for the same
|
|
178
|
+
* account adopts the in-flight promise instead of starting a second consume.
|
|
179
|
+
*/
|
|
180
|
+
export interface CodexAutoRedeemCoordinator {
|
|
181
|
+
attemptedBlockKeys: Set<string>;
|
|
182
|
+
lastAttemptAtByAccount: Map<string, number>;
|
|
183
|
+
inFlightByAccount: Map<string, Promise<boolean>>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const defaultCodexAutoRedeemCoordinator: CodexAutoRedeemCoordinator = {
|
|
187
|
+
attemptedBlockKeys: new Set(),
|
|
188
|
+
lastAttemptAtByAccount: new Map(),
|
|
189
|
+
inFlightByAccount: new Map(),
|
|
190
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
|
6
6
|
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
7
8
|
import {
|
|
8
9
|
type BashExecutionMessage,
|
|
9
10
|
type BranchSummaryMessage,
|
|
@@ -47,6 +48,12 @@ function stripTypeBoxFields(obj: unknown): unknown {
|
|
|
47
48
|
return obj;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/** Resolve tool parameters to a plain JSON Schema object for dump output. */
|
|
52
|
+
function toolParametersToJsonSchema(parameters: unknown): unknown {
|
|
53
|
+
if (isZodSchema(parameters)) return zodToWireSchema(parameters);
|
|
54
|
+
return stripTypeBoxFields(parameters);
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
/** Serialize an object as XML parameter elements, one per key. */
|
|
51
58
|
function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
|
|
52
59
|
const parts: string[] = [];
|
|
@@ -89,7 +96,7 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
|
|
|
89
96
|
for (const tool of tools) {
|
|
90
97
|
lines.push(`<tool name="${tool.name}">`);
|
|
91
98
|
lines.push(tool.description);
|
|
92
|
-
const parametersClean =
|
|
99
|
+
const parametersClean = toolParametersToJsonSchema(tool.parameters);
|
|
93
100
|
lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
|
|
94
101
|
lines.push("<" + "/tool>\n");
|
|
95
102
|
}
|