@kenkaiiii/ggcoder 5.25.0 → 5.26.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/dist/app-sidecar.js +44 -0
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +12 -0
- package/dist/config.test.js.map +1 -1
- package/dist/core/agent-notifications.d.ts +62 -0
- package/dist/core/agent-notifications.d.ts.map +1 -0
- package/dist/core/agent-notifications.js +92 -0
- package/dist/core/agent-notifications.js.map +1 -0
- package/dist/core/agent-notifications.test.d.ts +2 -0
- package/dist/core/agent-notifications.test.d.ts.map +1 -0
- package/dist/core/agent-notifications.test.js +97 -0
- package/dist/core/agent-notifications.test.js.map +1 -0
- package/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/dist/core/agent-session-model-switch.test.js +161 -0
- package/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/dist/core/agent-session-notifications.test.d.ts +2 -0
- package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
- package/dist/core/agent-session-notifications.test.js +114 -0
- package/dist/core/agent-session-notifications.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +68 -6
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +234 -25
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/event-bus.d.ts +6 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +7 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/memory/compaction-notes.d.ts +31 -0
- package/dist/core/memory/compaction-notes.d.ts.map +1 -0
- package/dist/core/memory/compaction-notes.js +162 -0
- package/dist/core/memory/compaction-notes.js.map +1 -0
- package/dist/core/memory/compaction-notes.test.d.ts +2 -0
- package/dist/core/memory/compaction-notes.test.d.ts.map +1 -0
- package/dist/core/memory/compaction-notes.test.js +110 -0
- package/dist/core/memory/compaction-notes.test.js.map +1 -0
- package/dist/core/memory/journal.d.ts +55 -0
- package/dist/core/memory/journal.d.ts.map +1 -0
- package/dist/core/memory/journal.js +212 -0
- package/dist/core/memory/journal.js.map +1 -0
- package/dist/core/memory/journal.test.d.ts +2 -0
- package/dist/core/memory/journal.test.d.ts.map +1 -0
- package/dist/core/memory/journal.test.js +184 -0
- package/dist/core/memory/journal.test.js.map +1 -0
- package/dist/core/process-manager-notifications.test.d.ts +2 -0
- package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
- package/dist/core/process-manager-notifications.test.js +113 -0
- package/dist/core/process-manager-notifications.test.js.map +1 -0
- package/dist/core/process-manager.d.ts +29 -0
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +123 -0
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +2 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +7 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/steering.d.ts +12 -0
- package/dist/core/steering.d.ts.map +1 -1
- package/dist/core/steering.js +16 -0
- package/dist/core/steering.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +13 -0
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +30 -0
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +16 -0
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +5 -0
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
- package/dist/modes/subagent-worker-mode.js +3 -1
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/subagent-control.d.ts.map +1 -1
- package/dist/tools/subagent-control.js +6 -2
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +6 -0
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js +6 -0
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/task-output.d.ts.map +1 -1
- package/dist/tools/task-output.js +3 -1
- package/dist/tools/task-output.js.map +1 -1
- package/dist/ui/App.d.ts +2 -0
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +35 -1
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/render.d.ts +2 -0
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +1 -0
- package/dist/ui/render.js.map +1 -1
- package/package.json +4 -4
|
@@ -32,13 +32,21 @@ import { detectProjectStack } from "./language-detector.js";
|
|
|
32
32
|
import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageMessage, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
|
|
33
33
|
import { evaluateLoopBreak, buildLoopBreakMessage, CycleDetector, ToolCallProgressTracker, detectTextRepetition, } from "./loop-breaker.js";
|
|
34
34
|
import { buildRegroundingMessage } from "./regrounding.js";
|
|
35
|
-
import { wrapSteeringText, STEERING_PREFIX } from "./steering.js";
|
|
35
|
+
import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
|
|
36
|
+
import { AgentNotificationQueue } from "./agent-notifications.js";
|
|
37
|
+
import { ProjectJournal, buildJournalPromptTail } from "./memory/journal.js";
|
|
38
|
+
import { buildCompactionNotes } from "./memory/compaction-notes.js";
|
|
36
39
|
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
37
40
|
import { normalizeMessageImages } from "./message-images.js";
|
|
38
41
|
import crypto from "node:crypto";
|
|
39
42
|
import fs from "node:fs/promises";
|
|
40
43
|
import os from "node:os";
|
|
41
44
|
import path from "node:path";
|
|
45
|
+
/**
|
|
46
|
+
* A run whose tool calls fail more often than this is thrashing, not
|
|
47
|
+
* progressing — refuse to extend its turn budget.
|
|
48
|
+
*/
|
|
49
|
+
const TURN_EXTENSION_MAX_FAILURE_RATIO = 0.5;
|
|
42
50
|
// ── Tool-result policy ─────────────────────────────────────
|
|
43
51
|
/** Resolve the per-result cap passed to the agent loop for the active transport. */
|
|
44
52
|
export function resolveSessionToolResultCharLimit(model, provider, accountId) {
|
|
@@ -57,6 +65,22 @@ export function resolveSessionTurnToolResultCharLimit(model, provider, accountId
|
|
|
57
65
|
const contextChars = getContextWindow(model, { provider, accountId }) * 3.5;
|
|
58
66
|
return Math.max(100_000, Math.min(Math.floor(contextChars * 0.15), 240_000));
|
|
59
67
|
}
|
|
68
|
+
/** Marker the compactor prepends to the summary message it injects. */
|
|
69
|
+
const COMPACTION_SUMMARY_MARKER = "[Previous conversation summary]";
|
|
70
|
+
/** The compactor's summary text, identified by its marker. */
|
|
71
|
+
function isCompactionSummary(content) {
|
|
72
|
+
return typeof content === "string" && content.startsWith(COMPACTION_SUMMARY_MARKER);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* True when an assistant message ends a turn with tool calls still awaiting
|
|
76
|
+
* their results. Inserting a user message there would orphan the tool_use
|
|
77
|
+
* blocks and the provider rejects the next request.
|
|
78
|
+
*/
|
|
79
|
+
function hasUnresolvedToolCalls(message) {
|
|
80
|
+
if (typeof message.content === "string" || !Array.isArray(message.content))
|
|
81
|
+
return false;
|
|
82
|
+
return message.content.some((part) => part.type === "tool_call");
|
|
83
|
+
}
|
|
60
84
|
// ── Agent Session ──────────────────────────────────────────
|
|
61
85
|
export class AgentSession {
|
|
62
86
|
eventBus = new EventBus();
|
|
@@ -132,6 +156,18 @@ export class AgentSession {
|
|
|
132
156
|
processManager;
|
|
133
157
|
lspManager;
|
|
134
158
|
subAgentManager;
|
|
159
|
+
/**
|
|
160
|
+
* Out-of-band push notifications (finished children, background-process
|
|
161
|
+
* progress). Producers enqueue; `getHookSteeringMessages` drains into the
|
|
162
|
+
* live turn so the agent never has to spend a turn asking.
|
|
163
|
+
*/
|
|
164
|
+
notifications = new AgentNotificationQueue();
|
|
165
|
+
/**
|
|
166
|
+
* Durable cross-session journal at `<project>/.gg/memory.md`. Written only by
|
|
167
|
+
* the compactor, and only as past-tense history — see `memory/journal.ts` for
|
|
168
|
+
* why nothing else may write it. Undefined when `memoryEnabled` is off.
|
|
169
|
+
*/
|
|
170
|
+
journal;
|
|
135
171
|
managerAbortSignal;
|
|
136
172
|
managerAbortHandler = () => {
|
|
137
173
|
void this.subAgentManager?.interruptAll();
|
|
@@ -227,6 +263,12 @@ export class AgentSession {
|
|
|
227
263
|
await this.settingsManager.load();
|
|
228
264
|
this.authStorage = new AuthStorage(paths.authFile);
|
|
229
265
|
await this.authStorage.load();
|
|
266
|
+
// Opt-in: it is the only feature here that adds tokens to every turn.
|
|
267
|
+
// Transient sessions (Ken, sub-agents) never write a project's journal —
|
|
268
|
+
// their compactions are not the project's history.
|
|
269
|
+
if (this.settingsManager.get("memoryEnabled") && !this.opts.transient) {
|
|
270
|
+
this.journal = new ProjectJournal(this.cwd);
|
|
271
|
+
}
|
|
230
272
|
// Session manager. Agent-specific roots keep chat and coder histories isolated.
|
|
231
273
|
this.sessionManager = new SessionManager(this.opts.sessionRootDir ?? paths.sessionsDir);
|
|
232
274
|
const projectCustomization = this.opts.projectCustomization !== false;
|
|
@@ -285,6 +327,7 @@ export class AgentSession {
|
|
|
285
327
|
getMaxPerModel: () => this.settingsManager.get("subagentMaxPerModel"),
|
|
286
328
|
disableAsyncSubagents: this.opts.subagentWorker,
|
|
287
329
|
onSubAgentState: (snapshot) => this.eventBus.emit("subagent_state", snapshot),
|
|
330
|
+
notifications: this.notifications,
|
|
288
331
|
// Plan mode: only wired when the host supplies callbacks. The ref is
|
|
289
332
|
// shared so bash/edit/write enforce read-only restrictions live.
|
|
290
333
|
...(this.opts.onEnterPlan || this.opts.onExitPlan
|
|
@@ -324,7 +367,6 @@ export class AgentSession {
|
|
|
324
367
|
(await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider, this.promptEnvironment()));
|
|
325
368
|
this.baseSystemPrompt = basePrompt;
|
|
326
369
|
this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
|
|
327
|
-
this.syncUltraOrchestrationPrompt();
|
|
328
370
|
// Load or create session. Transient sessions (subagent spawns) never
|
|
329
371
|
// touch the session store — sessionPath stays empty and persistMessage
|
|
330
372
|
// is a no-op so their transcripts can't pollute `ggcoder continue`.
|
|
@@ -728,12 +770,31 @@ export class AgentSession {
|
|
|
728
770
|
}
|
|
729
771
|
}
|
|
730
772
|
/**
|
|
731
|
-
* Mid-loop steering hook:
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
773
|
+
* Mid-loop steering hook: delivers pushed notifications and queued user
|
|
774
|
+
* steering, fires the loop-breaker when the agent looks stuck, then
|
|
775
|
+
* post-compaction re-grounding. At most one loop-break/re-grounding per run.
|
|
776
|
+
* Mirrors the TUI's getSteeringMessages ordering.
|
|
735
777
|
*/
|
|
736
778
|
getHookSteeringMessages() {
|
|
779
|
+
// Push notifications: a child that finished or a background build that
|
|
780
|
+
// exited is new *fact*, not a correction, and it is cheap (bounded to 1 KiB
|
|
781
|
+
// per drain). Drained above the loop-break checks so the agent can act on
|
|
782
|
+
// it in the very next turn instead of discovering it at the pre-stop
|
|
783
|
+
// completion gate — but it never displaces user steering, which rides out
|
|
784
|
+
// in the same batch when both are pending.
|
|
785
|
+
const notified = this.notifications.drain();
|
|
786
|
+
const notificationMessage = notified.length > 0
|
|
787
|
+
? {
|
|
788
|
+
role: "user",
|
|
789
|
+
content: buildNotificationSteeringText(notified.map((entry) => entry.text)),
|
|
790
|
+
}
|
|
791
|
+
: null;
|
|
792
|
+
if (notificationMessage) {
|
|
793
|
+
log("INFO", "notifications", "Injecting pushed notifications", {
|
|
794
|
+
count: String(notified.length),
|
|
795
|
+
chars: String(notificationMessage.content.length),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
737
798
|
// User steering wins: drain any messages queued during this run first so the
|
|
738
799
|
// agent sees them mid-loop instead of after it stops.
|
|
739
800
|
if (this.userQueue.length > 0) {
|
|
@@ -743,7 +804,7 @@ export class AgentSession {
|
|
|
743
804
|
// the original task and silently drops it. ONE message per queued item
|
|
744
805
|
// (not merged): each persists as its own user message, so a resumed
|
|
745
806
|
// session shows the same number of bubbles the live run did.
|
|
746
|
-
|
|
807
|
+
const steeringMessages = queued.map((m) => {
|
|
747
808
|
if (m.attachments.length === 0) {
|
|
748
809
|
return { role: "user", content: wrapSteeringText(m.text) };
|
|
749
810
|
}
|
|
@@ -755,7 +816,10 @@ export class AgentSession {
|
|
|
755
816
|
];
|
|
756
817
|
return { role: "user", content: parts };
|
|
757
818
|
});
|
|
819
|
+
return notificationMessage ? [...steeringMessages, notificationMessage] : steeringMessages;
|
|
758
820
|
}
|
|
821
|
+
if (notificationMessage)
|
|
822
|
+
return [notificationMessage];
|
|
759
823
|
if (this.opts.selfCorrectionHooks === false)
|
|
760
824
|
return null;
|
|
761
825
|
if (!this.settingsManager.get("idealReviewEnabled"))
|
|
@@ -797,6 +861,39 @@ export class AgentSession {
|
|
|
797
861
|
}
|
|
798
862
|
return null;
|
|
799
863
|
}
|
|
864
|
+
/**
|
|
865
|
+
* Turn-budget extension gate. The loop consults this instead of stopping
|
|
866
|
+
* mid-task when it exhausts `maxTurns`. Grant ONLY on evidence of progress —
|
|
867
|
+
* handing more turns to a spinning agent just buys it more tokens to spin
|
|
868
|
+
* with — so reuse the same stuck signals that drive the loop-breaker.
|
|
869
|
+
*/
|
|
870
|
+
shouldExtendTurnBudget(ctx) {
|
|
871
|
+
const refusals = [];
|
|
872
|
+
const stuck = evaluateLoopBreak({
|
|
873
|
+
consecutiveFailures: this.hookConsecutiveFailures,
|
|
874
|
+
repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
|
|
875
|
+
textRepetitionDetected: detectTextRepetition(this.hookText),
|
|
876
|
+
...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
|
|
877
|
+
});
|
|
878
|
+
if (stuck.shouldBreak)
|
|
879
|
+
refusals.push(...stuck.reasons);
|
|
880
|
+
// Stage 2 means the loop-breaker already detected spinning twice and told
|
|
881
|
+
// the agent to stop and report. Do not overrule that with more turns.
|
|
882
|
+
if (this.loopBreakInjected >= 2)
|
|
883
|
+
refusals.push("loop-breaker already escalated");
|
|
884
|
+
const { toolCalls, toolFailures } = this.hookStats;
|
|
885
|
+
if (toolCalls > 0 && toolFailures / toolCalls > TURN_EXTENSION_MAX_FAILURE_RATIO) {
|
|
886
|
+
refusals.push(`${toolFailures}/${toolCalls} tool calls failed`);
|
|
887
|
+
}
|
|
888
|
+
const granted = refusals.length === 0;
|
|
889
|
+
log("INFO", "turn-budget", granted ? "Extending turn budget" : "Refusing turn-budget extension", {
|
|
890
|
+
turn: String(ctx.turn),
|
|
891
|
+
maxTurns: String(ctx.maxTurns),
|
|
892
|
+
extension: String(ctx.extension),
|
|
893
|
+
...(granted ? {} : { reasons: refusals.join(", ") }),
|
|
894
|
+
});
|
|
895
|
+
return granted;
|
|
896
|
+
}
|
|
800
897
|
/**
|
|
801
898
|
* Pre-stop Ideal review phase machine. Once review starts, completion is
|
|
802
899
|
* blocked until harness-owned post-injection reads cover every changed file.
|
|
@@ -963,6 +1060,7 @@ export class AgentSession {
|
|
|
963
1060
|
webSearch: true,
|
|
964
1061
|
maxTokens: this.maxTokens,
|
|
965
1062
|
maxTurns: this.opts.maxTurns,
|
|
1063
|
+
maxTurnExtensions: this.opts.maxTurnExtensions,
|
|
966
1064
|
thinking: this.thinkingLevel,
|
|
967
1065
|
apiKey,
|
|
968
1066
|
baseUrl: effectiveBaseUrl,
|
|
@@ -991,6 +1089,7 @@ export class AgentSession {
|
|
|
991
1089
|
// polled mid-loop; the ideal review is polled when the agent would stop.
|
|
992
1090
|
getSteeringMessages: () => this.getHookSteeringMessages(),
|
|
993
1091
|
getFollowUpMessages: () => this.getHookFollowUpMessages(),
|
|
1092
|
+
onTurnBudgetExhausted: (ctx) => this.shouldExtendTurnBudget(ctx),
|
|
994
1093
|
// Check authoritative provider usage before every model/tool step.
|
|
995
1094
|
// Forced overflow recovery bypasses settings and cooldown; proactive
|
|
996
1095
|
// checks honor both and estimate only messages unseen by the provider.
|
|
@@ -1168,6 +1267,11 @@ export class AgentSession {
|
|
|
1168
1267
|
}
|
|
1169
1268
|
async switchModel(provider, model) {
|
|
1170
1269
|
const prevProvider = this.provider;
|
|
1270
|
+
const prevModel = this.model;
|
|
1271
|
+
// Diff gate: a "switch" to the model already in use is not state change.
|
|
1272
|
+
// Recording it anyway would write a redundant marker and a redundant
|
|
1273
|
+
// history note on every no-op re-selection from the UI.
|
|
1274
|
+
const changed = model !== prevModel || (Boolean(provider) && provider !== prevProvider);
|
|
1171
1275
|
if (provider)
|
|
1172
1276
|
this.provider = provider;
|
|
1173
1277
|
this.model = model;
|
|
@@ -1176,7 +1280,6 @@ export class AgentSession {
|
|
|
1176
1280
|
// the live selection after an in-session model switch.
|
|
1177
1281
|
this.opts.provider = this.provider;
|
|
1178
1282
|
this.opts.model = this.model;
|
|
1179
|
-
this.syncUltraOrchestrationPrompt();
|
|
1180
1283
|
setEstimatorModel(model);
|
|
1181
1284
|
// maxTokens must follow the active model — it was frozen at the boot
|
|
1182
1285
|
// model's `maxOutputTokens` in the constructor, so without this a session
|
|
@@ -1197,6 +1300,27 @@ export class AgentSession {
|
|
|
1197
1300
|
const newReadTool = this.rebuildReadTool(model);
|
|
1198
1301
|
this.tools = this.tools.map((t) => (t.name === "read" ? newReadTool : t));
|
|
1199
1302
|
}
|
|
1303
|
+
// Model-dependent guidance lives in the uncached tail, so this rewrites
|
|
1304
|
+
// only the bytes after the cache marker — the cached prefix survives the
|
|
1305
|
+
// switch intact and the next turn still reads from cache.
|
|
1306
|
+
this.refreshSystemPromptTail();
|
|
1307
|
+
if (changed) {
|
|
1308
|
+
// Durable, replayable record of which model produced which segment. Rides
|
|
1309
|
+
// the existing app-marker path (parentId null, anchored by message count)
|
|
1310
|
+
// so a resumed session reconstructs the switch without the LLM seeing an
|
|
1311
|
+
// extra transcript row.
|
|
1312
|
+
await this.persistAppMarker("model_switch", {
|
|
1313
|
+
from: prevModel,
|
|
1314
|
+
to: this.model,
|
|
1315
|
+
provider: this.provider,
|
|
1316
|
+
fromProvider: prevProvider,
|
|
1317
|
+
}).catch((error) => {
|
|
1318
|
+
log("WARN", "session", "Failed to persist model-switch marker", {
|
|
1319
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1320
|
+
});
|
|
1321
|
+
});
|
|
1322
|
+
this.appendModelSwitchNote(prevModel, prevProvider);
|
|
1323
|
+
}
|
|
1200
1324
|
// Update provider-specific tools when provider changes
|
|
1201
1325
|
if (provider && provider !== prevProvider) {
|
|
1202
1326
|
// Add/remove client-side web_search tool based on provider.
|
|
@@ -1251,6 +1375,30 @@ export class AgentSession {
|
|
|
1251
1375
|
}
|
|
1252
1376
|
}
|
|
1253
1377
|
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Record the switch as its own trailing message at the point it happened,
|
|
1380
|
+
* rather than by rewriting the system prompt. Two reasons: the system prompt
|
|
1381
|
+
* is the cached prefix and rewriting it costs a full cache write, and a
|
|
1382
|
+
* resumed transcript can only attribute output to the right model if the
|
|
1383
|
+
* switch sits in message order.
|
|
1384
|
+
*
|
|
1385
|
+
* Skipped before the conversation starts (nothing to disambiguate) and while
|
|
1386
|
+
* an assistant turn has unresolved tool calls, where inserting a user message
|
|
1387
|
+
* would break tool_use/tool_result pairing.
|
|
1388
|
+
*/
|
|
1389
|
+
appendModelSwitchNote(prevModel, prevProvider) {
|
|
1390
|
+
const last = this.messages[this.messages.length - 1];
|
|
1391
|
+
if (!last || last.role === "system")
|
|
1392
|
+
return;
|
|
1393
|
+
if (last.role === "assistant" && hasUnresolvedToolCalls(last))
|
|
1394
|
+
return;
|
|
1395
|
+
const from = prevProvider === this.provider ? prevModel : `${prevProvider}/${prevModel}`;
|
|
1396
|
+
const to = prevProvider === this.provider ? this.model : `${this.provider}/${this.model}`;
|
|
1397
|
+
this.messages.push({
|
|
1398
|
+
role: "user",
|
|
1399
|
+
content: `[Model switched from ${from} to ${to}. Everything above was produced under the previous model; continue the current task from here.]`,
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1254
1402
|
async compact(existingCredentials) {
|
|
1255
1403
|
this.lastCompactionCompacted = false;
|
|
1256
1404
|
const creds = existingCredentials ??
|
|
@@ -1262,6 +1410,7 @@ export class AgentSession {
|
|
|
1262
1410
|
accountId: creds.accountId,
|
|
1263
1411
|
});
|
|
1264
1412
|
this.eventBus.emit("compaction_start", { messageCount: this.messages.length });
|
|
1413
|
+
const messagesBefore = [...this.messages];
|
|
1265
1414
|
const result = await compact(this.messages, {
|
|
1266
1415
|
provider: this.provider,
|
|
1267
1416
|
model: this.model,
|
|
@@ -1272,6 +1421,10 @@ export class AgentSession {
|
|
|
1272
1421
|
contextWindow,
|
|
1273
1422
|
signal: this.opts.signal,
|
|
1274
1423
|
});
|
|
1424
|
+
// Messages the summary is about to replace, identified by reference:
|
|
1425
|
+
// compaction keeps the retained tail as the same objects it was handed.
|
|
1426
|
+
const retained = new Set(result.messages);
|
|
1427
|
+
const droppedMessages = messagesBefore.filter((message) => !retained.has(message));
|
|
1275
1428
|
this.messages = result.messages;
|
|
1276
1429
|
this.lastCompactionCompacted = result.result.compacted;
|
|
1277
1430
|
if (!result.result.compacted) {
|
|
@@ -1283,6 +1436,7 @@ export class AgentSession {
|
|
|
1283
1436
|
return;
|
|
1284
1437
|
}
|
|
1285
1438
|
this.providerContext = null;
|
|
1439
|
+
await this.writeCompactionJournal(droppedMessages);
|
|
1286
1440
|
// Transient sessions (Ken chat/autopilot, subagent spawns) must NEVER touch
|
|
1287
1441
|
// the session store: without this guard, the first auto-compaction called
|
|
1288
1442
|
// sessionManager.create() and assigned a real sessionPath, silently turning
|
|
@@ -1350,7 +1504,6 @@ export class AgentSession {
|
|
|
1350
1504
|
(await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider, this.promptEnvironment()));
|
|
1351
1505
|
this.baseSystemPrompt = basePrompt;
|
|
1352
1506
|
this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
|
|
1353
|
-
this.syncUltraOrchestrationPrompt();
|
|
1354
1507
|
// Fresh conversation — new entries must not chain onto the old DAG's leaf.
|
|
1355
1508
|
this.currentLeafId = null;
|
|
1356
1509
|
// Transient sessions (Ken chat/autopilot, subagent spawns) never touch the
|
|
@@ -1498,7 +1651,6 @@ export class AgentSession {
|
|
|
1498
1651
|
else {
|
|
1499
1652
|
this.messages.unshift({ role: "system", content });
|
|
1500
1653
|
}
|
|
1501
|
-
this.syncUltraOrchestrationPrompt();
|
|
1502
1654
|
}
|
|
1503
1655
|
/**
|
|
1504
1656
|
* Toggle plan mode: flips the shared ref (so tools enforce read-only
|
|
@@ -1593,18 +1745,56 @@ export class AgentSession {
|
|
|
1593
1745
|
else {
|
|
1594
1746
|
this.messages.unshift({ role: "system", content });
|
|
1595
1747
|
}
|
|
1596
|
-
this.syncUltraOrchestrationPrompt();
|
|
1597
1748
|
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Compose the system message: stable prefix, then everything volatile behind
|
|
1751
|
+
* the `<!-- uncached -->` marker that providers split on for cache control.
|
|
1752
|
+
*
|
|
1753
|
+
* Anything that varies with the live model/provider/thinking level belongs in
|
|
1754
|
+
* the tail. Putting it in the prefix means every model switch rewrites cached
|
|
1755
|
+
* bytes and the next turn pays a full cache write instead of a read.
|
|
1756
|
+
*/
|
|
1598
1757
|
withSystemPromptTail(basePrompt) {
|
|
1599
|
-
|
|
1758
|
+
const tailParts = [];
|
|
1759
|
+
const orchestration = this.orchestrationPolicyTail();
|
|
1760
|
+
if (orchestration)
|
|
1761
|
+
tailParts.push(orchestration);
|
|
1762
|
+
// Journal is re-read on every refresh, so an entry written this turn (or a
|
|
1763
|
+
// human edit to .gg/memory.md) is visible on the next one. Uncached by
|
|
1764
|
+
// construction: it changes as work proceeds and must never sit in the
|
|
1765
|
+
// cached prefix.
|
|
1766
|
+
if (this.journal) {
|
|
1767
|
+
const journalTail = buildJournalPromptTail(this.journal);
|
|
1768
|
+
if (journalTail)
|
|
1769
|
+
tailParts.push(journalTail);
|
|
1770
|
+
}
|
|
1771
|
+
const hostTail = this.opts.getSystemPromptTail?.();
|
|
1772
|
+
if (hostTail)
|
|
1773
|
+
tailParts.push(hostTail);
|
|
1774
|
+
if (tailParts.length === 0)
|
|
1600
1775
|
return basePrompt;
|
|
1601
|
-
return `${basePrompt}\n\n<!-- uncached -->\n${
|
|
1776
|
+
return `${basePrompt}\n\n<!-- uncached -->\n${tailParts.join("\n\n")}`;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Sol/Terra async-orchestration guidance for the CURRENT model and thinking
|
|
1780
|
+
* level. Per-switch volatile by definition, so it is rendered as a tail part
|
|
1781
|
+
* rather than spliced into the cached prefix.
|
|
1782
|
+
*/
|
|
1783
|
+
orchestrationPolicyTail() {
|
|
1784
|
+
if (this.opts.orchestrationPrompt === false)
|
|
1785
|
+
return "";
|
|
1786
|
+
return applyAsyncSubagentPolicy("", this.provider, this.model, this.thinkingLevel, this.tools.map((tool) => tool.name)).trim();
|
|
1602
1787
|
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Re-render the uncached tail of messages[0] in place. The cached prefix is
|
|
1790
|
+
* byte-identical afterwards, so this is safe to call on every model /
|
|
1791
|
+
* thinking-level change.
|
|
1792
|
+
*/
|
|
1603
1793
|
refreshSystemPromptTail() {
|
|
1604
|
-
if (!this.opts.getSystemPromptTail)
|
|
1605
|
-
return;
|
|
1606
1794
|
const content = this.withSystemPromptTail(this.baseSystemPrompt);
|
|
1607
1795
|
if (this.messages[0]?.role === "system") {
|
|
1796
|
+
if (this.messages[0].content === content)
|
|
1797
|
+
return;
|
|
1608
1798
|
this.messages[0] = { role: "system", content };
|
|
1609
1799
|
}
|
|
1610
1800
|
else {
|
|
@@ -1783,6 +1973,34 @@ export class AgentSession {
|
|
|
1783
1973
|
};
|
|
1784
1974
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1785
1975
|
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Record what this compaction is about to discard as past-tense journal
|
|
1978
|
+
* entries. Compaction is one-way — without this the specifics are gone for
|
|
1979
|
+
* good, which is precisely the hole the journal exists to fill. No-op when
|
|
1980
|
+
* memory is off, and bounded to a few entries so the file stays readable.
|
|
1981
|
+
*/
|
|
1982
|
+
async writeCompactionJournal(dropped) {
|
|
1983
|
+
if (!this.journal || dropped.length === 0)
|
|
1984
|
+
return;
|
|
1985
|
+
const summary = this.messages.map((message) => message.content).find(isCompactionSummary) ?? "";
|
|
1986
|
+
const notes = buildCompactionNotes(dropped, summary);
|
|
1987
|
+
if (notes.length === 0)
|
|
1988
|
+
return;
|
|
1989
|
+
try {
|
|
1990
|
+
const written = await this.journal.append(notes);
|
|
1991
|
+
if (written > 0) {
|
|
1992
|
+
log("INFO", "memory", "Recorded compaction history", {
|
|
1993
|
+
entries: String(written),
|
|
1994
|
+
file: this.journal.filePath,
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
catch (error) {
|
|
1999
|
+
log("WARN", "memory", "Failed to write journal entries", {
|
|
2000
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
1786
2004
|
/** Re-append the in-memory app markers to the current session file. Mirrors
|
|
1787
2005
|
* `rePersistKenTurns` — called after a continuation/compaction file is
|
|
1788
2006
|
* created so display-only rows survive the rewrite. */
|
|
@@ -1842,16 +2060,7 @@ export class AgentSession {
|
|
|
1842
2060
|
* effect on the next prompt, since the in-flight loop reads it at start. */
|
|
1843
2061
|
setThinkingLevel(level) {
|
|
1844
2062
|
this.thinkingLevel = level;
|
|
1845
|
-
this.
|
|
1846
|
-
}
|
|
1847
|
-
/** Sol/Terra Ultra delegates proactively; lower levels require an explicit request. */
|
|
1848
|
-
syncUltraOrchestrationPrompt() {
|
|
1849
|
-
if (this.opts.orchestrationPrompt === false)
|
|
1850
|
-
return;
|
|
1851
|
-
const systemMessage = this.messages[0];
|
|
1852
|
-
if (systemMessage?.role !== "system" || typeof systemMessage.content !== "string")
|
|
1853
|
-
return;
|
|
1854
|
-
systemMessage.content = applyAsyncSubagentPolicy(systemMessage.content, this.provider, this.model, this.thinkingLevel, this.tools.map((tool) => tool.name));
|
|
2063
|
+
this.refreshSystemPromptTail();
|
|
1855
2064
|
}
|
|
1856
2065
|
/** Replace the abort signal (e.g. after cancellation). */
|
|
1857
2066
|
setSignal(signal) {
|