@kenkaiiii/ggcoder 5.16.0 → 5.18.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 (162) hide show
  1. package/README.md +2 -2
  2. package/dist/app-sidecar.js +303 -88
  3. package/dist/app-sidecar.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +24 -2
  6. package/dist/cli.js.map +1 -1
  7. package/dist/core/agent-session-review-coverage.test.d.ts +2 -0
  8. package/dist/core/agent-session-review-coverage.test.d.ts.map +1 -0
  9. package/dist/core/agent-session-review-coverage.test.js +57 -0
  10. package/dist/core/agent-session-review-coverage.test.js.map +1 -0
  11. package/dist/core/agent-session-tool-result-policy.test.d.ts +2 -0
  12. package/dist/core/agent-session-tool-result-policy.test.d.ts.map +1 -0
  13. package/dist/core/agent-session-tool-result-policy.test.js +12 -0
  14. package/dist/core/agent-session-tool-result-policy.test.js.map +1 -0
  15. package/dist/core/agent-session.d.ts +19 -12
  16. package/dist/core/agent-session.d.ts.map +1 -1
  17. package/dist/core/agent-session.js +185 -66
  18. package/dist/core/agent-session.js.map +1 -1
  19. package/dist/core/auth-providers.js +1 -1
  20. package/dist/core/auth-providers.js.map +1 -1
  21. package/dist/core/compaction/compactor.test.js +2 -1
  22. package/dist/core/compaction/compactor.test.js.map +1 -1
  23. package/dist/core/event-bus.d.ts +2 -0
  24. package/dist/core/event-bus.d.ts.map +1 -1
  25. package/dist/core/event-bus.js.map +1 -1
  26. package/dist/core/ideal-review.d.ts +27 -0
  27. package/dist/core/ideal-review.d.ts.map +1 -1
  28. package/dist/core/ideal-review.js +64 -0
  29. package/dist/core/ideal-review.js.map +1 -1
  30. package/dist/core/ideal-review.test.js +34 -1
  31. package/dist/core/ideal-review.test.js.map +1 -1
  32. package/dist/core/lsp/client.d.ts +3 -0
  33. package/dist/core/lsp/client.d.ts.map +1 -1
  34. package/dist/core/lsp/client.js +20 -0
  35. package/dist/core/lsp/client.js.map +1 -1
  36. package/dist/core/lsp/manager.d.ts +32 -9
  37. package/dist/core/lsp/manager.d.ts.map +1 -1
  38. package/dist/core/lsp/manager.js +89 -37
  39. package/dist/core/lsp/manager.js.map +1 -1
  40. package/dist/core/lsp/manager.test.js +56 -4
  41. package/dist/core/lsp/manager.test.js.map +1 -1
  42. package/dist/core/project-discovery.d.ts +4 -11
  43. package/dist/core/project-discovery.d.ts.map +1 -1
  44. package/dist/core/project-discovery.js +33 -30
  45. package/dist/core/project-discovery.js.map +1 -1
  46. package/dist/core/project-discovery.test.js +148 -0
  47. package/dist/core/project-discovery.test.js.map +1 -1
  48. package/dist/core/run-lifecycle.d.ts +40 -0
  49. package/dist/core/run-lifecycle.d.ts.map +1 -0
  50. package/dist/core/run-lifecycle.js +95 -0
  51. package/dist/core/run-lifecycle.js.map +1 -0
  52. package/dist/core/run-lifecycle.test.d.ts +2 -0
  53. package/dist/core/run-lifecycle.test.d.ts.map +1 -0
  54. package/dist/core/run-lifecycle.test.js +64 -0
  55. package/dist/core/run-lifecycle.test.js.map +1 -0
  56. package/dist/core/session-compaction.d.ts +3 -0
  57. package/dist/core/session-compaction.d.ts.map +1 -1
  58. package/dist/core/session-compaction.js +14 -1
  59. package/dist/core/session-compaction.js.map +1 -1
  60. package/dist/core/session-compaction.test.js +5 -0
  61. package/dist/core/session-compaction.test.js.map +1 -1
  62. package/dist/core/session-manager.d.ts +33 -2
  63. package/dist/core/session-manager.d.ts.map +1 -1
  64. package/dist/core/session-manager.js +73 -2
  65. package/dist/core/session-manager.js.map +1 -1
  66. package/dist/core/session-manager.test.js +97 -2
  67. package/dist/core/session-manager.test.js.map +1 -1
  68. package/dist/core/session-preview.d.ts +11 -0
  69. package/dist/core/session-preview.d.ts.map +1 -0
  70. package/dist/core/session-preview.js +50 -0
  71. package/dist/core/session-preview.js.map +1 -0
  72. package/dist/core/subagent-manager.d.ts +42 -7
  73. package/dist/core/subagent-manager.d.ts.map +1 -1
  74. package/dist/core/subagent-manager.js +346 -30
  75. package/dist/core/subagent-manager.js.map +1 -1
  76. package/dist/core/subagent-manager.test.js +195 -3
  77. package/dist/core/subagent-manager.test.js.map +1 -1
  78. package/dist/core/subagent-store.d.ts +20 -0
  79. package/dist/core/subagent-store.d.ts.map +1 -0
  80. package/dist/core/subagent-store.js +177 -0
  81. package/dist/core/subagent-store.js.map +1 -0
  82. package/dist/core/subagent-store.test.d.ts +2 -0
  83. package/dist/core/subagent-store.test.d.ts.map +1 -0
  84. package/dist/core/subagent-store.test.js +94 -0
  85. package/dist/core/subagent-store.test.js.map +1 -0
  86. package/dist/modes/agent-home-mode.d.ts.map +1 -1
  87. package/dist/modes/agent-home-mode.js +10 -3
  88. package/dist/modes/agent-home-mode.js.map +1 -1
  89. package/dist/modes/serve-mode.d.ts.map +1 -1
  90. package/dist/modes/serve-mode.js +10 -3
  91. package/dist/modes/serve-mode.js.map +1 -1
  92. package/dist/modes/subagent-worker-mode.d.ts +2 -0
  93. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  94. package/dist/modes/subagent-worker-mode.js +13 -3
  95. package/dist/modes/subagent-worker-mode.js.map +1 -1
  96. package/dist/tools/subagent-shared.d.ts +8 -0
  97. package/dist/tools/subagent-shared.d.ts.map +1 -1
  98. package/dist/tools/subagent-shared.js.map +1 -1
  99. package/dist/tools/subagent.d.ts +3 -8
  100. package/dist/tools/subagent.d.ts.map +1 -1
  101. package/dist/tools/subagent.js +10 -8
  102. package/dist/tools/subagent.js.map +1 -1
  103. package/dist/tools/subagent.test.js +17 -1
  104. package/dist/tools/subagent.test.js.map +1 -1
  105. package/dist/tools/tasks.d.ts +1 -1
  106. package/dist/tools/web-fetch.d.ts +1 -1
  107. package/dist/ui/App.d.ts +7 -3
  108. package/dist/ui/App.d.ts.map +1 -1
  109. package/dist/ui/App.js +66 -83
  110. package/dist/ui/App.js.map +1 -1
  111. package/dist/ui/components/Footer.d.ts +1 -0
  112. package/dist/ui/components/Footer.d.ts.map +1 -1
  113. package/dist/ui/components/Footer.js +3 -3
  114. package/dist/ui/components/Footer.js.map +1 -1
  115. package/dist/ui/components/Footer.test.d.ts +2 -0
  116. package/dist/ui/components/Footer.test.d.ts.map +1 -0
  117. package/dist/ui/components/Footer.test.js +16 -0
  118. package/dist/ui/components/Footer.test.js.map +1 -0
  119. package/dist/ui/components/SubAgentPanel.d.ts +2 -0
  120. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  121. package/dist/ui/components/SubAgentPanel.js +4 -2
  122. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  123. package/dist/ui/hooks/useAgentLoop.d.ts +8 -3
  124. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  125. package/dist/ui/hooks/useAgentLoop.js +64 -8
  126. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  127. package/dist/ui/hooks/useSessionPersistence.d.ts +4 -2
  128. package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -1
  129. package/dist/ui/hooks/useSessionPersistence.js +10 -1
  130. package/dist/ui/hooks/useSessionPersistence.js.map +1 -1
  131. package/dist/ui/hooks/useTerminalTitle.d.ts +3 -3
  132. package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
  133. package/dist/ui/hooks/useTerminalTitle.js +5 -9
  134. package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
  135. package/dist/ui/login.d.ts.map +1 -1
  136. package/dist/ui/login.js +5 -1
  137. package/dist/ui/login.js.map +1 -1
  138. package/dist/ui/render.d.ts +7 -2
  139. package/dist/ui/render.d.ts.map +1 -1
  140. package/dist/ui/render.js +4 -4
  141. package/dist/ui/render.js.map +1 -1
  142. package/dist/ui/render.test.js +10 -1
  143. package/dist/ui/render.test.js.map +1 -1
  144. package/dist/ui/terminal-history.js +6 -2
  145. package/dist/ui/terminal-history.js.map +1 -1
  146. package/dist/ui/terminal-history.test.js +2 -2
  147. package/dist/ui/terminal-history.test.js.map +1 -1
  148. package/dist/ui/tui-history-parity.test.js +8 -1
  149. package/dist/ui/tui-history-parity.test.js.map +1 -1
  150. package/dist/utils/git.d.ts +2 -0
  151. package/dist/utils/git.d.ts.map +1 -1
  152. package/dist/utils/git.js +12 -0
  153. package/dist/utils/git.js.map +1 -1
  154. package/dist/utils/git.test.d.ts +2 -0
  155. package/dist/utils/git.test.d.ts.map +1 -0
  156. package/dist/utils/git.test.js +36 -0
  157. package/dist/utils/git.test.js.map +1 -0
  158. package/package.json +4 -4
  159. package/dist/utils/session-title.d.ts +0 -19
  160. package/dist/utils/session-title.d.ts.map +0 -1
  161. package/dist/utils/session-title.js +0 -82
  162. package/dist/utils/session-title.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { agentLoop, isAbortError } from "@kenkaiiii/gg-agent";
