@kenkaiiii/ggcoder 5.29.1 → 5.30.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/dist/app-sidecar-auth-broadcast.test.js +14 -3
- package/dist/app-sidecar-auth-broadcast.test.js.map +1 -1
- package/dist/app-sidecar.js +27 -20
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/command-routing.d.ts +1 -1
- package/dist/cli/command-routing.d.ts.map +1 -1
- package/dist/cli/command-routing.js +1 -0
- package/dist/cli/command-routing.js.map +1 -1
- package/dist/cli/command-routing.test.js +1 -0
- package/dist/cli/command-routing.test.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +167 -47
- 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 +10 -0
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +10 -1
- package/dist/config.test.js.map +1 -1
- package/dist/core/agent-session-compaction.test.js +252 -2
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session-crash-durability.test.js +9 -2
- package/dist/core/agent-session-crash-durability.test.js.map +1 -1
- package/dist/core/agent-session-process-gate.test.js +5 -1
- package/dist/core/agent-session-process-gate.test.js.map +1 -1
- package/dist/core/agent-session-review-coverage.test.js +5 -1
- package/dist/core/agent-session-review-coverage.test.js.map +1 -1
- package/dist/core/agent-session.d.ts +10 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +277 -130
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/active-context.test.js +21 -0
- package/dist/core/compaction/active-context.test.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts +27 -8
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +255 -101
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +107 -8
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/policy.d.ts +17 -0
- package/dist/core/compaction/policy.d.ts.map +1 -0
- package/dist/core/compaction/policy.js +22 -0
- package/dist/core/compaction/policy.js.map +1 -0
- package/dist/core/compaction/policy.test.d.ts +2 -0
- package/dist/core/compaction/policy.test.d.ts.map +1 -0
- package/dist/core/compaction/policy.test.js +35 -0
- package/dist/core/compaction/policy.test.js.map +1 -0
- package/dist/core/compaction/tool-result-pruner.d.ts +18 -11
- package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -1
- package/dist/core/compaction/tool-result-pruner.js +72 -24
- package/dist/core/compaction/tool-result-pruner.js.map +1 -1
- package/dist/core/compaction/tool-result-pruner.test.js +109 -6
- package/dist/core/compaction/tool-result-pruner.test.js.map +1 -1
- package/dist/core/foreign-session-import.d.ts.map +1 -1
- package/dist/core/foreign-session-import.js +5 -1
- package/dist/core/foreign-session-import.js.map +1 -1
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +4 -0
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/loop-breaker.d.ts.map +1 -1
- package/dist/core/loop-breaker.js +1 -0
- package/dist/core/loop-breaker.js.map +1 -1
- package/dist/core/process-gate.d.ts.map +1 -1
- package/dist/core/process-gate.js +9 -1
- package/dist/core/process-gate.js.map +1 -1
- package/dist/core/process-manager-notifications.test.js +4 -2
- package/dist/core/process-manager-notifications.test.js.map +1 -1
- package/dist/core/regrounding.d.ts.map +1 -1
- package/dist/core/regrounding.js +1 -0
- package/dist/core/regrounding.js.map +1 -1
- package/dist/core/session-compaction.d.ts +5 -0
- package/dist/core/session-compaction.d.ts.map +1 -1
- package/dist/core/session-compaction.js +10 -1
- package/dist/core/session-compaction.js.map +1 -1
- package/dist/core/session-compaction.test.js +16 -1
- package/dist/core/session-compaction.test.js.map +1 -1
- package/dist/core/session-export.d.ts.map +1 -1
- package/dist/core/session-export.js +11 -8
- package/dist/core/session-export.js.map +1 -1
- package/dist/core/session-history.d.ts +7 -2
- package/dist/core/session-history.d.ts.map +1 -1
- package/dist/core/session-history.js +52 -9
- package/dist/core/session-history.js.map +1 -1
- package/dist/core/session-history.test.js +26 -1
- package/dist/core/session-history.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +106 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +316 -15
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +109 -1
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/session-notification-visibility.test.js +34 -0
- package/dist/core/session-notification-visibility.test.js.map +1 -1
- package/dist/core/session-preview.d.ts +2 -2
- package/dist/core/session-preview.d.ts.map +1 -1
- package/dist/core/session-preview.js +8 -5
- package/dist/core/session-preview.js.map +1 -1
- package/dist/core/session-restore-display.test.js +25 -0
- package/dist/core/session-restore-display.test.js.map +1 -1
- package/dist/core/session-storage.d.ts.map +1 -1
- package/dist/core/session-storage.js +39 -3
- package/dist/core/session-storage.js.map +1 -1
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +9 -1
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +4 -3
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +17 -3
- package/dist/interactive.js.map +1 -1
- package/dist/modes/acp-mode.d.ts +82 -0
- package/dist/modes/acp-mode.d.ts.map +1 -0
- package/dist/modes/acp-mode.js +802 -0
- package/dist/modes/acp-mode.js.map +1 -0
- package/dist/modes/acp-mode.test.d.ts +2 -0
- package/dist/modes/acp-mode.test.d.ts.map +1 -0
- package/dist/modes/acp-mode.test.js +611 -0
- package/dist/modes/acp-mode.test.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/session.d.ts +24 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +32 -0
- package/dist/session.js.map +1 -1
- package/dist/tools/web-fetch.d.ts +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -11,8 +11,9 @@ import { getClaudeCliUserAgent } from "./claude-code-version.js";
|
|
|
11
11
|
import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.js";
|
|
12
12
|
import { SessionManager, KEN_TURN_CUSTOM_KIND, AUTOPILOT_MARKER_CUSTOM_KIND, APP_MARKER_CUSTOM_KIND, } from "./session-manager.js";
|
|
13
13
|
import { ExtensionLoader } from "./extensions/loader.js";
|
|
14
|
-
import { shouldCompact, compact } from "./compaction/compactor.js";
|
|
14
|
+
import { shouldCompact, compact, } from "./compaction/compactor.js";
|
|
15
15
|
import { remapAnchorForCompaction, stripRecordedPosition } from "./session-history.js";
|
|
16
|
+
import { sourceFingerprint as computeSourceFingerprint } from "./session-compaction.js";
|
|
16
17
|
import { getAuthStorageKeys, getContextWindow, getModel, getToolResultCharLimit, MODELS, } from "./model-registry.js";
|
|
17
18
|
import { discoverSkills } from "./skills.js";
|
|
18
19
|
import { ensureAppDirs } from "../config.js";
|
|
@@ -30,6 +31,7 @@ import { createToolSearchTool } from "../tools/tool-search.js";
|
|
|
30
31
|
import { log } from "./logger.js";
|
|
31
32
|
import { setEstimatorModel, calibrateEstimatorFromUsage } from "./compaction/token-estimator.js";
|
|
32
33
|
import { calculateActiveContextTokens } from "./compaction/active-context.js";
|
|
34
|
+
import { resolveCompactionPolicy } from "./compaction/policy.js";
|
|
33
35
|
import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
34
36
|
import { discoverAgents } from "./agents.js";
|
|
35
37
|
import { enhancePrompt } from "../utils/prompt-enhancer.js";
|
|
@@ -149,6 +151,8 @@ export class AgentSession {
|
|
|
149
151
|
compactionOccurred = false;
|
|
150
152
|
lastCompactionCompacted = false;
|
|
151
153
|
compactionRetryAfter = 0;
|
|
154
|
+
/** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
|
|
155
|
+
deferredCompactionPending = false;
|
|
152
156
|
/** Latest provider count, anchored to the assistant response it measured. */
|
|
153
157
|
providerContext = null;
|
|
154
158
|
originalRequest = "";
|
|
@@ -206,6 +210,7 @@ export class AgentSession {
|
|
|
206
210
|
/** Extra workspace roots added with `/add-dir` (resolved, de-duplicated). */
|
|
207
211
|
additionalRoots = [];
|
|
208
212
|
sessionId = "";
|
|
213
|
+
checkpointGeneration = 0;
|
|
209
214
|
/** Stable identity shared by compaction and approved-plan checkpoint files. */
|
|
210
215
|
conversationId = "";
|
|
211
216
|
/** Original user-authored prompt, retained when internal messages replace history. */
|
|
@@ -753,12 +758,16 @@ export class AgentSession {
|
|
|
753
758
|
/**
|
|
754
759
|
* Process user input. Handles slash commands or runs agent loop.
|
|
755
760
|
*/
|
|
756
|
-
async prompt(content
|
|
761
|
+
async prompt(content, provenance = {
|
|
762
|
+
source: "human",
|
|
763
|
+
kind: "prompt",
|
|
764
|
+
visibility: "transcript",
|
|
765
|
+
}) {
|
|
766
|
+
await this.adoptDeferredCheckpointBeforePrompt();
|
|
757
767
|
const slash = await this.resolveSlashInput(content);
|
|
758
768
|
if (slash?.kind === "template") {
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
const userMessage = { role: "user", content: slash.fullPrompt };
|
|
769
|
+
// Prompt templates remain human-originated: the user invoked the command.
|
|
770
|
+
const userMessage = { role: "user", content: slash.fullPrompt, provenance };
|
|
762
771
|
this.messages.push(userMessage);
|
|
763
772
|
await this.persistMessage(userMessage);
|
|
764
773
|
this.lastPersistedIndex = this.messages.length;
|
|
@@ -774,7 +783,7 @@ export class AgentSession {
|
|
|
774
783
|
return;
|
|
775
784
|
}
|
|
776
785
|
// Push user message
|
|
777
|
-
const userMessage = { role: "user", content };
|
|
786
|
+
const userMessage = { role: "user", content, provenance };
|
|
778
787
|
this.messages.push(userMessage);
|
|
779
788
|
await this.persistMessage(userMessage);
|
|
780
789
|
this.lastPersistedIndex = this.messages.length;
|
|
@@ -788,10 +797,15 @@ export class AgentSession {
|
|
|
788
797
|
* attachments are always a direct conversational turn.
|
|
789
798
|
*/
|
|
790
799
|
async promptWithAttachments(text, attachments) {
|
|
800
|
+
await this.adoptDeferredCheckpointBeforePrompt();
|
|
791
801
|
const parts = this.buildAttachmentParts(text, attachments);
|
|
792
802
|
if (parts.length === 0)
|
|
793
803
|
return;
|
|
794
|
-
const userMessage = {
|
|
804
|
+
const userMessage = {
|
|
805
|
+
role: "user",
|
|
806
|
+
content: parts,
|
|
807
|
+
provenance: { source: "human", kind: "prompt", visibility: "transcript" },
|
|
808
|
+
};
|
|
795
809
|
this.messages.push(userMessage);
|
|
796
810
|
await this.persistMessage(userMessage);
|
|
797
811
|
this.lastPersistedIndex = this.messages.length;
|
|
@@ -993,6 +1007,7 @@ export class AgentSession {
|
|
|
993
1007
|
? {
|
|
994
1008
|
role: "user",
|
|
995
1009
|
content: buildNotificationSteeringText(notified.map((entry) => entry.text)),
|
|
1010
|
+
provenance: { source: "runtime", kind: "notification", visibility: "hidden" },
|
|
996
1011
|
}
|
|
997
1012
|
: null;
|
|
998
1013
|
if (notificationMessage) {
|
|
@@ -1016,8 +1031,13 @@ export class AgentSession {
|
|
|
1016
1031
|
// (not merged): each persists as its own user message, so a resumed
|
|
1017
1032
|
// session shows the same number of bubbles the live run did.
|
|
1018
1033
|
const steeringMessages = queued.map((m) => {
|
|
1034
|
+
const provenance = {
|
|
1035
|
+
source: "human",
|
|
1036
|
+
kind: "steering",
|
|
1037
|
+
visibility: "transcript",
|
|
1038
|
+
};
|
|
1019
1039
|
if (m.attachments.length === 0) {
|
|
1020
|
-
return { role: "user", content: wrapSteeringText(m.text) };
|
|
1040
|
+
return { role: "user", content: wrapSteeringText(m.text), provenance };
|
|
1021
1041
|
}
|
|
1022
1042
|
// Queued attachments ride the same native-block path as a non-queued
|
|
1023
1043
|
// attachment prompt, prefixed with the steering framing.
|
|
@@ -1025,7 +1045,7 @@ export class AgentSession {
|
|
|
1025
1045
|
{ type: "text", text: STEERING_PREFIX },
|
|
1026
1046
|
...this.buildAttachmentParts(m.text, m.attachments),
|
|
1027
1047
|
];
|
|
1028
|
-
return { role: "user", content: parts };
|
|
1048
|
+
return { role: "user", content: parts, provenance };
|
|
1029
1049
|
});
|
|
1030
1050
|
return notificationMessage ? [...steeringMessages, notificationMessage] : steeringMessages;
|
|
1031
1051
|
}
|
|
@@ -1207,7 +1227,11 @@ export class AgentSession {
|
|
|
1207
1227
|
: []),
|
|
1208
1228
|
"Do not describe those files as compiler-clean without other evidence.",
|
|
1209
1229
|
];
|
|
1210
|
-
return {
|
|
1230
|
+
return {
|
|
1231
|
+
role: "user",
|
|
1232
|
+
provenance: message.provenance,
|
|
1233
|
+
content: `${String(message.content)}\n\n${notes.join(" ")}`,
|
|
1234
|
+
};
|
|
1211
1235
|
}
|
|
1212
1236
|
/** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
|
|
1213
1237
|
async runLoop() {
|
|
@@ -1247,7 +1271,14 @@ export class AgentSession {
|
|
|
1247
1271
|
provider: this.provider,
|
|
1248
1272
|
accountId: creds.accountId,
|
|
1249
1273
|
});
|
|
1250
|
-
const
|
|
1274
|
+
const policy = resolveCompactionPolicy({
|
|
1275
|
+
provider: this.provider,
|
|
1276
|
+
model: this.model,
|
|
1277
|
+
contextWindow,
|
|
1278
|
+
threshold: this.settingsManager.get("compactThreshold"),
|
|
1279
|
+
accountId: creds.accountId,
|
|
1280
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
1281
|
+
});
|
|
1251
1282
|
let activeTokens;
|
|
1252
1283
|
if (this.providerContext) {
|
|
1253
1284
|
const anchorIndex = this.messages.lastIndexOf(this.providerContext.anchor);
|
|
@@ -1261,9 +1292,17 @@ export class AgentSession {
|
|
|
1261
1292
|
this.providerContext = null;
|
|
1262
1293
|
}
|
|
1263
1294
|
}
|
|
1264
|
-
|
|
1295
|
+
log("INFO", "compaction", "Pre-run compaction decision", {
|
|
1296
|
+
provider: this.provider,
|
|
1297
|
+
model: this.model,
|
|
1298
|
+
transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
|
|
1299
|
+
contextWindow: String(contextWindow),
|
|
1300
|
+
activeTokens: activeTokens === undefined ? "estimated" : String(activeTokens),
|
|
1301
|
+
triggerLimit: String(policy.targetTokens),
|
|
1302
|
+
});
|
|
1303
|
+
if (shouldCompact(this.messages, contextWindow, policy.threshold, activeTokens)) {
|
|
1265
1304
|
try {
|
|
1266
|
-
await this.compact(creds);
|
|
1305
|
+
await this.compact(creds, "automatic");
|
|
1267
1306
|
if (this.lastCompactionCompacted) {
|
|
1268
1307
|
// Re-grounding hook keys off this — the context was just summarized.
|
|
1269
1308
|
this.compactionOccurred = true;
|
|
@@ -1349,6 +1388,7 @@ export class AgentSession {
|
|
|
1349
1388
|
this.providerContext = null;
|
|
1350
1389
|
log("INFO", "compaction", "Pruned stale tool outputs", {
|
|
1351
1390
|
prunedResults: String(pruneResult.prunedResults),
|
|
1391
|
+
compactedToolCalls: String(pruneResult.compactedToolCalls),
|
|
1352
1392
|
freedTokens: String(pruneResult.freedTokens),
|
|
1353
1393
|
});
|
|
1354
1394
|
}
|
|
@@ -1373,12 +1413,27 @@ export class AgentSession {
|
|
|
1373
1413
|
provider: this.provider,
|
|
1374
1414
|
accountId,
|
|
1375
1415
|
});
|
|
1376
|
-
const
|
|
1416
|
+
const policy = resolveCompactionPolicy({
|
|
1417
|
+
provider: this.provider,
|
|
1418
|
+
model: this.model,
|
|
1419
|
+
contextWindow,
|
|
1420
|
+
threshold: this.settingsManager.get("compactThreshold"),
|
|
1421
|
+
accountId,
|
|
1422
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
1423
|
+
});
|
|
1377
1424
|
const activeTokens = calculateActiveContextTokens(messages, {
|
|
1378
1425
|
usage,
|
|
1379
1426
|
pendingMessages,
|
|
1380
1427
|
});
|
|
1381
|
-
|
|
1428
|
+
log("INFO", "compaction", "In-flight compaction decision", {
|
|
1429
|
+
provider: this.provider,
|
|
1430
|
+
model: this.model,
|
|
1431
|
+
transport: this.provider === "openai" && accountId ? "codex_oauth" : "public_api",
|
|
1432
|
+
contextWindow: String(contextWindow),
|
|
1433
|
+
activeTokens: String(activeTokens),
|
|
1434
|
+
triggerLimit: String(policy.targetTokens),
|
|
1435
|
+
});
|
|
1436
|
+
if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens))
|
|
1382
1437
|
return messages;
|
|
1383
1438
|
}
|
|
1384
1439
|
// compact() operates on this.messages, while an earlier transform may
|
|
@@ -1391,7 +1446,7 @@ export class AgentSession {
|
|
|
1391
1446
|
accountId,
|
|
1392
1447
|
projectId,
|
|
1393
1448
|
baseUrl: effectiveBaseUrl,
|
|
1394
|
-
});
|
|
1449
|
+
}, force ? "forced" : "automatic");
|
|
1395
1450
|
}
|
|
1396
1451
|
catch (error) {
|
|
1397
1452
|
this.messages = messages;
|
|
@@ -1640,9 +1695,66 @@ export class AgentSession {
|
|
|
1640
1695
|
this.messages.push({
|
|
1641
1696
|
role: "user",
|
|
1642
1697
|
content: `[Model switched from ${from} to ${to}. Everything above was produced under the previous model; continue the current task from here.]`,
|
|
1698
|
+
provenance: { source: "runtime", kind: "model_switch", visibility: "hidden" },
|
|
1643
1699
|
});
|
|
1644
1700
|
}
|
|
1645
|
-
async
|
|
1701
|
+
async adoptCompactionCheckpoint(loaded) {
|
|
1702
|
+
const systemMessage = this.messages[0];
|
|
1703
|
+
const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
|
|
1704
|
+
this.messages = [systemMessage, ...loadedMessages];
|
|
1705
|
+
this.sessionId = loaded.header.id;
|
|
1706
|
+
this.conversationId = loaded.header.conversationId ?? loaded.header.id;
|
|
1707
|
+
this.checkpointGeneration = loaded.header.generation ?? 0;
|
|
1708
|
+
this.currentLeafId = loaded.header.leafId;
|
|
1709
|
+
this.setSessionPath(loaded.path);
|
|
1710
|
+
this.kenTurns = this.sessionManager.getKenTurns(loaded.entries, loaded.header.leafId);
|
|
1711
|
+
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
|
|
1712
|
+
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
|
|
1713
|
+
this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
|
|
1714
|
+
this.lastPersistedIndex = this.messages.length;
|
|
1715
|
+
this.providerContext = null;
|
|
1716
|
+
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
1717
|
+
}
|
|
1718
|
+
/** Canonicalize a deferred restore before a new prompt can fork stale history. */
|
|
1719
|
+
async adoptDeferredCheckpointBeforePrompt() {
|
|
1720
|
+
if (!this.deferredCompactionPending || !this.conversationId)
|
|
1721
|
+
return;
|
|
1722
|
+
this.deferredCompactionPending = false;
|
|
1723
|
+
const canonicalPath = await this.sessionManager.resolveCanonicalSession(this.conversationId, this.cwd);
|
|
1724
|
+
if (!canonicalPath || canonicalPath === this.sessionPath)
|
|
1725
|
+
return;
|
|
1726
|
+
await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath));
|
|
1727
|
+
}
|
|
1728
|
+
async persistCompactionCheckpoint(sourceFingerprint, result) {
|
|
1729
|
+
const parentSessionId = this.sessionId || undefined;
|
|
1730
|
+
const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
|
|
1731
|
+
conversationId: this.conversationId || undefined,
|
|
1732
|
+
generation: this.checkpointGeneration + 1,
|
|
1733
|
+
parentSessionId,
|
|
1734
|
+
sourceFingerprint,
|
|
1735
|
+
preview: this.sessionPreview || undefined,
|
|
1736
|
+
});
|
|
1737
|
+
this.sessionId = session.id;
|
|
1738
|
+
this.checkpointGeneration = session.header.generation ?? 0;
|
|
1739
|
+
this.conversationId = session.header.conversationId ?? session.id;
|
|
1740
|
+
this.currentLeafId = null;
|
|
1741
|
+
this.setSessionPath(session.path);
|
|
1742
|
+
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
1743
|
+
for (const message of this.messages) {
|
|
1744
|
+
if (message.role !== "system")
|
|
1745
|
+
await this.persistMessage(message);
|
|
1746
|
+
}
|
|
1747
|
+
this.lastPersistedIndex = this.messages.length;
|
|
1748
|
+
await this.rePersistTurnMetrics();
|
|
1749
|
+
await this.rePersistKenTurns();
|
|
1750
|
+
await this.rePersistAutopilotMarkers();
|
|
1751
|
+
await this.rePersistAppMarkers();
|
|
1752
|
+
await this.persistAppMarker("compaction", {
|
|
1753
|
+
originalCount: result.originalCount,
|
|
1754
|
+
newCount: result.newCount,
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
async compact(existingCredentials, mode = "manual") {
|
|
1646
1758
|
this.lastCompactionCompacted = false;
|
|
1647
1759
|
const creds = existingCredentials ??
|
|
1648
1760
|
(await this.authStorage.resolveCredentials(this.provider, {
|
|
@@ -1652,8 +1764,17 @@ export class AgentSession {
|
|
|
1652
1764
|
provider: this.provider,
|
|
1653
1765
|
accountId: creds.accountId,
|
|
1654
1766
|
});
|
|
1655
|
-
|
|
1656
|
-
|
|
1767
|
+
const policy = resolveCompactionPolicy({
|
|
1768
|
+
provider: this.provider,
|
|
1769
|
+
model: this.model,
|
|
1770
|
+
contextWindow,
|
|
1771
|
+
threshold: this.settingsManager.get("compactThreshold"),
|
|
1772
|
+
accountId: creds.accountId,
|
|
1773
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
1774
|
+
});
|
|
1775
|
+
const originalCount = this.messages.length;
|
|
1776
|
+
this.eventBus.emit("compaction_start", { messageCount: originalCount });
|
|
1777
|
+
const runCompactor = () => compact(this.messages, {
|
|
1657
1778
|
provider: this.provider,
|
|
1658
1779
|
model: this.model,
|
|
1659
1780
|
apiKey: creds.accessToken,
|
|
@@ -1661,67 +1782,106 @@ export class AgentSession {
|
|
|
1661
1782
|
projectId: creds.projectId,
|
|
1662
1783
|
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
1663
1784
|
contextWindow,
|
|
1785
|
+
targetTokens: policy.targetTokens,
|
|
1664
1786
|
signal: this.opts.signal,
|
|
1787
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
1665
1788
|
});
|
|
1666
|
-
|
|
1667
|
-
this.
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
// Compaction rewrote the message list, so every transcript anchor recorded
|
|
1678
|
-
// against the old indices must move with it — otherwise the markers are
|
|
1679
|
-
// re-persisted pointing at positions that no longer mean anything and
|
|
1680
|
-
// replay far below where they happened (or past the end).
|
|
1681
|
-
this.remapMarkerAnchors(result.result.anchorRemap);
|
|
1682
|
-
// Transient sessions (Ken chat/autopilot, subagent spawns) must NEVER touch
|
|
1683
|
-
// the session store: without this guard, the first auto-compaction called
|
|
1684
|
-
// sessionManager.create() and assigned a real sessionPath, silently turning
|
|
1685
|
-
// the "in-memory only" session into a persisted one — every later turn (and
|
|
1686
|
-
// every further compaction) then leaked a Ken transcript file into the
|
|
1687
|
-
// project's session list. Compact in memory only and keep sessionPath empty.
|
|
1688
|
-
if (this.opts.transient) {
|
|
1689
|
-
this.lastPersistedIndex = this.messages.length;
|
|
1789
|
+
let finalCount = originalCount;
|
|
1790
|
+
if (this.opts.transient || !this.conversationId) {
|
|
1791
|
+
const result = await runCompactor();
|
|
1792
|
+
finalCount = result.result.newCount;
|
|
1793
|
+
this.messages = result.messages;
|
|
1794
|
+
this.lastCompactionCompacted = result.result.compacted;
|
|
1795
|
+
if (result.result.compacted) {
|
|
1796
|
+
this.providerContext = null;
|
|
1797
|
+
this.remapMarkerAnchors(result.result.anchorRemap);
|
|
1798
|
+
this.lastPersistedIndex = this.messages.length;
|
|
1799
|
+
}
|
|
1690
1800
|
}
|
|
1691
1801
|
else {
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1802
|
+
const conversationId = this.conversationId;
|
|
1803
|
+
await this.sessionManager.withCompactionLease(conversationId, this.opts.signal, async () => {
|
|
1804
|
+
let sourceFingerprint = computeSourceFingerprint(this.messages);
|
|
1805
|
+
const canonicalPath = await this.sessionManager.resolveCanonicalSession(conversationId, this.cwd);
|
|
1806
|
+
if (canonicalPath && canonicalPath !== this.sessionPath) {
|
|
1807
|
+
const newest = await this.sessionManager.load(canonicalPath);
|
|
1808
|
+
if (newest.header.sourceFingerprint === sourceFingerprint) {
|
|
1809
|
+
await this.adoptCompactionCheckpoint(newest);
|
|
1810
|
+
this.lastCompactionCompacted = true;
|
|
1811
|
+
finalCount = this.messages.length;
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
// The canonical branch contains different progress. Rebase onto it before
|
|
1815
|
+
// compacting so a stale caller cannot supersede newer history by generation.
|
|
1816
|
+
await this.adoptCompactionCheckpoint(newest);
|
|
1817
|
+
sourceFingerprint = computeSourceFingerprint(this.messages);
|
|
1818
|
+
}
|
|
1819
|
+
const attempt = await this.sessionManager.readCompactionAttemptState(conversationId);
|
|
1820
|
+
const attemptActive = !attempt?.expiresAt || Date.parse(attempt.expiresAt) > Date.now();
|
|
1821
|
+
if (mode === "automatic" &&
|
|
1822
|
+
attempt?.fingerprint === sourceFingerprint &&
|
|
1823
|
+
attempt.policyKey === policy.policyKey &&
|
|
1824
|
+
attemptActive) {
|
|
1825
|
+
if (attempt.outcome === "success" && attempt.checkpointId) {
|
|
1826
|
+
const checkpointPath = await this.sessionManager.findById(this.cwd, attempt.checkpointId);
|
|
1827
|
+
if (checkpointPath) {
|
|
1828
|
+
const checkpoint = await this.sessionManager.load(checkpointPath);
|
|
1829
|
+
if (checkpoint.header.sourceFingerprint === sourceFingerprint) {
|
|
1830
|
+
await this.adoptCompactionCheckpoint(checkpoint);
|
|
1831
|
+
this.lastCompactionCompacted = true;
|
|
1832
|
+
finalCount = this.messages.length;
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
else if (attempt.outcome === "failed" || attempt.outcome === "noop") {
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
try {
|
|
1842
|
+
const result = await runCompactor();
|
|
1843
|
+
finalCount = result.result.newCount;
|
|
1844
|
+
this.messages = result.messages;
|
|
1845
|
+
this.lastCompactionCompacted = result.result.compacted;
|
|
1846
|
+
if (!result.result.compacted) {
|
|
1847
|
+
await this.sessionManager.writeCompactionAttemptState(conversationId, {
|
|
1848
|
+
fingerprint: sourceFingerprint,
|
|
1849
|
+
policyKey: policy.policyKey,
|
|
1850
|
+
outcome: "noop",
|
|
1851
|
+
updatedAt: new Date().toISOString(),
|
|
1852
|
+
expiresAt: new Date(Date.now() + 30_000).toISOString(),
|
|
1853
|
+
});
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
this.providerContext = null;
|
|
1857
|
+
this.remapMarkerAnchors(result.result.anchorRemap);
|
|
1858
|
+
await this.persistCompactionCheckpoint(sourceFingerprint, result.result);
|
|
1859
|
+
await this.sessionManager.writeCompactionAttemptState(conversationId, {
|
|
1860
|
+
fingerprint: sourceFingerprint,
|
|
1861
|
+
policyKey: policy.policyKey,
|
|
1862
|
+
outcome: "success",
|
|
1863
|
+
checkpointId: this.sessionId,
|
|
1864
|
+
updatedAt: new Date().toISOString(),
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
catch (error) {
|
|
1868
|
+
await this.sessionManager
|
|
1869
|
+
.writeCompactionAttemptState(conversationId, {
|
|
1870
|
+
fingerprint: sourceFingerprint,
|
|
1871
|
+
policyKey: policy.policyKey,
|
|
1872
|
+
outcome: "failed",
|
|
1873
|
+
updatedAt: new Date().toISOString(),
|
|
1874
|
+
expiresAt: new Date(Date.now() + 30_000).toISOString(),
|
|
1875
|
+
})
|
|
1876
|
+
.catch(() => { });
|
|
1877
|
+
throw error;
|
|
1878
|
+
}
|
|
1719
1879
|
});
|
|
1720
1880
|
}
|
|
1721
1881
|
this.eventBus.emit("compaction_end", {
|
|
1722
|
-
compacted:
|
|
1723
|
-
originalCount
|
|
1724
|
-
newCount:
|
|
1882
|
+
compacted: this.lastCompactionCompacted,
|
|
1883
|
+
originalCount,
|
|
1884
|
+
newCount: finalCount,
|
|
1725
1885
|
});
|
|
1726
1886
|
}
|
|
1727
1887
|
async newSession(preserveConversation = false) {
|
|
@@ -1729,6 +1889,7 @@ export class AgentSession {
|
|
|
1729
1889
|
// explicit new sessions reset the conversation identity.
|
|
1730
1890
|
if (!preserveConversation) {
|
|
1731
1891
|
this.conversationId = "";
|
|
1892
|
+
this.checkpointGeneration = 0;
|
|
1732
1893
|
this.sessionPreview = "";
|
|
1733
1894
|
}
|
|
1734
1895
|
// A fresh session drops any in-flight plan state so its prompt is clean.
|
|
@@ -1756,6 +1917,7 @@ export class AgentSession {
|
|
|
1756
1917
|
if (this.opts.transient) {
|
|
1757
1918
|
this.sessionId = "";
|
|
1758
1919
|
this.conversationId = "";
|
|
1920
|
+
this.checkpointGeneration = 0;
|
|
1759
1921
|
this.sessionPreview = "";
|
|
1760
1922
|
this.setSessionPath("");
|
|
1761
1923
|
this.lastPersistedIndex = this.messages.length;
|
|
@@ -2400,19 +2562,28 @@ export class AgentSession {
|
|
|
2400
2562
|
this.sessionManager.registerActivePath(nextPath);
|
|
2401
2563
|
}
|
|
2402
2564
|
async createNewSession() {
|
|
2565
|
+
const continuingConversation = Boolean(this.conversationId && this.sessionId);
|
|
2403
2566
|
const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
|
|
2404
2567
|
conversationId: this.conversationId || undefined,
|
|
2568
|
+
generation: continuingConversation ? this.checkpointGeneration + 1 : 0,
|
|
2569
|
+
parentSessionId: continuingConversation ? this.sessionId : undefined,
|
|
2405
2570
|
preview: this.sessionPreview || undefined,
|
|
2406
2571
|
});
|
|
2407
2572
|
this.sessionId = session.id;
|
|
2573
|
+
this.checkpointGeneration = session.header.generation ?? 0;
|
|
2408
2574
|
this.conversationId = session.header.conversationId ?? session.id;
|
|
2409
2575
|
this.setSessionPath(session.path);
|
|
2410
2576
|
this.lastPersistedIndex = this.messages.length;
|
|
2411
2577
|
}
|
|
2412
2578
|
async loadExistingSession(sessionPath) {
|
|
2413
|
-
|
|
2579
|
+
// A stale physical checkpoint is only an address, not the conversation tip.
|
|
2580
|
+
// Resolve every resume—not just over-threshold/deferred compaction resumes—
|
|
2581
|
+
// before reading history so the next prompt cannot continue an old branch.
|
|
2582
|
+
const canonicalPath = (await this.sessionManager.resolveCanonicalSession(sessionPath, this.cwd)) ?? sessionPath;
|
|
2583
|
+
const loaded = await this.sessionManager.load(canonicalPath);
|
|
2414
2584
|
// Use the leaf from the header to walk the correct branch
|
|
2415
2585
|
const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
|
|
2586
|
+
this.checkpointGeneration = loaded.header.generation ?? 0;
|
|
2416
2587
|
this.conversationId = loaded.header.conversationId ?? loaded.header.id;
|
|
2417
2588
|
const legacyLabel = [...loaded.entries]
|
|
2418
2589
|
.reverse()
|
|
@@ -2461,67 +2632,41 @@ export class AgentSession {
|
|
|
2461
2632
|
provider: this.provider,
|
|
2462
2633
|
accountId: creds.accountId,
|
|
2463
2634
|
});
|
|
2635
|
+
this.sessionId = loaded.header.id;
|
|
2636
|
+
this.setSessionPath(loaded.path);
|
|
2637
|
+
this.lastPersistedIndex = this.messages.length;
|
|
2638
|
+
const loadPolicy = resolveCompactionPolicy({
|
|
2639
|
+
provider: this.provider,
|
|
2640
|
+
model: this.model,
|
|
2641
|
+
contextWindow,
|
|
2642
|
+
threshold: this.settingsManager.get("compactThreshold"),
|
|
2643
|
+
accountId: creds.accountId,
|
|
2644
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
2645
|
+
});
|
|
2646
|
+
log("INFO", "compaction", "Restore compaction decision", {
|
|
2647
|
+
provider: this.provider,
|
|
2648
|
+
model: this.model,
|
|
2649
|
+
transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
|
|
2650
|
+
contextWindow: String(contextWindow),
|
|
2651
|
+
activeTokens: "estimated",
|
|
2652
|
+
triggerLimit: String(loadPolicy.targetTokens),
|
|
2653
|
+
});
|
|
2464
2654
|
const needsLoadCompaction = this.settingsManager.get("autoCompact") &&
|
|
2465
|
-
shouldCompact(this.messages, contextWindow,
|
|
2655
|
+
shouldCompact(this.messages, contextWindow, loadPolicy.threshold);
|
|
2466
2656
|
if (needsLoadCompaction && this.opts.deferLoadCompaction) {
|
|
2467
|
-
//
|
|
2468
|
-
//
|
|
2469
|
-
|
|
2657
|
+
// Canonicalize again immediately before the first prompt is persisted:
|
|
2658
|
+
// another process may create the shared checkpoint after this load.
|
|
2659
|
+
this.deferredCompactionPending = true;
|
|
2470
2660
|
log("INFO", "session", "Restored session exceeds context — deferring compaction to first prompt");
|
|
2471
2661
|
}
|
|
2472
2662
|
else if (needsLoadCompaction) {
|
|
2473
2663
|
await this.subAgentManager?.hydrate(loaded.header.id);
|
|
2474
2664
|
log("INFO", "session", `Restored session exceeds context — auto-compacting`);
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
accountId: creds.accountId,
|
|
2480
|
-
projectId: creds.projectId,
|
|
2481
|
-
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
2482
|
-
contextWindow,
|
|
2483
|
-
signal: this.opts.signal,
|
|
2484
|
-
});
|
|
2485
|
-
this.messages = compacted.messages;
|
|
2486
|
-
// Same anchor rebase as compact(): the restored markers were recorded
|
|
2487
|
-
// against the pre-compaction transcript.
|
|
2488
|
-
this.remapMarkerAnchors(compacted.result.anchorRemap);
|
|
2489
|
-
log("INFO", "session", `Auto-compaction complete`, {
|
|
2490
|
-
before: String(compacted.result.originalCount),
|
|
2491
|
-
after: String(compacted.result.newCount),
|
|
2492
|
-
});
|
|
2493
|
-
// Compaction rewrote history, so the on-disk file no longer reflects
|
|
2494
|
-
// what's in memory — fork a fresh session file for the compacted state
|
|
2495
|
-
// (mirrors compact()'s own persistence) so `ggcoder continue` picks up
|
|
2496
|
-
// the summary instead of the full original transcript.
|
|
2497
|
-
const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
|
|
2498
|
-
conversationId: this.conversationId || undefined,
|
|
2499
|
-
preview: this.sessionPreview || undefined,
|
|
2500
|
-
});
|
|
2501
|
-
this.sessionId = session.id;
|
|
2502
|
-
this.conversationId = session.header.conversationId ?? session.id;
|
|
2503
|
-
this.setSessionPath(session.path);
|
|
2504
|
-
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
2505
|
-
this.currentLeafId = null;
|
|
2506
|
-
// Re-persist (compacted) messages — skip system, it's rebuilt on load
|
|
2507
|
-
for (const msg of this.messages) {
|
|
2508
|
-
if (msg.role === "system")
|
|
2509
|
-
continue;
|
|
2510
|
-
await this.persistMessage(msg);
|
|
2665
|
+
await this.compact(creds, "automatic");
|
|
2666
|
+
if (this.lastCompactionCompacted) {
|
|
2667
|
+
await this.recordInterruptedRuns(interruptedRuns);
|
|
2668
|
+
return;
|
|
2511
2669
|
}
|
|
2512
|
-
this.lastPersistedIndex = this.messages.length;
|
|
2513
|
-
// Carry restored evidence into the continuation file.
|
|
2514
|
-
await this.rePersistTurnMetrics();
|
|
2515
|
-
await this.rePersistKenTurns();
|
|
2516
|
-
await this.rePersistAutopilotMarkers();
|
|
2517
|
-
await this.rePersistAppMarkers();
|
|
2518
|
-
// Record this load-time auto-compaction's counts for the resumed notice.
|
|
2519
|
-
await this.persistAppMarker("compaction", {
|
|
2520
|
-
originalCount: compacted.result.originalCount,
|
|
2521
|
-
newCount: compacted.result.newCount,
|
|
2522
|
-
});
|
|
2523
|
-
await this.recordInterruptedRuns(interruptedRuns);
|
|
2524
|
-
return;
|
|
2525
2670
|
}
|
|
2526
2671
|
// Plain resume (no compaction needed): keep using the original session
|
|
2527
2672
|
// file/id and append future turns to it in place. Forking a new file here
|
|
@@ -2560,8 +2705,10 @@ export class AgentSession {
|
|
|
2560
2705
|
return this.messages;
|
|
2561
2706
|
}
|
|
2562
2707
|
async persistMessage(message) {
|
|
2563
|
-
if (!this.sessionPreview &&
|
|
2564
|
-
|
|
2708
|
+
if (!this.sessionPreview &&
|
|
2709
|
+
message.role === "user" &&
|
|
2710
|
+
(message.provenance?.source === "human" || !message.provenance)) {
|
|
2711
|
+
this.sessionPreview = getUserSessionPrompt(message.content, message.provenance) ?? "";
|
|
2565
2712
|
}
|
|
2566
2713
|
// Transient sessions (subagent spawns) have no session file — skip.
|
|
2567
2714
|
if (!this.sessionPath)
|
|
@@ -2581,7 +2728,7 @@ export class AgentSession {
|
|
|
2581
2728
|
createSlashCommandContext() {
|
|
2582
2729
|
return {
|
|
2583
2730
|
switchModel: (provider, model) => this.switchModel(provider, model),
|
|
2584
|
-
compact: () => this.compact(),
|
|
2731
|
+
compact: () => this.compact(undefined, "manual"),
|
|
2585
2732
|
newSession: () => this.newSession(),
|
|
2586
2733
|
listSessions: async () => {
|
|
2587
2734
|
const sessions = await this.sessionManager.list(this.cwd);
|