@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.1
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 +26 -0
- package/dist/cli.js +3336 -3318
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +830 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +288 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/chat-transcript-builder.ts +22 -1
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/selector-controller.ts +15 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/sdk.ts +33 -3
- package/src/session/agent-session.ts +239 -17
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
|
@@ -153,14 +153,18 @@ import {
|
|
|
153
153
|
AdvisorEmissionGuard,
|
|
154
154
|
type AdvisorMessageDetails,
|
|
155
155
|
type AdvisorNote,
|
|
156
|
+
AdvisorOutputQuarantinedError,
|
|
156
157
|
AdvisorRuntime,
|
|
157
158
|
type AdvisorSeverity,
|
|
158
159
|
AdvisorTranscriptRecorder,
|
|
159
160
|
advisorTranscriptFilename,
|
|
161
|
+
annotateForStaleness,
|
|
162
|
+
buildAdvisorQuarantineSourceText,
|
|
160
163
|
formatAdvisorBatchContent,
|
|
161
164
|
getOrCreateAdvisorProviderSessionId,
|
|
162
165
|
isAdvisorInterruptImmuneTurnActive,
|
|
163
166
|
isInterruptingSeverity,
|
|
167
|
+
quarantineAdvisorUnsafeOutput,
|
|
164
168
|
resolveAdvisorDeliveryChannel,
|
|
165
169
|
slugifyAdvisorName,
|
|
166
170
|
} from "../advisor";
|
|
@@ -183,7 +187,7 @@ import {
|
|
|
183
187
|
resolveModelOverride,
|
|
184
188
|
resolveModelRoleValue,
|
|
185
189
|
} from "../config/model-resolver";
|
|
186
|
-
import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
190
|
+
import { getKnownRoleIds, MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
187
191
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
188
192
|
import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
|
|
189
193
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
@@ -350,6 +354,7 @@ import {
|
|
|
350
354
|
import { findCompactMode } from "./compact-modes";
|
|
351
355
|
import {
|
|
352
356
|
collectPendingToolCalls,
|
|
357
|
+
createInterruptedTurnAbortMessage,
|
|
353
358
|
SESSION_EXIT_CUSTOM_TYPE,
|
|
354
359
|
type SessionExitData,
|
|
355
360
|
summarizeToolArguments,
|
|
@@ -416,6 +421,59 @@ const MID_RUN_TODO_NUDGE_MUTATING_TOOLS: Record<string, true> = {
|
|
|
416
421
|
write: true,
|
|
417
422
|
ast_edit: true,
|
|
418
423
|
};
|
|
424
|
+
const MARKDOWN_PROMPT_PREFIX_RE = /^(?:>\s*)?(?:(?:[-*+]|\d+[.)])\s+)*/;
|
|
425
|
+
const PROMPT_LABEL_RE = /^(?:q(?:uestion)?|ask)\s*\d*\s*[:.)-]\s*/i;
|
|
426
|
+
const QUESTION_PROMPT_RE =
|
|
427
|
+
/^(?:what|which|when|where|why|how|who|whom|whose|do|does|did|can|could|would|will|should|is|are|am|may|shall)\b/i;
|
|
428
|
+
const USER_DIRECTED_PROMPT_RE = /\b(?:you|your|we|our)\b/i;
|
|
429
|
+
const USER_RESPONSE_CUE_RE =
|
|
430
|
+
/^(?:please\s+)?(?:confirm|reply|choose|pick|decide|advise)\b|^(?:please\s+)?answer\b|^(?:please\s+)?(?:let\s+me\s+know|tell\s+me)\b/i;
|
|
431
|
+
|
|
432
|
+
function assistantText(message: AssistantMessage): string {
|
|
433
|
+
return message.content
|
|
434
|
+
.filter((content): content is TextContent => content.type === "text")
|
|
435
|
+
.map(content => content.text)
|
|
436
|
+
.join("\n")
|
|
437
|
+
.trim();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
interface PromptLine {
|
|
441
|
+
text: string;
|
|
442
|
+
hadPromptLabel: boolean;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function promptLine(line: string): PromptLine {
|
|
446
|
+
const withoutMarkdownPrefix = line.trim().replace(MARKDOWN_PROMPT_PREFIX_RE, "").trim();
|
|
447
|
+
const withoutPromptLabel = withoutMarkdownPrefix.replace(PROMPT_LABEL_RE, "").trim();
|
|
448
|
+
return {
|
|
449
|
+
text: withoutPromptLabel,
|
|
450
|
+
hadPromptLabel: withoutPromptLabel !== withoutMarkdownPrefix,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function isQuestionPromptLine(line: string): boolean {
|
|
455
|
+
const candidate = promptLine(line);
|
|
456
|
+
if (!/[??]\s*$/.test(candidate.text)) return false;
|
|
457
|
+
return (
|
|
458
|
+
candidate.hadPromptLabel ||
|
|
459
|
+
QUESTION_PROMPT_RE.test(candidate.text) ||
|
|
460
|
+
USER_DIRECTED_PROMPT_RE.test(candidate.text)
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function isResponseCueLine(line: string): boolean {
|
|
465
|
+
const candidate = promptLine(line)
|
|
466
|
+
.text.replace(/[.!?。!?]+$/, "")
|
|
467
|
+
.trim();
|
|
468
|
+
return USER_RESPONSE_CUE_RE.test(candidate);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function isAwaitingUserAnswer(message: AssistantMessage): boolean {
|
|
472
|
+
const text = assistantText(message);
|
|
473
|
+
if (!text) return false;
|
|
474
|
+
const lastLine = text.split(/\r?\n/).at(-1)?.trim();
|
|
475
|
+
return lastLine !== undefined && (isQuestionPromptLine(lastLine) || isResponseCueLine(lastLine));
|
|
476
|
+
}
|
|
419
477
|
/** `customType` for the hidden mid-run todo nudge; `display: false`, so it reaches
|
|
420
478
|
* the model but never renders in the TUI or transcript. */
|
|
421
479
|
const MID_RUN_TODO_NUDGE_MESSAGE_TYPE = "mid-run-todo-nudge";
|
|
@@ -1507,6 +1565,21 @@ function isAdvisorCard(message: AgentMessage): message is CustomMessage {
|
|
|
1507
1565
|
return message.role === "custom" && message.customType === "advisor";
|
|
1508
1566
|
}
|
|
1509
1567
|
|
|
1568
|
+
function isTerminalTextAssistantAnswer(message: AgentMessage | undefined): message is AssistantMessage {
|
|
1569
|
+
if (message?.role !== "assistant" || message.stopReason !== "stop") return false;
|
|
1570
|
+
let hasText = false;
|
|
1571
|
+
for (const part of message.content) {
|
|
1572
|
+
if (part.type === "toolCall") return false;
|
|
1573
|
+
if (part.type === "text") {
|
|
1574
|
+
if (part.text.trim().length > 0) hasText = true;
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
if (part.type === "thinking" || part.type === "redactedThinking" || part.type === "fallback") continue;
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
return hasText;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1510
1583
|
/**
|
|
1511
1584
|
* A queued message the user can restore to the editor / pull back as a draft.
|
|
1512
1585
|
* Only genuinely user-authored messages qualify: plain user turns, or custom
|
|
@@ -1942,6 +2015,7 @@ export class AgentSession {
|
|
|
1942
2015
|
// on `agent_end` can fire its next `prompt` before #promptWithMessage's finally
|
|
1943
2016
|
#emptyStopRetryCount = 0;
|
|
1944
2017
|
#unexpectedStopRetryCount = 0;
|
|
2018
|
+
#acceptTerminalEmptyStopForPrompt = false;
|
|
1945
2019
|
#promptGeneration = 0;
|
|
1946
2020
|
#pendingAgentEndEmit: AgentSessionEvent | undefined;
|
|
1947
2021
|
#pendingContextSnapshot:
|
|
@@ -1982,6 +2056,7 @@ export class AgentSession {
|
|
|
1982
2056
|
this.#emptyStopRetryCount = 0;
|
|
1983
2057
|
this.#unexpectedStopRetryCount = 0;
|
|
1984
2058
|
this.#yieldTerminationPending = false;
|
|
2059
|
+
this.#acceptTerminalEmptyStopForPrompt = false;
|
|
1985
2060
|
}
|
|
1986
2061
|
|
|
1987
2062
|
#acquirePowerAssertion(): void {
|
|
@@ -2518,7 +2593,7 @@ export class AgentSession {
|
|
|
2518
2593
|
this.#advisorPrimaryTurnsCompleted++;
|
|
2519
2594
|
if (this.#advisors.length > 0) {
|
|
2520
2595
|
for (const a of this.#advisors) {
|
|
2521
|
-
if (!a.runtime.disposed) a.runtime.onTurnEnd(messages);
|
|
2596
|
+
if (!a.runtime.disposed) a.runtime.onTurnEnd(messages, { willContinue: context?.willContinue });
|
|
2522
2597
|
}
|
|
2523
2598
|
const syncBacklog = this.settings.get("advisor.syncBacklog");
|
|
2524
2599
|
if (syncBacklog !== "off") {
|
|
@@ -2862,6 +2937,14 @@ export class AgentSession {
|
|
|
2862
2937
|
|
|
2863
2938
|
const names = config.tools === undefined ? ADVISOR_DEFAULT_TOOL_NAMES : new Set(config.tools);
|
|
2864
2939
|
const tools = (this.#advisorTools ?? []).filter(t => names.has(t.name));
|
|
2940
|
+
const availableAdvisorToolNames = new Set<string>();
|
|
2941
|
+
availableAdvisorToolNames.add(adviseTool.name);
|
|
2942
|
+
for (const tool of tools) {
|
|
2943
|
+
availableAdvisorToolNames.add(tool.name);
|
|
2944
|
+
if (tool.customWireName !== undefined) availableAdvisorToolNames.add(tool.customWireName);
|
|
2945
|
+
}
|
|
2946
|
+
let quarantinedAdvisorOutput: string | undefined;
|
|
2947
|
+
let currentAdvisorInput = "";
|
|
2865
2948
|
|
|
2866
2949
|
const primaryProviderSessionId = this.sessionId;
|
|
2867
2950
|
const advisorSessionLabel = slug
|
|
@@ -2917,6 +3000,13 @@ export class AgentSession {
|
|
|
2917
3000
|
onSseEvent: this.#onSseEvent,
|
|
2918
3001
|
transformProviderContext: this.#transformProviderContext,
|
|
2919
3002
|
intentTracing: false,
|
|
3003
|
+
transformAssistantMessage: message => {
|
|
3004
|
+
quarantinedAdvisorOutput = quarantineAdvisorUnsafeOutput(
|
|
3005
|
+
message,
|
|
3006
|
+
availableAdvisorToolNames,
|
|
3007
|
+
buildAdvisorQuarantineSourceText(currentAdvisorInput, advisorAgent.state.messages),
|
|
3008
|
+
);
|
|
3009
|
+
},
|
|
2920
3010
|
telemetry: advisorTelemetry,
|
|
2921
3011
|
serviceTier: undefined,
|
|
2922
3012
|
serviceTierResolver: advisorServiceTierResolver,
|
|
@@ -2924,7 +3014,19 @@ export class AgentSession {
|
|
|
2924
3014
|
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
2925
3015
|
|
|
2926
3016
|
const advisorAgentFacade: AdvisorAgent = {
|
|
2927
|
-
prompt: input =>
|
|
3017
|
+
prompt: async input => {
|
|
3018
|
+
let quarantined: string | undefined;
|
|
3019
|
+
try {
|
|
3020
|
+
quarantinedAdvisorOutput = undefined;
|
|
3021
|
+
currentAdvisorInput = input;
|
|
3022
|
+
await advisorAgent.prompt(input);
|
|
3023
|
+
quarantined = quarantinedAdvisorOutput;
|
|
3024
|
+
} finally {
|
|
3025
|
+
quarantinedAdvisorOutput = undefined;
|
|
3026
|
+
currentAdvisorInput = "";
|
|
3027
|
+
}
|
|
3028
|
+
if (quarantined) throw new AdvisorOutputQuarantinedError(quarantined);
|
|
3029
|
+
},
|
|
2928
3030
|
abort: reason => advisorAgent.abort(reason),
|
|
2929
3031
|
reset: () => {
|
|
2930
3032
|
advisorAgent.reset();
|
|
@@ -3034,18 +3136,33 @@ export class AgentSession {
|
|
|
3034
3136
|
/**
|
|
3035
3137
|
* Route one accepted advice note from `advisor` to the primary. Concern and
|
|
3036
3138
|
* blocker interrupt the running agent through the steering channel; once the
|
|
3037
|
-
* loop has yielded, `triggerTurn` resumes it.
|
|
3038
|
-
*
|
|
3039
|
-
* card
|
|
3040
|
-
*
|
|
3041
|
-
*
|
|
3042
|
-
*
|
|
3139
|
+
* loop has yielded, `triggerTurn` resumes it. If the loop already ended with a
|
|
3140
|
+
* terminal text answer and no queued work remains, the note is preserved as an
|
|
3141
|
+
* advisor card instead of waking a duplicate completion turn. After a deliberate
|
|
3142
|
+
* user interrupt auto-resume is suppressed while idle/unwinding (the note
|
|
3143
|
+
* becomes a preserved card re-entering on resume); a live-streaming turn is
|
|
3144
|
+
* steered in directly. A plain nit always rides the non-interrupting YieldQueue
|
|
3145
|
+
* aside. Suppression by the per-advisor emission guard drops the note silently —
|
|
3146
|
+
* the model still saw `Recorded.`, so it isn't tempted to rephrase the same note
|
|
3147
|
+
* past the dedupe.
|
|
3043
3148
|
*/
|
|
3149
|
+
#hasTerminalTextAnswerWithoutQueuedWork(): boolean {
|
|
3150
|
+
if (this.agent.hasQueuedMessages() || this.#pendingNextTurnMessages.length > 0) return false;
|
|
3151
|
+
const messages = this.agent.state.messages;
|
|
3152
|
+
let tail = messages.length - 1;
|
|
3153
|
+
while (tail >= 0 && isAdvisorCard(messages[tail])) tail--;
|
|
3154
|
+
return isTerminalTextAssistantAnswer(messages[tail]);
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3044
3157
|
#routeAdvice(advisor: ActiveAdvisor, note: string, severity?: AdvisorSeverity): void {
|
|
3045
3158
|
if (!advisor.emissionGuard.accept(note)) {
|
|
3046
3159
|
logger.debug("advisor advice suppressed by emission guard", { severity, advisor: advisor.name });
|
|
3047
3160
|
return;
|
|
3048
3161
|
}
|
|
3162
|
+
// When newer primary turns already arrived while the advisor model was
|
|
3163
|
+
// processing this batch, the advice was generated without seeing them.
|
|
3164
|
+
// Append a lightweight staleness caveat so the primary can weigh recency.
|
|
3165
|
+
const deliveredNote = annotateForStaleness(note, advisor.runtime.hasFreshBacklog);
|
|
3049
3166
|
// The implicit single ("default") advisor stamps no source name, so its
|
|
3050
3167
|
// agent-facing `<advisory>` bytes stay identical to the pre-multi-advisor path.
|
|
3051
3168
|
const source = advisor.slug ? advisor.name : undefined;
|
|
@@ -3058,13 +3175,14 @@ export class AgentSession {
|
|
|
3058
3175
|
// loop consumes a steer at its next boundary.
|
|
3059
3176
|
streaming: this.agent.state.isStreaming,
|
|
3060
3177
|
aborting: this.#abortInProgress,
|
|
3178
|
+
terminalAnswerNoQueuedWork: this.#hasTerminalTextAnswerWithoutQueuedWork(),
|
|
3061
3179
|
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
3062
3180
|
});
|
|
3063
3181
|
if (channel === "aside") {
|
|
3064
|
-
this.yieldQueue.enqueue("advisor", { note, severity, advisor: source });
|
|
3182
|
+
this.yieldQueue.enqueue("advisor", { note: deliveredNote, severity, advisor: source });
|
|
3065
3183
|
return;
|
|
3066
3184
|
}
|
|
3067
|
-
const notes: AdvisorNote[] = [{ note, severity, advisor: source }];
|
|
3185
|
+
const notes: AdvisorNote[] = [{ note: deliveredNote, severity, advisor: source }];
|
|
3068
3186
|
const content = formatAdvisorBatchContent(notes);
|
|
3069
3187
|
const details = { notes } satisfies AdvisorMessageDetails;
|
|
3070
3188
|
if (channel === "preserve") {
|
|
@@ -4527,7 +4645,7 @@ export class AgentSession {
|
|
|
4527
4645
|
await emitAgentEndNotification();
|
|
4528
4646
|
return;
|
|
4529
4647
|
}
|
|
4530
|
-
const todoContinuationScheduled = await this.#checkTodoCompletion();
|
|
4648
|
+
const todoContinuationScheduled = await this.#checkTodoCompletion(msg);
|
|
4531
4649
|
if (todoContinuationScheduled) {
|
|
4532
4650
|
await emitAgentEndNotification();
|
|
4533
4651
|
return;
|
|
@@ -7124,6 +7242,8 @@ export class AgentSession {
|
|
|
7124
7242
|
abort: () => {
|
|
7125
7243
|
this.agent.abort();
|
|
7126
7244
|
},
|
|
7245
|
+
settings: this.settings,
|
|
7246
|
+
localProtocolOptions: this.#localProtocolOptions(),
|
|
7127
7247
|
});
|
|
7128
7248
|
|
|
7129
7249
|
for (const customTool of mcpTools) {
|
|
@@ -7668,6 +7788,30 @@ export class AgentSession {
|
|
|
7668
7788
|
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model);
|
|
7669
7789
|
}
|
|
7670
7790
|
|
|
7791
|
+
/**
|
|
7792
|
+
* Resolve the explicit thinking suffix that should apply when a temporary
|
|
7793
|
+
* picker selects a model already assigned to a configured role.
|
|
7794
|
+
*/
|
|
7795
|
+
resolveTemporaryModelThinkingLevel(model: Model): ConfiguredThinkingLevel | undefined {
|
|
7796
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
7797
|
+
if (availableModels.length === 0) return undefined;
|
|
7798
|
+
|
|
7799
|
+
const matchPreferences = getModelMatchPreferences(this.settings);
|
|
7800
|
+
for (const role of getKnownRoleIds(this.settings)) {
|
|
7801
|
+
const roleValue = this.settings.getModelRole(role);
|
|
7802
|
+
if (!roleValue) continue;
|
|
7803
|
+
|
|
7804
|
+
const resolved = resolveModelRoleValue(roleValue, availableModels, {
|
|
7805
|
+
settings: this.settings,
|
|
7806
|
+
matchPreferences,
|
|
7807
|
+
});
|
|
7808
|
+
if (!resolved.explicitThinkingLevel || resolved.thinkingLevel === undefined || !resolved.model) continue;
|
|
7809
|
+
if (modelsAreEqual(resolved.model, model)) return resolved.thinkingLevel;
|
|
7810
|
+
}
|
|
7811
|
+
|
|
7812
|
+
return undefined;
|
|
7813
|
+
}
|
|
7814
|
+
|
|
7671
7815
|
get promptTemplates(): ReadonlyArray<PromptTemplate> {
|
|
7672
7816
|
return this.#promptTemplates;
|
|
7673
7817
|
}
|
|
@@ -8172,6 +8316,7 @@ export class AgentSession {
|
|
|
8172
8316
|
options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck"> & {
|
|
8173
8317
|
prependMessages?: AgentMessage[];
|
|
8174
8318
|
skipPostPromptRecoveryWait?: boolean;
|
|
8319
|
+
acceptTerminalEmptyStop?: boolean;
|
|
8175
8320
|
},
|
|
8176
8321
|
): Promise<void> {
|
|
8177
8322
|
this.#beginInFlight();
|
|
@@ -8188,6 +8333,7 @@ export class AgentSession {
|
|
|
8188
8333
|
this.#mutationsSinceLastTodoTouch = 0;
|
|
8189
8334
|
this.#midRunNudgeCount = 0;
|
|
8190
8335
|
this.#resetPromptMaintenanceState();
|
|
8336
|
+
this.#acceptTerminalEmptyStopForPrompt = options?.acceptTerminalEmptyStop === true;
|
|
8191
8337
|
|
|
8192
8338
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
8193
8339
|
|
|
@@ -8729,12 +8875,21 @@ export class AgentSession {
|
|
|
8729
8875
|
}
|
|
8730
8876
|
}
|
|
8731
8877
|
|
|
8732
|
-
async #promptAgentInitiatedMessage(
|
|
8878
|
+
async #promptAgentInitiatedMessage(
|
|
8879
|
+
message: CustomMessage,
|
|
8880
|
+
options?: { acceptTerminalEmptyStop?: boolean },
|
|
8881
|
+
): Promise<void> {
|
|
8733
8882
|
this.#beginInFlight();
|
|
8734
8883
|
try {
|
|
8884
|
+
const acceptTerminalEmptyStop = options?.acceptTerminalEmptyStop === true;
|
|
8885
|
+
if (acceptTerminalEmptyStop) {
|
|
8886
|
+
this.#resetPromptMaintenanceState();
|
|
8887
|
+
}
|
|
8888
|
+
this.#acceptTerminalEmptyStopForPrompt = acceptTerminalEmptyStop;
|
|
8735
8889
|
await this.agent.prompt(message);
|
|
8736
8890
|
await this.#waitForPostPromptRecovery();
|
|
8737
8891
|
} finally {
|
|
8892
|
+
this.#acceptTerminalEmptyStopForPrompt = false;
|
|
8738
8893
|
this.#endInFlight();
|
|
8739
8894
|
}
|
|
8740
8895
|
}
|
|
@@ -8789,7 +8944,12 @@ export class AgentSession {
|
|
|
8789
8944
|
*/
|
|
8790
8945
|
async sendCustomMessage<T = unknown>(
|
|
8791
8946
|
message: CustomMessagePayload<T>,
|
|
8792
|
-
options?: {
|
|
8947
|
+
options?: {
|
|
8948
|
+
triggerTurn?: boolean;
|
|
8949
|
+
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
8950
|
+
queueChipText?: string;
|
|
8951
|
+
acceptTerminalEmptyStop?: boolean;
|
|
8952
|
+
},
|
|
8793
8953
|
): Promise<boolean> {
|
|
8794
8954
|
const normalizedPayload = normalizeCustomMessagePayload<T>(message);
|
|
8795
8955
|
const details =
|
|
@@ -8832,7 +8992,9 @@ export class AgentSession {
|
|
|
8832
8992
|
this.#queueHiddenNextTurnMessage(normalizedAppMessage, false);
|
|
8833
8993
|
return false;
|
|
8834
8994
|
}
|
|
8835
|
-
await this.#promptAgentInitiatedMessage(normalizedAppMessage
|
|
8995
|
+
await this.#promptAgentInitiatedMessage(normalizedAppMessage, {
|
|
8996
|
+
acceptTerminalEmptyStop: options.acceptTerminalEmptyStop === true,
|
|
8997
|
+
});
|
|
8836
8998
|
return true;
|
|
8837
8999
|
}
|
|
8838
9000
|
this.agent.appendMessage(normalizedAppMessage);
|
|
@@ -11344,6 +11506,13 @@ export class AgentSession {
|
|
|
11344
11506
|
return false;
|
|
11345
11507
|
}
|
|
11346
11508
|
|
|
11509
|
+
if (this.#acceptTerminalEmptyStopForPrompt && assistantMessage.stopReason === "stop") {
|
|
11510
|
+
this.#acceptTerminalEmptyStopForPrompt = false;
|
|
11511
|
+
this.#discardAcceptedTerminalEmptyStop(assistantMessage);
|
|
11512
|
+
this.#emptyStopRetryCount = 0;
|
|
11513
|
+
return false;
|
|
11514
|
+
}
|
|
11515
|
+
|
|
11347
11516
|
this.#emptyStopRetryCount++;
|
|
11348
11517
|
if (this.#emptyStopRetryCount > EMPTY_STOP_MAX_RETRIES) {
|
|
11349
11518
|
const attempts = this.#emptyStopRetryCount - 1;
|
|
@@ -11561,6 +11730,38 @@ export class AgentSession {
|
|
|
11561
11730
|
this.agent.appendMessage(assistantMessage);
|
|
11562
11731
|
}
|
|
11563
11732
|
|
|
11733
|
+
#discardAcceptedTerminalEmptyStop(assistantMessage: AssistantMessage): void {
|
|
11734
|
+
const branch = this.sessionManager.getBranch();
|
|
11735
|
+
const branchEntry = branch
|
|
11736
|
+
.slice()
|
|
11737
|
+
.reverse()
|
|
11738
|
+
.find(
|
|
11739
|
+
entry =>
|
|
11740
|
+
entry.type === "message" &&
|
|
11741
|
+
entry.message.role === "assistant" &&
|
|
11742
|
+
this.#isSameAssistantMessage(entry.message, assistantMessage),
|
|
11743
|
+
);
|
|
11744
|
+
const parentEntry =
|
|
11745
|
+
branchEntry?.parentId === null || branchEntry?.parentId === undefined
|
|
11746
|
+
? undefined
|
|
11747
|
+
: branch.find(entry => entry.id === branchEntry.parentId);
|
|
11748
|
+
const prunePrompt = parentEntry?.type === "custom_message";
|
|
11749
|
+
|
|
11750
|
+
this.#removeAssistantMessageFromActiveContext(assistantMessage, "accepted-terminal-empty-stop");
|
|
11751
|
+
if (prunePrompt && this.agent.state.messages.at(-1)?.role === "custom") {
|
|
11752
|
+
this.agent.replaceMessages(this.agent.state.messages.slice(0, -1));
|
|
11753
|
+
}
|
|
11754
|
+
|
|
11755
|
+
if (!branchEntry) return;
|
|
11756
|
+
const targetParentId = prunePrompt ? parentEntry.parentId : branchEntry.parentId;
|
|
11757
|
+
if (targetParentId === null) {
|
|
11758
|
+
this.sessionManager.resetLeaf();
|
|
11759
|
+
} else {
|
|
11760
|
+
this.sessionManager.branch(targetParentId);
|
|
11761
|
+
}
|
|
11762
|
+
this.sessionManager.appendCustomEntry("accepted-terminal-empty-stop");
|
|
11763
|
+
}
|
|
11764
|
+
|
|
11564
11765
|
/**
|
|
11565
11766
|
* Drop an assistant turn from BOTH the live agent context and the persisted
|
|
11566
11767
|
* session branch (reparenting the leaf to the turn's parent), so a discarded
|
|
@@ -11890,7 +12091,7 @@ export class AgentSession {
|
|
|
11890
12091
|
/**
|
|
11891
12092
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
11892
12093
|
*/
|
|
11893
|
-
async #checkTodoCompletion(): Promise<boolean> {
|
|
12094
|
+
async #checkTodoCompletion(message: AssistantMessage): Promise<boolean> {
|
|
11894
12095
|
// Skip todo reminders when the most recent turn was driven by an explicit user force —
|
|
11895
12096
|
// the user wanted exactly that tool, not a follow-up nag about incomplete todos.
|
|
11896
12097
|
const lastServedLabel = this.#toolChoiceQueue.consumeLastServedLabel();
|
|
@@ -11955,6 +12156,13 @@ export class AgentSession {
|
|
|
11955
12156
|
return false;
|
|
11956
12157
|
}
|
|
11957
12158
|
|
|
12159
|
+
if (isAwaitingUserAnswer(message)) {
|
|
12160
|
+
logger.debug("Todo completion: assistant is waiting for user input; skipping reminder", {
|
|
12161
|
+
incomplete: incomplete.length,
|
|
12162
|
+
});
|
|
12163
|
+
return false;
|
|
12164
|
+
}
|
|
12165
|
+
|
|
11958
12166
|
// Background async jobs (bash/task) owned by this agent re-wake the loop
|
|
11959
12167
|
// when they complete: the result delivery enqueues an async-result
|
|
11960
12168
|
// follow-up that continues the run, and todos are re-evaluated at that
|
|
@@ -15521,7 +15729,7 @@ export class AgentSession {
|
|
|
15521
15729
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
15522
15730
|
this.#rekeyMnemopiMemoryForCurrentSessionId();
|
|
15523
15731
|
|
|
15524
|
-
|
|
15732
|
+
let sessionContext = this.buildDisplaySessionContext();
|
|
15525
15733
|
const didReloadConversationChange =
|
|
15526
15734
|
previousSessionContext !== undefined &&
|
|
15527
15735
|
this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
|
|
@@ -15579,6 +15787,20 @@ export class AgentSession {
|
|
|
15579
15787
|
}
|
|
15580
15788
|
}
|
|
15581
15789
|
|
|
15790
|
+
const model = this.model;
|
|
15791
|
+
if (model) {
|
|
15792
|
+
const interruptedTurnAbort = createInterruptedTurnAbortMessage(this.sessionManager.getBranch(), {
|
|
15793
|
+
api: model.api,
|
|
15794
|
+
provider: model.provider,
|
|
15795
|
+
model: model.id,
|
|
15796
|
+
});
|
|
15797
|
+
if (interruptedTurnAbort) {
|
|
15798
|
+
this.sessionManager.appendMessage(interruptedTurnAbort);
|
|
15799
|
+
sessionContext = this.buildDisplaySessionContext();
|
|
15800
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
15801
|
+
}
|
|
15802
|
+
}
|
|
15803
|
+
|
|
15582
15804
|
const hasThinkingEntry = this.sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
15583
15805
|
const hasServiceTierEntry = this.sessionManager
|
|
15584
15806
|
.getBranch()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
3
|
import type { SessionEntry } from "./session-entries";
|
|
3
4
|
|
|
4
5
|
export const TOOL_EXECUTION_START_CUSTOM_TYPE = "tool_execution_start";
|
|
@@ -53,10 +54,117 @@ interface ToolCallContent {
|
|
|
53
54
|
arguments?: unknown;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
export interface AssistantModelMetadata {
|
|
58
|
+
api: AssistantMessage["api"];
|
|
59
|
+
provider: string;
|
|
60
|
+
model: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
57
64
|
if (typeof value !== "object") return false;
|
|
58
65
|
return value !== null;
|
|
59
66
|
}
|
|
67
|
+
function isPendingToolCallDiagnostic(value: unknown): value is PendingToolCallDiagnostic {
|
|
68
|
+
if (!isObject(value) || typeof value.toolName !== "string") return false;
|
|
69
|
+
if ("toolCallId" in value && typeof value.toolCallId !== "string") return false;
|
|
70
|
+
if ("intent" in value && typeof value.intent !== "string") return false;
|
|
71
|
+
if ("assistantTimestamp" in value && typeof value.assistantTimestamp !== "number") return false;
|
|
72
|
+
if ("startedAt" in value && typeof value.startedAt !== "string") return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readPendingToolCalls(value: unknown): PendingToolCallDiagnostic[] | undefined {
|
|
77
|
+
if (!Array.isArray(value) || !value.every(isPendingToolCallDiagnostic)) return undefined;
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readSessionExit(entry: SessionEntry): SessionExitData | undefined {
|
|
82
|
+
if (entry.type !== "custom" || entry.customType !== SESSION_EXIT_CUSTOM_TYPE || !isObject(entry.data)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const { reason, kind, recordedAt } = entry.data;
|
|
86
|
+
if (
|
|
87
|
+
typeof reason !== "string" ||
|
|
88
|
+
(kind !== "normal" && kind !== "signal" && kind !== "fatal" && kind !== "process_exit") ||
|
|
89
|
+
typeof recordedAt !== "string"
|
|
90
|
+
) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
reason,
|
|
95
|
+
kind,
|
|
96
|
+
recordedAt,
|
|
97
|
+
pendingToolCalls: readPendingToolCalls(entry.data.pendingToolCalls),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* createInterruptedTurnAbortMessage returns a terminal assistant record when
|
|
103
|
+
* the latest persisted process exit follows a non-terminal conversation tail.
|
|
104
|
+
*/
|
|
105
|
+
export function createInterruptedTurnAbortMessage(
|
|
106
|
+
entries: readonly SessionEntry[],
|
|
107
|
+
fallbackModel?: AssistantModelMetadata,
|
|
108
|
+
): AssistantMessage | undefined {
|
|
109
|
+
let exitIndex = -1;
|
|
110
|
+
let exit: SessionExitData | undefined;
|
|
111
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
112
|
+
const candidate = readSessionExit(entries[index]!);
|
|
113
|
+
if (!candidate) continue;
|
|
114
|
+
exitIndex = index;
|
|
115
|
+
exit = candidate;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (!exit || (exit.kind === "normal" && !exit.pendingToolCalls?.length)) return undefined;
|
|
119
|
+
|
|
120
|
+
let tailIndex = -1;
|
|
121
|
+
let tail: AgentMessage | undefined;
|
|
122
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
123
|
+
const entry = entries[index]!;
|
|
124
|
+
if (entry.type !== "message") continue;
|
|
125
|
+
tailIndex = index;
|
|
126
|
+
tail = entry.message;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (!tail || tailIndex > exitIndex) return undefined;
|
|
130
|
+
if (tail.role === "assistant" && !tail.content.some(isToolCallContent)) return undefined;
|
|
131
|
+
|
|
132
|
+
let previousAssistant: AssistantMessage | undefined;
|
|
133
|
+
for (let index = tailIndex; index >= 0; index--) {
|
|
134
|
+
const entry = entries[index]!;
|
|
135
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
136
|
+
previousAssistant = entry.message;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
tail.role === "toolResult" &&
|
|
141
|
+
(previousAssistant?.stopReason === "error" || previousAssistant?.stopReason === "aborted")
|
|
142
|
+
) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const model = previousAssistant ?? fallbackModel;
|
|
146
|
+
if (!model) return undefined;
|
|
147
|
+
|
|
148
|
+
const recordedAt = Date.parse(exit.recordedAt);
|
|
149
|
+
return {
|
|
150
|
+
role: "assistant",
|
|
151
|
+
content: [],
|
|
152
|
+
api: model.api,
|
|
153
|
+
provider: model.provider,
|
|
154
|
+
model: model.model,
|
|
155
|
+
usage: {
|
|
156
|
+
input: 0,
|
|
157
|
+
output: 0,
|
|
158
|
+
cacheRead: 0,
|
|
159
|
+
cacheWrite: 0,
|
|
160
|
+
totalTokens: 0,
|
|
161
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
162
|
+
},
|
|
163
|
+
stopReason: "aborted",
|
|
164
|
+
errorMessage: "Previous OMP process exited before completing the turn.",
|
|
165
|
+
timestamp: Number.isFinite(recordedAt) ? recordedAt : Date.now(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
60
168
|
|
|
61
169
|
function isToolCallContent(value: unknown): value is ToolCallContent {
|
|
62
170
|
if (!isObject(value)) return false;
|
|
@@ -735,6 +735,7 @@ export class OutputSink {
|
|
|
735
735
|
#truncated = false;
|
|
736
736
|
#lastChunkTime = 0;
|
|
737
737
|
#pendingChunk = "";
|
|
738
|
+
#pendingChunkTimer: Timer | undefined;
|
|
738
739
|
|
|
739
740
|
// Per-line column cap streaming state (persists across `push` calls so a
|
|
740
741
|
// long line split across chunks still trips the same trigger).
|
|
@@ -806,20 +807,18 @@ export class OutputSink {
|
|
|
806
807
|
push(chunk: string): void {
|
|
807
808
|
chunk = sanitizeWithOptionalSixelPassthrough(chunk, sanitizeText);
|
|
808
809
|
|
|
809
|
-
// Throttled onChunk: coalesce chunks arriving inside the throttle window
|
|
810
|
-
//
|
|
811
|
-
// final
|
|
810
|
+
// Throttled onChunk: coalesce chunks arriving inside the throttle window.
|
|
811
|
+
// A timer flushes quiet tails at the throttle boundary; dump() catches a
|
|
812
|
+
// final pending chunk when the process exits before that timer fires.
|
|
812
813
|
// Live preview gets the raw (pre-cap) chunk so the TUI never lags behind
|
|
813
814
|
// what reached the sink — the column cap is for the persisted LLM view.
|
|
814
815
|
if (this.#onChunk) {
|
|
815
816
|
const now = Date.now();
|
|
816
817
|
if (now - this.#lastChunkTime >= this.#chunkThrottleMs) {
|
|
817
|
-
this.#
|
|
818
|
-
const merged = this.#pendingChunk + chunk;
|
|
819
|
-
this.#pendingChunk = "";
|
|
820
|
-
this.#onChunk(merged);
|
|
818
|
+
this.#emitPendingChunkWith(chunk, now);
|
|
821
819
|
} else {
|
|
822
820
|
this.#pendingChunk += chunk;
|
|
821
|
+
this.#schedulePendingChunkFlush();
|
|
823
822
|
}
|
|
824
823
|
}
|
|
825
824
|
|
|
@@ -1121,6 +1120,7 @@ export class OutputSink {
|
|
|
1121
1120
|
* branch in `dump()` against stale totals.
|
|
1122
1121
|
*/
|
|
1123
1122
|
replace(text: string): void {
|
|
1123
|
+
this.#clearPendingChunkTimer();
|
|
1124
1124
|
this.#buffer = text;
|
|
1125
1125
|
this.#bufferBytes = Buffer.byteLength(text, "utf-8");
|
|
1126
1126
|
this.#head = "";
|
|
@@ -1138,6 +1138,38 @@ export class OutputSink {
|
|
|
1138
1138
|
this.#pendingChunk = "";
|
|
1139
1139
|
}
|
|
1140
1140
|
|
|
1141
|
+
#clearPendingChunkTimer(): void {
|
|
1142
|
+
if (!this.#pendingChunkTimer) return;
|
|
1143
|
+
clearTimeout(this.#pendingChunkTimer);
|
|
1144
|
+
this.#pendingChunkTimer = undefined;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
#emitPendingChunkWith(chunk: string, now: number): void {
|
|
1148
|
+
this.#clearPendingChunkTimer();
|
|
1149
|
+
this.#lastChunkTime = now;
|
|
1150
|
+
const merged = this.#pendingChunk + chunk;
|
|
1151
|
+
this.#pendingChunk = "";
|
|
1152
|
+
this.#onChunk?.(merged);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
#flushPendingChunk(): void {
|
|
1156
|
+
if (this.#pendingChunk.length === 0) {
|
|
1157
|
+
this.#clearPendingChunkTimer();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
this.#emitPendingChunkWith("", Date.now());
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
#schedulePendingChunkFlush(): void {
|
|
1164
|
+
if (this.#chunkThrottleMs <= 0 || this.#pendingChunkTimer) return;
|
|
1165
|
+
const elapsed = Date.now() - this.#lastChunkTime;
|
|
1166
|
+
const delay = Math.max(0, this.#chunkThrottleMs - elapsed);
|
|
1167
|
+
this.#pendingChunkTimer = setTimeout(() => {
|
|
1168
|
+
this.#pendingChunkTimer = undefined;
|
|
1169
|
+
this.#flushPendingChunk();
|
|
1170
|
+
}, delay);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1141
1173
|
/**
|
|
1142
1174
|
* Replay the rolling tail ring back into the artifact sink. When bytes
|
|
1143
1175
|
* were actually dropped from the middle (the head budget was exhausted
|
|
@@ -1179,11 +1211,7 @@ export class OutputSink {
|
|
|
1179
1211
|
|
|
1180
1212
|
// Flush any chunk still held back by the throttle so the live preview
|
|
1181
1213
|
// ends with the complete stream.
|
|
1182
|
-
|
|
1183
|
-
const pending = this.#pendingChunk;
|
|
1184
|
-
this.#pendingChunk = "";
|
|
1185
|
-
this.#onChunk(pending);
|
|
1186
|
-
}
|
|
1214
|
+
this.#flushPendingChunk();
|
|
1187
1215
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
1188
1216
|
|
|
1189
1217
|
if (this.#file) {
|