1
+ import { agentLoop, isAbortError, } from "@kenkaiiii/gg-agent";
2
2
  import { ProviderError, } from "@kenkaiiii/gg-ai";
3
3
  import { EventBus } from "./event-bus.js";
4
4
  import { SlashCommandRegistry, createBuiltinCommands, } from "./slash-commands.js";
@@ -11,11 +11,12 @@ import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.js";
11
11
  import { SessionManager, KEN_TURN_CUSTOM_KIND, AUTOPILOT_MARKER_CUSTOM_KIND, APP_MARKER_CUSTOM_KIND, } from "./session-manager.js";
12
12
  import { ExtensionLoader } from "./extensions/loader.js";
13
13
  import { shouldCompact, compact, getCompactionReserveTokens } from "./compaction/compactor.js";
14
- import { getAuthStorageKeys, getContextWindow, getModel, MODELS } from "./model-registry.js";
14
+ import { getAuthStorageKeys, getContextWindow, getModel, getToolResultCharLimit, MODELS, } from "./model-registry.js";
15
15
  import { discoverSkills } from "./skills.js";
16
16
  import { ensureAppDirs } from "../config.js";
17
17
  import { buildSystemPrompt } from "../system-prompt.js";
18
18
  import { createTools, createWebSearchTool, } from "../tools/index.js";
