@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -3
- package/dist/cli.js +841 -803
- package/dist/types/async/index.d.ts +0 -1
- 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 +56 -33
- 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/modes/components/agent-hub.d.ts +30 -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/tool-execution.d.ts +8 -0
- 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/interactive-mode.d.ts +3 -2
- package/dist/types/modes/theme/theme.d.ts +2 -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/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/cli/gallery-cli.ts +1 -1
- 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/settings-schema.ts +56 -40
- package/src/config/settings.ts +7 -0
- 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 +38 -13
- 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 +8 -8
- 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/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/tool-execution.ts +31 -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 +65 -0
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +18 -2
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/interactive-mode.ts +8 -13
- package/src/modes/theme/theme.ts +18 -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 +38 -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 +243 -237
- 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 +718 -794
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +133 -63
- 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/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- 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
|
|
@@ -557,15 +570,15 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
|
|
|
557
570
|
return `${selector.provider}/${selector.id}`;
|
|
558
571
|
}
|
|
559
572
|
|
|
560
|
-
const
|
|
573
|
+
const EPHEMERAL_REPLY_MAX_BYTES = 4096;
|
|
561
574
|
|
|
562
575
|
/**
|
|
563
|
-
* Collapse degenerate
|
|
576
|
+
* Collapse degenerate ephemeral replies (/btw, /omfg side-channel turns).
|
|
564
577
|
* Models occasionally loop on a single line (~16 reports of N-times-repeated
|
|
565
578
|
* replies); compress runs longer than 3 down to one instance + `[…N×]`, then
|
|
566
579
|
* cap at 4 KiB so a runaway reply can't flood the channel.
|
|
567
580
|
*/
|
|
568
|
-
function
|
|
581
|
+
function dedupeEphemeralReply(text: string): string {
|
|
569
582
|
if (!text) return text;
|
|
570
583
|
const lines = text.split("\n");
|
|
571
584
|
const out: string[] = [];
|
|
@@ -582,11 +595,11 @@ function dedupeIrcReply(text: string): string {
|
|
|
582
595
|
i = j;
|
|
583
596
|
}
|
|
584
597
|
let result = out.join("\n");
|
|
585
|
-
if (Buffer.byteLength(result, "utf8") >
|
|
598
|
+
if (Buffer.byteLength(result, "utf8") > EPHEMERAL_REPLY_MAX_BYTES) {
|
|
586
599
|
// Trim by characters until we're under the byte budget — handles multi-byte
|
|
587
600
|
// glyphs at the boundary without splitting them.
|
|
588
601
|
const suffix = "\n[…truncated]";
|
|
589
|
-
const budget =
|
|
602
|
+
const budget = EPHEMERAL_REPLY_MAX_BYTES - Buffer.byteLength(suffix, "utf8");
|
|
590
603
|
while (Buffer.byteLength(result, "utf8") > budget) {
|
|
591
604
|
result = result.slice(0, -1);
|
|
592
605
|
}
|
|
@@ -941,13 +954,11 @@ export class AgentSession {
|
|
|
941
954
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
942
955
|
#evalExecutionDisposing = false;
|
|
943
956
|
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
#
|
|
947
|
-
|
|
948
|
-
// Agent identity + registry for IRC relay forwarding to the main session UI.
|
|
957
|
+
// Incoming IRC messages received while a turn was streaming; drained as
|
|
958
|
+
// non-interrupting asides at the next step boundary (see the aside provider).
|
|
959
|
+
#pendingIrcAsides: CustomMessage[] = [];
|
|
960
|
+
// Agent identity (registry id) used for IRC routing and job ownership.
|
|
949
961
|
#agentId: string | undefined;
|
|
950
|
-
#agentRegistry: AgentRegistry | undefined;
|
|
951
962
|
#providerSessionId: string | undefined;
|
|
952
963
|
#freshProviderSessionId: string | undefined;
|
|
953
964
|
#isDisposed = false;
|
|
@@ -1204,7 +1215,13 @@ export class AgentSession {
|
|
|
1204
1215
|
// Background-job completions / late diagnostics are pulled into the run at
|
|
1205
1216
|
// each step boundary as non-interrupting asides (see Agent.getAsideMessages),
|
|
1206
1217
|
// so they reach the model between requests without waiting for a yield.
|
|
1207
|
-
this.agent.setAsideMessageProvider(() =>
|
|
1218
|
+
this.agent.setAsideMessageProvider(() => {
|
|
1219
|
+
const pendingIrc = this.#pendingIrcAsides;
|
|
1220
|
+
this.#pendingIrcAsides = [];
|
|
1221
|
+
const thunks: AsideMessage[] = pendingIrc.map(record => () => record);
|
|
1222
|
+
thunks.push(...this.yieldQueue.drainLazy());
|
|
1223
|
+
return thunks;
|
|
1224
|
+
});
|
|
1208
1225
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
1209
1226
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
1210
1227
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
@@ -1235,7 +1252,6 @@ export class AgentSession {
|
|
|
1235
1252
|
this.#ttsrManager = config.ttsrManager;
|
|
1236
1253
|
this.#obfuscator = config.obfuscator;
|
|
1237
1254
|
this.#agentId = config.agentId;
|
|
1238
|
-
this.#agentRegistry = config.agentRegistry;
|
|
1239
1255
|
this.#providerSessionId = config.providerSessionId;
|
|
1240
1256
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1241
1257
|
const event: AgentEvent = {
|
|
@@ -3091,15 +3107,28 @@ export class AgentSession {
|
|
|
3091
3107
|
}
|
|
3092
3108
|
|
|
3093
3109
|
/**
|
|
3094
|
-
*
|
|
3095
|
-
*
|
|
3110
|
+
* Synchronously mark the session as disposing so new work is rejected
|
|
3111
|
+
* immediately: Python/eval starts throw, queued asides are dropped, and the
|
|
3112
|
+
* aside provider is detached. Idempotent; `dispose()` runs it first.
|
|
3113
|
+
*
|
|
3114
|
+
* Wrappers that await other teardown before delegating to `dispose()` MUST
|
|
3115
|
+
* call this before their first await — otherwise work started in that async
|
|
3116
|
+
* gap slips past the disposal guards.
|
|
3096
3117
|
*/
|
|
3097
|
-
|
|
3118
|
+
beginDispose(): void {
|
|
3098
3119
|
this.#isDisposed = true;
|
|
3099
|
-
this.#
|
|
3120
|
+
this.#pendingIrcAsides = [];
|
|
3100
3121
|
this.yieldQueue.clear();
|
|
3101
3122
|
this.agent.setAsideMessageProvider(undefined);
|
|
3102
3123
|
this.#evalExecutionDisposing = true;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
/**
|
|
3127
|
+
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
3128
|
+
* Call this when completely done with the session.
|
|
3129
|
+
*/
|
|
3130
|
+
async dispose(): Promise<void> {
|
|
3131
|
+
this.beginDispose();
|
|
3103
3132
|
try {
|
|
3104
3133
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
3105
3134
|
await this.#extensionRunner.emit({ type: "session_shutdown" });
|
|
@@ -4032,6 +4061,16 @@ export class AgentSession {
|
|
|
4032
4061
|
return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
|
|
4033
4062
|
}
|
|
4034
4063
|
|
|
4064
|
+
/**
|
|
4065
|
+
* Full-history transcript for TUI display: every path entry in
|
|
4066
|
+
* chronological order with compactions rendered inline at the point they
|
|
4067
|
+
* fired (instead of replacing prior history). Display-only — NEVER feed
|
|
4068
|
+
* the result to `agent.replaceMessages` or a provider.
|
|
4069
|
+
*/
|
|
4070
|
+
buildTranscriptSessionContext(): SessionContext {
|
|
4071
|
+
return deobfuscateSessionContext(this.sessionManager.buildSessionContext({ transcript: true }), this.#obfuscator);
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4035
4074
|
#obfuscateForProvider<T>(value: T): T {
|
|
4036
4075
|
if (!this.#obfuscator?.hasSecrets()) return value;
|
|
4037
4076
|
return this.#obfuscator.obfuscateObject(value);
|
|
@@ -4634,7 +4673,7 @@ export class AgentSession {
|
|
|
4634
4673
|
// Flush any pending bash messages before the new prompt
|
|
4635
4674
|
this.#flushPendingBashMessages();
|
|
4636
4675
|
this.#flushPendingPythonMessages();
|
|
4637
|
-
this.#
|
|
4676
|
+
this.#flushPendingIrcAsides();
|
|
4638
4677
|
|
|
4639
4678
|
// Reset todo reminder count on new user prompt
|
|
4640
4679
|
this.#todoReminderCount = 0;
|
|
@@ -6048,6 +6087,35 @@ export class AgentSession {
|
|
|
6048
6087
|
return result;
|
|
6049
6088
|
}
|
|
6050
6089
|
|
|
6090
|
+
/**
|
|
6091
|
+
* Per-turn supersede pass: prune older `read` results that a newer read of
|
|
6092
|
+
* the same file has made stale. Cache-aware (only fires when the suffix
|
|
6093
|
+
* after a candidate is small or the session has been idle long enough that
|
|
6094
|
+
* the provider prompt cache is cold), so it is cheap to run every turn.
|
|
6095
|
+
* Gated on the `compaction.supersedeReads` setting.
|
|
6096
|
+
*/
|
|
6097
|
+
async #pruneSupersededReads(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
6098
|
+
if (!this.settings.getGroup("compaction").supersedeReads) return undefined;
|
|
6099
|
+
const branchEntries = this.sessionManager.getBranch();
|
|
6100
|
+
const result = pruneSupersededToolResults(
|
|
6101
|
+
branchEntries,
|
|
6102
|
+
this.#withPlanProtection({
|
|
6103
|
+
supersedeKey: readToolSupersedeKey,
|
|
6104
|
+
protectedTools: [...DEFAULT_PRUNE_CONFIG.protectedTools],
|
|
6105
|
+
}),
|
|
6106
|
+
);
|
|
6107
|
+
if (result.prunedCount === 0) {
|
|
6108
|
+
return undefined;
|
|
6109
|
+
}
|
|
6110
|
+
|
|
6111
|
+
await this.sessionManager.rewriteEntries();
|
|
6112
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
6113
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
6114
|
+
this.#syncTodoPhasesFromBranch();
|
|
6115
|
+
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6116
|
+
return result;
|
|
6117
|
+
}
|
|
6118
|
+
|
|
6051
6119
|
/**
|
|
6052
6120
|
* Strip image content blocks from every message on the current branch and
|
|
6053
6121
|
* persist the rewrite. Walks `SessionManager.getBranch()` in place — both
|
|
@@ -6237,6 +6305,20 @@ export class AgentSession {
|
|
|
6237
6305
|
|
|
6238
6306
|
const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
|
|
6239
6307
|
|
|
6308
|
+
// Strategy honored on manual /compact too. Custom instructions imply a
|
|
6309
|
+
// directed LLM summary; a text-only model cannot read the frames back —
|
|
6310
|
+
// both take the summarizer path (the latter loudly).
|
|
6311
|
+
const wantsSnapcompact =
|
|
6312
|
+
compactionPrep.kind !== "fromHook" && compactionSettings.strategy === "snapcompact" && !customInstructions;
|
|
6313
|
+
const snapcompactReady = wantsSnapcompact && this.model.input.includes("image");
|
|
6314
|
+
if (wantsSnapcompact && !snapcompactReady) {
|
|
6315
|
+
this.emitNotice(
|
|
6316
|
+
"warning",
|
|
6317
|
+
`snapcompact needs a vision-capable model (${this.model.id} is text-only) — using an LLM summary instead`,
|
|
6318
|
+
"compaction",
|
|
6319
|
+
);
|
|
6320
|
+
}
|
|
6321
|
+
|
|
6240
6322
|
let summary: string;
|
|
6241
6323
|
let shortSummary: string | undefined;
|
|
6242
6324
|
let firstKeptEntryId: string;
|
|
@@ -6250,6 +6332,14 @@ export class AgentSession {
|
|
|
6250
6332
|
tokensBefore = compactionPrep.tokensBefore;
|
|
6251
6333
|
details = compactionPrep.details;
|
|
6252
6334
|
preserveData = compactionPrep.preserveData;
|
|
6335
|
+
} else if (snapcompactReady) {
|
|
6336
|
+
const snapcompactResult = await snapcompactCompact(preparation, { convertToLlm, model: this.model });
|
|
6337
|
+
summary = snapcompactResult.summary;
|
|
6338
|
+
shortSummary = snapcompactResult.shortSummary;
|
|
6339
|
+
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
6340
|
+
tokensBefore = snapcompactResult.tokensBefore;
|
|
6341
|
+
details = snapcompactResult.details;
|
|
6342
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
|
6253
6343
|
} else {
|
|
6254
6344
|
// Generate compaction result. Only convert known abort-shaped
|
|
6255
6345
|
// rejections (AbortError raised while the abort signal is set,
|
|
@@ -6669,7 +6759,10 @@ export class AgentSession {
|
|
|
6669
6759
|
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
6670
6760
|
strategy: incompleteCompactionSettings.strategy,
|
|
6671
6761
|
});
|
|
6672
|
-
await this.#runAutoCompaction("incomplete", true, false, allowDefer, {
|
|
6762
|
+
await this.#runAutoCompaction("incomplete", true, false, allowDefer, {
|
|
6763
|
+
autoContinue,
|
|
6764
|
+
triggerContextTokens: calculateContextTokens(assistantMessage.usage),
|
|
6765
|
+
});
|
|
6673
6766
|
} else {
|
|
6674
6767
|
// Neither promotion nor compaction is available — surface the dead-end so
|
|
6675
6768
|
// the user understands why the turn yielded with nothing.
|
|
@@ -6680,6 +6773,10 @@ export class AgentSession {
|
|
|
6680
6773
|
return false;
|
|
6681
6774
|
}
|
|
6682
6775
|
|
|
6776
|
+
// Supersede pass runs every turn, before any threshold gating: it is cheap
|
|
6777
|
+
// (bails when no candidate) and independent of the compaction setting.
|
|
6778
|
+
const supersedeResult = await this.#pruneSupersededReads();
|
|
6779
|
+
|
|
6683
6780
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6684
6781
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
6685
6782
|
|
|
@@ -6688,6 +6785,9 @@ export class AgentSession {
|
|
|
6688
6785
|
if (assistantMessage.stopReason === "error") return false;
|
|
6689
6786
|
const pruneResult = await this.#pruneToolOutputs();
|
|
6690
6787
|
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
6788
|
+
if (supersedeResult) {
|
|
6789
|
+
contextTokens = Math.max(0, contextTokens - supersedeResult.tokensSaved);
|
|
6790
|
+
}
|
|
6691
6791
|
if (pruneResult) {
|
|
6692
6792
|
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
6693
6793
|
}
|
|
@@ -6695,7 +6795,10 @@ export class AgentSession {
|
|
|
6695
6795
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
6696
6796
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
6697
6797
|
if (!promoted) {
|
|
6698
|
-
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
6798
|
+
return await this.#runAutoCompaction("threshold", false, false, allowDefer, {
|
|
6799
|
+
autoContinue,
|
|
6800
|
+
triggerContextTokens: contextTokens,
|
|
6801
|
+
});
|
|
6699
6802
|
}
|
|
6700
6803
|
}
|
|
6701
6804
|
return false;
|
|
@@ -7540,7 +7643,7 @@ export class AgentSession {
|
|
|
7540
7643
|
willRetry: boolean,
|
|
7541
7644
|
deferred = false,
|
|
7542
7645
|
allowDefer = true,
|
|
7543
|
-
options: { autoContinue?: boolean } = {},
|
|
7646
|
+
options: { autoContinue?: boolean; triggerContextTokens?: number } = {},
|
|
7544
7647
|
): Promise<boolean> {
|
|
7545
7648
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7546
7649
|
if (compactionSettings.strategy === "off") return false;
|
|
@@ -7551,7 +7654,13 @@ export class AgentSession {
|
|
|
7551
7654
|
// reclaims nothing we fall through to the summary-compaction body below so
|
|
7552
7655
|
// the oversized input still gets resolved.
|
|
7553
7656
|
if (compactionSettings.strategy === "shake") {
|
|
7554
|
-
const outcome = await this.#runAutoShake(
|
|
7657
|
+
const outcome = await this.#runAutoShake(
|
|
7658
|
+
reason,
|
|
7659
|
+
willRetry,
|
|
7660
|
+
generation,
|
|
7661
|
+
shouldAutoContinue,
|
|
7662
|
+
options.triggerContextTokens,
|
|
7663
|
+
);
|
|
7555
7664
|
if (outcome !== "fallback") return false;
|
|
7556
7665
|
}
|
|
7557
7666
|
// "overflow" and "incomplete" force inline execution because they are recovery
|
|
@@ -7578,9 +7687,25 @@ export class AgentSession {
|
|
|
7578
7687
|
|
|
7579
7688
|
// "overflow" forces context-full because the input itself is broken — a handoff
|
|
7580
7689
|
// 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
|
-
|
|
7690
|
+
// so a handoff request on the existing context is still viable. Snapcompact is
|
|
7691
|
+
// safe for every reason (it makes no LLM call at all) but requires a vision
|
|
7692
|
+
// model to be worth anything — fall back to context-full otherwise.
|
|
7693
|
+
let action: "context-full" | "handoff" | "snapcompact" =
|
|
7583
7694
|
compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
|
|
7695
|
+
if (compactionSettings.strategy === "snapcompact") {
|
|
7696
|
+
if (this.model?.input.includes("image")) {
|
|
7697
|
+
action = "snapcompact";
|
|
7698
|
+
} else {
|
|
7699
|
+
logger.warn("Snapcompact compaction requires a vision-capable model; falling back to context-full", {
|
|
7700
|
+
model: this.model?.id,
|
|
7701
|
+
});
|
|
7702
|
+
this.emitNotice(
|
|
7703
|
+
"warning",
|
|
7704
|
+
`snapcompact needs a vision-capable model (${this.model?.id ?? "unknown"} is text-only) — using an LLM summary instead`,
|
|
7705
|
+
"compaction",
|
|
7706
|
+
);
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7584
7709
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
7585
7710
|
// Abort any older auto-compaction before installing this run's controller.
|
|
7586
7711
|
this.#autoCompactionAbortController?.abort();
|
|
@@ -7719,6 +7844,16 @@ export class AgentSession {
|
|
|
7719
7844
|
tokensBefore = compactionPrep.tokensBefore;
|
|
7720
7845
|
details = compactionPrep.details;
|
|
7721
7846
|
preserveData = compactionPrep.preserveData;
|
|
7847
|
+
} else if (action === "snapcompact") {
|
|
7848
|
+
// Local, deterministic: render discarded history onto PNG frames.
|
|
7849
|
+
// No model candidates, no API key, no retry loop.
|
|
7850
|
+
const snapcompactResult = await snapcompactCompact(preparation, { convertToLlm, model: this.model });
|
|
7851
|
+
summary = snapcompactResult.summary;
|
|
7852
|
+
shortSummary = snapcompactResult.shortSummary;
|
|
7853
|
+
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
7854
|
+
tokensBefore = snapcompactResult.tokensBefore;
|
|
7855
|
+
details = snapcompactResult.details;
|
|
7856
|
+
preserveData = { ...(compactionPrep.preserveData ?? {}), ...(snapcompactResult.preserveData ?? {}) };
|
|
7722
7857
|
} else {
|
|
7723
7858
|
const candidates = this.#getCompactionModelCandidates(availableModels);
|
|
7724
7859
|
const retrySettings = this.settings.getGroup("retry");
|
|
@@ -7958,6 +8093,7 @@ export class AgentSession {
|
|
|
7958
8093
|
willRetry: boolean,
|
|
7959
8094
|
generation: number,
|
|
7960
8095
|
autoContinue: boolean,
|
|
8096
|
+
triggerContextTokens?: number,
|
|
7961
8097
|
): Promise<"handled" | "fallback"> {
|
|
7962
8098
|
const action = "shake";
|
|
7963
8099
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
@@ -7978,8 +8114,8 @@ export class AgentSession {
|
|
|
7978
8114
|
return "handled";
|
|
7979
8115
|
}
|
|
7980
8116
|
const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
|
|
7981
|
-
// Detect the dead-loop reported in
|
|
7982
|
-
// shake runs, but
|
|
8117
|
+
// Detect the dead-loop reported in issues #2119/#2275: the threshold check
|
|
8118
|
+
// fires, shake runs, but residual context is still above the configured
|
|
7983
8119
|
// threshold. The next agent_end would re-trigger shake, which has nothing
|
|
7984
8120
|
// new to drop on the second pass, so the loop spins until the user kills it.
|
|
7985
8121
|
// Same hazard for "incomplete" (the retry would re-hit the length cap) and
|
|
@@ -7987,10 +8123,30 @@ export class AgentSession {
|
|
|
7987
8123
|
// reason we hand off to the summarization-driven context-full path so the
|
|
7988
8124
|
// situation actually resolves; "idle" is exempt because its 60s+ timer
|
|
7989
8125
|
// re-checks usage before re-firing and cannot dead-loop on its own.
|
|
8126
|
+
//
|
|
8127
|
+
// #2275: the post-shake check MUST be anchored on the same metric that
|
|
8128
|
+
// triggered compaction. The local estimator (`#estimatePendingPromptTokens`)
|
|
8129
|
+
// undercounts thinking-signature payloads, so on thinking-heavy sessions it
|
|
8130
|
+
// reads well below the provider-reported usage that fired the threshold.
|
|
8131
|
+
// When that estimate slips under the threshold, the fallback never fires
|
|
8132
|
+
// and the auto-continue prompt re-injects every turn. Prefer the trigger's
|
|
8133
|
+
// own `contextTokens` (provider-anchored) when the caller supplies it, and
|
|
8134
|
+
// add hysteresis (80% recovery band) so we don't oscillate at the boundary
|
|
8135
|
+
// while shake keeps reclaiming a trickle of the previous turn's output.
|
|
7990
8136
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7991
8137
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7992
|
-
|
|
7993
|
-
|
|
8138
|
+
let stillOverThreshold = false;
|
|
8139
|
+
if (contextWindow > 0) {
|
|
8140
|
+
if (typeof triggerContextTokens === "number" && Number.isFinite(triggerContextTokens)) {
|
|
8141
|
+
const correctedTokens = Math.max(0, triggerContextTokens - result.tokensFreed);
|
|
8142
|
+
const thresholdTokens = resolveThresholdTokens(contextWindow, compactionSettings);
|
|
8143
|
+
const recoveryBand = Math.floor(thresholdTokens * SHAKE_RECOVERY_BAND);
|
|
8144
|
+
stillOverThreshold = correctedTokens > recoveryBand;
|
|
8145
|
+
} else {
|
|
8146
|
+
const postShakeTokens = this.#estimatePendingPromptTokens([]);
|
|
8147
|
+
stillOverThreshold = shouldCompact(postShakeTokens, contextWindow, compactionSettings);
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
7994
8150
|
const shouldFallBack = reason !== "idle" && ((reason === "overflow" && !reclaimed) || stillOverThreshold);
|
|
7995
8151
|
if (shouldFallBack) {
|
|
7996
8152
|
const errorMessage = reclaimed
|
|
@@ -8926,118 +9082,56 @@ export class AgentSession {
|
|
|
8926
9082
|
}
|
|
8927
9083
|
|
|
8928
9084
|
// =========================================================================
|
|
8929
|
-
//
|
|
9085
|
+
// IRC Delivery
|
|
8930
9086
|
// =========================================================================
|
|
8931
9087
|
|
|
8932
9088
|
/**
|
|
8933
|
-
*
|
|
8934
|
-
*
|
|
9089
|
+
* Deliver an IRC message into this session (recipient side; called by the
|
|
9090
|
+
* IrcBus). Emits the `irc_message` session event for UI cards and injects
|
|
9091
|
+
* the rendered message into the model's context as an `irc:incoming`
|
|
9092
|
+
* custom message:
|
|
8935
9093
|
*
|
|
8936
|
-
*
|
|
8937
|
-
*
|
|
8938
|
-
*
|
|
8939
|
-
*
|
|
8940
|
-
*
|
|
8941
|
-
*
|
|
9094
|
+
* - mid-turn → queued on the aside channel and folded in at the next step
|
|
9095
|
+
* boundary (non-interrupting, like async-result deliveries) → "injected";
|
|
9096
|
+
* - idle → starts a real turn with the message so the recipient wakes
|
|
9097
|
+
* → "woken".
|
|
9098
|
+
*
|
|
9099
|
+
* Never blocks on the recipient's turn: the wake turn is fire-and-forget.
|
|
8942
9100
|
*/
|
|
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 = {
|
|
9101
|
+
async deliverIrcMessage(msg: IrcMessage): Promise<"injected" | "woken"> {
|
|
9102
|
+
if (this.#isDisposed) {
|
|
9103
|
+
throw new Error("Recipient session is disposed.");
|
|
9104
|
+
}
|
|
9105
|
+
const record: CustomMessage = {
|
|
8952
9106
|
role: "custom",
|
|
8953
9107
|
customType: "irc:incoming",
|
|
8954
|
-
content:
|
|
9108
|
+
content: prompt.render(ircIncomingTemplate, {
|
|
9109
|
+
from: msg.from,
|
|
9110
|
+
message: msg.body,
|
|
9111
|
+
replyTo: msg.replyTo ?? "",
|
|
9112
|
+
}),
|
|
8955
9113
|
display: true,
|
|
8956
|
-
details: { from:
|
|
9114
|
+
details: { id: msg.id, from: msg.from, message: msg.body, ...(msg.replyTo ? { replyTo: msg.replyTo } : {}) },
|
|
8957
9115
|
attribution: "agent",
|
|
8958
|
-
timestamp:
|
|
9116
|
+
timestamp: msg.ts,
|
|
8959
9117
|
};
|
|
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 };
|
|
9118
|
+
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
9119
|
+
if (this.isStreaming) {
|
|
9120
|
+
this.#pendingIrcAsides.push(record);
|
|
9121
|
+
return "injected";
|
|
8972
9122
|
}
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
9123
|
+
// Idle: same wake primitive the yield queue uses for async-result
|
|
9124
|
+
// delivery — prompt the agent directly so a real turn runs.
|
|
9125
|
+
this.agent.prompt(record).catch(error => {
|
|
9126
|
+
logger.warn("IRC wake turn failed", { from: msg.from, to: msg.to, error: String(error) });
|
|
8977
9127
|
});
|
|
8978
|
-
|
|
8979
|
-
promptText: incomingPrompt,
|
|
8980
|
-
signal: args.signal,
|
|
8981
|
-
});
|
|
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);
|
|
9128
|
+
return "woken";
|
|
9036
9129
|
}
|
|
9037
9130
|
|
|
9038
9131
|
/**
|
|
9039
9132
|
* Emit an IRC relay observation event on this session for UI rendering only.
|
|
9040
|
-
* Does not persist the record to history.
|
|
9133
|
+
* Does not persist the record to history. Called by the IrcBus to surface
|
|
9134
|
+
* agent↔agent traffic on the main session.
|
|
9041
9135
|
*/
|
|
9042
9136
|
emitIrcRelayObservation(record: CustomMessage): void {
|
|
9043
9137
|
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
@@ -9049,7 +9143,7 @@ export class AgentSession {
|
|
|
9049
9143
|
* does not block on, or interfere with, any in-flight main turn. The
|
|
9050
9144
|
* session's history and persisted state are NOT modified by this call.
|
|
9051
9145
|
*
|
|
9052
|
-
* Used by `
|
|
9146
|
+
* Used by `BtwController` (`/btw`) and `OmfgController` (`/omfg`) to share
|
|
9053
9147
|
* the snapshot + stream pipeline. The snapshot includes any in-flight
|
|
9054
9148
|
* streaming assistant text so the model sees the half-finished response
|
|
9055
9149
|
* rather than missing context.
|
|
@@ -9137,7 +9231,7 @@ export class AgentSession {
|
|
|
9137
9231
|
args.onTextDelta(replyText.slice(emittedReplyText.length));
|
|
9138
9232
|
}
|
|
9139
9233
|
return {
|
|
9140
|
-
replyText: args.dedupeReply === false ? replyText.trim() :
|
|
9234
|
+
replyText: args.dedupeReply === false ? replyText.trim() : dedupeEphemeralReply(replyText.trim()),
|
|
9141
9235
|
assistantMessage,
|
|
9142
9236
|
};
|
|
9143
9237
|
}
|
|
@@ -9188,46 +9282,21 @@ export class AgentSession {
|
|
|
9188
9282
|
return messages;
|
|
9189
9283
|
}
|
|
9190
9284
|
|
|
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
|
-
}
|
|
9285
|
+
/**
|
|
9286
|
+
* Persist any IRC asides that missed their step-boundary injection (the
|
|
9287
|
+
* message landed after the turn's last aside drain). Called at the start
|
|
9288
|
+
* of the next prompt so the model still sees them.
|
|
9289
|
+
*/
|
|
9290
|
+
#flushPendingIrcAsides(): void {
|
|
9291
|
+
if (this.#pendingIrcAsides.length === 0) return;
|
|
9292
|
+
const records = this.#pendingIrcAsides;
|
|
9293
|
+
this.#pendingIrcAsides = [];
|
|
9294
|
+
for (const record of records) {
|
|
9295
|
+
// emitExternalEvent on message_end appends to agent state and dispatches
|
|
9296
|
+
// to all session listeners, which in turn handle TUI rendering and
|
|
9297
|
+
// sessionManager persistence via #handleAgentEvent.
|
|
9298
|
+
this.agent.emitExternalEvent({ type: "message_start", message: record });
|
|
9299
|
+
this.agent.emitExternalEvent({ type: "message_end", message: record });
|
|
9231
9300
|
}
|
|
9232
9301
|
}
|
|
9233
9302
|
|
|
@@ -10032,69 +10101,6 @@ export class AgentSession {
|
|
|
10032
10101
|
});
|
|
10033
10102
|
}
|
|
10034
10103
|
|
|
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
10104
|
// =========================================================================
|
|
10099
10105
|
// Extension System
|
|
10100
10106
|
// =========================================================================
|