@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.0
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 +92 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-be1f2t8h.md} +92 -0
- package/dist/cli.js +9986 -9776
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +63 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-registry.ts +103 -25
- package/src/config/settings-schema.ts +65 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +4 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +15 -9
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +31 -17
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/grep.md +7 -6
- package/src/sdk.ts +178 -15
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +302 -175
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +131 -3
- package/src/system-prompt.ts +3 -2
- package/src/task/executor.ts +17 -4
- package/src/task/render.ts +20 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +28 -4
- package/src/tools/path-utils.ts +88 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/types.ts +1 -1
package/src/advisor/runtime.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
3
3
|
import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
5
|
+
import { raceWithSignal } from "@oh-my-pi/pi-ai/utils/abort";
|
|
5
6
|
import { type CursorExecResolvedCarrier, kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
6
7
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
|
|
@@ -47,7 +48,7 @@ export interface AdvisorRuntimeHost {
|
|
|
47
48
|
* recovery path must never replay the full primary transcript.
|
|
48
49
|
* Optional: hosts that omit it get no proactive maintenance.
|
|
49
50
|
*/
|
|
50
|
-
maintainContext?(incomingTokens: number): Promise<boolean>;
|
|
51
|
+
maintainContext?(incomingTokens: number, signal: AbortSignal): Promise<boolean>;
|
|
51
52
|
/**
|
|
52
53
|
* Called immediately before each `agent.prompt(batch)` cycle. Lets the host
|
|
53
54
|
* clear per-update advisor state — currently the one-advise-per-update gate
|
|
@@ -67,6 +68,7 @@ export interface AdvisorRuntimeHost {
|
|
|
67
68
|
onTurnError?(
|
|
68
69
|
error: unknown,
|
|
69
70
|
failedMessages: readonly AgentMessage[],
|
|
71
|
+
signal: AbortSignal,
|
|
70
72
|
): Promise<boolean | undefined> | boolean | undefined;
|
|
71
73
|
/** Called after a successful advisor turn so the host can finish fallback lifecycle reporting. */
|
|
72
74
|
onTurnSuccess?(): Promise<void> | void;
|
|
@@ -76,6 +78,8 @@ export interface AdvisorRuntimeHost {
|
|
|
76
78
|
* recovery (credential switch, fallback chain) declined. Cleared only by
|
|
77
79
|
* an explicit reset (`/new`, config rebuild, session restart). */
|
|
78
80
|
notifyQuotaExhausted?(): void;
|
|
81
|
+
/** Stable identity for the live advisor model. Used to restore full transcript rendering after a model switch. */
|
|
82
|
+
getModelIdentity?(): string;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/**
|
|
@@ -228,7 +232,6 @@ const MAX_COALESCE_ROUNDS = 3;
|
|
|
228
232
|
const MAX_QUARANTINE_RETRIES = 2;
|
|
229
233
|
|
|
230
234
|
const ADVISOR_RENDER_OPTIONS = {
|
|
231
|
-
includeThinking: true,
|
|
232
235
|
includeToolIntent: true,
|
|
233
236
|
watchedRoles: true,
|
|
234
237
|
expandPrimaryContext: true,
|
|
@@ -285,11 +288,17 @@ export class AdvisorRuntime {
|
|
|
285
288
|
#advisorRegexSecretValues = new Set<string>();
|
|
286
289
|
#pending: PendingDelta[] = [];
|
|
287
290
|
#busy = false;
|
|
291
|
+
#sessionTransitionPaused = false;
|
|
292
|
+
#promptInFlight: Promise<void> | undefined;
|
|
293
|
+
#iterationAbort: AbortController | undefined;
|
|
288
294
|
#backlog = 0;
|
|
289
295
|
#consecutiveFailures = 0;
|
|
290
296
|
#failureNotified = false;
|
|
291
297
|
/** Consecutive quarantined turns since the last success/reset (issue #6661). */
|
|
292
298
|
#consecutiveQuarantines = 0;
|
|
299
|
+
/** Whether primary reasoning is included in advisor deltas for the current model. */
|
|
300
|
+
#includeThinking = true;
|
|
301
|
+
#modelIdentity: string | undefined;
|
|
293
302
|
/** Completed 3-failure backlog-drop cycles since the last success/reset. */
|
|
294
303
|
#droppedBacklogs = 0;
|
|
295
304
|
/**
|
|
@@ -428,6 +437,7 @@ export class AdvisorRuntime {
|
|
|
428
437
|
}
|
|
429
438
|
|
|
430
439
|
dispose(): void {
|
|
440
|
+
this.#iterationAbort?.abort("advisor disposed");
|
|
431
441
|
this.disposed = true;
|
|
432
442
|
this.#epoch++;
|
|
433
443
|
this.#pending = [];
|
|
@@ -490,6 +500,31 @@ export class AdvisorRuntime {
|
|
|
490
500
|
});
|
|
491
501
|
}
|
|
492
502
|
|
|
503
|
+
/** Stop new advisor work and wait only for the active prompt's recorder-visible events. */
|
|
504
|
+
pauseForSessionTransition(): Promise<void> {
|
|
505
|
+
if (!this.#sessionTransitionPaused) {
|
|
506
|
+
this.#sessionTransitionPaused = true;
|
|
507
|
+
this.#wakeAllWaiters();
|
|
508
|
+
this.#iterationAbort?.abort("advisor session transition");
|
|
509
|
+
try {
|
|
510
|
+
this.agent.abort("advisor session transition");
|
|
511
|
+
} catch {}
|
|
512
|
+
}
|
|
513
|
+
return (
|
|
514
|
+
this.#promptInFlight?.then(
|
|
515
|
+
() => {},
|
|
516
|
+
() => {},
|
|
517
|
+
) ?? Promise.resolve()
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Continue queued work after a session transition rolls back or preserves the conversation. */
|
|
522
|
+
resumeAfterSessionTransition(): void {
|
|
523
|
+
if (!this.#sessionTransitionPaused) return;
|
|
524
|
+
this.#sessionTransitionPaused = false;
|
|
525
|
+
if (!this.#quotaExhausted && !this.#halted) void this.#drain();
|
|
526
|
+
}
|
|
527
|
+
|
|
493
528
|
/**
|
|
494
529
|
* Re-prime the advisor after a history rewrite (compaction, session
|
|
495
530
|
* switch/resume, branch). Clears the advisor's own (non-persisted) context
|
|
@@ -498,7 +533,9 @@ export class AdvisorRuntime {
|
|
|
498
533
|
* leaving it blind to everything before the rewrite.
|
|
499
534
|
*/
|
|
500
535
|
reset(): void {
|
|
536
|
+
this.#iterationAbort?.abort("advisor reset");
|
|
501
537
|
this.#epoch++;
|
|
538
|
+
this.#sessionTransitionPaused = false;
|
|
502
539
|
this.#quotaExhausted = false;
|
|
503
540
|
this.#halted = false;
|
|
504
541
|
this.#failing = false;
|
|
@@ -530,13 +567,23 @@ export class AdvisorRuntime {
|
|
|
530
567
|
this.#wakeAllWaiters();
|
|
531
568
|
}
|
|
532
569
|
|
|
570
|
+
#syncModelIdentity(): void {
|
|
571
|
+
const identity = this.host.getModelIdentity?.();
|
|
572
|
+
if (identity === undefined || identity === this.#modelIdentity) return;
|
|
573
|
+
this.#modelIdentity = identity;
|
|
574
|
+
this.#includeThinking = true;
|
|
575
|
+
}
|
|
576
|
+
|
|
533
577
|
#formatRawDelta(rawMessages: AgentMessage[], wip = false): string | null {
|
|
534
578
|
const delta = rawMessages
|
|
535
579
|
.filter(message => !(message.role === "custom" && message.customType === "advisor"))
|
|
536
580
|
.map(message => this.#dedupContextMessage(message));
|
|
537
581
|
if (delta.length === 0) return null;
|
|
538
582
|
const obfuscator = this.host.obfuscator;
|
|
539
|
-
let md = formatSessionHistoryMarkdown(delta,
|
|
583
|
+
let md = formatSessionHistoryMarkdown(delta, {
|
|
584
|
+
...ADVISOR_RENDER_OPTIONS,
|
|
585
|
+
includeThinking: this.#includeThinking,
|
|
586
|
+
});
|
|
540
587
|
if (!md.trim()) return null;
|
|
541
588
|
if (obfuscator?.hasSecrets()) {
|
|
542
589
|
let discoveredNewRegexSecretValue = false;
|
|
@@ -570,7 +617,7 @@ export class AdvisorRuntime {
|
|
|
570
617
|
? obfuscateAdvisorMessage(obfuscator, message, this.#advisorRegexSecretValues)
|
|
571
618
|
: message,
|
|
572
619
|
),
|
|
573
|
-
ADVISOR_RENDER_OPTIONS,
|
|
620
|
+
{ ...ADVISOR_RENDER_OPTIONS, includeThinking: this.#includeThinking },
|
|
574
621
|
);
|
|
575
622
|
md = obfuscator.obfuscate(md, this.#advisorRegexSecretValues);
|
|
576
623
|
}
|
|
@@ -696,6 +743,7 @@ export class AdvisorRuntime {
|
|
|
696
743
|
epoch: number,
|
|
697
744
|
initial: PendingDelta[],
|
|
698
745
|
recoveringOverflow: boolean,
|
|
746
|
+
signal: AbortSignal,
|
|
699
747
|
): Promise<{
|
|
700
748
|
batch: string | null;
|
|
701
749
|
rawMessages: AgentMessage[];
|
|
@@ -712,11 +760,12 @@ export class AdvisorRuntime {
|
|
|
712
760
|
let wip = initial.at(-1)?.wip ?? false;
|
|
713
761
|
|
|
714
762
|
for (let round = 0; round < MAX_COALESCE_ROUNDS; round++) {
|
|
763
|
+
if (this.#sessionTransitionPaused) break;
|
|
715
764
|
if (this.host.maintainContext) {
|
|
716
765
|
const incomingTokens = estimateTokens({ role: "user", content: batchText, timestamp: Date.now() });
|
|
717
766
|
let shouldResetContext = false;
|
|
718
767
|
try {
|
|
719
|
-
shouldResetContext = await this.host.maintainContext(incomingTokens);
|
|
768
|
+
shouldResetContext = await this.host.maintainContext(incomingTokens, signal);
|
|
720
769
|
} catch (err) {
|
|
721
770
|
logger.debug("advisor context maintenance failed", { err: String(err) });
|
|
722
771
|
}
|
|
@@ -733,6 +782,7 @@ export class AdvisorRuntime {
|
|
|
733
782
|
// remain queued and ship as their own subsequent batch.
|
|
734
783
|
if (round > 0) {
|
|
735
784
|
const lateItems = this.#pending.splice(0);
|
|
785
|
+
initial.push(...lateItems);
|
|
736
786
|
turns += lateItems.reduce((sum, b) => sum + b.turns, 0);
|
|
737
787
|
if (lateItems.length > 0) {
|
|
738
788
|
wip = lateItems.at(-1)!.wip;
|
|
@@ -769,6 +819,7 @@ export class AdvisorRuntime {
|
|
|
769
819
|
// update WIP state, and re-check the maintenance budget.
|
|
770
820
|
const late = this.#pending.splice(0);
|
|
771
821
|
if (late.length === 0) break;
|
|
822
|
+
initial.push(...late);
|
|
772
823
|
batchText = [batchText, ...late.map(b => b.text)].join("\n\n");
|
|
773
824
|
rawMessages = rawMessages.concat(late.flatMap(b => b.rawMessages));
|
|
774
825
|
turns += late.reduce((sum, b) => sum + b.turns, 0);
|
|
@@ -802,10 +853,12 @@ export class AdvisorRuntime {
|
|
|
802
853
|
}
|
|
803
854
|
|
|
804
855
|
async #drain(): Promise<void> {
|
|
805
|
-
if (this.#busy) return;
|
|
856
|
+
if (this.#busy || this.#sessionTransitionPaused) return;
|
|
806
857
|
this.#busy = true;
|
|
807
858
|
try {
|
|
808
|
-
|
|
859
|
+
this.#syncModelIdentity();
|
|
860
|
+
while (!this.disposed && !this.#sessionTransitionPaused && this.#pending.length) {
|
|
861
|
+
this.#syncModelIdentity();
|
|
809
862
|
let popped: PendingDelta[];
|
|
810
863
|
if (this.#pending[0]?.overflowRecovery) {
|
|
811
864
|
const recovery = this.#pending.shift();
|
|
@@ -814,6 +867,8 @@ export class AdvisorRuntime {
|
|
|
814
867
|
} else {
|
|
815
868
|
popped = this.#pending.splice(0);
|
|
816
869
|
}
|
|
870
|
+
const iterationAbort = new AbortController();
|
|
871
|
+
this.#iterationAbort = iterationAbort;
|
|
817
872
|
const epoch = this.#epoch;
|
|
818
873
|
for (const delta of popped) {
|
|
819
874
|
if (delta.renderRevision === this.#renderRevision) continue;
|
|
@@ -822,10 +877,19 @@ export class AdvisorRuntime {
|
|
|
822
877
|
delta.renderRevision = this.#renderRevision;
|
|
823
878
|
}
|
|
824
879
|
const recoveringOverflow = popped.some(delta => delta.overflowRecovery === true);
|
|
825
|
-
const result = await this.#collectAndMaintainBatch(
|
|
880
|
+
const result = await this.#collectAndMaintainBatch(
|
|
881
|
+
epoch,
|
|
882
|
+
popped,
|
|
883
|
+
recoveringOverflow,
|
|
884
|
+
iterationAbort.signal,
|
|
885
|
+
);
|
|
826
886
|
|
|
827
887
|
// Epoch was invalidated during batch collection; restart the loop.
|
|
828
888
|
if (result === null) continue;
|
|
889
|
+
if (this.#sessionTransitionPaused) {
|
|
890
|
+
this.#pending.unshift(...popped);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
829
893
|
|
|
830
894
|
const { batch, rawMessages, finalTurns, wip, resetContext } = result;
|
|
831
895
|
|
|
@@ -847,7 +911,13 @@ export class AdvisorRuntime {
|
|
|
847
911
|
// Reset the host's per-update advisor state (one-advise-per-update
|
|
848
912
|
// gate) before each model cycle so the new batch starts fresh.
|
|
849
913
|
this.host.beginAdvisorUpdate?.();
|
|
850
|
-
|
|
914
|
+
const prompt = this.agent.prompt(batch);
|
|
915
|
+
this.#promptInFlight = prompt;
|
|
916
|
+
try {
|
|
917
|
+
await prompt;
|
|
918
|
+
} finally {
|
|
919
|
+
if (this.#promptInFlight === prompt) this.#promptInFlight = undefined;
|
|
920
|
+
}
|
|
851
921
|
// Agent.#runLoop catches provider/stream failures internally and
|
|
852
922
|
// resolves prompt() cleanly with stopReason: "error". Treat that
|
|
853
923
|
// as a failed turn so endpoint rejections trip the retry path.
|
|
@@ -868,12 +938,17 @@ export class AdvisorRuntime {
|
|
|
868
938
|
this.#consecutiveQuarantines = 0;
|
|
869
939
|
if (this.host.onTurnSuccess) {
|
|
870
940
|
try {
|
|
871
|
-
await this.host.onTurnSuccess();
|
|
941
|
+
await raceWithSignal(Promise.resolve(this.host.onTurnSuccess()), iterationAbort.signal);
|
|
872
942
|
} catch (hookErr) {
|
|
873
943
|
logger.debug("advisor onTurnSuccess hook failed", { err: String(hookErr) });
|
|
874
944
|
}
|
|
875
945
|
}
|
|
876
946
|
} catch (err) {
|
|
947
|
+
if (this.#sessionTransitionPaused) {
|
|
948
|
+
this.#rollbackFailedTurn(messageSnapshot);
|
|
949
|
+
this.#pending.unshift(...popped);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
877
952
|
// reset()/dispose() aborts the in-flight prompt; treat it as a
|
|
878
953
|
// reset, not a transient failure — drop the stale batch.
|
|
879
954
|
if (this.#epoch !== epoch) continue;
|
|
@@ -885,6 +960,9 @@ export class AdvisorRuntime {
|
|
|
885
960
|
this.#wakeAllWaiters();
|
|
886
961
|
const failedMessages = this.agent.state.messages.slice(messageSnapshot);
|
|
887
962
|
const terminalFailure = this.#terminalAssistantFailure(messageSnapshot);
|
|
963
|
+
const classifierRefusal =
|
|
964
|
+
(terminalFailure !== undefined && isClassifierRefusal(terminalFailure)) ||
|
|
965
|
+
AIError.is(AIError.classify(err), AIError.Flag.ContentBlocked);
|
|
888
966
|
const terminalFailureId =
|
|
889
967
|
terminalFailure === undefined ? undefined : AIError.classifyMessage(terminalFailure);
|
|
890
968
|
const contextOverflow =
|
|
@@ -900,12 +978,43 @@ export class AdvisorRuntime {
|
|
|
900
978
|
AIError.is(terminalFailureId, AIError.Flag.ContextOverflow);
|
|
901
979
|
this.#rollbackFailedTurn(messageSnapshot);
|
|
902
980
|
logger.debug("advisor turn failed", { err: String(err) });
|
|
981
|
+
if (classifierRefusal) {
|
|
982
|
+
if (this.#includeThinking) {
|
|
983
|
+
this.#includeThinking = false;
|
|
984
|
+
const strippedBatch = this.#formatRawDelta(rawMessages, wip);
|
|
985
|
+
if (strippedBatch) {
|
|
986
|
+
this.#pending.unshift({
|
|
987
|
+
text: strippedBatch,
|
|
988
|
+
rawMessages,
|
|
989
|
+
renderRevision: this.#renderRevision,
|
|
990
|
+
turns: finalTurns,
|
|
991
|
+
wip,
|
|
992
|
+
overflowRecovery: recoveringOverflow || undefined,
|
|
993
|
+
});
|
|
994
|
+
logger.debug("advisor refusal recovered by stripping primary reasoning");
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
this.#notifyFailureOnce(err);
|
|
999
|
+
this.#clearSeenContext();
|
|
1000
|
+
this.#backlog = Math.max(0, this.#backlog - finalTurns);
|
|
1001
|
+
this.#notifyWaiters();
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
903
1004
|
let recovered = false;
|
|
904
1005
|
try {
|
|
905
|
-
recovered =
|
|
1006
|
+
recovered =
|
|
1007
|
+
(await raceWithSignal(
|
|
1008
|
+
Promise.resolve(this.host.onTurnError?.(err, failedMessages, iterationAbort.signal)),
|
|
1009
|
+
iterationAbort.signal,
|
|
1010
|
+
)) === true;
|
|
906
1011
|
} catch (hookErr) {
|
|
907
1012
|
logger.debug("advisor onTurnError hook failed", { err: String(hookErr) });
|
|
908
1013
|
}
|
|
1014
|
+
if (this.#sessionTransitionPaused) {
|
|
1015
|
+
this.#pending.unshift(...popped);
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
909
1018
|
if (err instanceof AdvisorOutputQuarantinedError) {
|
|
910
1019
|
// A quarantine discards the advisor's whole turn before dispatch, so
|
|
911
1020
|
// its advice never reaches the primary. One re-prime is allowed to
|
|
@@ -1020,7 +1129,15 @@ export class AdvisorRuntime {
|
|
|
1020
1129
|
wip,
|
|
1021
1130
|
overflowRecovery: recoveringOverflow || undefined,
|
|
1022
1131
|
});
|
|
1023
|
-
|
|
1132
|
+
if (this.retryDelayMs <= 0) {
|
|
1133
|
+
await Bun.sleep(0);
|
|
1134
|
+
} else {
|
|
1135
|
+
try {
|
|
1136
|
+
await raceWithSignal(Bun.sleep(this.retryDelayMs), iterationAbort.signal);
|
|
1137
|
+
} catch (sleepError) {
|
|
1138
|
+
if (!iterationAbort.signal.aborted) throw sleepError;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1024
1141
|
}
|
|
1025
1142
|
}
|
|
1026
1143
|
}
|
|
@@ -1031,11 +1148,20 @@ export class AdvisorRuntime {
|
|
|
1031
1148
|
}
|
|
1032
1149
|
}
|
|
1033
1150
|
} finally {
|
|
1151
|
+
this.#iterationAbort = undefined;
|
|
1034
1152
|
this.#busy = false;
|
|
1035
1153
|
}
|
|
1036
1154
|
}
|
|
1037
1155
|
}
|
|
1038
1156
|
|
|
1157
|
+
/** Mirrors turn recovery's refusal classification and retains AIError's provider-neutral content-block fallback. */
|
|
1158
|
+
function isClassifierRefusal(message: AssistantMessage): boolean {
|
|
1159
|
+
if (message.stopReason !== "error") return false;
|
|
1160
|
+
const stopType = message.stopDetails?.type;
|
|
1161
|
+
if (stopType === "refusal" || stopType === "sensitive") return true;
|
|
1162
|
+
return AIError.is(AIError.classifyMessage(message), AIError.Flag.ContentBlocked);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1039
1165
|
/**
|
|
1040
1166
|
* The only malformed advisor turn shape: the prompt resolved but produced no
|
|
1041
1167
|
* assistant response at all. Everything an assistant message carries — advice,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
4
|
import type { Message, UserMessage } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { visitEntriesFromFileStream } from "../session/session-loader";
|
|
5
7
|
import { SessionManager } from "../session/session-manager";
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -31,6 +33,60 @@ export function isAdvisorTranscriptName(name: string): boolean {
|
|
|
31
33
|
);
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Sum the advisor spend already persisted next to a primary session transcript,
|
|
38
|
+
* keyed by advisor slug.
|
|
39
|
+
*
|
|
40
|
+
* The ledger a session keeps in memory only covers the current process, so a
|
|
41
|
+
* resumed session would report zero until the next advisor turn. The recorded
|
|
42
|
+
* transcripts are the durable copy of exactly the same finalized messages, so
|
|
43
|
+
* they are read back through the shared loader - no lock, no writer, and no
|
|
44
|
+
* second parser to keep in step with the session format.
|
|
45
|
+
*
|
|
46
|
+
* Only the session's own advisors count: subagent advisors write to
|
|
47
|
+
* `<session>/<SubId>/__advisor.jsonl`, and their spend belongs to the subagent,
|
|
48
|
+
* not to this roster. Hence the scan stays at the top level of the directory.
|
|
49
|
+
*/
|
|
50
|
+
export async function loadAdvisorTranscriptCosts(sessionFile: string | undefined): Promise<Map<string, number>> {
|
|
51
|
+
const costs = new Map<string, number>();
|
|
52
|
+
if (!sessionFile?.endsWith(JSONL_SUFFIX)) return costs;
|
|
53
|
+
const directory = sessionFile.slice(0, -JSONL_SUFFIX.length);
|
|
54
|
+
const dirents = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
55
|
+
for (const dirent of dirents) {
|
|
56
|
+
if (!dirent.isFile() || !isAdvisorTranscriptName(dirent.name)) continue;
|
|
57
|
+
const slug =
|
|
58
|
+
dirent.name === ADVISOR_TRANSCRIPT_FILENAME
|
|
59
|
+
? ""
|
|
60
|
+
: dirent.name.slice(`${ADVISOR_TRANSCRIPT_STEM}.`.length, -JSONL_SUFFIX.length);
|
|
61
|
+
let total = 0;
|
|
62
|
+
let validHeader: boolean | undefined;
|
|
63
|
+
try {
|
|
64
|
+
await visitEntriesFromFileStream(path.join(directory, dirent.name), entry => {
|
|
65
|
+
const isObject = typeof entry === "object" && entry !== null;
|
|
66
|
+
if (validHeader === undefined) {
|
|
67
|
+
validHeader = isObject && entry.type === "session" && typeof entry.id === "string";
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// A syntactically valid but non-object entry (e.g. a bare `null`
|
|
71
|
+
// line) must cost only itself, not crash entry.type access and
|
|
72
|
+
// discard everything accumulated for this transcript.
|
|
73
|
+
if (!validHeader || !isObject || entry.type !== "message") return;
|
|
74
|
+
const message = entry.message;
|
|
75
|
+
if (!message || typeof message !== "object" || message.role !== "assistant") return;
|
|
76
|
+
// One malformed usage block must cost that entry only, not the
|
|
77
|
+
// whole transcript's total.
|
|
78
|
+
const total_ = message.usage?.cost?.total;
|
|
79
|
+
if (typeof total_ === "number" && Number.isFinite(total_)) total += total_;
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.debug("advisor transcript cost read failed", { file: dirent.name, err: String(err) });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (total > 0) costs.set(slug, total);
|
|
86
|
+
}
|
|
87
|
+
return costs;
|
|
88
|
+
}
|
|
89
|
+
|
|
34
90
|
/**
|
|
35
91
|
* Append-only persister for an advisor agent's transcript.
|
|
36
92
|
*
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* {@link Effort}, clamped into the active model's supported range (never below
|
|
6
6
|
* {@link Effort.Low}). Two backends, selected by `providers.autoThinkingModel`:
|
|
7
7
|
*
|
|
8
|
-
* - `online` (default): a smol model classifies into `low|medium|high|xhigh
|
|
8
|
+
* - `online` (default): a smol model classifies into `low|medium|high|xhigh`,
|
|
9
|
+
* plus `max` when the target model exposes that tier.
|
|
9
10
|
* - a local key: an on-device memory model classifies into the coarser
|
|
10
11
|
* `trivial|moderate|hard` scheme (3-class is more reliable than 4-way ordinal
|
|
11
12
|
* on sub-2B models), mapped to `low|high|xhigh`.
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
* the caller falls back to a concrete level and continues the turn.
|
|
15
16
|
*/
|
|
16
17
|
import { type AssistantMessage, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
|
|
18
|
+
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
17
19
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
18
20
|
|
|
19
21
|
import type { ModelRegistry } from "../config/model-registry";
|
|
@@ -30,7 +32,31 @@ import {
|
|
|
30
32
|
} from "../tiny/models";
|
|
31
33
|
import { tinyModelClient } from "../tiny/title-client";
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Rendered classifier prompts, keyed by whether `max` is offered as a label.
|
|
37
|
+
* Two variants only, so both are memoized on first use.
|
|
38
|
+
*/
|
|
39
|
+
const DIFFICULTY_SYSTEM_PROMPTS: Partial<Record<"max" | "xhigh", string>> = {};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Highest effort this turn's classification may resolve to: the configured
|
|
43
|
+
* ceiling, further limited by what the target model actually exposes. The
|
|
44
|
+
* default keeps `auto` one tier below the top, so only an explicit
|
|
45
|
+
* `ultrathink` reaches {@link Effort.Max}.
|
|
46
|
+
*/
|
|
47
|
+
function autoEffortCeiling(deps: ClassifyDifficultyDeps): Effort {
|
|
48
|
+
if (deps.settings.get("providers.autoThinkingMaxEffort") !== Effort.Max) return Effort.XHigh;
|
|
49
|
+
return getSupportedEfforts(deps.model).includes(Effort.Max) ? Effort.Max : Effort.XHigh;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function difficultySystemPromptFor(ceiling: Effort): string {
|
|
53
|
+
const key = ceiling === Effort.Max ? "max" : "xhigh";
|
|
54
|
+
const cached = DIFFICULTY_SYSTEM_PROMPTS[key];
|
|
55
|
+
if (cached !== undefined) return cached;
|
|
56
|
+
const rendered = prompt.render(difficultySystemPrompt, { allowMax: key === "max" });
|
|
57
|
+
DIFFICULTY_SYSTEM_PROMPTS[key] = rendered;
|
|
58
|
+
return rendered;
|
|
59
|
+
}
|
|
34
60
|
|
|
35
61
|
/** Local classifiers occasionally need more room for chat-template boilerplate. */
|
|
36
62
|
const LOCAL_ANSWER_MAX_TOKENS = 16;
|
|
@@ -64,14 +90,18 @@ export async function classifyDifficulty(
|
|
|
64
90
|
): Promise<Effort | undefined> {
|
|
65
91
|
const backend = deps.settings.get("providers.autoThinkingModel");
|
|
66
92
|
const input = preprocessTinyMessage(promptText);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
const online = backend === ONLINE_AUTO_THINKING_MODEL_KEY;
|
|
94
|
+
// The 3-bucket local classifier cannot select `max`, so its ceiling stays at
|
|
95
|
+
// XHigh whatever the setting says — otherwise a sparse ladder would snap its
|
|
96
|
+
// `hard` bucket up to a tier it never chose.
|
|
97
|
+
const ceiling = online ? autoEffortCeiling(deps) : Effort.XHigh;
|
|
98
|
+
const effort = online ? await classifyOnline(input, deps, ceiling) : await classifyLocal(input, backend, deps);
|
|
99
|
+
// The ceiling goes into the clamp itself: capping the request alone is not
|
|
100
|
+
// enough, because a sparse ladder snaps an excluded request back up.
|
|
101
|
+
return clampAutoThinkingEffort(deps.model, effort, ceiling);
|
|
72
102
|
}
|
|
73
103
|
|
|
74
|
-
async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Promise<Effort> {
|
|
104
|
+
async function classifyOnline(input: string, deps: ClassifyDifficultyDeps, ceiling: Effort): Promise<Effort> {
|
|
75
105
|
const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable());
|
|
76
106
|
const model = resolved?.model;
|
|
77
107
|
if (!model) {
|
|
@@ -88,7 +118,7 @@ async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Prom
|
|
|
88
118
|
const response = await completeSimple(
|
|
89
119
|
model,
|
|
90
120
|
{
|
|
91
|
-
systemPrompt: [
|
|
121
|
+
systemPrompt: [difficultySystemPromptFor(ceiling)],
|
|
92
122
|
messages: [{ role: "user", content: input, timestamp: Date.now() }],
|
|
93
123
|
},
|
|
94
124
|
{
|
|
@@ -134,7 +164,13 @@ async function classifyLocal(input: string, modelKey: string, deps: ClassifyDiff
|
|
|
134
164
|
return effort;
|
|
135
165
|
}
|
|
136
166
|
|
|
137
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* Map an online level keyword to an {@link Effort}; earliest match wins.
|
|
169
|
+
*
|
|
170
|
+
* `max` is only offered to the classifier when the target model exposes that
|
|
171
|
+
* tier, but it is always parsed: an unsupported `max` is snapped back down by
|
|
172
|
+
* {@link clampAutoThinkingEffort} rather than failing the turn.
|
|
173
|
+
*/
|
|
138
174
|
export function parseDifficultyLevel(text: string): Effort | undefined {
|
|
139
175
|
const lower = text.toLowerCase();
|
|
140
176
|
const candidates: Array<[number, Effort]> = [];
|
|
@@ -142,6 +178,8 @@ export function parseDifficultyLevel(text: string): Effort | undefined {
|
|
|
142
178
|
// inside "xhigh" (no word boundary between `x` and `h`), so the two never collide.
|
|
143
179
|
const xhigh = lower.search(/x[\s_-]?high/);
|
|
144
180
|
if (xhigh >= 0) candidates.push([xhigh, Effort.XHigh]);
|
|
181
|
+
const max = lower.search(/\bmax\b/);
|
|
182
|
+
if (max >= 0) candidates.push([max, Effort.Max]);
|
|
145
183
|
const high = lower.search(/\bhigh\b/);
|
|
146
184
|
if (high >= 0) candidates.push([high, Effort.High]);
|
|
147
185
|
const medium = lower.search(/\bmed(?:ium)?\b/);
|
package/src/cli/args.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface Args {
|
|
|
44
44
|
advisor?: boolean;
|
|
45
45
|
continue?: boolean;
|
|
46
46
|
resume?: string | true;
|
|
47
|
+
fromClaude?: boolean;
|
|
48
|
+
fromCodex?: boolean;
|
|
47
49
|
help?: boolean;
|
|
48
50
|
version?: boolean;
|
|
49
51
|
mode?: Mode;
|
|
@@ -227,6 +229,10 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
|
|
|
227
229
|
result.alias = arg.slice("--alias=".length);
|
|
228
230
|
} else if (arg === "--continue" || arg === "-c") {
|
|
229
231
|
result.continue = true;
|
|
232
|
+
} else if (arg === "--from-claude") {
|
|
233
|
+
result.fromClaude = true;
|
|
234
|
+
} else if (arg === "--from-codex") {
|
|
235
|
+
result.fromCodex = true;
|
|
230
236
|
} else if (arg === "--no-session") {
|
|
231
237
|
result.noSession = true;
|
|
232
238
|
} else if (arg === "--no-tools") {
|
package/src/cli/flag-tables.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: sample source-code strings (read fixtures) intentionally contain literal ${...}.
|
|
2
2
|
// Gallery fixtures for the filesystem tools (read, write, glob).
|
|
3
|
+
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
3
4
|
import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
|
|
4
5
|
import type { GalleryFixture, GalleryFixtureState, GalleryResult } from "./types";
|
|
5
6
|
|
|
@@ -47,6 +48,15 @@ const groupedReadDelimitedPath = groupedReadTargets.join(",");
|
|
|
47
48
|
const groupedReadRepeatedFile = "packages/coding-agent/src/task/render.ts";
|
|
48
49
|
const groupedReadRepeatedRanges = `${groupedReadRepeatedFile}:507-605,1070-1194,1210-1240,1270-1274`;
|
|
49
50
|
|
|
51
|
+
const GROUPED_READ_USAGE: Usage = {
|
|
52
|
+
input: 2400,
|
|
53
|
+
output: 113,
|
|
54
|
+
cacheRead: 103_000,
|
|
55
|
+
cacheWrite: 0,
|
|
56
|
+
totalTokens: 105_513,
|
|
57
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
58
|
+
};
|
|
59
|
+
|
|
50
60
|
function textResult(text: string, details?: unknown, isError?: boolean): GalleryResult {
|
|
51
61
|
return { content: [{ type: "text", text }], details, isError };
|
|
52
62
|
}
|
|
@@ -81,6 +91,13 @@ function renderReadGroupFixtureState(state: GalleryFixtureState, width: number,
|
|
|
81
91
|
false,
|
|
82
92
|
"read-delimited",
|
|
83
93
|
);
|
|
94
|
+
component.attachUsage(
|
|
95
|
+
["read-delimited"],
|
|
96
|
+
GROUPED_READ_USAGE,
|
|
97
|
+
5300,
|
|
98
|
+
2200,
|
|
99
|
+
new Date(2026, 6, 28, 21, 5, 47).getTime(),
|
|
100
|
+
);
|
|
84
101
|
|
|
85
102
|
if (state === "error") {
|
|
86
103
|
component.updateResult(
|
|
@@ -88,10 +105,18 @@ function renderReadGroupFixtureState(state: GalleryFixtureState, width: number,
|
|
|
88
105
|
false,
|
|
89
106
|
"read-ranges",
|
|
90
107
|
);
|
|
108
|
+
component.attachUsage(
|
|
109
|
+
["read-ranges"],
|
|
110
|
+
GROUPED_READ_USAGE,
|
|
111
|
+
4700,
|
|
112
|
+
1900,
|
|
113
|
+
new Date(2026, 6, 28, 21, 5, 52).getTime(),
|
|
114
|
+
);
|
|
91
115
|
return component.render(width);
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
component.updateResult(textResult("Read four render.ts ranges."), false, "read-ranges");
|
|
119
|
+
component.attachUsage(["read-ranges"], GROUPED_READ_USAGE, 4700, 1900, new Date(2026, 6, 28, 21, 5, 52).getTime());
|
|
95
120
|
return component.render(width);
|
|
96
121
|
}
|
|
97
122
|
|