@kenkaiiii/ggcoder 5.29.0 → 5.30.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.
Files changed (143) hide show
  1. package/dist/app-sidecar-auth-broadcast.test.js +14 -3
  2. package/dist/app-sidecar-auth-broadcast.test.js.map +1 -1
  3. package/dist/app-sidecar.js +27 -20
  4. package/dist/app-sidecar.js.map +1 -1
  5. package/dist/cli/command-routing.d.ts +1 -1
  6. package/dist/cli/command-routing.d.ts.map +1 -1
  7. package/dist/cli/command-routing.js +1 -0
  8. package/dist/cli/command-routing.js.map +1 -1
  9. package/dist/cli/command-routing.test.js +1 -0
  10. package/dist/cli/command-routing.test.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +167 -47
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +2 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +10 -0
  17. package/dist/config.js.map +1 -1
  18. package/dist/config.test.js +10 -1
  19. package/dist/config.test.js.map +1 -1
  20. package/dist/core/agent-session-compaction.test.js +240 -2
  21. package/dist/core/agent-session-compaction.test.js.map +1 -1
  22. package/dist/core/agent-session-crash-durability.test.js +9 -2
  23. package/dist/core/agent-session-crash-durability.test.js.map +1 -1
  24. package/dist/core/agent-session-process-gate.test.js +5 -1
  25. package/dist/core/agent-session-process-gate.test.js.map +1 -1
  26. package/dist/core/agent-session-review-coverage.test.js +80 -23
  27. package/dist/core/agent-session-review-coverage.test.js.map +1 -1
  28. package/dist/core/agent-session.d.ts +12 -3
  29. package/dist/core/agent-session.d.ts.map +1 -1
  30. package/dist/core/agent-session.js +294 -134
  31. package/dist/core/agent-session.js.map +1 -1
  32. package/dist/core/compaction/active-context.test.js +21 -0
  33. package/dist/core/compaction/active-context.test.js.map +1 -1
  34. package/dist/core/compaction/compactor.d.ts +24 -6
  35. package/dist/core/compaction/compactor.d.ts.map +1 -1
  36. package/dist/core/compaction/compactor.js +246 -98
  37. package/dist/core/compaction/compactor.js.map +1 -1
  38. package/dist/core/compaction/compactor.test.js +107 -8
  39. package/dist/core/compaction/compactor.test.js.map +1 -1
  40. package/dist/core/compaction/policy.d.ts +17 -0
  41. package/dist/core/compaction/policy.d.ts.map +1 -0
  42. package/dist/core/compaction/policy.js +22 -0
  43. package/dist/core/compaction/policy.js.map +1 -0
  44. package/dist/core/compaction/policy.test.d.ts +2 -0
  45. package/dist/core/compaction/policy.test.d.ts.map +1 -0
  46. package/dist/core/compaction/policy.test.js +35 -0
  47. package/dist/core/compaction/policy.test.js.map +1 -0
  48. package/dist/core/foreign-session-import.d.ts.map +1 -1
  49. package/dist/core/foreign-session-import.js +5 -1
  50. package/dist/core/foreign-session-import.js.map +1 -1
  51. package/dist/core/ideal-review.d.ts +20 -1
  52. package/dist/core/ideal-review.d.ts.map +1 -1
  53. package/dist/core/ideal-review.js +37 -2
  54. package/dist/core/ideal-review.js.map +1 -1
  55. package/dist/core/ideal-review.test.js +99 -3
  56. package/dist/core/ideal-review.test.js.map +1 -1
  57. package/dist/core/loop-breaker.d.ts.map +1 -1
  58. package/dist/core/loop-breaker.js +1 -0
  59. package/dist/core/loop-breaker.js.map +1 -1
  60. package/dist/core/process-gate.d.ts.map +1 -1
  61. package/dist/core/process-gate.js +9 -1
  62. package/dist/core/process-gate.js.map +1 -1
  63. package/dist/core/process-manager-notifications.test.js +4 -2
  64. package/dist/core/process-manager-notifications.test.js.map +1 -1
  65. package/dist/core/regrounding.d.ts.map +1 -1
  66. package/dist/core/regrounding.js +1 -0
  67. package/dist/core/regrounding.js.map +1 -1
  68. package/dist/core/session-compaction.d.ts +5 -0
  69. package/dist/core/session-compaction.d.ts.map +1 -1
  70. package/dist/core/session-compaction.js +10 -1
  71. package/dist/core/session-compaction.js.map +1 -1
  72. package/dist/core/session-compaction.test.js +16 -1
  73. package/dist/core/session-compaction.test.js.map +1 -1
  74. package/dist/core/session-export.d.ts.map +1 -1
  75. package/dist/core/session-export.js +11 -8
  76. package/dist/core/session-export.js.map +1 -1
  77. package/dist/core/session-history.d.ts +7 -2
  78. package/dist/core/session-history.d.ts.map +1 -1
  79. package/dist/core/session-history.js +52 -9
  80. package/dist/core/session-history.js.map +1 -1
  81. package/dist/core/session-history.test.js +26 -1
  82. package/dist/core/session-history.test.js.map +1 -1
  83. package/dist/core/session-manager.d.ts +106 -0
  84. package/dist/core/session-manager.d.ts.map +1 -1
  85. package/dist/core/session-manager.js +316 -15
  86. package/dist/core/session-manager.js.map +1 -1
  87. package/dist/core/session-manager.test.js +109 -1
  88. package/dist/core/session-manager.test.js.map +1 -1
  89. package/dist/core/session-notification-visibility.test.js +34 -0
  90. package/dist/core/session-notification-visibility.test.js.map +1 -1
  91. package/dist/core/session-preview.d.ts +2 -2
  92. package/dist/core/session-preview.d.ts.map +1 -1
  93. package/dist/core/session-preview.js +8 -5
  94. package/dist/core/session-preview.js.map +1 -1
  95. package/dist/core/session-restore-display.test.js +25 -0
  96. package/dist/core/session-restore-display.test.js.map +1 -1
  97. package/dist/core/session-storage.d.ts.map +1 -1
  98. package/dist/core/session-storage.js +39 -3
  99. package/dist/core/session-storage.js.map +1 -1
  100. package/dist/core/subagent-manager.d.ts.map +1 -1
  101. package/dist/core/subagent-manager.js +9 -1
  102. package/dist/core/subagent-manager.js.map +1 -1
  103. package/dist/core/subagent-manager.test.js +4 -3
  104. package/dist/core/subagent-manager.test.js.map +1 -1
  105. package/dist/interactive.d.ts.map +1 -1
  106. package/dist/interactive.js +17 -3
  107. package/dist/interactive.js.map +1 -1
  108. package/dist/modes/acp-mode.d.ts +82 -0
  109. package/dist/modes/acp-mode.d.ts.map +1 -0
  110. package/dist/modes/acp-mode.js +802 -0
  111. package/dist/modes/acp-mode.js.map +1 -0
  112. package/dist/modes/acp-mode.test.d.ts +2 -0
  113. package/dist/modes/acp-mode.test.d.ts.map +1 -0
  114. package/dist/modes/acp-mode.test.js +611 -0
  115. package/dist/modes/acp-mode.test.js.map +1 -0
  116. package/dist/modes/index.d.ts +1 -0
  117. package/dist/modes/index.d.ts.map +1 -1
  118. package/dist/modes/index.js +1 -0
  119. package/dist/modes/index.js.map +1 -1
  120. package/dist/modes/json-mode.d.ts +28 -0
  121. package/dist/modes/json-mode.d.ts.map +1 -1
  122. package/dist/modes/json-mode.js +32 -0
  123. package/dist/modes/json-mode.js.map +1 -1
  124. package/dist/modes/json-mode.test.d.ts +2 -0
  125. package/dist/modes/json-mode.test.d.ts.map +1 -0
  126. package/dist/modes/json-mode.test.js +84 -0
  127. package/dist/modes/json-mode.test.js.map +1 -0
  128. package/dist/session.d.ts +24 -0
  129. package/dist/session.d.ts.map +1 -1
  130. package/dist/session.js +32 -0
  131. package/dist/session.js.map +1 -1
  132. package/dist/tools/subagent.d.ts.map +1 -1
  133. package/dist/tools/subagent.js +27 -6
  134. package/dist/tools/subagent.js.map +1 -1
  135. package/dist/tools/subagent.test.js +53 -3
  136. package/dist/tools/subagent.test.js.map +1 -1
  137. package/dist/tools/web-fetch.d.ts +1 -1
  138. package/dist/types.d.ts +7 -0
  139. package/dist/types.d.ts.map +1 -1
  140. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  141. package/dist/ui/hooks/useAgentLoop.js +18 -4
  142. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  143. 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,11 +31,12 @@ 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";