19
+ import { buildSubAgentCompletionFollowUp } from "./subagent-manager.js";
19
20
  import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
20
21
  import { MCPClientManager, getAllMcpServers } from "./mcp/index.js";
21
22
  import { DeferredToolCatalog } from "./mcp/deferred-catalog.js";
@@ -23,16 +24,22 @@ import { createToolSearchTool } from "../tools/tool-search.js";
23
24
  import { log } from "./logger.js";
24
25
  import { setEstimatorModel } from "./compaction/token-estimator.js";
25
26
  import { discoverAgents } from "./agents.js";
26
- import { generateSessionTitle } from "../utils/session-title.js";
27
27
  import { enhancePrompt } from "../utils/prompt-enhancer.js";
28
28
  import { detectProjectStack } from "./language-detector.js";
29
- import { evaluateIdealReview, buildIdealReviewMessage, detectTestDrift, } from "./ideal-review.js";
29
+ import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageMessage, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
30
30
  import { evaluateLoopBreak, buildLoopBreakMessage, ToolCallProgressTracker, detectTextRepetition, } from "./loop-breaker.js";
31
31
  import { buildRegroundingMessage } from "./regrounding.js";
32
32
  import { wrapSteeringText, STEERING_PREFIX } from "./steering.js";
33
+ import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
33
34
  import crypto from "node:crypto";
34
35
  import fs from "node:fs/promises";
35
36
  import path from "node:path";
37
+ // ── Tool-result policy ─────────────────────────────────────
38
+ /** Resolve the per-result cap passed to the agent loop for the active transport. */
39
+ export function resolveSessionToolResultCharLimit(model, provider, accountId) {
40
+ return (getToolResultCharLimit(model, { provider, accountId }) ??
41
+ Math.floor(getContextWindow(model, { provider, accountId }) * 3.5 * 0.3));
42
+ }
36
43
  // ── Agent Session ──────────────────────────────────────────
