@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.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 +90 -4
- package/dist/cli.js +869 -825
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +66 -34
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +1 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -2
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +3 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/session/agent-session.d.ts +35 -30
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +23 -0
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/index.d.ts +11 -4
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +55 -51
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +1 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/tools/render-utils.d.ts +22 -0
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/capability/mcp.ts +1 -0
- package/src/cli/gallery-cli.ts +6 -5
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli.ts +20 -6
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/config/keybindings.ts +6 -1
- package/src/config/mcp-schema.json +4 -0
- package/src/config/settings-schema.ts +68 -41
- package/src/config/settings.ts +7 -0
- package/src/edit/renderer.ts +96 -46
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/prelude.py +5 -6
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +44 -14
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +8 -60
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/oauth-discovery.ts +27 -2
- package/src/mcp/oauth-flow.ts +47 -1
- package/src/mcp/transports/stdio.ts +3 -0
- package/src/mcp/types.ts +2 -0
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +15 -0
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/mcp-add-wizard.ts +13 -0
- package/src/modes/components/settings-selector.ts +2 -0
- package/src/modes/components/status-line/component.ts +22 -12
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/transcript-container.ts +99 -18
- package/src/modes/components/tree-selector.ts +6 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/event-controller.ts +93 -4
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +18 -2
- package/src/modes/controllers/mcp-command-controller.ts +34 -2
- package/src/modes/controllers/selector-controller.ts +25 -17
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +17 -15
- package/src/modes/theme/theme.ts +24 -5
- package/src/modes/types.ts +3 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +43 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +29 -9
- package/src/session/agent-session.ts +268 -241
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +60 -0
- package/src/task/executor.ts +855 -466
- package/src/task/index.ts +723 -794
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +142 -66
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +73 -66
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +15 -5
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/index.ts +4 -12
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/render-utils.ts +56 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/tools/write.ts +65 -47
- package/src/web/search/providers/anthropic.ts +29 -4
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type AgentState,
|
|
30
30
|
type AgentTool,
|
|
31
31
|
AppendOnlyContextManager,
|
|
32
|
+
type AsideMessage,
|
|
32
33
|
resolveTelemetry,
|
|
33
34
|
ThinkingLevel,
|
|
34
35
|
} from "@oh-my-pi/pi-agent-core";
|
|
@@ -50,12 +51,18 @@ import {
|
|
|
50
51
|
generateBranchSummary,
|
|
51
52
|
generateHandoff,
|
|
52
53
|
prepareCompaction,
|
|
54
|
+
resolveThresholdTokens,
|
|
53
55
|
type ShakeConfig,
|
|
54
56
|
type ShakeRegion,
|
|
55
57
|
type SummaryOptions,
|
|
56
58
|
shouldCompact,
|
|
57
59
|
} from "@oh-my-pi/pi-agent-core/compaction";
|
|
58
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
DEFAULT_PRUNE_CONFIG,
|
|
62
|
+
pruneSupersededToolResults,
|
|
63
|
+
pruneToolOutputs,
|
|
64
|
+
readToolSupersedeKey,
|
|
65
|
+
} from "@oh-my-pi/pi-agent-core/compaction/pruning";
|
|
59
66
|
import type { ProtectedToolMatcher } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
|
|
60
67
|
import type {
|
|
61
68
|
AssistantMessage,
|
|
@@ -100,6 +107,7 @@ import {
|
|
|
100
107
|
relativePathWithinRoot,
|
|
101
108
|
Snowflake,
|
|
102
109
|
} from "@oh-my-pi/pi-utils";
|
|
110
|
+
import { snapcompactCompact } from "@oh-my-pi/snapcompact";
|
|
103
111
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
104
112
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
105
113
|
import { reset as resetCapabilities } from "../capability";
|
|
@@ -163,6 +171,7 @@ import { GoalRuntime } from "../goals/runtime";
|
|
|
163
171
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
164
172
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
165
173
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
174
|
+
import type { IrcMessage } from "../irc/bus";
|
|
166
175
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
167
176
|
import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
|
|
168
177
|
import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
@@ -184,7 +193,6 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
|
|
184
193
|
};
|
|
185
194
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
186
195
|
import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
|
|
187
|
-
import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
188
196
|
import {
|
|
189
197
|
deobfuscateSessionContext,
|
|
190
198
|
obfuscateProviderContext,
|
|
@@ -230,10 +238,8 @@ import type { AuthStorage } from "./auth-storage";
|
|
|
230
238
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
231
239
|
import {
|
|
232
240
|
type BashExecutionMessage,
|
|
233
|
-
type CompactionSummaryMessage,
|
|
234
241
|
type CustomMessage,
|
|
235
242
|
convertToLlm,
|
|
236
|
-
type FileMentionMessage,
|
|
237
243
|
type PythonExecutionMessage,
|
|
238
244
|
readPendingDisplayTag,
|
|
239
245
|
SILENT_ABORT_MARKER,
|
|
@@ -259,11 +265,11 @@ export type AgentSessionEvent =
|
|
|
259
265
|
| {
|
|
260
266
|
type: "auto_compaction_start";
|
|
261
267
|
reason: "threshold" | "overflow" | "idle" | "incomplete";
|
|
262
|
-
action: "context-full" | "handoff" | "shake";
|
|
268
|
+
action: "context-full" | "handoff" | "shake" | "snapcompact";
|
|
263
269
|
}
|
|
264
270
|
| {
|
|
265
271
|
type: "auto_compaction_end";
|
|
266
|
-
action: "context-full" | "handoff" | "shake";
|
|
272
|
+
action: "context-full" | "handoff" | "shake" | "snapcompact";
|
|
267
273
|
result: CompactionResult | undefined;
|
|
268
274
|
aborted: boolean;
|
|
269
275
|
willRetry: boolean;
|
|
@@ -297,6 +303,15 @@ export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "la
|
|
|
297
303
|
const EMPTY_STOP_MAX_RETRIES = 3;
|
|
298
304
|
const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
|
|
299
305
|
const RETRY_BACKOFF_JITTER_RATIO = 0.25;
|
|
306
|
+
/**
|
|
307
|
+
* Hysteresis band for the post-shake "did we actually create headroom?" check.
|
|
308
|
+
* Shake counts as having resolved threshold pressure only when residual context
|
|
309
|
+
* lands at or below `SHAKE_RECOVERY_BAND × threshold`. Re-checking against the
|
|
310
|
+
* raw threshold lets shake keep reclaiming a trickle of the previous turn's
|
|
311
|
+
* output and land just under the line every turn, sustaining the auto-continue
|
|
312
|
+
* dead loop reported in #2275.
|
|
313
|
+
*/
|
|
314
|
+
const SHAKE_RECOVERY_BAND = 0.8;
|
|
300
315
|
|
|
301
316
|
function calculateRetryBackoffDelayMs(baseDelayMs: number, attempt: number): number {
|
|
302
317
|
const cappedDelayMs = Math.min(Math.max(0, baseDelayMs) * 2 ** Math.max(0, attempt - 1), RETRY_BACKOFF_MAX_DELAY_MS);
|
|
@@ -413,8 +428,6 @@ export interface AgentSessionConfig {
|
|
|
413
428
|
asyncJobManager?: AsyncJobManager;
|
|
414
429
|
/** Agent identity (registry id like "Main" or "Alice") used for IRC routing. */
|
|
415
430
|
agentId?: string;
|
|
416
|
-
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
417
|
-
agentRegistry?: AgentRegistry;
|
|
418
431
|
/**
|
|
419
432
|
* Override the provider-facing session ID for all API requests from this session.
|
|
420
433
|
* When absent, `sessionManager.getSessionId()` is used. Needed when benchmark or
|
|
@@ -533,6 +546,7 @@ interface ActiveRetryFallbackState {
|
|
|
533
546
|
originalSelector: string;
|
|
534
547
|
originalThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
535
548
|
lastAppliedFallbackThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
549
|
+
pinned: boolean;
|
|
536
550
|
}
|
|
537
551
|
|
|
538
552
|
function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
|
|
@@ -557,15 +571,15 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
|
|
|
557
571
|
return `${selector.provider}/${selector.id}`;
|
|
558
572
|
}
|
|
559
573
|
|
|
560
|
-
const
|
|
574
|
+
const EPHEMERAL_REPLY_MAX_BYTES = 4096;
|
|
561
575
|
|
|
562
576
|
/**
|
|
563
|
-
* Collapse degenerate
|
|
577
|
+
* Collapse degenerate ephemeral replies (/btw, /omfg side-channel turns).
|
|
564
578
|
* Models occasionally loop on a single line (~16 reports of N-times-repeated
|
|
565
579
|
* replies); compress runs longer than 3 down to one instance + `[…N×]`, then
|
|
566
580
|
* cap at 4 KiB so a runaway reply can't flood the channel.
|
|
567
581
|
*/
|
|
568
|
-
function
|
|
582
|
+
function dedupeEphemeralReply(text: string): string {
|
|
569
583
|
if (!text) return text;
|
|
570
584
|
const lines = text.split("\n");
|
|
571
585
|
const out: string[] = [];
|
|
@@ -582,11 +596,11 @@ function dedupeIrcReply(text: string): string {
|
|
|
582
596
|
i = j;
|
|
583
597
|
}
|
|
584
598
|
let result = out.join("\n");
|
|
585
|
-
if (Buffer.byteLength(result, "utf8") >
|
|
599
|
+
if (Buffer.byteLength(result, "utf8") > EPHEMERAL_REPLY_MAX_BYTES) {
|
|
586
600
|
// Trim by characters until we're under the byte budget — handles multi-byte
|
|
587
601
|
// glyphs at the boundary without splitting them.
|
|
588
602
|
const suffix = "\n[…truncated]";
|
|
589
|
-
const budget =
|
|
603
|
+
const budget = EPHEMERAL_REPLY_MAX_BYTES - Buffer.byteLength(suffix, "utf8");
|
|
590
604
|
while (Buffer.byteLength(result, "utf8") > budget) {
|
|
591
605
|
result = result.slice(0, -1);
|
|
592
606
|
}
|
|
@@ -941,13 +955,11 @@ export class AgentSession {
|
|
|
941
955
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
942
956
|
#evalExecutionDisposing = false;
|
|
943
957
|
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
#
|
|
947
|
-
|
|
948
|
-
// Agent identity + registry for IRC relay forwarding to the main session UI.
|
|
958
|
+
// Incoming IRC messages received while a turn was streaming; drained as
|
|
959
|
+
// non-interrupting asides at the next step boundary (see the aside provider).
|
|
960
|
+
#pendingIrcAsides: CustomMessage[] = [];
|
|
961
|
+
// Agent identity (registry id) used for IRC routing and job ownership.
|
|
949
962
|
#agentId: string | undefined;
|
|
950
|
-
#agentRegistry: AgentRegistry | undefined;
|
|
951
963
|
#providerSessionId: string | undefined;
|
|
952
964
|
#freshProviderSessionId: string | undefined;
|
|
953
965
|
#isDisposed = false;
|
|
@@ -1204,7 +1216,13 @@ export class AgentSession {
|
|
|
1204
1216
|
// Background-job completions / late diagnostics are pulled into the run at
|
|
1205
1217
|
// each step boundary as non-interrupting asides (see Agent.getAsideMessages),
|
|
1206
1218
|
// so they reach the model between requests without waiting for a yield.
|
|
1207
|
-
this.agent.setAsideMessageProvider(() =>
|
|
1219
|
+
this.agent.setAsideMessageProvider(() => {
|
|
1220
|
+
const pendingIrc = this.#pendingIrcAsides;
|
|
1221
|
+
this.#pendingIrcAsides = [];
|
|
1222
|
+
const thunks: AsideMessage[] = pendingIrc.map(record => () => record);
|
|
1223
|
+
thunks.push(...this.yieldQueue.drainLazy());
|
|
1224
|
+
return thunks;
|
|
1225
|
+
});
|
|
1208
1226
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
1209
1227
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
1210
1228
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
@@ -1235,7 +1253,6 @@ export class AgentSession {
|
|
|
1235
1253
|
this.#ttsrManager = config.ttsrManager;
|
|
1236
1254
|
this.#obfuscator = config.obfuscator;
|
|
1237
1255
|
this.#agentId = config.agentId;
|
|
1238
|
-
this.#agentRegistry = config.agentRegistry;
|
|
1239
1256
|
this.#providerSessionId = config.providerSessionId;
|
|
1240
1257
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1241
1258
|
const event: AgentEvent = {
|
|
@@ -3091,15 +3108,28 @@ export class AgentSession {
|
|
|
3091
3108
|
}
|
|
3092
3109
|
|
|
3093
3110
|
/**
|
|
3094
|
-
*
|
|
3095
|
-
*
|
|
3111
|
+
* Synchronously mark the session as disposing so new work is rejected
|
|
3112
|
+
* immediately: Python/eval starts throw, queued asides are dropped, and the
|
|
3113
|
+
* aside provider is detached. Idempotent; `dispose()` runs it first.
|
|
3114
|
+
*
|
|
3115
|
+
* Wrappers that await other teardown before delegating to `dispose()` MUST
|
|
3116
|
+
* call this before their first await — otherwise work started in that async
|
|
3117
|
+
* gap slips past the disposal guards.
|
|
3096
3118
|
*/
|
|
3097
|
-
|
|
3119
|
+
beginDispose(): void {
|
|
3098
3120
|
this.#isDisposed = true;
|
|
3099
|
-
this.#
|
|
3121
|
+
this.#pendingIrcAsides = [];
|
|
3100
3122
|
this.yieldQueue.clear();
|
|
3101
3123
|
this.agent.setAsideMessageProvider(undefined);
|
|
3102
3124
|
this.#evalExecutionDisposing = true;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
/**
|
|
3128
|
+
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
3129
|
+
* Call this when completely done with the session.
|
|
3130
|
+
*/
|
|
3131
|
+
async dispose(): Promise<void> {
|
|
3132
|
+
this.beginDispose();
|
|
3103
3133
|
try {
|
|
3104
3134
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
3105
3135
|
await this.#extensionRunner.emit({ type: "session_shutdown" });
|
|
@@ -4032,6 +4062,16 @@ export class AgentSession {
|
|
|
4032
4062
|
return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
|
|
4033
4063
|
}
|
|
4034
4064
|
|
|
4065
|
+
/**
|
|
4066
|
+
* Full-history transcript for TUI display: every path entry in
|
|
4067
|
+
* chronological order with compactions rendered inline at the point they
|
|
4068
|
+
* fired (instead of replacing prior history). Display-only — NEVER feed
|
|
4069
|
+
* the result to `agent.replaceMessages` or a provider.
|
|
4070
|
+
*/
|
|
4071
|
+
buildTranscriptSessionContext(): SessionContext {
|
|
4072
|
+
return deobfuscateSessionContext(this.sessionManager.buildSessionContext({ transcript: true }), this.#obfuscator);
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4035
4075
|
#obfuscateForProvider<T>(value: T): T {
|
|
4036
4076
|
if (!this.#obfuscator?.hasSecrets()) return value;
|
|
4037
4077
|
return this.#obfuscator.obfuscateObject(value);
|
|
@@ -4634,7 +4674,7 @@ export class AgentSession {
|
|
|
4634
4674
|
// Flush any pending bash messages before the new prompt
|
|
4635
4675
|
this.#flushPendingBashMessages();
|
|
4636
4676
|
this.#flushPendingPythonMessages();
|
|
4637
|
-
this.#
|
|
4677
|
+
this.#flushPendingIrcAsides();
|
|
4638
4678
|
|
|
4639
4679
|
// Reset todo reminder count on new user prompt
|
|
4640
4680
|
this.#todoReminderCount = 0;
|
|
@@ -6048,6 +6088,35 @@ export class AgentSession {
|
|
|
6048
6088
|
return result;
|
|
6049
6089
|
}
|
|
6050
6090
|
|
|
6091
|
+
/**
|
|
6092
|
+
* Per-turn supersede pass: prune older `read` results that a newer read of
|
|
6093
|
+
* the same file has made stale. Cache-aware (only fires when the suffix
|
|
6094
|
+
* after a candidate is small or the session has been idle long enough that
|
|
6095
|
+
* the provider prompt cache is cold), so it is cheap to run every turn.
|
|
6096
|
+
* Gated on the `compaction.supersedeReads` setting.
|
|
6097
|
+
*/
|
|
6098
|
+
async #pruneSupersededReads(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
6099
|
+
if (!this.settings.getGroup("compaction").supersedeReads) return undefined;
|
|
6100
|
+
const branchEntries = this.sessionManager.getBranch();
|
|
6101
|
+
const result = pruneSupersededToolResults(
|
|
6102
|
+
branchEntries,
|
|
6103
|
+
this.#withPlanProtection({
|
|
6104
|
+
supersedeKey: readToolSupersedeKey,
|
|
6105
|
+
protectedTools: [...DEFAULT_PRUNE_CONFIG.protectedTools],
|
|
6106
|
+
}),
|
|
6107
|
+
);
|
|
6108
|
+
if (result.prunedCount === 0) {
|
|
6109
|
+
return undefined;
|
|
6110
|
+
}
|
|
6111
|
+
|
|
6112
|
+
await this.sessionManager.rewriteEntries();
|
|
6113
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
6114
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
6115
|
+
this.#syncTodoPhasesFromBranch();
|
|
6116
|
+
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6117
|
+
return result;
|
|
6118
|
+
}
|
|
6119
|
+
|
|
6051
6120
|
/**
|
|
6052
6121
|
* Strip image content blocks from every message on the current branch and
|
|
6053
6122
|
* persist the rewrite. Walks `SessionManager.getBranch()` in place — both
|
|
@@ -6237,6 +6306,20 @@ export class AgentSession {
|
|
|
6237
6306
|
|
|
6238
6307
|
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
6239
6308
|
|
|
6309
|
+
// Strategy honored on manual /compact too. Custom instructions imply a
|
|
6310
|
+
// directed LLM summary; a text-only model cannot read the frames back —
|
|
6311
|
+
// both take the summarizer path (the latter loudly).
|
|
6312
|
+
const wantsSnapcompact =
|
|
6313
|
+
compactionPrep.kind !== "fromHook" && compactionSettings.strategy === "snapcompact" && !customInstructions;
|
|
6314
|
+
const snapcompactReady = wantsSnapcompact && this.model.input.includes("image");
|
|
6315
|
+
if (wantsSnapcompact && !snapcompactReady) {
|
|
6316
|
+
this.emitNotice(
|
|
6317
|
+
"warning",
|
|
6318
|
+
`snapcompact needs a vision-capable model (${this.model.id} is text-only) — using an LLM summary instead`,
|
|
6319
|
+
"compaction",
|
|
6320
|
+
);
|
|
6321
|
+
}
|
|
6322
|
+
|
|
6240
6323
|
let summary: string;
|
|
6241
6324
|
let shortSummary: string | undefined;
|
|
6242
6325
|
let firstKeptEntryId: string;
|
|
@@ -6250,6 +6333,14 @@ export class AgentSession {
|
|
|
6250
6333
|
tokensBefore = compactionPrep.tokensBefore;
|
|
6251
6334
|
details = compactionPrep.details;
|
|
6252
6335
|
preserveData = compactionPrep.preserveData;
|
|
6336
|
+
} else if (snapcompactReady) {
|
|
6337
|
+
const snapcompactResult = await snapcompactCompact(preparation, { convertToLlm, model: this.model });
|
|
6338
|
+
summary = snapcompactResult.summary;
|
|
6339
|
+
shortSummary = snapcompactResult.shortSummary;
|
|
6340
|
+
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
6341
|
+
tokensBefore = snapcompactResult.tokensBefore;
|
|
6342
|
+
details = snapcompactResult.details;
|
|
6343
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
|
6253
6344
|
} else {
|
|
6254
6345
|
// Generate compaction result. Only convert known abort-shaped
|
|
6255
6346
|
// rejections (AbortError raised while the abort signal is set,
|
|
@@ -6669,7 +6760,10 @@ export class AgentSession {
|
|
|
6669
6760
|
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
6670
6761
|
strategy: incompleteCompactionSettings.strategy,
|
|
6671
6762
|
});
|
|
6672
|
-
await this.#runAutoCompaction("incomplete", true, false, allowDefer, {
|
|
6763
|
+
await this.#runAutoCompaction("incomplete", true, false, allowDefer, {
|
|
6764
|
+
autoContinue,
|
|
6765
|
+
triggerContextTokens: calculateContextTokens(assistantMessage.usage),
|
|
6766
|
+
});
|
|
6673
6767
|
} else {
|
|
6674
6768
|
// Neither promotion nor compaction is available — surface the dead-end so
|
|
6675
6769
|
// the user understands why the turn yielded with nothing.
|
|
@@ -6680,6 +6774,10 @@ export class AgentSession {
|
|
|
6680
6774
|
return false;
|
|
6681
6775
|
}
|
|
6682
6776
|
|
|
6777
|
+
// Supersede pass runs every turn, before any threshold gating: it is cheap
|
|
6778
|
+
// (bails when no candidate) and independent of the compaction setting.
|
|
6779
|
+
const supersedeResult = await this.#pruneSupersededReads();
|
|
6780
|
+
|
|
6683
6781
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6684
6782
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
6685
6783
|
|
|
@@ -6688,6 +6786,9 @@ export class AgentSession {
|
|
|
6688
6786
|
if (assistantMessage.stopReason === "error") return false;
|
|
6689
6787
|
const pruneResult = await this.#pruneToolOutputs();
|
|
6690
6788
|
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
6789
|
+
if (supersedeResult) {
|
|
6790
|
+
contextTokens = Math.max(0, contextTokens - supersedeResult.tokensSaved);
|
|
6791
|
+
}
|
|
6691
6792
|
if (pruneResult) {
|
|
6692
6793
|
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
6693
6794
|
}
|
|
@@ -6695,7 +6796,10 @@ export class AgentSession {
|
|
|
6695
6796
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
6696
6797
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
6697
6798
|
if (!promoted) {
|
|
6698
|
-
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
6799
|
+
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
6800
|
+
autoContinue,
|
|
6801
|
+
triggerContextTokens: contextTokens,
|
|
6802
|
+
});
|
|
6699
6803
|
}
|
|
6700
6804
|
}
|
|
6701
6805
|
return false;
|
|
@@ -7540,7 +7644,7 @@ export class AgentSession {
|
|
|
7540
7644
|
willRetry: boolean,
|
|
7541
7645
|
deferred = false,
|
|
7542
7646
|
allowDefer = true,
|
|
7543
|
-
options: { autoContinue?: boolean } = {},
|
|
7647
|
+
options: { autoContinue?: boolean; triggerContextTokens?: number } = {},
|
|
7544
7648
|
): Promise<boolean> {
|
|
7545
7649
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7546
7650
|
if (compactionSettings.strategy === "off") return false;
|
|
@@ -7551,7 +7655,13 @@ export class AgentSession {
|
|
|
7551
7655
|
// reclaims nothing we fall through to the summary-compaction body below so
|
|
7552
7656
|
// the oversized input still gets resolved.
|
|
7553
7657
|
if (compactionSettings.strategy === "shake") {
|
|
7554
|
-
const outcome = await this.#runAutoShake(
|
|
7658
|
+
const outcome = await this.#runAutoShake(
|
|
7659
|
+
reason,
|
|
7660
|
+
willRetry,
|
|
7661
|
+
generation,
|
|
7662
|
+
shouldAutoContinue,
|
|
7663
|
+
options.triggerContextTokens,
|
|
7664
|
+
);
|
|
7555
7665
|
if (outcome !== "fallback") return false;
|
|
7556
7666
|
}
|
|
7557
7667
|
// "overflow" and "incomplete" force inline execution because they are recovery
|
|
@@ -7578,9 +7688,25 @@ export class AgentSession {
|
|
|
7578
7688
|
|
|
7579
7689
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
7580
7690
|
// LLM call would hit the same overflow. "incomplete" is an output-side problem,
|
|
7581
|
-
// so a handoff request on the existing context is still viable.
|
|
7582
|
-
|
|
7691
|
+
// so a handoff request on the existing context is still viable. Snapcompact is
|
|
7692
|
+
// safe for every reason (it makes no LLM call at all) but requires a vision
|
|
7693
|
+
// model to be worth anything — fall back to context-full otherwise.
|
|
7694
|
+
let action: "context-full" | "handoff" | "snapcompact" =
|
|
7583
7695
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
7696
|
+
if (compactionSettings.strategy === "snapcompact") {
|
|
7697
|
+
if (this.model?.input.includes("image")) {
|
|
7698
|
+
action = "snapcompact";
|
|
7699
|
+
} else {
|
|
7700
|
+
logger.warn("Snapcompact compaction requires a vision-capable model; falling back to context-full", {
|
|
7701
|
+
model: this.model?.id,
|
|
7702
|
+
});
|
|
7703
|
+
this.emitNotice(
|
|
7704
|
+
"warning",
|
|
7705
|
+
`snapcompact needs a vision-capable model (${this.model?.id ?? "unknown"} is text-only) — using an LLM summary instead`,
|
|
7706
|
+
"compaction",
|
|
7707
|
+
);
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7584
7710
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
7585
7711
|
// Abort any older auto-compaction before installing this run's controller.
|
|
7586
7712
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -7719,6 +7845,16 @@ export class AgentSession {
|
|
|
7719
7845
|
tokensBefore = compactionPrep.tokensBefore;
|
|
7720
7846
|
details = compactionPrep.details;
|
|
7721
7847
|
preserveData = compactionPrep.preserveData;
|
|
7848
|
+
} else if (action === "snapcompact") {
|
|
7849
|
+
// Local, deterministic: render discarded history onto PNG frames.
|
|
7850
|
+
// No model candidates, no API key, no retry loop.
|
|
7851
|
+
const snapcompactResult = await snapcompactCompact(preparation, { convertToLlm, model: this.model });
|
|
7852
|
+
summary = snapcompactResult.summary;
|
|
7853
|
+
shortSummary = snapcompactResult.shortSummary;
|
|
7854
|
+
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
7855
|
+
tokensBefore = snapcompactResult.tokensBefore;
|
|
7856
|
+
details = snapcompactResult.details;
|
|
7857
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
|
7722
7858
|
} else {
|
|
7723
7859
|
const candidates = this.#getCompactionModelCandidates(availableModels);
|
|
7724
7860
|
const retrySettings = this.settings.getGroup("retry");
|
|
@@ -7958,6 +8094,7 @@ export class AgentSession {
|
|
|
7958
8094
|
willRetry: boolean,
|
|
7959
8095
|
generation: number,
|
|
7960
8096
|
autoContinue: boolean,
|
|
8097
|
+
triggerContextTokens?: number,
|
|
7961
8098
|
): Promise<"handled" | "fallback"> {
|
|
7962
8099
|
const action = "shake";
|
|
7963
8100
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
@@ -7978,8 +8115,8 @@ export class AgentSession {
|
|
|
7978
8115
|
return "handled";
|
|
7979
8116
|
}
|
|
7980
8117
|
const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
|
|
7981
|
-
// Detect the dead-loop reported in
|
|
7982
|
-
// shake runs, but
|
|
8118
|
+
// Detect the dead-loop reported in issues #2119/#2275: the threshold check
|
|
8119
|
+
// fires, shake runs, but residual context is still above the configured
|
|
7983
8120
|
// threshold. The next agent_end would re-trigger shake, which has nothing
|
|
7984
8121
|
// new to drop on the second pass, so the loop spins until the user kills it.
|
|
7985
8122
|
// Same hazard for "incomplete" (the retry would re-hit the length cap) and
|
|
@@ -7987,10 +8124,30 @@ export class AgentSession {
|
|
|
7987
8124
|
// reason we hand off to the summarization-driven context-full path so the
|
|
7988
8125
|
// situation actually resolves; "idle" is exempt because its 60s+ timer
|
|
7989
8126
|
// re-checks usage before re-firing and cannot dead-loop on its own.
|
|
8127
|
+
//
|
|
8128
|
+
// #2275: the post-shake check MUST be anchored on the same metric that
|
|
8129
|
+
// triggered compaction. The local estimator (`#estimatePendingPromptTokens`)
|
|
8130
|
+
// undercounts thinking-signature payloads, so on thinking-heavy sessions it
|
|
8131
|
+
// reads well below the provider-reported usage that fired the threshold.
|
|
8132
|
+
// When that estimate slips under the threshold, the fallback never fires
|
|
8133
|
+
// and the auto-continue prompt re-injects every turn. Prefer the trigger's
|
|
8134
|
+
// own `contextTokens` (provider-anchored) when the caller supplies it, and
|
|
8135
|
+
// add hysteresis (80% recovery band) so we don't oscillate at the boundary
|
|
8136
|
+
// while shake keeps reclaiming a trickle of the previous turn's output.
|
|
7990
8137
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7991
8138
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7992
|
-
|
|
7993
|
-
|
|
8139
|
+
let stillOverThreshold = false;
|
|
8140
|
+
if (contextWindow > 0) {
|
|
8141
|
+
if (typeof triggerContextTokens === "number" && Number.isFinite(triggerContextTokens)) {
|
|
8142
|
+
const correctedTokens = Math.max(0, triggerContextTokens - result.tokensFreed);
|
|
8143
|
+
const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
|
|
8144
|
+
const recoveryBand = Math.floor(thresholdTokens * SHAKE_RECOVERY_BAND);
|
|
8145
|
+
stillOverThreshold = correctedTokens > recoveryBand;
|
|
8146
|
+
} else {
|
|
8147
|
+
const postShakeTokens = this.#estimatePendingPromptTokens([]);
|
|
8148
|
+
stillOverThreshold = shouldCompact(postShakeTokens, contextWindow, compactionSettings);
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
7994
8151
|
const shouldFallBack = reason !== "idle" && ((reason === "overflow" && !reclaimed) || stillOverThreshold);
|
|
7995
8152
|
if (shouldFallBack) {
|
|
7996
8153
|
const errorMessage = reclaimed
|
|
@@ -8101,10 +8258,18 @@ export class AgentSession {
|
|
|
8101
8258
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
8102
8259
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
8103
8260
|
|
|
8261
|
+
if (this.#isClassifierRefusal(message)) return true;
|
|
8262
|
+
|
|
8104
8263
|
const err = message.errorMessage;
|
|
8105
8264
|
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
8106
8265
|
}
|
|
8107
8266
|
|
|
8267
|
+
#isClassifierRefusal(message: AssistantMessage): boolean {
|
|
8268
|
+
if (message.stopReason !== "error") return false;
|
|
8269
|
+
const stopType = message.stopDetails?.type;
|
|
8270
|
+
return stopType === "refusal" || stopType === "sensitive";
|
|
8271
|
+
}
|
|
8272
|
+
|
|
8108
8273
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
8109
8274
|
return (
|
|
8110
8275
|
this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
|
|
@@ -8248,6 +8413,7 @@ export class AgentSession {
|
|
|
8248
8413
|
role: string,
|
|
8249
8414
|
selector: RetryFallbackSelector,
|
|
8250
8415
|
currentSelector: string,
|
|
8416
|
+
options?: { pinFallback?: boolean },
|
|
8251
8417
|
): Promise<void> {
|
|
8252
8418
|
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
8253
8419
|
if (!candidate) {
|
|
@@ -8273,9 +8439,11 @@ export class AgentSession {
|
|
|
8273
8439
|
originalSelector: currentSelector,
|
|
8274
8440
|
originalThinkingLevel: currentThinkingLevel,
|
|
8275
8441
|
lastAppliedFallbackThinkingLevel: nextThinkingLevel,
|
|
8442
|
+
pinned: options?.pinFallback === true,
|
|
8276
8443
|
};
|
|
8277
8444
|
} else {
|
|
8278
8445
|
this.#activeRetryFallback.lastAppliedFallbackThinkingLevel = nextThinkingLevel;
|
|
8446
|
+
this.#activeRetryFallback.pinned = this.#activeRetryFallback.pinned || options?.pinFallback === true;
|
|
8279
8447
|
}
|
|
8280
8448
|
await this.#emitSessionEvent({
|
|
8281
8449
|
type: "retry_fallback_applied",
|
|
@@ -8285,7 +8453,7 @@ export class AgentSession {
|
|
|
8285
8453
|
});
|
|
8286
8454
|
}
|
|
8287
8455
|
|
|
8288
|
-
async #tryRetryModelFallback(currentSelector: string): Promise<boolean> {
|
|
8456
|
+
async #tryRetryModelFallback(currentSelector: string, options?: { pinFallback?: boolean }): Promise<boolean> {
|
|
8289
8457
|
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
8290
8458
|
if (!role) return false;
|
|
8291
8459
|
|
|
@@ -8295,7 +8463,7 @@ export class AgentSession {
|
|
|
8295
8463
|
if (!candidate) continue;
|
|
8296
8464
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
8297
8465
|
if (!apiKey) continue;
|
|
8298
|
-
await this.#applyRetryFallbackCandidate(role, selector, currentSelector);
|
|
8466
|
+
await this.#applyRetryFallbackCandidate(role, selector, currentSelector, options);
|
|
8299
8467
|
return true;
|
|
8300
8468
|
}
|
|
8301
8469
|
|
|
@@ -8304,6 +8472,7 @@ export class AgentSession {
|
|
|
8304
8472
|
|
|
8305
8473
|
async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
|
|
8306
8474
|
if (!this.#activeRetryFallback) return;
|
|
8475
|
+
if (this.#activeRetryFallback.pinned) return;
|
|
8307
8476
|
if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
8308
8477
|
|
|
8309
8478
|
const {
|
|
@@ -8401,6 +8570,7 @@ export class AgentSession {
|
|
|
8401
8570
|
async #handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
|
8402
8571
|
const retrySettings = this.settings.getGroup("retry");
|
|
8403
8572
|
if (!retrySettings.enabled) return false;
|
|
8573
|
+
const classifierRefusal = this.#isClassifierRefusal(message);
|
|
8404
8574
|
|
|
8405
8575
|
const generation = this.#promptGeneration;
|
|
8406
8576
|
this.#retryAttempt++;
|
|
@@ -8474,8 +8644,10 @@ export class AgentSession {
|
|
|
8474
8644
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
8475
8645
|
if (!switchedCredential && currentSelector) {
|
|
8476
8646
|
if (retrySettings.modelFallback) {
|
|
8477
|
-
|
|
8478
|
-
|
|
8647
|
+
if (!classifierRefusal) {
|
|
8648
|
+
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
8649
|
+
}
|
|
8650
|
+
switchedModel = await this.#tryRetryModelFallback(currentSelector, { pinFallback: classifierRefusal });
|
|
8479
8651
|
}
|
|
8480
8652
|
if (switchedModel) {
|
|
8481
8653
|
delayMs = 0;
|
|
@@ -8483,6 +8655,11 @@ export class AgentSession {
|
|
|
8483
8655
|
delayMs = parsedRetryAfterMs;
|
|
8484
8656
|
}
|
|
8485
8657
|
}
|
|
8658
|
+
if (classifierRefusal && !switchedModel) {
|
|
8659
|
+
this.#retryAttempt = 0;
|
|
8660
|
+
this.#resolveRetry();
|
|
8661
|
+
return false;
|
|
8662
|
+
}
|
|
8486
8663
|
|
|
8487
8664
|
// Fail-fast cap: if the provider asks us to wait longer than
|
|
8488
8665
|
// retry.maxDelayMs and we have no fallback credential or model to
|
|
@@ -8926,118 +9103,56 @@ export class AgentSession {
|
|
|
8926
9103
|
}
|
|
8927
9104
|
|
|
8928
9105
|
// =========================================================================
|
|
8929
|
-
//
|
|
9106
|
+
// IRC Delivery
|
|
8930
9107
|
// =========================================================================
|
|
8931
9108
|
|
|
8932
9109
|
/**
|
|
8933
|
-
*
|
|
8934
|
-
*
|
|
9110
|
+
* Deliver an IRC message into this session (recipient side; called by the
|
|
9111
|
+
* IrcBus). Emits the `irc_message` session event for UI cards and injects
|
|
9112
|
+
* the rendered message into the model's context as an `irc:incoming`
|
|
9113
|
+
* custom message:
|
|
8935
9114
|
*
|
|
8936
|
-
*
|
|
8937
|
-
*
|
|
8938
|
-
*
|
|
8939
|
-
*
|
|
8940
|
-
*
|
|
8941
|
-
*
|
|
9115
|
+
* - mid-turn → queued on the aside channel and folded in at the next step
|
|
9116
|
+
* boundary (non-interrupting, like async-result deliveries) → "injected";
|
|
9117
|
+
* - idle → starts a real turn with the message so the recipient wakes
|
|
9118
|
+
* → "woken".
|
|
9119
|
+
*
|
|
9120
|
+
* Never blocks on the recipient's turn: the wake turn is fire-and-forget.
|
|
8942
9121
|
*/
|
|
8943
|
-
async
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
}): Promise<{ replyText: string | null }> {
|
|
8949
|
-
const awaitReply = args.awaitReply !== false;
|
|
8950
|
-
const incomingTimestamp = Date.now();
|
|
8951
|
-
const incomingRecord: CustomMessage = {
|
|
9122
|
+
async deliverIrcMessage(msg: IrcMessage): Promise<"injected" | "woken"> {
|
|
9123
|
+
if (this.#isDisposed) {
|
|
9124
|
+
throw new Error("Recipient session is disposed.");
|
|
9125
|
+
}
|
|
9126
|
+
const record: CustomMessage = {
|
|
8952
9127
|
role: "custom",
|
|
8953
9128
|
customType: "irc:incoming",
|
|
8954
|
-
content:
|
|
9129
|
+
content: prompt.render(ircIncomingTemplate, {
|
|
9130
|
+
from: msg.from,
|
|
9131
|
+
message: msg.body,
|
|
9132
|
+
replyTo: msg.replyTo ?? "",
|
|
9133
|
+
}),
|
|
8955
9134
|
display: true,
|
|
8956
|
-
details: { from:
|
|
9135
|
+
details: { id: msg.id, from: msg.from, message: msg.body, ...(msg.replyTo ? { replyTo: msg.replyTo } : {}) },
|
|
8957
9136
|
attribution: "agent",
|
|
8958
|
-
timestamp:
|
|
9137
|
+
timestamp: msg.ts,
|
|
8959
9138
|
};
|
|
8960
|
-
void this.#emitSessionEvent({ type: "irc_message", message:
|
|
8961
|
-
this
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
body: args.message,
|
|
8965
|
-
kind: "message",
|
|
8966
|
-
timestamp: incomingTimestamp,
|
|
8967
|
-
});
|
|
8968
|
-
|
|
8969
|
-
this.#queueBackgroundExchangeInjection([incomingRecord]);
|
|
8970
|
-
if (!awaitReply) {
|
|
8971
|
-
return { replyText: null };
|
|
9139
|
+
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
9140
|
+
if (this.isStreaming) {
|
|
9141
|
+
this.#pendingIrcAsides.push(record);
|
|
9142
|
+
return "injected";
|
|
8972
9143
|
}
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
});
|
|
8978
|
-
const { replyText } = await this.runEphemeralTurn({
|
|
8979
|
-
promptText: incomingPrompt,
|
|
8980
|
-
signal: args.signal,
|
|
9144
|
+
// Idle: same wake primitive the yield queue uses for async-result
|
|
9145
|
+
// delivery — prompt the agent directly so a real turn runs.
|
|
9146
|
+
this.agent.prompt(record).catch(error => {
|
|
9147
|
+
logger.warn("IRC wake turn failed", { from: msg.from, to: msg.to, error: String(error) });
|
|
8981
9148
|
});
|
|
8982
|
-
|
|
8983
|
-
const replyRecord: CustomMessage = {
|
|
8984
|
-
role: "custom",
|
|
8985
|
-
customType: "irc:autoreply",
|
|
8986
|
-
content: `[IRC you → \`${args.from}\` (auto)]\n\n${replyText}`,
|
|
8987
|
-
display: true,
|
|
8988
|
-
details: { to: args.from, reply: replyText },
|
|
8989
|
-
attribution: "agent",
|
|
8990
|
-
timestamp: Date.now(),
|
|
8991
|
-
};
|
|
8992
|
-
void this.#emitSessionEvent({ type: "irc_message", message: replyRecord });
|
|
8993
|
-
this.#forwardIrcRelayToMain({
|
|
8994
|
-
from: this.#agentId ?? "?",
|
|
8995
|
-
to: args.from,
|
|
8996
|
-
body: replyText,
|
|
8997
|
-
kind: "reply",
|
|
8998
|
-
timestamp: replyRecord.timestamp,
|
|
8999
|
-
});
|
|
9000
|
-
this.#queueBackgroundExchangeInjection([replyRecord]);
|
|
9001
|
-
|
|
9002
|
-
return { replyText };
|
|
9003
|
-
}
|
|
9004
|
-
|
|
9005
|
-
/**
|
|
9006
|
-
* Forward an IRC exchange observation to the main agent's session UI so the
|
|
9007
|
-
* user can see every IRC conversation in the main transcript, even when the
|
|
9008
|
-
* main agent is not a direct participant. The relay record is display-only:
|
|
9009
|
-
* it is NOT injected into the main agent's persisted history.
|
|
9010
|
-
*/
|
|
9011
|
-
#forwardIrcRelayToMain(args: {
|
|
9012
|
-
from: string;
|
|
9013
|
-
to: string;
|
|
9014
|
-
body: string;
|
|
9015
|
-
kind: "message" | "reply";
|
|
9016
|
-
timestamp: number;
|
|
9017
|
-
}): void {
|
|
9018
|
-
const registry = this.#agentRegistry;
|
|
9019
|
-
if (!registry) return;
|
|
9020
|
-
// If this session is the main agent, the local emit already reached the main UI.
|
|
9021
|
-
if (this.#agentId === MAIN_AGENT_ID) return;
|
|
9022
|
-
const mainRef = registry.get(MAIN_AGENT_ID);
|
|
9023
|
-
const mainSession = mainRef?.session;
|
|
9024
|
-
if (!mainSession || mainSession === this) return;
|
|
9025
|
-
const arrow = args.kind === "reply" ? "→ (auto)" : "→";
|
|
9026
|
-
const relayRecord: CustomMessage = {
|
|
9027
|
-
role: "custom",
|
|
9028
|
-
customType: "irc:relay",
|
|
9029
|
-
content: `[IRC \`${args.from}\` ${arrow} \`${args.to}\`]\n\n${args.body}`,
|
|
9030
|
-
display: true,
|
|
9031
|
-
details: { from: args.from, to: args.to, body: args.body, kind: args.kind },
|
|
9032
|
-
attribution: "agent",
|
|
9033
|
-
timestamp: args.timestamp,
|
|
9034
|
-
};
|
|
9035
|
-
mainSession.emitIrcRelayObservation(relayRecord);
|
|
9149
|
+
return "woken";
|
|
9036
9150
|
}
|
|
9037
9151
|
|
|
9038
9152
|
/**
|
|
9039
9153
|
* Emit an IRC relay observation event on this session for UI rendering only.
|
|
9040
|
-
* Does not persist the record to history.
|
|
9154
|
+
* Does not persist the record to history. Called by the IrcBus to surface
|
|
9155
|
+
* agent↔agent traffic on the main session.
|
|
9041
9156
|
*/
|
|
9042
9157
|
emitIrcRelayObservation(record: CustomMessage): void {
|
|
9043
9158
|
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
@@ -9049,7 +9164,7 @@ export class AgentSession {
|
|
|
9049
9164
|
* does not block on, or interfere with, any in-flight main turn. The
|
|
9050
9165
|
* session's history and persisted state are NOT modified by this call.
|
|
9051
9166
|
*
|
|
9052
|
-
* Used by `
|
|
9167
|
+
* Used by `BtwController` (`/btw`) and `OmfgController` (`/omfg`) to share
|
|
9053
9168
|
* the snapshot + stream pipeline. The snapshot includes any in-flight
|
|
9054
9169
|
* streaming assistant text so the model sees the half-finished response
|
|
9055
9170
|
* rather than missing context.
|
|
@@ -9137,7 +9252,7 @@ export class AgentSession {
|
|
|
9137
9252
|
args.onTextDelta(replyText.slice(emittedReplyText.length));
|
|
9138
9253
|
}
|
|
9139
9254
|
return {
|
|
9140
|
-
replyText: args.dedupeReply === false ? replyText.trim() :
|
|
9255
|
+
replyText: args.dedupeReply === false ? replyText.trim() : dedupeEphemeralReply(replyText.trim()),
|
|
9141
9256
|
assistantMessage,
|
|
9142
9257
|
};
|
|
9143
9258
|
}
|
|
@@ -9188,46 +9303,21 @@ export class AgentSession {
|
|
|
9188
9303
|
return messages;
|
|
9189
9304
|
}
|
|
9190
9305
|
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
this.#
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
this.#scheduledBackgroundExchangeFlush = false;
|
|
9207
|
-
return;
|
|
9208
|
-
}
|
|
9209
|
-
if (this.isStreaming) {
|
|
9210
|
-
setTimeout(attempt, 50);
|
|
9211
|
-
return;
|
|
9212
|
-
}
|
|
9213
|
-
this.#scheduledBackgroundExchangeFlush = false;
|
|
9214
|
-
this.#flushPendingBackgroundExchanges();
|
|
9215
|
-
};
|
|
9216
|
-
setTimeout(attempt, 0);
|
|
9217
|
-
}
|
|
9218
|
-
|
|
9219
|
-
#flushPendingBackgroundExchanges(): void {
|
|
9220
|
-
if (this.#pendingBackgroundExchanges.length === 0) return;
|
|
9221
|
-
const batches = this.#pendingBackgroundExchanges;
|
|
9222
|
-
this.#pendingBackgroundExchanges = [];
|
|
9223
|
-
for (const batch of batches) {
|
|
9224
|
-
for (const msg of batch) {
|
|
9225
|
-
// emitExternalEvent on message_end appends to agent state and dispatches
|
|
9226
|
-
// to all session listeners, which in turn handle TUI rendering and
|
|
9227
|
-
// sessionManager persistence via #handleAgentEvent.
|
|
9228
|
-
this.agent.emitExternalEvent({ type: "message_start", message: msg });
|
|
9229
|
-
this.agent.emitExternalEvent({ type: "message_end", message: msg });
|
|
9230
|
-
}
|
|
9306
|
+
/**
|
|
9307
|
+
* Persist any IRC asides that missed their step-boundary injection (the
|
|
9308
|
+
* message landed after the turn's last aside drain). Called at the start
|
|
9309
|
+
* of the next prompt so the model still sees them.
|
|
9310
|
+
*/
|
|
9311
|
+
#flushPendingIrcAsides(): void {
|
|
9312
|
+
if (this.#pendingIrcAsides.length === 0) return;
|
|
9313
|
+
const records = this.#pendingIrcAsides;
|
|
9314
|
+
this.#pendingIrcAsides = [];
|
|
9315
|
+
for (const record of records) {
|
|
9316
|
+
// emitExternalEvent on message_end appends to agent state and dispatches
|
|
9317
|
+
// to all session listeners, which in turn handle TUI rendering and
|
|
9318
|
+
// sessionManager persistence via #handleAgentEvent.
|
|
9319
|
+
this.agent.emitExternalEvent({ type: "message_start", message: record });
|
|
9320
|
+
this.agent.emitExternalEvent({ type: "message_end", message: record });
|
|
9231
9321
|
}
|
|
9232
9322
|
}
|
|
9233
9323
|
|
|
@@ -10032,69 +10122,6 @@ export class AgentSession {
|
|
|
10032
10122
|
});
|
|
10033
10123
|
}
|
|
10034
10124
|
|
|
10035
|
-
/**
|
|
10036
|
-
* Format the conversation as compact context for subagents.
|
|
10037
|
-
* Includes only user messages and assistant text responses.
|
|
10038
|
-
* Excludes: system prompt, tool definitions, tool calls/results, thinking blocks.
|
|
10039
|
-
*/
|
|
10040
|
-
formatCompactContext(): string {
|
|
10041
|
-
const lines: string[] = [];
|
|
10042
|
-
lines.push("# Conversation Context");
|
|
10043
|
-
lines.push("");
|
|
10044
|
-
lines.push(
|
|
10045
|
-
"This is a summary of the parent conversation. Read this if you need additional context about what was discussed or decided.",
|
|
10046
|
-
);
|
|
10047
|
-
lines.push("");
|
|
10048
|
-
|
|
10049
|
-
for (const msg of this.messages) {
|
|
10050
|
-
if (msg.role === "user" || msg.role === "developer") {
|
|
10051
|
-
lines.push(msg.role === "developer" ? "## Developer" : "## User");
|
|
10052
|
-
lines.push("");
|
|
10053
|
-
if (typeof msg.content === "string") {
|
|
10054
|
-
lines.push(msg.content);
|
|
10055
|
-
} else {
|
|
10056
|
-
for (const c of msg.content) {
|
|
10057
|
-
if (c.type === "text") {
|
|
10058
|
-
lines.push(c.text);
|
|
10059
|
-
} else if (c.type === "image") {
|
|
10060
|
-
lines.push("[Image attached]");
|
|
10061
|
-
}
|
|
10062
|
-
}
|
|
10063
|
-
}
|
|
10064
|
-
lines.push("");
|
|
10065
|
-
} else if (msg.role === "assistant") {
|
|
10066
|
-
const assistantMsg = msg as AssistantMessage;
|
|
10067
|
-
// Only include text content, skip tool calls and thinking
|
|
10068
|
-
const textParts: string[] = [];
|
|
10069
|
-
for (const c of assistantMsg.content) {
|
|
10070
|
-
if (c.type === "text" && c.text.trim()) {
|
|
10071
|
-
textParts.push(c.text);
|
|
10072
|
-
}
|
|
10073
|
-
}
|
|
10074
|
-
if (textParts.length > 0) {
|
|
10075
|
-
lines.push("## Assistant");
|
|
10076
|
-
lines.push("");
|
|
10077
|
-
lines.push(textParts.join("\n\n"));
|
|
10078
|
-
lines.push("");
|
|
10079
|
-
}
|
|
10080
|
-
} else if (msg.role === "fileMention") {
|
|
10081
|
-
const fileMsg = msg as FileMentionMessage;
|
|
10082
|
-
const paths = fileMsg.files.map(f => f.path).join(", ");
|
|
10083
|
-
lines.push(`[Files referenced: ${paths}]`);
|
|
10084
|
-
lines.push("");
|
|
10085
|
-
} else if (msg.role === "compactionSummary") {
|
|
10086
|
-
const compactMsg = msg as CompactionSummaryMessage;
|
|
10087
|
-
lines.push("## Earlier Context (Summarized)");
|
|
10088
|
-
lines.push("");
|
|
10089
|
-
lines.push(compactMsg.summary);
|
|
10090
|
-
lines.push("");
|
|
10091
|
-
}
|
|
10092
|
-
// Skip: toolResult, bashExecution, pythonExecution, branchSummary, custom, hookMessage
|
|
10093
|
-
}
|
|
10094
|
-
|
|
10095
|
-
return lines.join("\n").trim();
|
|
10096
|
-
}
|
|
10097
|
-
|
|
10098
10125
|
// =========================================================================
|
|
10099
10126
|
// Extension System
|
|
10100
10127
|
// =========================================================================
|