36
38
  import { detectProjectStack } from "./language-detector.js";
37
- import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageMessage, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
39
+ import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageEscalationMessage, buildReviewCoverageMessage, MAX_REVIEW_COVERAGE_INJECTIONS, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
38
40
  import { evaluateLoopBreak, buildLoopBreakMessage, CycleDetector, ToolCallProgressTracker, detectTextRepetition, } from "./loop-breaker.js";
39
41
  import { buildRegroundingMessage } from "./regrounding.js";
40
42
  import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
@@ -137,6 +139,8 @@ export class AgentSession {
137
139
  /** Runtime-only suppression while Ken owns verification in autopilot mode. */
138
140
  idealReviewSuppressed = false;
139
141
  reviewCoverage;
142
+ /** Coverage follow-ups spent this run, capped by MAX_REVIEW_COVERAGE_INJECTIONS. */
143
+ reviewCoverageInjected = 0;
140
144
  /** 0 = none; 1 = first nudge sent; 2 = final stop-and-report injected. */
141
145
  loopBreakInjected = 0;
142
146
  regroundingInjected = false;
@@ -147,6 +151,8 @@ export class AgentSession {
147
151
  compactionOccurred = false;
148
152
  lastCompactionCompacted = false;
149
153
  compactionRetryAfter = 0;
154
+ /** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
155
+ deferredCompactionPending = false;
150
156
  /** Latest provider count, anchored to the assistant response it measured. */
151
157
  providerContext = null;
152
158
  originalRequest = "";
@@ -204,6 +210,7 @@ export class AgentSession {
204
210
  /** Extra workspace roots added with `/add-dir` (resolved, de-duplicated). */
205
211
  additionalRoots = [];
206
212
  sessionId = "";
213
+ checkpointGeneration = 0;
207
214
  /** Stable identity shared by compaction and approved-plan checkpoint files. */
208
215
  conversationId = "";
209
216
  /** Original user-authored prompt, retained when internal messages replace history. */
@@ -751,12 +758,16 @@ export class AgentSession {
751
758
  /**
752
759
  * Process user input. Handles slash commands or runs agent loop.
753
760
  */
754
- async prompt(content) {
761
+ async prompt(content, provenance = {
762
+ source: "human",
763
+ kind: "prompt",
764
+ visibility: "transcript",
765
+ }) {
766
+ await this.adoptDeferredCheckpointBeforePrompt();
755
767
  const slash = await this.resolveSlashInput(content);
756
768
  if (slash?.kind === "template") {
757
- // Inject the prompt-template command as a user message to the agent, then
758
- // run as a normal prompt (push message + agent loop).
759
- 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 };
760
771
  this.messages.push(userMessage);
761
772
  await this.persistMessage(userMessage);
762
773
  this.lastPersistedIndex = this.messages.length;
@@ -772,7 +783,7 @@ export class AgentSession {
772
783
  return;
773
784
  }
774
785
  // Push user message
775
- const userMessage = { role: "user", content };
786
+ const userMessage = { role: "user", content, provenance };
776
787
  this.messages.push(userMessage);
777
788
  await this.persistMessage(userMessage);
778
789
  this.lastPersistedIndex = this.messages.length;
@@ -786,10 +797,15 @@ export class AgentSession {
786
797
  * attachments are always a direct conversational turn.
787
798
  */
788
799
  async promptWithAttachments(text, attachments) {
800
+ await this.adoptDeferredCheckpointBeforePrompt();
789
801
  const parts = this.buildAttachmentParts(text, attachments);
790
802
  if (parts.length === 0)
791
803
  return;
792
- const userMessage = { role: "user", content: parts };
804
+ const userMessage = {
805
+ role: "user",
806
+ content: parts,
807
+ provenance: { source: "human", kind: "prompt", visibility: "transcript" },
808
+ };
793
809
  this.messages.push(userMessage);
794
810
  await this.persistMessage(userMessage);
795
811
  this.lastPersistedIndex = this.messages.length;
@@ -878,6 +894,7 @@ export class AgentSession {
878
894
  this.hookFileEditCounts.clear();
879
895
  this.hookToolCalls.clear();
880
896
  this.reviewCoverage.reset();
897
+ this.reviewCoverageInjected = 0;
881
898
  this.idealReviewPhase = "idle";
882
899
  this.loopBreakInjected = 0;
883
900
  this.regroundingInjected = false;
@@ -990,6 +1007,7 @@ export class AgentSession {
990
1007
  ? {
991
1008
  role: "user",
992
1009
  content: buildNotificationSteeringText(notified.map((entry) => entry.text)),
1010
+ provenance: { source: "runtime", kind: "notification", visibility: "hidden" },
993
1011
  }
994
1012
  : null;
995
1013
  if (notificationMessage) {
@@ -1013,8 +1031,13 @@ export class AgentSession {
1013
1031
  // (not merged): each persists as its own user message, so a resumed
1014
1032
  // session shows the same number of bubbles the live run did.
1015
1033
  const steeringMessages = queued.map((m) => {
1034
+ const provenance = {
1035
+ source: "human",
1036
+ kind: "steering",
1037
+ visibility: "transcript",
1038
+ };
1016
1039
  if (m.attachments.length === 0) {
1017
- return { role: "user", content: wrapSteeringText(m.text) };
1040
+ return { role: "user", content: wrapSteeringText(m.text), provenance };
1018
1041
  }
1019
1042
  // Queued attachments ride the same native-block path as a non-queued
1020
1043
  // attachment prompt, prefixed with the steering framing.
@@ -1022,7 +1045,7 @@ export class AgentSession {
1022
1045
  { type: "text", text: STEERING_PREFIX },
1023
1046
  ...this.buildAttachmentParts(m.text, m.attachments),
1024
1047
  ];
1025
- return { role: "user", content: parts };
1048
+ return { role: "user", content: parts, provenance };
1026
1049
  });
1027
1050
  return notificationMessage ? [...steeringMessages, notificationMessage] : steeringMessages;
1028
1051
  }
@@ -1133,9 +1156,20 @@ export class AgentSession {
1133
1156
  lspMissing: lspEvidence.missing,
1134
1157
  });
1135
1158
  if (coverage.missing.length > 0) {
1136
- return [
1137
- this.withReviewLspEvidence(buildReviewCoverageMessage(coverage.missing), lspEvidence),
1138
- ];
1159
+ if (this.reviewCoverageInjected < MAX_REVIEW_COVERAGE_INJECTIONS) {
1160
+ this.reviewCoverageInjected += 1;
1161
+ return [
1162
+ this.withReviewLspEvidence(buildReviewCoverageMessage(coverage.missing), lspEvidence),
1163
+ ];
1164
+ }
1165
+ // Budget spent: close the gate so the run cannot spin on a file that
1166
+ // never becomes readable, and require the gap be reported to the user.
1167
+ this.idealReviewPhase = "complete";
1168
+ log("INFO", "ideal", "Ideal review coverage escalated after retry budget", {
1169
+ injected: String(this.reviewCoverageInjected),
1170
+ missing: coverage.missing,
1171
+ });
1172
+ return [buildReviewCoverageEscalationMessage(coverage.missing)];
1139
1173
  }
1140
1174
  this.idealReviewPhase = "complete";
1141
1175
  return null;
@@ -1193,7 +1227,11 @@ export class AgentSession {
1193
1227
  : []),
1194
1228
  "Do not describe those files as compiler-clean without other evidence.",
1195
1229
  ];
1196
- return { role: "user", content: `${String(message.content)}\n\n${notes.join(" ")}` };
1230
+ return {
1231
+ role: "user",
1232
+ provenance: message.provenance,
1233
+ content: `${String(message.content)}\n\n${notes.join(" ")}`,
1234
+ };
1197
1235
  }
1198
1236
  /** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
1199
1237
  async runLoop() {
@@ -1233,7 +1271,14 @@ export class AgentSession {
1233
1271
  provider: this.provider,
1234
1272
  accountId: creds.accountId,
1235
1273
  });
1236
- const threshold = this.settingsManager.get("compactThreshold");
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
+ });
1237
1282
  let activeTokens;
1238
1283
  if (this.providerContext) {
1239
1284
  const anchorIndex = this.messages.lastIndexOf(this.providerContext.anchor);
@@ -1247,9 +1292,17 @@ export class AgentSession {
1247
1292
  this.providerContext = null;
1248
1293
  }
1249
1294
  }
1250
- if (shouldCompact(this.messages, contextWindow, threshold, activeTokens)) {
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)) {
1251
1304
  try {
1252
- await this.compact(creds);
1305
+ await this.compact(creds, "automatic");
1253
1306
  if (this.lastCompactionCompacted) {
1254
1307
  // Re-grounding hook keys off this — the context was just summarized.
1255
1308
  this.compactionOccurred = true;
@@ -1359,12 +1412,27 @@ export class AgentSession {
1359
1412
  provider: this.provider,
1360
1413
  accountId,
1361
1414
  });
1362
- const threshold = this.settingsManager.get("compactThreshold");
1415
+ const policy = resolveCompactionPolicy({
1416
+ provider: this.provider,
1417
+ model: this.model,
1418
+ contextWindow,
1419
+ threshold: this.settingsManager.get("compactThreshold"),
1420
+ accountId,
1421
+ approvedPlanPath: this.approvedPlanPath,
1422
+ });
1363
1423
  const activeTokens = calculateActiveContextTokens(messages, {
1364
1424
  usage,
1365
1425
  pendingMessages,
1366
1426
  });
1367
- if (!shouldCompact(messages, contextWindow, threshold, activeTokens))
1427
+ log("INFO", "compaction", "In-flight compaction decision", {
1428
+ provider: this.provider,
1429
+ model: this.model,
1430
+ transport: this.provider === "openai" && accountId ? "codex_oauth" : "public_api",
1431
+ contextWindow: String(contextWindow),
1432
+ activeTokens: String(activeTokens),
1433
+ triggerLimit: String(policy.targetTokens),
1434
+ });
1435
+ if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens))
1368
1436
  return messages;
1369
1437
  }
1370
1438
  // compact() operates on this.messages, while an earlier transform may
@@ -1377,7 +1445,7 @@ export class AgentSession {
1377
1445
  accountId,
1378
1446
  projectId,
1379
1447
  baseUrl: effectiveBaseUrl,
1380
- });
1448
+ }, force ? "forced" : "automatic");
1381
1449
  }
1382
1450
  catch (error) {
1383
1451
  this.messages = messages;
@@ -1626,9 +1694,66 @@ export class AgentSession {
1626
1694
  this.messages.push({
1627
1695
  role: "user",
1628
1696
  content: `[Model switched from ${from} to ${to}. Everything above was produced under the previous model; continue the current task from here.]`,
1697
+ provenance: { source: "runtime", kind: "model_switch", visibility: "hidden" },
1629
1698
  });
1630
1699
  }
1631
- async compact(existingCredentials) {
1700
+ async adoptCompactionCheckpoint(loaded) {
1701
+ const systemMessage = this.messages[0];
1702
+ const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
1703
+ this.messages = [systemMessage, ...loadedMessages];
1704
+ this.sessionId = loaded.header.id;
1705
+ this.conversationId = loaded.header.conversationId ?? loaded.header.id;
1706
+ this.checkpointGeneration = loaded.header.generation ?? 0;
1707
+ this.currentLeafId = loaded.header.leafId;
1708
+ this.setSessionPath(loaded.path);
1709
+ this.kenTurns = this.sessionManager.getKenTurns(loaded.entries, loaded.header.leafId);
1710
+ this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
1711
+ this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
1712
+ this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
1713
+ this.lastPersistedIndex = this.messages.length;
1714
+ this.providerContext = null;
1715
+ await this.subAgentManager?.rebindParentSession(this.sessionId);
1716
+ }
1717
+ /** Canonicalize a deferred restore before a new prompt can fork stale history. */
1718
+ async adoptDeferredCheckpointBeforePrompt() {
1719
+ if (!this.deferredCompactionPending || !this.conversationId)
1720
+ return;
1721
+ this.deferredCompactionPending = false;
1722
+ const canonicalPath = await this.sessionManager.resolveCanonicalSession(this.conversationId, this.cwd);
1723
+ if (!canonicalPath || canonicalPath === this.sessionPath)
1724
+ return;
1725
+ await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath));
1726
+ }
1727
+ async persistCompactionCheckpoint(sourceFingerprint, result) {
1728
+ const parentSessionId = this.sessionId || undefined;
1729
+ const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1730
+ conversationId: this.conversationId || undefined,
1731
+ generation: this.checkpointGeneration + 1,
1732
+ parentSessionId,
1733
+ sourceFingerprint,
1734
+ preview: this.sessionPreview || undefined,
1735
+ });
1736
+ this.sessionId = session.id;
1737
+ this.checkpointGeneration = session.header.generation ?? 0;
1738
+ this.conversationId = session.header.conversationId ?? session.id;
1739
+ this.currentLeafId = null;
1740
+ this.setSessionPath(session.path);
1741
+ await this.subAgentManager?.rebindParentSession(this.sessionId);
1742
+ for (const message of this.messages) {
1743
+ if (message.role !== "system")
1744
+ await this.persistMessage(message);
1745
+ }
1746
+ this.lastPersistedIndex = this.messages.length;
1747
+ await this.rePersistTurnMetrics();
1748
+ await this.rePersistKenTurns();
1749
+ await this.rePersistAutopilotMarkers();
1750
+ await this.rePersistAppMarkers();
1751
+ await this.persistAppMarker("compaction", {
1752
+ originalCount: result.originalCount,
1753
+ newCount: result.newCount,
1754
+ });
1755
+ }
1756
+ async compact(existingCredentials, mode = "manual") {
1632
1757
  this.lastCompactionCompacted = false;
1633
1758
  const creds = existingCredentials ??
1634
1759
  (await this.authStorage.resolveCredentials(this.provider, {
@@ -1638,8 +1763,17 @@ export class AgentSession {
1638
1763
  provider: this.provider,
1639
1764
  accountId: creds.accountId,
1640
1765
  });
1641
- this.eventBus.emit("compaction_start", { messageCount: this.messages.length });
1642
- const result = await compact(this.messages, {
1766
+ const policy = resolveCompactionPolicy({
1767
+ provider: this.provider,
1768
+ model: this.model,
1769
+ contextWindow,
1770
+ threshold: this.settingsManager.get("compactThreshold"),
1771
+ accountId: creds.accountId,
1772
+ approvedPlanPath: this.approvedPlanPath,
1773
+ });
1774
+ const originalCount = this.messages.length;
1775
+ this.eventBus.emit("compaction_start", { messageCount: originalCount });
1776
+ const runCompactor = () => compact(this.messages, {
1643
1777
  provider: this.provider,
1644
1778
  model: this.model,
1645
1779
  apiKey: creds.accessToken,
@@ -1647,67 +1781,106 @@ export class AgentSession {
1647
1781
  projectId: creds.projectId,
1648
1782
  baseUrl: this.baseUrl ?? creds.baseUrl,
1649
1783
  contextWindow,
1784
+ targetTokens: policy.targetTokens,
1650
1785
  signal: this.opts.signal,
1786
+ approvedPlanPath: this.approvedPlanPath,
1651
1787
  });
1652
- this.messages = result.messages;
1653
- this.lastCompactionCompacted = result.result.compacted;
1654
- if (!result.result.compacted) {
1655
- this.eventBus.emit("compaction_end", {
1656
- compacted: false,
1657
- originalCount: result.result.originalCount,
1658
- newCount: result.result.newCount,
1659
- });
1660
- return;
1661
- }
1662
- this.providerContext = null;
1663
- // Compaction rewrote the message list, so every transcript anchor recorded
1664
- // against the old indices must move with it — otherwise the markers are
1665
- // re-persisted pointing at positions that no longer mean anything and
1666
- // replay far below where they happened (or past the end).
1667
- this.remapMarkerAnchors(result.result.anchorRemap);
1668
- // Transient sessions (Ken chat/autopilot, subagent spawns) must NEVER touch
1669
- // the session store: without this guard, the first auto-compaction called
1670
- // sessionManager.create() and assigned a real sessionPath, silently turning
1671
- // the "in-memory only" session into a persisted one — every later turn (and
1672
- // every further compaction) then leaked a Ken transcript file into the
1673
- // project's session list. Compact in memory only and keep sessionPath empty.
1674
- if (this.opts.transient) {
1675
- this.lastPersistedIndex = this.messages.length;
1788
+ let finalCount = originalCount;
1789
+ if (this.opts.transient || !this.conversationId) {
1790
+ const result = await runCompactor();
1791
+ finalCount = result.result.newCount;
1792
+ this.messages = result.messages;
1793
+ this.lastCompactionCompacted = result.result.compacted;
1794
+ if (result.result.compacted) {
1795
+ this.providerContext = null;
1796
+ this.remapMarkerAnchors(result.result.anchorRemap);
1797
+ this.lastPersistedIndex = this.messages.length;
1798
+ }
1676
1799
  }
1677
1800
  else {
1678
- // Persist compacted messages to a new session file so `ggcoder continue`
1679
- // picks up the compacted state instead of the full original history.
1680
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1681
- conversationId: this.conversationId || undefined,
1682
- preview: this.sessionPreview || undefined,
1683
- });
1684
- this.sessionId = session.id;
1685
- this.conversationId = session.header.conversationId ?? session.id;
1686
- this.setSessionPath(session.path);
1687
- await this.subAgentManager?.rebindParentSession(this.sessionId);
1688
- // Write compacted messages (skip system — it's rebuilt on load)
1689
- for (const msg of this.messages) {
1690
- if (msg.role === "system")
1691
- continue;
1692
- await this.persistMessage(msg);
1693
- }
1694
- this.lastPersistedIndex = this.messages.length;
1695
- // Carry evidence records into the new file so they survive compaction.
1696
- await this.rePersistTurnMetrics();
1697
- await this.rePersistKenTurns();
1698
- await this.rePersistAutopilotMarkers();
1699
- await this.rePersistAppMarkers();
1700
- // Persist the compaction counts so a resumed session's quiet notice can
1701
- // show the same "N → M messages" summary the live run did.
1702
- await this.persistAppMarker("compaction", {
1703
- originalCount: result.result.originalCount,
1704
- newCount: result.result.newCount,
1801
+ const conversationId = this.conversationId;
1802
+ await this.sessionManager.withCompactionLease(conversationId, this.opts.signal, async () => {
1803
+ let sourceFingerprint = computeSourceFingerprint(this.messages);
1804
+ const canonicalPath = await this.sessionManager.resolveCanonicalSession(conversationId, this.cwd);
1805
+ if (canonicalPath && canonicalPath !== this.sessionPath) {
1806
+ const newest = await this.sessionManager.load(canonicalPath);
1807
+ if (newest.header.sourceFingerprint === sourceFingerprint) {
1808
+ await this.adoptCompactionCheckpoint(newest);
1809
+ this.lastCompactionCompacted = true;
1810
+ finalCount = this.messages.length;
1811
+ return;
1812
+ }
1813
+ // The canonical branch contains different progress. Rebase onto it before
1814
+ // compacting so a stale caller cannot supersede newer history by generation.
1815
+ await this.adoptCompactionCheckpoint(newest);
1816
+ sourceFingerprint = computeSourceFingerprint(this.messages);
1817
+ }
1818
+ const attempt = await this.sessionManager.readCompactionAttemptState(conversationId);
1819
+ const attemptActive = !attempt?.expiresAt || Date.parse(attempt.expiresAt) > Date.now();
1820
+ if (mode === "automatic" &&
1821
+ attempt?.fingerprint === sourceFingerprint &&
1822
+ attempt.policyKey === policy.policyKey &&
1823
+ attemptActive) {
1824
+ if (attempt.outcome === "success" && attempt.checkpointId) {
1825
+ const checkpointPath = await this.sessionManager.findById(this.cwd, attempt.checkpointId);
1826
+ if (checkpointPath) {
1827
+ const checkpoint = await this.sessionManager.load(checkpointPath);
1828
+ if (checkpoint.header.sourceFingerprint === sourceFingerprint) {
1829
+ await this.adoptCompactionCheckpoint(checkpoint);
1830
+ this.lastCompactionCompacted = true;
1831
+ finalCount = this.messages.length;
1832
+ return;
1833
+ }
1834
+ }
1835
+ }
1836
+ else if (attempt.outcome === "failed" || attempt.outcome === "noop") {
1837
+ return;
1838
+ }
1839
+ }
1840
+ try {
1841
+ const result = await runCompactor();
1842
+ finalCount = result.result.newCount;
1843
+ this.messages = result.messages;
1844
+ this.lastCompactionCompacted = result.result.compacted;
1845
+ if (!result.result.compacted) {
1846
+ await this.sessionManager.writeCompactionAttemptState(conversationId, {
1847
+ fingerprint: sourceFingerprint,
1848
+ policyKey: policy.policyKey,
1849
+ outcome: "noop",
1850
+ updatedAt: new Date().toISOString(),
1851
+ expiresAt: new Date(Date.now() + 30_000).toISOString(),
1852
+ });
1853
+ return;
1854
+ }
1855
+ this.providerContext = null;
1856
+ this.remapMarkerAnchors(result.result.anchorRemap);
1857
+ await this.persistCompactionCheckpoint(sourceFingerprint, result.result);
1858
+ await this.sessionManager.writeCompactionAttemptState(conversationId, {
1859
+ fingerprint: sourceFingerprint,
1860
+ policyKey: policy.policyKey,
1861
+ outcome: "success",
1862
+ checkpointId: this.sessionId,
1863
+ updatedAt: new Date().toISOString(),
1864
+ });
1865
+ }
1866
+ catch (error) {
1867
+ await this.sessionManager
1868
+ .writeCompactionAttemptState(conversationId, {
1869
+ fingerprint: sourceFingerprint,
1870
+ policyKey: policy.policyKey,
1871
+ outcome: "failed",
1872
+ updatedAt: new Date().toISOString(),
1873
+ expiresAt: new Date(Date.now() + 30_000).toISOString(),
1874
+ })
1875
+ .catch(() => { });
1876
+ throw error;
1877
+ }
1705
1878
  });
1706
1879
  }
1707
1880
  this.eventBus.emit("compaction_end", {
1708
- compacted: true,
1709
- originalCount: result.result.originalCount,
1710
- newCount: result.result.newCount,
1881
+ compacted: this.lastCompactionCompacted,
1882
+ originalCount,
1883
+ newCount: finalCount,
1711
1884
  });
1712
1885
  }
1713
1886
  async newSession(preserveConversation = false) {
@@ -1715,6 +1888,7 @@ export class AgentSession {
1715
1888
  // explicit new sessions reset the conversation identity.
1716
1889
  if (!preserveConversation) {
1717
1890
  this.conversationId = "";
1891
+ this.checkpointGeneration = 0;
1718
1892
  this.sessionPreview = "";
1719
1893
  }
1720
1894
  // A fresh session drops any in-flight plan state so its prompt is clean.
@@ -1742,6 +1916,7 @@ export class AgentSession {
1742
1916
  if (this.opts.transient) {
1743
1917
  this.sessionId = "";
1744
1918
  this.conversationId = "";
1919
+ this.checkpointGeneration = 0;
1745
1920
  this.sessionPreview = "";
1746
1921
  this.setSessionPath("");
1747
1922
  this.lastPersistedIndex = this.messages.length;
@@ -2386,19 +2561,28 @@ export class AgentSession {
2386
2561
  this.sessionManager.registerActivePath(nextPath);
2387
2562
  }
2388
2563
  async createNewSession() {
2564
+ const continuingConversation = Boolean(this.conversationId && this.sessionId);
2389
2565
  const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
2390
2566
  conversationId: this.conversationId || undefined,
2567
+ generation: continuingConversation ? this.checkpointGeneration + 1 : 0,
2568
+ parentSessionId: continuingConversation ? this.sessionId : undefined,
2391
2569
  preview: this.sessionPreview || undefined,
2392
2570
  });
2393
2571
  this.sessionId = session.id;
2572
+ this.checkpointGeneration = session.header.generation ?? 0;
2394
2573
  this.conversationId = session.header.conversationId ?? session.id;
2395
2574
  this.setSessionPath(session.path);
2396
2575
  this.lastPersistedIndex = this.messages.length;
2397
2576
  }
2398
2577
  async loadExistingSession(sessionPath) {
2399
- const loaded = await this.sessionManager.load(sessionPath);
2578
+ // A stale physical checkpoint is only an address, not the conversation tip.
2579
+ // Resolve every resume—not just over-threshold/deferred compaction resumes—
2580
+ // before reading history so the next prompt cannot continue an old branch.
2581
+ const canonicalPath = (await this.sessionManager.resolveCanonicalSession(sessionPath, this.cwd)) ?? sessionPath;
2582
+ const loaded = await this.sessionManager.load(canonicalPath);
2400
2583
  // Use the leaf from the header to walk the correct branch
2401
2584
  const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
2585
+ this.checkpointGeneration = loaded.header.generation ?? 0;
2402
2586
  this.conversationId = loaded.header.conversationId ?? loaded.header.id;
2403
2587
  const legacyLabel = [...loaded.entries]
2404
2588
  .reverse()
@@ -2447,67 +2631,41 @@ export class AgentSession {
2447
2631
  provider: this.provider,
2448
2632
  accountId: creds.accountId,
2449
2633
  });
2634
+ this.sessionId = loaded.header.id;
2635
+ this.setSessionPath(loaded.path);
2636
+ this.lastPersistedIndex = this.messages.length;
2637
+ const loadPolicy = resolveCompactionPolicy({
2638
+ provider: this.provider,
2639
+ model: this.model,
2640
+ contextWindow,
2641
+ threshold: this.settingsManager.get("compactThreshold"),
2642
+ accountId: creds.accountId,
2643
+ approvedPlanPath: this.approvedPlanPath,
2644
+ });
2645
+ log("INFO", "compaction", "Restore compaction decision", {
2646
+ provider: this.provider,
2647
+ model: this.model,
2648
+ transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
2649
+ contextWindow: String(contextWindow),
2650
+ activeTokens: "estimated",
2651
+ triggerLimit: String(loadPolicy.targetTokens),
2652
+ });
2450
2653
  const needsLoadCompaction = this.settingsManager.get("autoCompact") &&
2451
- shouldCompact(this.messages, contextWindow, this.settingsManager.get("compactThreshold"));
2654
+ shouldCompact(this.messages, contextWindow, loadPolicy.threshold);
2452
2655
  if (needsLoadCompaction && this.opts.deferLoadCompaction) {
2453
- // Host readiness is gated on initialize() don't block it on a summary
2454
- // LLM call (up to 30s). runLoop()'s pre-run auto-compaction picks this
2455
- // up on the first prompt and emits compaction_start/_end for the UI.
2656
+ // Canonicalize again immediately before the first prompt is persisted:
2657
+ // another process may create the shared checkpoint after this load.
2658
+ this.deferredCompactionPending = true;
2456
2659
  log("INFO", "session", "Restored session exceeds context — deferring compaction to first prompt");
2457
2660
  }
2458
2661
  else if (needsLoadCompaction) {
2459
2662
  await this.subAgentManager?.hydrate(loaded.header.id);
2460
2663
  log("INFO", "session", `Restored session exceeds context — auto-compacting`);
2461
- const compacted = await compact(this.messages, {
2462
- provider: this.provider,
2463
- model: this.model,
2464
- apiKey: creds.accessToken,
2465
- accountId: creds.accountId,
2466
- projectId: creds.projectId,
2467
- baseUrl: this.baseUrl ?? creds.baseUrl,
2468
- contextWindow,
2469
- signal: this.opts.signal,
2470
- });
2471
- this.messages = compacted.messages;
2472
- // Same anchor rebase as compact(): the restored markers were recorded
2473
- // against the pre-compaction transcript.
2474
- this.remapMarkerAnchors(compacted.result.anchorRemap);
2475
- log("INFO", "session", `Auto-compaction complete`, {
2476
- before: String(compacted.result.originalCount),
2477
- after: String(compacted.result.newCount),
2478
- });
2479
- // Compaction rewrote history, so the on-disk file no longer reflects
2480
- // what's in memory — fork a fresh session file for the compacted state
2481
- // (mirrors compact()'s own persistence) so `ggcoder continue` picks up
2482
- // the summary instead of the full original transcript.
2483
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
2484
- conversationId: this.conversationId || undefined,
2485
- preview: this.sessionPreview || undefined,
2486
- });
2487
- this.sessionId = session.id;
2488
- this.conversationId = session.header.conversationId ?? session.id;
2489
- this.setSessionPath(session.path);
2490
- await this.subAgentManager?.rebindParentSession(this.sessionId);
2491
- this.currentLeafId = null;
2492
- // Re-persist (compacted) messages — skip system, it's rebuilt on load
2493
- for (const msg of this.messages) {
2494
- if (msg.role === "system")
2495
- continue;
2496
- await this.persistMessage(msg);
2664
+ await this.compact(creds, "automatic");
2665
+ if (this.lastCompactionCompacted) {
2666
+ await this.recordInterruptedRuns(interruptedRuns);
2667
+ return;
2497
2668
  }
2498
- this.lastPersistedIndex = this.messages.length;
2499
- // Carry restored evidence into the continuation file.
2500
- await this.rePersistTurnMetrics();
2501
- await this.rePersistKenTurns();
2502
- await this.rePersistAutopilotMarkers();
2503
- await this.rePersistAppMarkers();
2504
- // Record this load-time auto-compaction's counts for the resumed notice.
2505
- await this.persistAppMarker("compaction", {
2506
- originalCount: compacted.result.originalCount,
2507
- newCount: compacted.result.newCount,
2508
- });
2509
- await this.recordInterruptedRuns(interruptedRuns);
2510
- return;
2511
2669
  }
2512
2670
  // Plain resume (no compaction needed): keep using the original session
2513
2671
  // file/id and append future turns to it in place. Forking a new file here
@@ -2546,8 +2704,10 @@ export class AgentSession {
2546
2704
  return this.messages;
2547
2705
  }
2548
2706
  async persistMessage(message) {
2549
- if (!this.sessionPreview && message.role === "user") {
2550
- this.sessionPreview = getUserSessionPrompt(message.content) ?? "";
2707
+ if (!this.sessionPreview &&
2708
+ message.role === "user" &&
2709
+ (message.provenance?.source === "human" || !message.provenance)) {
2710
+ this.sessionPreview = getUserSessionPrompt(message.content, message.provenance) ?? "";
2551
2711
  }
2552
2712
  // Transient sessions (subagent spawns) have no session file — skip.
2553
2713
  if (!this.sessionPath)
@@ -2567,7 +2727,7 @@ export class AgentSession {
2567
2727
  createSlashCommandContext() {
2568
2728
  return {
2569
2729
  switchModel: (provider, model) => this.switchModel(provider, model),
2570
- compact: () => this.compact(),
2730
+ compact: () => this.compact(undefined, "manual"),
2571
2731
  newSession: () => this.newSession(),
2572
2732
  listSessions: async () => {
2573
2733
  const sessions = await this.sessionManager.list(this.cwd);