37
44
  export class AgentSession {
38
45
  eventBus = new EventBus();
@@ -59,6 +66,7 @@ export class AgentSession {
59
66
  // display only, persisted + reloaded so a resumed session shows the same
60
67
  // transcript rows the live run showed.
61
68
  appMarkers = [];
69
+ turnMetrics = [];
62
70
  tools = [];
63
71
  /** Rebuilds the read tool for a new model (video byte cap is baked in at
64
72
  * creation). Called from switchModel so video-capable models get the
@@ -84,7 +92,8 @@ export class AgentSession {
84
92
  hookProgressTracker = new ToolCallProgressTracker();
85
93
  hookFileEditCounts = new Map();
86
94
  hookToolCalls = new Map();
87
- idealReviewInjected = false;
95
+ idealReviewPhase = "idle";
96
+ reviewCoverage;
88
97
  loopBreakInjected = false;
89
98
  regroundingInjected = false;
90
99
  compactionOccurred = false;
@@ -125,6 +134,10 @@ export class AgentSession {
125
134
  * model emits step-completion markers the UI's plan-progress widget reads. */
126
135
  approvedPlanPath;
127
136
  sessionId = "";
137
+ /** Stable identity shared by compaction and approved-plan checkpoint files. */
138
+ conversationId = "";
139
+ /** Original user-authored prompt, retained when internal messages replace history. */
140
+ sessionPreview = "";
128
141
  /** Runtime conversation identity for provider transport headers. Transient
129
142
  * children need one even though they intentionally have no persisted session. */
130
143
  transportSessionId = crypto.randomUUID();
@@ -138,6 +151,7 @@ export class AgentSession {
138
151
  this.provider = options.provider;
139
152
  this.model = options.model;
140
153
  this.cwd = options.cwd;
154
+ this.reviewCoverage = new ReviewCoverageTracker(this.cwd);
141
155
  this.baseUrl = options.baseUrl;
142
156
  this.maxTokens = this.resolveMaxTokens(options.model);
143
157
  this.thinkingLevel = options.thinkingLevel;
@@ -203,6 +217,12 @@ export class AgentSession {
203
217
  model: this.model,
204
218
  lspDiagnostics: this.settingsManager.get("lspDiagnostics"),
205
219
  authStorage: this.authStorage,
220
+ onFileRead: (filePath) => this.reviewCoverage.recordRead(filePath),
221
+ onFileMutated: (filePath) => {
222
+ const relative = path.relative(this.cwd, filePath) || path.basename(filePath);
223
+ this.hookFileEditCounts.set(relative, (this.hookFileEditCounts.get(relative) ?? 0) + 1);
224
+ this.reviewCoverage.recordChanged(filePath);
225
+ },
206
226
  // Lazy — sessionId/model/provider can change after createTools() runs, so
207
227
  // sub-agent spawns read the current parent state at execution time.
208
228
  getProvider: () => this.provider,
@@ -273,6 +293,8 @@ export class AgentSession {
273
293
  else {
274
294
  await this.createNewSession();
275
295
  }
296
+ if (this.sessionId)
297
+ await this.subAgentManager?.hydrate(this.sessionId);
276
298
  // GG Coder owns its command registry. Other agents start with an isolated
277
299
  // empty registry and can register their own commands in their own file.
278
300
  if (this.opts.coderSlashCommands !== false) {
@@ -567,7 +589,8 @@ export class AgentSession {
567
589
  this.hookProgressTracker.reset();
568
590
  this.hookFileEditCounts.clear();
569
591
  this.hookToolCalls.clear();
570
- this.idealReviewInjected = false;
592
+ this.reviewCoverage.reset();
593
+ this.idealReviewPhase = "idle";
571
594
  this.loopBreakInjected = false;
572
595
  this.regroundingInjected = false;
573
596
  this.compactionOccurred = false;
@@ -578,7 +601,7 @@ export class AgentSession {
578
601
  * the same signals the TUI's useAgentLoop collects, so the loop-break and
579
602
  * ideal-review decisions match across the CLI and the app.
580
603
  */
581
- trackHookEvent(event) {
604
+ async trackHookEvent(event) {
582
605
  switch (event.type) {
583
606
  case "text_delta":
584
607
  this.hookText += event.text;
@@ -601,13 +624,6 @@ export class AgentSession {
601
624
  this.hookStats.bashCalls += 1;
602
625
  this.hookConsecutiveFailures = event.isError ? this.hookConsecutiveFailures + 1 : 0;
603
626
  this.hookRepeatedNoProgressCalls = this.hookProgressTracker.record(name, args, event.result, event.isError);
604
- if ((name === "edit" || name === "write") && args) {
605
- const filePath = args.file_path;
606
- if (typeof filePath === "string") {
607
- const fileNext = (this.hookFileEditCounts.get(filePath) ?? 0) + 1;
608
- this.hookFileEditCounts.set(filePath, fileNext);
609
- }
610
- }
611
627
  if (name === "edit" && !event.isError) {
612
628
  const diff = event.details?.diff ?? event.result;
613
629
  const added = (diff.match(/^\+[^+]/gm) ?? []).length;
@@ -618,6 +634,7 @@ export class AgentSession {
618
634
  }
619
635
  case "turn_end":
620
636
  this.hookStats.turns = event.turn;
637
+ await this.persistTurnMetric(event);
621
638
  break;
622
639
  }
623
640
  }
@@ -677,15 +694,35 @@ export class AgentSession {
677
694
  return null;
678
695
  }
679
696
  /**
680
- * Pre-stop follow-up hook: runs the ideal review once, when the agent would
681
- * otherwise finish and the change set is substantial enough to warrant it.
697
+ * Pre-stop Ideal review phase machine. Once review starts, completion is
698
+ * blocked until harness-owned post-injection reads cover every changed file.
682
699
  */
683
700
  getHookFollowUpMessages() {
701
+ const childCompletionFollowUp = buildSubAgentCompletionFollowUp(this.subAgentManager);
702
+ if (childCompletionFollowUp)
703
+ return childCompletionFollowUp;
684
704
  if (this.opts.selfCorrectionHooks === false)
685
705
  return null;
686
- if (!this.settingsManager.get("idealReviewEnabled"))
706
+ if (this.idealReviewPhase === "reviewing") {
707
+ const coverage = this.reviewCoverage.evidence();
708
+ const lspEvidence = this.reviewLspEvidence(coverage.expected);
709
+ log("INFO", "ideal", "Ideal review coverage check", {
710
+ covered: coverage.covered,
711
+ missing: coverage.missing,
712
+ lspLowConfidence: lspEvidence.lowConfidence,
713
+ lspMissing: lspEvidence.missing,
714
+ });
715
+ if (coverage.missing.length > 0) {
716
+ return [
717
+ this.withReviewLspEvidence(buildReviewCoverageMessage(coverage.missing), lspEvidence),
718
+ ];
719
+ }
720
+ this.idealReviewPhase = "complete";
721
+ return null;
722
+ }
723
+ if (this.idealReviewPhase === "complete")
687
724
  return null;
688
- if (this.idealReviewInjected)
725
+ if (!this.settingsManager.get("idealReviewEnabled"))
689
726
  return null;
690
727
  const decision = evaluateIdealReview(this.hookStats);
691
728
  // Test drift fires the review even on a small change the score would skip:
@@ -693,9 +730,50 @@ export class AgentSession {
693
730
  const driftedFiles = detectTestDrift(this.hookFileEditCounts.keys(), this.cwd).slice(0, 5);
694
731
  if (!decision.shouldReview && driftedFiles.length === 0)
695
732
  return null;
696
- this.idealReviewInjected = true;
697
- this.eventBus.emit("hook", { kind: "ideal" });
698
- return [buildIdealReviewMessage(decision.reasons, driftedFiles)];
733
+ this.reviewCoverage.start(this.hookFileEditCounts.keys());
734
+ this.idealReviewPhase = "reviewing";
735
+ const coverage = this.reviewCoverage.evidence();
736
+ const lspEvidence = this.reviewLspEvidence(coverage.expected);
737
+ this.eventBus.emit("hook", {
738
+ kind: "ideal",
739
+ coverageExpected: coverage.expected,
740
+ coverageMissing: coverage.missing,
741
+ });
742
+ log("INFO", "ideal", "Injecting ideal review before final response", {
743
+ coverageExpected: coverage.expected,
744
+ coverageMissing: coverage.missing,
745
+ lspLowConfidence: lspEvidence.lowConfidence,
746
+ lspMissing: lspEvidence.missing,
747
+ });
748
+ return [
749
+ this.withReviewLspEvidence(buildIdealReviewMessage(decision.reasons, driftedFiles), lspEvidence),
750
+ ];
751
+ }
752
+ reviewLspEvidence(files) {
753
+ const lowConfidence = [];
754
+ const missing = [];
755
+ for (const filePath of files) {
756
+ const outcome = this.lspManager?.getLatestOutcome(filePath);
757
+ if (outcome?.kind === "low_confidence")
758
+ lowConfidence.push(filePath);
759
+ else if (outcome?.kind !== "clean" && outcome?.kind !== "diagnostics")
760
+ missing.push(filePath);
761
+ }
762
+ return { lowConfidence, missing };
763
+ }
764
+ withReviewLspEvidence(message, evidence) {
765
+ if (evidence.lowConfidence.length === 0 && evidence.missing.length === 0)
766
+ return message;
767
+ const notes = [
768
+ ...(evidence.lowConfidence.length > 0
769
+ ? [`Diagnostics are low confidence while indexing: ${evidence.lowConfidence.join(", ")}.`]
770
+ : []),
771
+ ...(evidence.missing.length > 0
772
+ ? [`Diagnostics evidence is unavailable or missing: ${evidence.missing.join(", ")}.`]
773
+ : []),
774
+ "Do not describe those files as compiler-clean without other evidence.",
775
+ ];
776
+ return { role: "user", content: `${String(message.content)}\n\n${notes.join(" ")}` };
699
777
  }
700
778
  /** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
701
779
  async runLoop() {
@@ -779,9 +857,9 @@ export class AgentSession {
779
857
  supportsImages: modelInfo?.supportsImages,
780
858
  supportsVideo: modelInfo?.supportsVideo,
781
859
  userAgent,
782
- // clearToolUses disabled causes model to output unsolicited context summaries
783
- // Single tool result shouldn't exceed 30% of context window (in chars)
784
- maxToolResultChars: Math.floor(getContextWindow(this.model, { provider: this.provider, accountId }) * 3.5 * 0.3),
860
+ // Codex caps each tool output at 10K tokens. Other transports retain the
861
+ // generic 30%-of-context allowance used before this provider policy.
862
+ maxToolResultChars: resolveSessionToolResultCharLimit(this.model, this.provider, accountId),
785
863
  // Self-correction hooks (same as the TUI): loop-break + re-grounding are
786
864
  // polled mid-loop; the ideal review is polled when the agent would stop.
787
865
  getSteeringMessages: () => this.getHookSteeringMessages(),
@@ -807,7 +885,7 @@ export class AgentSession {
807
885
  },
808
886
  });
809
887
  for await (const event of generator) {
810
- this.trackHookEvent(event);
888
+ await this.trackHookEvent(event);
811
889
  this.eventBus.forwardAgentEvent(event);
812
890
  }
813
891
  };
@@ -967,9 +1045,14 @@ export class AgentSession {
967
1045
  else {
968
1046
  // Persist compacted messages to a new session file so `ggcoder continue`
969
1047
  // picks up the compacted state instead of the full original history.
970
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model);
1048
+ const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1049
+ conversationId: this.conversationId || undefined,
1050
+ preview: this.sessionPreview || undefined,
1051
+ });
971
1052
  this.sessionId = session.id;
1053
+ this.conversationId = session.header.conversationId ?? session.id;
972
1054
  this.sessionPath = session.path;
1055
+ await this.subAgentManager?.rebindParentSession(this.sessionId);
973
1056
  // Write compacted messages (skip system — it's rebuilt on load)
974
1057
  for (const msg of this.messages) {
975
1058
  if (msg.role === "system")
@@ -977,7 +1060,8 @@ export class AgentSession {
977
1060
  await this.persistMessage(msg);
978
1061
  }
979
1062
  this.lastPersistedIndex = this.messages.length;
980
- // Carry Ken's advisory turns into the new file so they survive compaction.
1063
+ // Carry evidence records into the new file so they survive compaction.
1064
+ await this.rePersistTurnMetrics();
981
1065
  await this.rePersistKenTurns();
982
1066
  await this.rePersistAutopilotMarkers();
983
1067
  await this.rePersistAppMarkers();
@@ -993,7 +1077,13 @@ export class AgentSession {
993
1077
  newCount: result.result.newCount,
994
1078
  });
995
1079
  }
996
- async newSession() {
1080
+ async newSession(preserveConversation = false) {
1081
+ // Approved-plan execution is a clean checkpoint of the same conversation;
1082
+ // explicit new sessions reset the conversation identity.
1083
+ if (!preserveConversation) {
1084
+ this.conversationId = "";
1085
+ this.sessionPreview = "";
1086
+ }
997
1087
  // A fresh session drops any in-flight plan state so its prompt is clean.
998
1088
  this.planModeRef.current = false;
999
1089
  this.approvedPlanPath = undefined;
@@ -1004,6 +1094,7 @@ export class AgentSession {
1004
1094
  this.kenTurns = [];
1005
1095
  this.autopilotMarkers = [];
1006
1096
  this.appMarkers = [];
1097
+ this.turnMetrics = [];
1007
1098
  const basePrompt = this.customSystemPrompt ??
1008
1099
  (await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider));
1009
1100
  this.baseSystemPrompt = basePrompt;
@@ -1018,16 +1109,21 @@ export class AgentSession {
1018
1109
  // polluted the project's session list.
1019
1110
  if (this.opts.transient) {
1020
1111
  this.sessionId = "";
1112
+ this.conversationId = "";
1113
+ this.sessionPreview = "";
1021
1114
  this.sessionPath = "";
1022
1115
  this.lastPersistedIndex = this.messages.length;
1023
1116
  }
1024
1117
  else {
1025
1118
  await this.createNewSession();
1119
+ await this.subAgentManager?.resetParentSession(this.sessionId);
1026
1120
  }
1027
1121
  this.eventBus.emit("session_start", { sessionId: this.sessionId });
1028
1122
  }
1029
1123
  async loadSession(sessionPath) {
1030
1124
  await this.loadExistingSession(sessionPath);
1125
+ if (this.sessionId)
1126
+ await this.subAgentManager?.hydrate(this.sessionId);
1031
1127
  this.eventBus.emit("session_start", { sessionId: this.sessionId });
1032
1128
  }
1033
1129
  /**
@@ -1197,6 +1293,39 @@ export class AgentSession {
1197
1293
  getMessages() {
1198
1294
  return this.messages;
1199
1295
  }
1296
+ getTurnMetrics() {
1297
+ return this.turnMetrics.map((metric) => ({
1298
+ ...metric,
1299
+ usage: { ...metric.usage },
1300
+ timing: { ...metric.timing },
1301
+ cost: { ...metric.cost },
1302
+ }));
1303
+ }
1304
+ async persistTurnMetric(event) {
1305
+ const payload = {
1306
+ version: 1,
1307
+ turn: event.turn,
1308
+ provider: this.provider,
1309
+ model: this.model,
1310
+ stopReason: event.stopReason,
1311
+ usage: { ...event.usage },
1312
+ timing: { ...event.timing },
1313
+ cost: {
1314
+ status: "unavailable",
1315
+ reason: "No authoritative effective-dated provider pricing is available",
1316
+ },
1317
+ };
1318
+ this.turnMetrics.push(payload);
1319
+ if (this.sessionPath)
1320
+ await this.sessionManager.appendTurnMetric(this.sessionPath, payload);
1321
+ }
1322
+ async rePersistTurnMetrics() {
1323
+ if (!this.sessionPath)
1324
+ return;
1325
+ for (const metric of this.turnMetrics) {
1326
+ await this.sessionManager.appendTurnMetric(this.sessionPath, metric);
1327
+ }
1328
+ }
1200
1329
  /** Ken Kai (mentor) turns recorded against this session, in record order. Used
1201
1330
  * by the host to interleave Ken's advisory exchanges back into the transcript
1202
1331
  * on resume. Never part of the LLM message history. */
@@ -1351,46 +1480,12 @@ export class AgentSession {
1351
1480
  await this.sessionManager.appendEntry(this.sessionPath, entry);
1352
1481
  }
1353
1482
  }
1354
- /**
1355
- * Generate a short LLM session title from the conversation so far (first user
1356
- * message + first assistant reply). Best-effort; returns null on failure or
1357
- * when there's no user message yet. Uses the cheapest model for the provider.
1358
- */
1359
- async generateTitle() {
1360
- const extractText = (content) => typeof content === "string"
1361
- ? content
1362
- : content
1363
- .map((c) => c.type === "text" && "text" in c && typeof c.text === "string" ? c.text : "")
1364
- .join(" ");
1365
- const userMsg = this.messages.find((m) => m.role === "user");
1366
- const assistantMsg = this.messages.find((m) => m.role === "assistant");
1367
- const userText = userMsg ? extractText(userMsg.content) : "";
1368
- if (!userText.trim())
1369
- return null;
1370
- try {
1371
- const creds = await this.authStorage.resolveCredentials(this.provider, {
1372
- storageKeys: this.currentAuthStorageKeys(),
1373
- });
1374
- const title = await generateSessionTitle({
1375
- provider: this.provider,
1376
- userMessage: userText,
1377
- assistantPreview: assistantMsg ? extractText(assistantMsg.content).slice(0, 200) : "",
1378
- apiKey: creds.accessToken,
1379
- baseUrl: this.baseUrl ?? creds.baseUrl,
1380
- accountId: creds.accountId,
1381
- });
1382
- return title || null;
1383
- }
1384
- catch {
1385
- return null;
1386
- }
1387
- }
1388
1483
  /**
1389
1484
  * Rewrite a draft prompt into a tighter, terminology-correct version using
1390
1485
  * the ACTIVE provider/model. A stateless one-off LLM call (no agent loop, no
1391
1486
  * tools, no session mutation) — safe to run even mid-run. Returns the plain
1392
1487
  * enhanced text plus typed segments marking each corrected term. Errors throw
1393
- * so the caller can surface them (unlike best-effort title generation).
1488
+ * so the caller can surface them.
1394
1489
  */
1395
1490
  async enhancePrompt(text) {
1396
1491
  if (!text.trim())
@@ -1488,8 +1583,12 @@ export class AgentSession {
1488
1583
  }
1489
1584
  // ── Private ────────────────────────────────────────────
1490
1585
  async createNewSession() {
1491
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model);
1586
+ const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1587
+ conversationId: this.conversationId || undefined,
1588
+ preview: this.sessionPreview || undefined,
1589
+ });
1492
1590
  this.sessionId = session.id;
1591
+ this.conversationId = session.header.conversationId ?? session.id;
1493
1592
  this.sessionPath = session.path;
1494
1593
  this.lastPersistedIndex = this.messages.length;
1495
1594
  }
@@ -1497,6 +1596,15 @@ export class AgentSession {
1497
1596
  const loaded = await this.sessionManager.load(sessionPath);
1498
1597
  // Use the leaf from the header to walk the correct branch
1499
1598
  const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
1599
+ this.conversationId = loaded.header.conversationId ?? loaded.header.id;
1600
+ const legacyLabel = [...loaded.entries]
1601
+ .reverse()
1602
+ .find((entry) => entry.type === "label")
1603
+ ?.label.replace(/\s+/g, " ")
1604
+ .trim()
1605
+ .slice(0, 80);
1606
+ this.sessionPreview =
1607
+ legacyLabel || loaded.header.preview || findUserSessionPrompt(loadedMessages);
1500
1608
  // Restore Ken's advisory turns (custom entries, not on the message branch) so
1501
1609
  // they reappear in the transcript and survive into the continuation file.
1502
1610
  this.kenTurns = this.sessionManager.getKenTurns(loaded.entries);
@@ -1504,6 +1612,7 @@ export class AgentSession {
1504
1612
  this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries);
1505
1613
  // Restore app transcript markers (plan banner / task header / errors / hints).
1506
1614
  this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries);
1615
+ this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
1507
1616
  // Track the current leaf for subsequent entries
1508
1617
  this.currentLeafId = loaded.header.leafId;
1509
1618
  // Rebuild messages: keep system, add loaded
@@ -1523,6 +1632,7 @@ export class AgentSession {
1523
1632
  accountId: creds.accountId,
1524
1633
  });
1525
1634
  if (shouldCompact(this.messages, contextWindow, 0.8, undefined, getCompactionReserveTokens(this.maxTokens))) {
1635
+ await this.subAgentManager?.hydrate(loaded.header.id);
1526
1636
  log("INFO", "session", `Restored session exceeds context — auto-compacting`);
1527
1637
  const compacted = await compact(this.messages, {
1528
1638
  provider: this.provider,
@@ -1543,9 +1653,14 @@ export class AgentSession {
1543
1653
  // what's in memory — fork a fresh session file for the compacted state
1544
1654
  // (mirrors compact()'s own persistence) so `ggcoder continue` picks up
1545
1655
  // the summary instead of the full original transcript.
1546
- const session = await this.sessionManager.create(this.cwd, this.provider, this.model);
1656
+ const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
1657
+ conversationId: this.conversationId || undefined,
1658
+ preview: this.sessionPreview || undefined,
1659
+ });
1547
1660
  this.sessionId = session.id;
1661
+ this.conversationId = session.header.conversationId ?? session.id;
1548
1662
  this.sessionPath = session.path;
1663
+ await this.subAgentManager?.rebindParentSession(this.sessionId);
1549
1664
  this.currentLeafId = null;
1550
1665
  // Re-persist (compacted) messages — skip system, it's rebuilt on load
1551
1666
  for (const msg of this.messages) {
@@ -1554,7 +1669,8 @@ export class AgentSession {
1554
1669
  await this.persistMessage(msg);
1555
1670
  }
1556
1671
  this.lastPersistedIndex = this.messages.length;
1557
- // Carry Ken's restored turns into the continuation file.
1672
+ // Carry restored evidence into the continuation file.
1673
+ await this.rePersistTurnMetrics();
1558
1674
  await this.rePersistKenTurns();
1559
1675
  await this.rePersistAutopilotMarkers();
1560
1676
  await this.rePersistAppMarkers();
@@ -1578,6 +1694,9 @@ export class AgentSession {
1578
1694
  return this.messages;
1579
1695
  }
1580
1696
  async persistMessage(message) {
1697
+ if (!this.sessionPreview && message.role === "user") {
1698
+ this.sessionPreview = getUserSessionPrompt(message.content) ?? "";
1699
+ }
1581
1700
  // Transient sessions (subagent spawns) have no session file — skip.
1582
1701
  if (!this.sessionPath)
1583
1702
  return;