@oh-my-pi/pi-coding-agent 16.1.14 → 16.1.16
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 +69 -0
- package/dist/cli.js +3705 -4015
- package/dist/types/advisor/runtime.d.ts +3 -0
- package/dist/types/cli/args.d.ts +2 -5
- package/dist/types/cli/flag-tables.d.ts +2 -2
- package/dist/types/cli/session-picker.d.ts +4 -2
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +32 -1
- package/dist/types/eval/agent-bridge.d.ts +19 -0
- package/dist/types/eval/js/shared/helpers.d.ts +1 -13
- package/dist/types/eval/js/shared/types.d.ts +1 -1
- package/dist/types/eval/js/worker-protocol.d.ts +1 -1
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/export/share.d.ts +8 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/transports/stdio.d.ts +12 -1
- package/dist/types/modes/components/plugin-settings.d.ts +5 -0
- package/dist/types/modes/components/session-selector.d.ts +25 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/secrets/obfuscator.d.ts +3 -3
- package/dist/types/task/isolation-runner.d.ts +128 -0
- package/dist/types/task/worktree.d.ts +14 -1
- package/dist/types/thinking.d.ts +15 -0
- package/dist/types/tools/eval-render.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +11 -17
- package/dist/types/tools/todo.d.ts +26 -28
- package/dist/types/tui/output-block.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/shell-snapshot.d.ts +10 -0
- package/dist/types/web/search/providers/exa.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +17 -3
- package/package.json +12 -12
- package/scripts/build-binary.ts +18 -4
- package/src/advisor/__tests__/advisor.test.ts +114 -0
- package/src/advisor/runtime.ts +129 -1
- package/src/cli/args.ts +4 -5
- package/src/cli/flag-tables.ts +3 -3
- package/src/cli/gallery-fixtures/interaction.ts +6 -9
- package/src/cli/gallery-fixtures/shell.ts +15 -23
- package/src/cli/session-picker.ts +17 -3
- package/src/commands/launch.ts +3 -3
- package/src/config/model-registry.ts +12 -4
- package/src/config/settings-schema.ts +37 -1
- package/src/edit/renderer.ts +34 -12
- package/src/eval/__tests__/agent-bridge.test.ts +462 -3
- package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
- package/src/eval/__tests__/julia-prelude.test.ts +1 -30
- package/src/eval/__tests__/prelude-agent.test.ts +42 -8
- package/src/eval/agent-bridge.ts +301 -71
- package/src/eval/jl/prelude.jl +32 -227
- package/src/eval/jl/runner.jl +38 -12
- package/src/eval/js/shared/helpers.ts +1 -114
- package/src/eval/js/shared/prelude.txt +13 -27
- package/src/eval/js/shared/runtime.ts +0 -6
- package/src/eval/js/shared/types.ts +1 -1
- package/src/eval/js/worker-protocol.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +13 -0
- package/src/eval/py/executor.ts +1 -1
- package/src/eval/py/prelude.py +47 -105
- package/src/eval/py/runner.py +0 -6
- package/src/eval/rb/prelude.rb +21 -189
- package/src/eval/rb/runner.rb +116 -9
- package/src/exec/bash-executor.ts +44 -0
- package/src/export/html/tool-views.generated.js +29 -29
- package/src/export/share.ts +51 -28
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/local-protocol.ts +100 -53
- package/src/main.ts +15 -4
- package/src/mcp/transports/stdio.ts +20 -4
- package/src/modes/acp/acp-event-mapper.ts +7 -2
- package/src/modes/components/custom-editor.test.ts +22 -0
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/footer.ts +4 -3
- package/src/modes/components/plugin-settings.ts +7 -1
- package/src/modes/components/session-selector.ts +143 -29
- package/src/modes/components/status-line/component.ts +5 -1
- package/src/modes/components/status-line/context-thresholds.ts +11 -3
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/controllers/command-controller.ts +6 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/utils/copy-targets.ts +7 -2
- package/src/prompts/system/system-prompt.md +6 -6
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/bash.md +18 -2
- package/src/prompts/tools/eval.md +19 -19
- package/src/prompts/tools/search.md +1 -0
- package/src/prompts/tools/todo.md +1 -1
- package/src/secrets/obfuscator.ts +3 -9
- package/src/session/agent-session.ts +264 -52
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/task/index.ts +61 -207
- package/src/task/isolation-runner.ts +354 -0
- package/src/task/worktree.ts +46 -9
- package/src/thinking.ts +20 -0
- package/src/tools/ask.ts +44 -38
- package/src/tools/bash.ts +9 -2
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval-render.ts +34 -27
- package/src/tools/eval.ts +100 -103
- package/src/tools/index.ts +8 -1
- package/src/tools/read.ts +136 -60
- package/src/tools/todo.ts +60 -64
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +11 -0
- package/src/utils/image-resize.ts +30 -0
- package/src/utils/shell-snapshot.ts +63 -1
- package/src/web/search/providers/exa.ts +85 -1
- package/src/web/search/providers/perplexity.ts +18 -6
|
@@ -55,8 +55,9 @@ import {
|
|
|
55
55
|
effectiveReserveTokens,
|
|
56
56
|
estimateTokens,
|
|
57
57
|
generateBranchSummary,
|
|
58
|
-
|
|
58
|
+
generateHandoffFromContext,
|
|
59
59
|
prepareCompaction,
|
|
60
|
+
renderHandoffPrompt,
|
|
60
61
|
resolveThresholdTokens,
|
|
61
62
|
type SessionEntry,
|
|
62
63
|
type SessionMessageEntry,
|
|
@@ -103,7 +104,7 @@ import {
|
|
|
103
104
|
resolveServiceTier,
|
|
104
105
|
streamSimple,
|
|
105
106
|
} from "@oh-my-pi/pi-ai";
|
|
106
|
-
import {
|
|
107
|
+
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
107
108
|
import { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
108
109
|
import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
109
110
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
@@ -1619,6 +1620,7 @@ export class AgentSession {
|
|
|
1619
1620
|
await this.#advisorRuntime.waitForCatchup(30000, threshold, signal);
|
|
1620
1621
|
}
|
|
1621
1622
|
}
|
|
1623
|
+
await this.#maintainContextMidRun(messages, signal);
|
|
1622
1624
|
});
|
|
1623
1625
|
this.yieldQueue = new YieldQueue({
|
|
1624
1626
|
isStreaming: () => this.isStreaming,
|
|
@@ -1936,6 +1938,7 @@ export class AgentSession {
|
|
|
1936
1938
|
snapshotMessages: () => this.agent.state.messages,
|
|
1937
1939
|
enqueueAdvice,
|
|
1938
1940
|
maintainContext: incomingTokens => this.#maintainAdvisorContext(incomingTokens),
|
|
1941
|
+
obfuscator: this.#obfuscator,
|
|
1939
1942
|
});
|
|
1940
1943
|
if (seedToCurrent) {
|
|
1941
1944
|
this.#advisorRuntime.seedTo(this.agent.state.messages.length);
|
|
@@ -2776,10 +2779,32 @@ export class AgentSession {
|
|
|
2776
2779
|
this.#lastAssistantMessage = undefined;
|
|
2777
2780
|
if (!msg) {
|
|
2778
2781
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2782
|
+
logger.debug("agent_end maintenance routing", {
|
|
2783
|
+
reason: "no-assistant-message",
|
|
2784
|
+
goalModeEnabled: this.#goalModeState?.enabled === true,
|
|
2785
|
+
goalStatus: this.#goalModeState?.goal.status,
|
|
2786
|
+
});
|
|
2779
2787
|
await emitAgentEndNotification();
|
|
2780
2788
|
return;
|
|
2781
2789
|
}
|
|
2782
2790
|
|
|
2791
|
+
const maintenanceRoute = (route: string, extra?: Record<string, unknown>) => {
|
|
2792
|
+
logger.debug("agent_end maintenance routing", {
|
|
2793
|
+
route,
|
|
2794
|
+
stopReason: msg.stopReason,
|
|
2795
|
+
provider: msg.provider,
|
|
2796
|
+
model: msg.model,
|
|
2797
|
+
contentBlocks: msg.content.length,
|
|
2798
|
+
hasToolCalls: msg.content.some(content => content.type === "toolCall"),
|
|
2799
|
+
hasText: msg.content.some(content => content.type === "text"),
|
|
2800
|
+
goalModeEnabled: this.#goalModeState?.enabled === true,
|
|
2801
|
+
goalStatus: this.#goalModeState?.goal.status,
|
|
2802
|
+
successfulYield: this.#assistantEndedWithSuccessfulYield(msg),
|
|
2803
|
+
...extra,
|
|
2804
|
+
});
|
|
2805
|
+
};
|
|
2806
|
+
maintenanceRoute("entered");
|
|
2807
|
+
|
|
2783
2808
|
// Invalidate GitHub Copilot credentials on auth failure so stale tokens
|
|
2784
2809
|
// aren't reused on the next request
|
|
2785
2810
|
if (
|
|
@@ -2793,27 +2818,61 @@ export class AgentSession {
|
|
|
2793
2818
|
if (this.#skipPostTurnMaintenanceAssistantTimestamp === msg.timestamp) {
|
|
2794
2819
|
this.#skipPostTurnMaintenanceAssistantTimestamp = undefined;
|
|
2795
2820
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2821
|
+
maintenanceRoute("skip-post-turn-maintenance");
|
|
2796
2822
|
await emitAgentEndNotification();
|
|
2797
2823
|
return;
|
|
2798
2824
|
}
|
|
2799
2825
|
|
|
2826
|
+
const activeGoal = this.#goalModeState?.enabled === true && this.#goalModeState.goal.status === "active";
|
|
2800
2827
|
if (this.#assistantEndedWithSuccessfulYield(msg)) {
|
|
2801
2828
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2802
|
-
if (
|
|
2829
|
+
if (activeGoal) {
|
|
2830
|
+
maintenanceRoute("successful-yield-active-goal-checkCompaction");
|
|
2803
2831
|
const compactionTask = this.#checkCompaction(msg);
|
|
2804
2832
|
this.#trackPostPromptTask(compactionTask);
|
|
2805
2833
|
await compactionTask;
|
|
2834
|
+
} else {
|
|
2835
|
+
maintenanceRoute("successful-yield-no-active-goal");
|
|
2806
2836
|
}
|
|
2807
2837
|
await emitAgentEndNotification();
|
|
2808
2838
|
return;
|
|
2809
2839
|
}
|
|
2810
2840
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2811
2841
|
|
|
2842
|
+
// Empty-stop cleanup MUST run before any compaction continuation: an
|
|
2843
|
+
// empty toolUse stop must be stripped from active context + session
|
|
2844
|
+
// history before we schedule another turn, otherwise the next
|
|
2845
|
+
// Anthropic turn carries a tool_use block with no matching
|
|
2846
|
+
// tool_result and corrupts message history. The handler also
|
|
2847
|
+
// schedules its own retry, so a real empty stop never needs the
|
|
2848
|
+
// active-goal threshold pre-empt below.
|
|
2812
2849
|
if (await this.#handleEmptyAssistantStop(msg)) {
|
|
2850
|
+
maintenanceRoute("empty-stop-handled");
|
|
2813
2851
|
await emitAgentEndNotification();
|
|
2814
2852
|
return;
|
|
2815
2853
|
}
|
|
2854
|
+
|
|
2855
|
+
let compactionResult = COMPACTION_CHECK_NONE;
|
|
2856
|
+
let checkedCompaction = false;
|
|
2857
|
+
if (activeGoal) {
|
|
2858
|
+
maintenanceRoute("active-goal-pre-empt-checkCompaction");
|
|
2859
|
+
const compactionTask = this.#checkCompaction(msg);
|
|
2860
|
+
this.#trackPostPromptTask(compactionTask);
|
|
2861
|
+
compactionResult = await compactionTask;
|
|
2862
|
+
checkedCompaction = true;
|
|
2863
|
+
if (compactionResult.deferredHandoff || compactionResult.continuationScheduled) {
|
|
2864
|
+
maintenanceRoute("active-goal-pre-empt-continuation-scheduled", {
|
|
2865
|
+
deferredHandoff: compactionResult.deferredHandoff,
|
|
2866
|
+
continuationScheduled: compactionResult.continuationScheduled,
|
|
2867
|
+
});
|
|
2868
|
+
this.#resolveRetry();
|
|
2869
|
+
await emitAgentEndNotification();
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2816
2874
|
if (await this.#handleUnexpectedAssistantStop(msg)) {
|
|
2875
|
+
maintenanceRoute("unexpected-stop-handled");
|
|
2817
2876
|
await emitAgentEndNotification();
|
|
2818
2877
|
return;
|
|
2819
2878
|
}
|
|
@@ -2853,9 +2912,12 @@ export class AgentSession {
|
|
|
2853
2912
|
}
|
|
2854
2913
|
this.#resolveRetry();
|
|
2855
2914
|
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2915
|
+
if (!checkedCompaction) {
|
|
2916
|
+
maintenanceRoute("bottom-checkCompaction");
|
|
2917
|
+
const compactionTask = this.#checkCompaction(msg);
|
|
2918
|
+
this.#trackPostPromptTask(compactionTask);
|
|
2919
|
+
compactionResult = await compactionTask;
|
|
2920
|
+
}
|
|
2859
2921
|
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
2860
2922
|
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
2861
2923
|
if (hasToolCalls) {
|
|
@@ -8048,27 +8110,60 @@ export class AgentSession {
|
|
|
8048
8110
|
throw new Error(`No API key for ${model.provider}`);
|
|
8049
8111
|
}
|
|
8050
8112
|
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8113
|
+
// Build the handoff request through the SAME pipeline a live turn uses
|
|
8114
|
+
// (`runEphemeralTurn` / `/btw` share it) so the oneshot reads the
|
|
8115
|
+
// provider prompt cache the main turn populated instead of cold-missing
|
|
8116
|
+
// the whole prefix: identical system prompt, normalized tools, and
|
|
8117
|
+
// transform-/obfuscation-matched message history via
|
|
8118
|
+
// `convertMessagesToLlm` + `buildSideRequestContext`, plus the live turn's
|
|
8119
|
+
// effective provider cache key with a unique side `sessionId` so
|
|
8120
|
+
// OpenAI/Codex append-only state never mixes with the live turn.
|
|
8121
|
+
const cacheSessionId = this.sessionId;
|
|
8122
|
+
// The loop sends `promptCacheKey` (providerPromptCacheKey) and falls back to
|
|
8123
|
+
// the provider session id; providers route on `promptCacheKey ?? sessionId`.
|
|
8124
|
+
// Both can diverge from this.sessionId (tan/subagent/shared sessions), so
|
|
8125
|
+
// mirror exactly what the live turn populated the cache under.
|
|
8126
|
+
const handoffPromptCacheKey = this.agent.promptCacheKey ?? this.agent.sessionId;
|
|
8127
|
+
const handoffPromptText = renderHandoffPrompt(this.#obfuscateTextForProvider(customInstructions));
|
|
8128
|
+
const handoffSnapshot: AgentMessage[] = [
|
|
8129
|
+
...this.agent.state.messages,
|
|
8055
8130
|
{
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8131
|
+
role: "user",
|
|
8132
|
+
content: [{ type: "text", text: handoffPromptText }],
|
|
8133
|
+
attribution: "agent",
|
|
8134
|
+
timestamp: Date.now(),
|
|
8135
|
+
},
|
|
8136
|
+
];
|
|
8137
|
+
const handoffLlmMessages = await this.convertMessagesToLlm(handoffSnapshot, handoffSignal);
|
|
8138
|
+
// Base system prompt, not a per-turn `before_agent_start` hook override —
|
|
8139
|
+
// the handoff seeds a fresh session and must not carry prompt-specific
|
|
8140
|
+
// hook state. Matches the prompt the old handoff path sent.
|
|
8141
|
+
const handoffContext = await this.agent.buildSideRequestContext(handoffLlmMessages, this.#baseSystemPrompt);
|
|
8142
|
+
const handoffStreamOptions = this.prepareSimpleStreamOptions(
|
|
8143
|
+
{
|
|
8144
|
+
apiKey: this.#modelRegistry.resolver(model, cacheSessionId),
|
|
8145
|
+
sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
|
|
8146
|
+
promptCacheKey: handoffPromptCacheKey,
|
|
8147
|
+
preferWebsockets: false,
|
|
8148
|
+
serviceTier: this.#effectiveServiceTier(model),
|
|
8149
|
+
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
8062
8150
|
initiatorOverride: "agent",
|
|
8063
|
-
|
|
8151
|
+
signal: handoffSignal,
|
|
8152
|
+
},
|
|
8153
|
+
model.provider,
|
|
8154
|
+
);
|
|
8155
|
+
const rawHandoffText = await generateHandoffFromContext(
|
|
8156
|
+
obfuscateProviderContext(this.#obfuscator, handoffContext),
|
|
8157
|
+
model,
|
|
8158
|
+
{
|
|
8159
|
+
streamOptions: handoffStreamOptions,
|
|
8064
8160
|
telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
|
|
8065
|
-
// Honor the user's /model thinking selection on the handoff
|
|
8066
|
-
//
|
|
8067
|
-
// resolveCompactionEffort so unsupported-effort models don't
|
|
8068
|
-
//
|
|
8161
|
+
// Honor the user's /model thinking selection on the handoff path.
|
|
8162
|
+
// Clamped per-model inside generateHandoffFromContext via
|
|
8163
|
+
// resolveCompactionEffort so unsupported-effort models don't trip
|
|
8164
|
+
// requireSupportedEffort.
|
|
8069
8165
|
thinkingLevel: this.thinkingLevel,
|
|
8070
8166
|
},
|
|
8071
|
-
handoffSignal,
|
|
8072
8167
|
);
|
|
8073
8168
|
const handoffText = this.#deobfuscateFromProvider(rawHandoffText);
|
|
8074
8169
|
|
|
@@ -8212,6 +8307,58 @@ export class AgentSession {
|
|
|
8212
8307
|
});
|
|
8213
8308
|
}
|
|
8214
8309
|
|
|
8310
|
+
/**
|
|
8311
|
+
* Compact active `/goal` runs that never settle to `agent_end`.
|
|
8312
|
+
*
|
|
8313
|
+
* Long autonomous goals can keep producing tool calls inside one agent run.
|
|
8314
|
+
* The post-turn `agent_end` threshold check never fires in that shape, so
|
|
8315
|
+
* context can grow until provider overflow. `onTurnEnd` is the safe boundary:
|
|
8316
|
+
* tool results for the just-finished turn are already paired in
|
|
8317
|
+
* `activeMessages`, the live array the agent loop reads before its next
|
|
8318
|
+
* model call. Run maintenance here and splice the compacted state back into
|
|
8319
|
+
* that array, mirroring [`AgentSession.#applyRewind`].
|
|
8320
|
+
*/
|
|
8321
|
+
async #maintainContextMidRun(activeMessages: AgentMessage[], signal?: AbortSignal): Promise<void> {
|
|
8322
|
+
if (signal?.aborted || this.#isDisposed || this.isCompacting || this.isGeneratingHandoff) return;
|
|
8323
|
+
if (!(this.#goalModeState?.enabled === true && this.#goalModeState.goal.status === "active")) return;
|
|
8324
|
+
|
|
8325
|
+
const model = this.model;
|
|
8326
|
+
const contextWindow = model?.contextWindow ?? 0;
|
|
8327
|
+
if (contextWindow <= 0) return;
|
|
8328
|
+
|
|
8329
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
8330
|
+
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
8331
|
+
|
|
8332
|
+
const lastAssistant = [...activeMessages]
|
|
8333
|
+
.reverse()
|
|
8334
|
+
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
8335
|
+
if (!lastAssistant || lastAssistant.stopReason === "aborted" || lastAssistant.stopReason === "error") return;
|
|
8336
|
+
|
|
8337
|
+
const billedContextTokens = calculateContextTokens(lastAssistant.usage);
|
|
8338
|
+
const storedContextTokens = this.#estimateStoredContextTokens();
|
|
8339
|
+
const contextTokens = compactionContextTokens(billedContextTokens, storedContextTokens);
|
|
8340
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
|
|
8341
|
+
|
|
8342
|
+
const messagesBefore = activeMessages.length;
|
|
8343
|
+
await this.#runAutoCompaction("threshold", false, false, false, {
|
|
8344
|
+
autoContinue: false,
|
|
8345
|
+
suppressContinuation: true,
|
|
8346
|
+
triggerContextTokens: contextTokens,
|
|
8347
|
+
});
|
|
8348
|
+
|
|
8349
|
+
if (signal?.aborted) return;
|
|
8350
|
+
const compactedMessages = this.agent.state.messages;
|
|
8351
|
+
if (compactedMessages !== activeMessages) {
|
|
8352
|
+
activeMessages.splice(0, activeMessages.length, ...compactedMessages);
|
|
8353
|
+
}
|
|
8354
|
+
logger.debug("Mid-run goal compaction ran between tool-call turns", {
|
|
8355
|
+
contextTokens,
|
|
8356
|
+
contextWindow,
|
|
8357
|
+
strategy: compactionSettings.strategy,
|
|
8358
|
+
messagesBefore,
|
|
8359
|
+
messagesAfter: activeMessages.length,
|
|
8360
|
+
});
|
|
8361
|
+
}
|
|
8215
8362
|
/**
|
|
8216
8363
|
* Check if context maintenance or promotion is needed and run it.
|
|
8217
8364
|
* Called after agent_end and before prompt submission.
|
|
@@ -8337,28 +8484,58 @@ export class AgentSession {
|
|
|
8337
8484
|
// Skip if this was an error (non-overflow errors don't have usage data)
|
|
8338
8485
|
if (assistantMessage.stopReason === "error") return COMPACTION_CHECK_NONE;
|
|
8339
8486
|
const pruneResult = await this.#pruneToolOutputs();
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
//
|
|
8348
|
-
//
|
|
8349
|
-
//
|
|
8350
|
-
//
|
|
8351
|
-
//
|
|
8352
|
-
|
|
8353
|
-
|
|
8487
|
+
const maintenanceTokensFreed = (supersedeResult?.tokensSaved ?? 0) + (pruneResult?.tokensSaved ?? 0);
|
|
8488
|
+
const assistantUsageContextTokens = calculateContextTokens(assistantMessage.usage);
|
|
8489
|
+
const storedContextTokens = this.#estimateStoredContextTokens();
|
|
8490
|
+
// Pruning frees bytes for the NEXT prompt; it does not change the size of
|
|
8491
|
+
// the prompt the LLM just billed for. Earlier revisions subtracted the
|
|
8492
|
+
// per-turn supersede/prune `tokensSaved` from the threshold input, which
|
|
8493
|
+
// let a long-running `/goal` session sit above `compaction.thresholdTokens`
|
|
8494
|
+
// indefinitely whenever per-turn pruning saved enough to drop the
|
|
8495
|
+
// post-prune estimate below the user-configured trigger — the visible
|
|
8496
|
+
// context (anchored to the same provider billing) still showed >threshold,
|
|
8497
|
+
// but `shouldCompact` no-op'd (#3174). Anchor the initial trigger on the
|
|
8498
|
+
// last turn's billed context tokens, floored by the post-prune
|
|
8499
|
+
// stored-conversation estimate so a payload-compression hook still can't
|
|
8500
|
+
// deflate the trigger.
|
|
8501
|
+
const contextTokens = compactionContextTokens(assistantUsageContextTokens, storedContextTokens);
|
|
8502
|
+
const postMaintenanceContextTokens = compactionContextTokens(
|
|
8503
|
+
Math.max(0, assistantUsageContextTokens - maintenanceTokensFreed),
|
|
8504
|
+
storedContextTokens,
|
|
8505
|
+
);
|
|
8506
|
+
const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
|
|
8507
|
+
const shouldThresholdCompact = shouldCompact(contextTokens, contextWindow, compactionSettings);
|
|
8508
|
+
logger.debug("Auto-compaction threshold decision", {
|
|
8509
|
+
phase: "post-agent-end",
|
|
8510
|
+
goalModeEnabled: this.#goalModeState?.enabled === true,
|
|
8511
|
+
goalStatus: this.#goalModeState?.goal.status,
|
|
8512
|
+
stopReason: assistantMessage.stopReason,
|
|
8513
|
+
sameModel: sameModel === true,
|
|
8514
|
+
contextWindow,
|
|
8515
|
+
strategy: compactionSettings.strategy,
|
|
8516
|
+
thresholdTokens,
|
|
8517
|
+
assistantUsageContextTokens,
|
|
8518
|
+
storedContextTokens,
|
|
8519
|
+
resolvedContextTokens: contextTokens,
|
|
8520
|
+
postMaintenanceContextTokens,
|
|
8521
|
+
maintenanceTokensFreed,
|
|
8522
|
+
shouldCompact: shouldThresholdCompact,
|
|
8523
|
+
contextPromotionEnabled: this.settings.get("contextPromotion.enabled") === true,
|
|
8524
|
+
});
|
|
8525
|
+
if (shouldThresholdCompact) {
|
|
8354
8526
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
8355
8527
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
8356
8528
|
if (!promoted) {
|
|
8357
8529
|
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
8358
8530
|
autoContinue,
|
|
8359
|
-
triggerContextTokens:
|
|
8531
|
+
triggerContextTokens: postMaintenanceContextTokens,
|
|
8360
8532
|
});
|
|
8361
8533
|
}
|
|
8534
|
+
logger.debug("Auto-compaction threshold satisfied but context promotion took over", {
|
|
8535
|
+
contextTokens,
|
|
8536
|
+
contextWindow,
|
|
8537
|
+
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
8538
|
+
});
|
|
8362
8539
|
}
|
|
8363
8540
|
return COMPACTION_CHECK_NONE;
|
|
8364
8541
|
}
|
|
@@ -9026,6 +9203,21 @@ export class AgentSession {
|
|
|
9026
9203
|
providerKeys.add(`openai-responses:${nextModel.provider}`);
|
|
9027
9204
|
}
|
|
9028
9205
|
|
|
9206
|
+
// `openai-completions` sessions are keyed `openai-completions:<provider>:<resolvedBaseUrl>:<modelId>`
|
|
9207
|
+
// and cache backend-specific decisions (strict-tools disable scopes, reasoning-effort
|
|
9208
|
+
// fallbacks). The resolved request base URL can differ from the catalog `model.baseUrl`
|
|
9209
|
+
// (Moonshot env override, Alibaba Coding Plan enterprise URL, Azure deployment URL),
|
|
9210
|
+
// so evict by provider prefix when the user moves away from that completions backend.
|
|
9211
|
+
let completionsPrefixToEvict: string | undefined;
|
|
9212
|
+
if (currentModel.api === "openai-completions") {
|
|
9213
|
+
const currentScope = `${currentModel.provider}:${currentModel.baseUrl ?? ""}`;
|
|
9214
|
+
const nextScope =
|
|
9215
|
+
nextModel.api === "openai-completions" ? `${nextModel.provider}:${nextModel.baseUrl ?? ""}` : undefined;
|
|
9216
|
+
if (currentScope !== nextScope) {
|
|
9217
|
+
completionsPrefixToEvict = `openai-completions:${currentModel.provider}:`;
|
|
9218
|
+
}
|
|
9219
|
+
}
|
|
9220
|
+
|
|
9029
9221
|
for (const providerKey of providerKeys) {
|
|
9030
9222
|
const state = this.#providerSessionState.get(providerKey);
|
|
9031
9223
|
if (!state) continue;
|
|
@@ -9041,6 +9233,21 @@ export class AgentSession {
|
|
|
9041
9233
|
|
|
9042
9234
|
this.#providerSessionState.delete(providerKey);
|
|
9043
9235
|
}
|
|
9236
|
+
|
|
9237
|
+
if (completionsPrefixToEvict !== undefined) {
|
|
9238
|
+
for (const [key, state] of this.#providerSessionState) {
|
|
9239
|
+
if (!key.startsWith(completionsPrefixToEvict)) continue;
|
|
9240
|
+
try {
|
|
9241
|
+
state.close();
|
|
9242
|
+
} catch (error) {
|
|
9243
|
+
logger.warn("Failed to close provider session state during model switch", {
|
|
9244
|
+
providerKey: key,
|
|
9245
|
+
error: String(error),
|
|
9246
|
+
});
|
|
9247
|
+
}
|
|
9248
|
+
this.#providerSessionState.delete(key);
|
|
9249
|
+
}
|
|
9250
|
+
}
|
|
9044
9251
|
}
|
|
9045
9252
|
|
|
9046
9253
|
#normalizeProviderReplayValue(value: unknown): unknown {
|
|
@@ -9452,13 +9659,15 @@ export class AgentSession {
|
|
|
9452
9659
|
willRetry: boolean,
|
|
9453
9660
|
deferred = false,
|
|
9454
9661
|
allowDefer = true,
|
|
9455
|
-
options: { autoContinue?: boolean; triggerContextTokens?: number } = {},
|
|
9662
|
+
options: { autoContinue?: boolean; triggerContextTokens?: number; suppressContinuation?: boolean } = {},
|
|
9456
9663
|
): Promise<CompactionCheckResult> {
|
|
9457
9664
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
9458
9665
|
if (compactionSettings.strategy === "off") return COMPACTION_CHECK_NONE;
|
|
9459
9666
|
if (reason !== "idle" && !compactionSettings.enabled) return COMPACTION_CHECK_NONE;
|
|
9460
9667
|
const generation = this.#promptGeneration;
|
|
9461
|
-
const
|
|
9668
|
+
const suppressContinuation = options.suppressContinuation === true;
|
|
9669
|
+
const shouldAutoContinue =
|
|
9670
|
+
!suppressContinuation && options.autoContinue !== false && compactionSettings.autoContinue !== false;
|
|
9462
9671
|
// Shake runs inline (cheap, no remote LLM). On overflow recovery, if shake
|
|
9463
9672
|
// reclaims nothing we fall through to the summary-compaction body below so
|
|
9464
9673
|
// the oversized input still gets resolved.
|
|
@@ -9469,6 +9678,7 @@ export class AgentSession {
|
|
|
9469
9678
|
generation,
|
|
9470
9679
|
shouldAutoContinue,
|
|
9471
9680
|
options.triggerContextTokens,
|
|
9681
|
+
suppressContinuation,
|
|
9472
9682
|
);
|
|
9473
9683
|
if (outcome !== "fallback") return outcome;
|
|
9474
9684
|
}
|
|
@@ -9922,7 +10132,7 @@ export class AgentSession {
|
|
|
9922
10132
|
|
|
9923
10133
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
9924
10134
|
continuationScheduled = true;
|
|
9925
|
-
} else if (this.agent.hasQueuedMessages()) {
|
|
10135
|
+
} else if (!suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
9926
10136
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
9927
10137
|
// Kick the loop so queued messages are actually delivered.
|
|
9928
10138
|
this.#scheduleAgentContinue({
|
|
@@ -9982,6 +10192,7 @@ export class AgentSession {
|
|
|
9982
10192
|
generation: number,
|
|
9983
10193
|
autoContinue: boolean,
|
|
9984
10194
|
triggerContextTokens?: number,
|
|
10195
|
+
suppressContinuation = false,
|
|
9985
10196
|
): Promise<CompactionCheckResult | "fallback"> {
|
|
9986
10197
|
const action = "shake";
|
|
9987
10198
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -10012,15 +10223,16 @@ export class AgentSession {
|
|
|
10012
10223
|
// situation actually resolves; "idle" is exempt because its 60s+ timer
|
|
10013
10224
|
// re-checks usage before re-firing and cannot dead-loop on its own.
|
|
10014
10225
|
//
|
|
10015
|
-
// #2275: the post-shake check MUST
|
|
10016
|
-
//
|
|
10017
|
-
//
|
|
10018
|
-
//
|
|
10019
|
-
//
|
|
10020
|
-
//
|
|
10021
|
-
//
|
|
10022
|
-
//
|
|
10023
|
-
//
|
|
10226
|
+
// #2275: the post-shake check MUST stay provider-anchored when caller
|
|
10227
|
+
// usage and local estimates diverge. The local estimator undercounts
|
|
10228
|
+
// thinking-signature payloads, so thinking-heavy sessions can read well
|
|
10229
|
+
// below the provider usage that fired the threshold. Prefer the caller's
|
|
10230
|
+
// context figure when supplied, then subtract shake's own savings and add
|
|
10231
|
+
// hysteresis (80% recovery band) so we don't oscillate at the boundary.
|
|
10232
|
+
// Threshold callers pass the provider-billed trigger after accounting for
|
|
10233
|
+
// any supersede/drop-useless pruning that already rewrote the next prompt;
|
|
10234
|
+
// without that pre-shake savings, shake can fall through to context-full
|
|
10235
|
+
// even though the post-prune history is already inside the recovery band.
|
|
10024
10236
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
10025
10237
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
10026
10238
|
let stillOverThreshold = false;
|
|
@@ -10080,7 +10292,7 @@ export class AgentSession {
|
|
|
10080
10292
|
}
|
|
10081
10293
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
10082
10294
|
continuationScheduled = true;
|
|
10083
|
-
} else if (this.agent.hasQueuedMessages()) {
|
|
10295
|
+
} else if (!suppressContinuation && this.agent.hasQueuedMessages()) {
|
|
10084
10296
|
this.#scheduleAgentContinue({
|
|
10085
10297
|
delayMs: 100,
|
|
10086
10298
|
generation,
|
|
@@ -12168,8 +12380,8 @@ export class AgentSession {
|
|
|
12168
12380
|
pendingMessages?: AgentMessage[];
|
|
12169
12381
|
}): ContextUsageBreakdown | undefined {
|
|
12170
12382
|
const model = this.model;
|
|
12171
|
-
const
|
|
12172
|
-
|
|
12383
|
+
const rawContextWindow = options?.contextWindow ?? model?.contextWindow ?? 0;
|
|
12384
|
+
const contextWindow = Number.isFinite(rawContextWindow) && rawContextWindow > 0 ? rawContextWindow : 0;
|
|
12173
12385
|
|
|
12174
12386
|
const { skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens } = computeNonMessageBreakdown(this);
|
|
12175
12387
|
const categoryNonMessageTokens = skillsTokens + toolsTokens + systemContextTokens + systemPromptTokens;
|
|
@@ -602,11 +602,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
602
602
|
},
|
|
603
603
|
{
|
|
604
604
|
name: "share",
|
|
605
|
-
description: "Share session via an encrypted link (
|
|
605
|
+
description: "Share session via an encrypted link (share server or secret gist)",
|
|
606
606
|
handle: async (_command, runtime) => {
|
|
607
607
|
try {
|
|
608
608
|
const result = await shareSession(runtime.sessionManager, {
|
|
609
609
|
serverUrl: runtime.settings.get("share.serverUrl"),
|
|
610
|
+
store: runtime.settings.get("share.store"),
|
|
610
611
|
state: runtime.session.state,
|
|
611
612
|
obfuscator: runtime.settings.get("share.redactSecrets") ? runtime.session.obfuscator : undefined,
|
|
612
613
|
});
|