@kenkaiiii/ggcoder 5.59.0 → 5.59.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.
@@ -7,6 +7,7 @@ import type { BackgroundProcess } from "./process-manager.js";
7
7
  import type { MCPElicitHandler } from "./mcp/index.js";
8
8
  import { type ImportForeignTranscriptResult } from "./foreign-session-import.js";
9
9
  import { type EnhanceResult } from "../utils/prompt-enhancer.js";
10
+ import { type VerificationEvidence } from "./verification-evidence.js";
10
11
  /** A chat attachment (image / video / other file) prepared for the model. The
11
12
  * raw base64 `data` rides native blocks; `path` (when persisted to disk) lets
12
13
  * the agent's tools open the file directly. */
@@ -777,6 +778,9 @@ export declare class AgentSession {
777
778
  * instead of dropping the marker or falling back to a raw verdict string.
778
779
  * No-op persistence for transient sessions (kept in memory only).
779
780
  */
781
+ private recordFinishedBackgroundVerification;
782
+ getVerificationEvidence(): VerificationEvidence[];
783
+ private finishSnapshotVerification;
780
784
  getVerificationProblem(): string | null;
781
785
  private persistVerificationState;
782
786
  persistAutopilotMarker(phase: AutopilotMarkerPayload["phase"], extra?: {
@@ -49,6 +49,7 @@ import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from
49
49
  import { AgentNotificationQueue } from "./agent-notifications.js";
50
50
  import { VerificationGate, extractAddedLines, isCheckOwnFile, isCodeFilePath, VERIFICATION_STATE_KIND, isVerificationCommand, } from "./verification-gate.js";
51
51
  import { classifyVerificationCommand } from "./verification-evidence.js";
52
+ import { captureVerificationSnapshot } from "./verification-snapshot.js";
52
53
  import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
53
54
  import { normalizeMessageImages } from "./message-images.js";
54
55
  import crypto from "node:crypto";
@@ -1073,7 +1074,17 @@ export class AgentSession {
1073
1074
  // it poisoned the gate on green output and re-armed the hook into
1074
1075
  // every later question turn.
1075
1076
  const classification = classifyVerificationCommand(event.args.command);
1076
- this.verificationGate.requireFreshVerification(!classification.accepted && classification.mayMutate, event.args.command);
1077
+ if (classification.snapshotEligible && event.args.persist !== true) {
1078
+ const call = this.hookToolCalls.get(event.toolCallId);
1079
+ call.sourceSnapshot = await captureVerificationSnapshot(this.opts.cwd, [
1080
+ ...this.hookFileEditCounts.keys(),
1081
+ ]);
1082
+ if (call.sourceSnapshot === null)
1083
+ this.verificationGate.requireFreshVerification(true, event.args.command);
1084
+ }
1085
+ else {
1086
+ this.verificationGate.requireFreshVerification(!classification.accepted && classification.mayMutate, event.args.command);
1087
+ }
1077
1088
  await this.persistVerificationState();
1078
1089
  }
1079
1090
  break;
@@ -1133,20 +1144,32 @@ export class AgentSession {
1133
1144
  if (args && call && name === "bash") {
1134
1145
  const command = typeof args.command === "string" ? args.command : "";
1135
1146
  const classification = classifyVerificationCommand(command);
1136
- if (classification.accepted) {
1147
+ if (classification.accepted || classification.snapshotEligible) {
1137
1148
  if (args.run_in_background === true && !event.isError && args.persist !== true) {
1138
1149
  const id = /^ID:\s*(\S+)/m.exec(event.result)?.[1];
1139
1150
  // No parseable ID means the check cannot be tracked to a real exit
1140
1151
  // code — no evidence either way. Recording a FAILURE here made
1141
1152
  // every later green run of a different spelling look owed.
1142
1153
  if (id)
1143
- this.backgroundVerification.set(id, { revision: call.revision, command });
1154
+ this.backgroundVerification.set(id, {
1155
+ revision: call.revision,
1156
+ command,
1157
+ ...(classification.snapshotEligible
1158
+ ? { sourceSnapshot: call.sourceSnapshot ?? null }
1159
+ : {}),
1160
+ });
1161
+ else if (classification.snapshotEligible)
1162
+ this.verificationGate.requireFreshVerification(true, command);
1144
1163
  }
1145
1164
  else if (args.persist === true) {
1146
1165
  // Persistent-shell checks are not bounded evidence (steering can
1147
1166
  // interleave): neither a pass nor a failure. A recorded failure
1148
1167
  // here blocked approval for sessions that prefer the shell.
1149
1168
  }
1169
+ else if (classification.snapshotEligible) {
1170
+ await this.finishSnapshotVerification({ command, revision: call.revision, sourceSnapshot: call.sourceSnapshot ?? null }, !event.isError && /^Exit code:\s*0(?:\s|$)/i.test(event.result.trim()));
1171
+ verificationChanged = true;
1172
+ }
1150
1173
  else {
1151
1174
  if (!event.isError && /^Exit code:\s*0(?:\s|$)/i.test(event.result.trim())) {
1152
1175
  this.verificationGate.recordVerification(call.revision, command);
@@ -1156,6 +1179,7 @@ export class AgentSession {
1156
1179
  }
1157
1180
  verificationChanged = true;
1158
1181
  }
1182
+ delete call.sourceSnapshot;
1159
1183
  }
1160
1184
  else if (classification.candidate) {
1161
1185
  // Green but untrusted: remember WHY so the demand can tell the
@@ -1164,18 +1188,8 @@ export class AgentSession {
1164
1188
  }
1165
1189
  }
1166
1190
  if (!event.isError && args && name === "task_output" && typeof args.id === "string") {
1167
- const started = this.backgroundVerification.get(args.id);
1168
- const proc = this.processManager?.list().find((p) => p.id === args.id);
1169
- if (started && proc && proc.exitCode !== null) {
1170
- if (proc.exitCode === 0) {
1171
- this.verificationGate.recordVerification(started.revision, started.command);
1172
- }
1173
- else {
1174
- this.verificationGate.recordFailedVerification(started.command, started.revision);
1175
- }
1176
- this.backgroundVerification.delete(args.id);
1177
- verificationChanged = true;
1178
- }
1191
+ verificationChanged =
1192
+ (await this.recordFinishedBackgroundVerification(args.id)) || verificationChanged;
1179
1193
  }
1180
1194
  if (verificationChanged)
1181
1195
  await this.persistVerificationState();
@@ -1266,9 +1280,9 @@ export class AgentSession {
1266
1280
  // it in the very next turn instead of discovering it at the pre-stop
1267
1281
  // completion gate — but it never displaces user steering, which rides out
1268
1282
  // in the same batch when both are pending.
1269
- const diagnosticText = this.lspManager?.drainDiagnostics(this.getVerificationProblem() !== null);
1283
+ const diagnosticText = this.lspManager?.drainDiagnostics(this.getVerificationProblem() !== null, { deferUnverified: true });
1270
1284
  if (diagnosticText)
1271
- this.eventBus.emit("hook", { kind: "verification" });
1285
+ this.eventBus.emit("diagnostics", { text: diagnosticText });
1272
1286
  this.refreshVerificationArmed();
1273
1287
  const notified = this.notifications.drain();
1274
1288
  const notificationMessage = notified.length > 0 || diagnosticText
@@ -1673,6 +1687,15 @@ export class AgentSession {
1673
1687
  * blocked until harness-owned post-injection reads cover every changed file.
1674
1688
  */
1675
1689
  async getHookFollowUpMessages() {
1690
+ // Exit notifications and task_output refer to the same host process record.
1691
+ // Do not force an extra polling tool/turn merely to acknowledge a known exit.
1692
+ let backgroundChanged = false;
1693
+ for (const id of this.backgroundVerification.keys()) {
1694
+ backgroundChanged =
1695
+ (await this.recordFinishedBackgroundVerification(id)) || backgroundChanged;
1696
+ }
1697
+ if (backgroundChanged)
1698
+ await this.persistVerificationState();
1676
1699
  // Edits return immediately; only the completion boundary waits for remaining
1677
1700
  // checks. Queued timeouts stay explicitly unverified, never a false all-clear.
1678
1701
  await this.lspManager?.flushDiagnostics(this.opts.signal);
@@ -1680,20 +1703,20 @@ export class AgentSession {
1680
1703
  return null;
1681
1704
  const diagnosticText = this.lspManager?.drainDiagnostics(this.getVerificationProblem() !== null);
1682
1705
  if (diagnosticText)
1683
- this.eventBus.emit("hook", { kind: "verification" });
1706
+ this.eventBus.emit("diagnostics", { text: diagnosticText });
1684
1707
  this.refreshVerificationArmed();
1685
- if (diagnosticText) {
1686
- return [
1708
+ const diagnosticMessages = diagnosticText
1709
+ ? [
1687
1710
  {
1688
1711
  role: "user",
1689
1712
  content: buildNotificationSteeringText([diagnosticText]),
1690
1713
  provenance: { source: "runtime", kind: "notification", visibility: "hidden" },
1691
1714
  },
1692
- ];
1693
- }
1715
+ ]
1716
+ : [];
1694
1717
  const childCompletionFollowUp = buildSubAgentCompletionFollowUp(this.subAgentManager);
1695
1718
  if (childCompletionFollowUp)
1696
- return childCompletionFollowUp;
1719
+ return [...diagnosticMessages, ...childCompletionFollowUp];
1697
1720
  // Background processes started this run and never read block completion:
1698
1721
  // their progress/exit checkpoints only land on the steering path, which an
1699
1722
  // agent about to stop never reaches.
@@ -1703,7 +1726,7 @@ export class AgentSession {
1703
1726
  log("INFO", "process-gate", "Injecting background-process completion gate", {
1704
1727
  injected: String(this.processGateInjected),
1705
1728
  });
1706
- return processFollowUp;
1729
+ return [...diagnosticMessages, ...processFollowUp];
1707
1730
  }
1708
1731
  // Verification gate: code was edited but nothing verified since the last
1709
1732
  // edit. Above the Ideal review so checks RUN before the read-based review
@@ -1727,9 +1750,13 @@ export class AgentSession {
1727
1750
  : {}),
1728
1751
  });
1729
1752
  this.refreshHookArming();
1730
- return verificationFollowUp;
1753
+ return [...diagnosticMessages, ...verificationFollowUp];
1731
1754
  }
1732
1755
  }
1756
+ // Address real errors before review; unavailable checks share the existing
1757
+ // verification demand above instead of manufacturing a separate hook.
1758
+ if (diagnosticMessages.length > 0)
1759
+ return diagnosticMessages;
1733
1760
  if (this.opts.selfCorrectionHooks === false || this.idealReviewSuppressed)
1734
1761
  return null;
1735
1762
  if (this.idealReviewPhase === "reviewing") {
@@ -1788,7 +1815,6 @@ export class AgentSession {
1788
1815
  // Independent reviewer first (async, bounded): its findings ride in the
1789
1816
  // SAME follow-up batch as the in-thread review + coverage requirements, so
1790
1817
  // addressing everything still costs one extra turn.
1791
- const independentMessages = await this.runIndependentReview(decision);
1792
1818
  this.reviewCoverage.start(this.hookFileEditCounts.keys());
1793
1819
  this.idealReviewPhase = "reviewing";
1794
1820
  const coverage = this.reviewCoverage.evidence();
@@ -1805,6 +1831,8 @@ export class AgentSession {
1805
1831
  // coverage is outstanding injects again. Disarm lands later, on the read
1806
1832
  // that closes the last gap (or when the retry budget escalates).
1807
1833
  this.refreshIdealReviewArmed();
1834
+ // Announce the phase before the reviewer starts, not after its bounded wait.
1835
+ const independentMessages = await this.runIndependentReview(decision);
1808
1836
  log("INFO", "ideal", "Injecting ideal review before final response", {
1809
1837
  coverageExpected: coverage.expected,
1810
1838
  coverageMissing: coverage.missing,
@@ -3111,7 +3139,49 @@ export class AgentSession {
3111
3139
  * instead of dropping the marker or falling back to a raw verdict string.
3112
3140
  * No-op persistence for transient sessions (kept in memory only).
3113
3141
  */
3142
+ async recordFinishedBackgroundVerification(id) {
3143
+ const started = this.backgroundVerification.get(id);
3144
+ const proc = this.processManager?.list().find((entry) => entry.id === id);
3145
+ if (!started || !proc || proc.exitCode === null)
3146
+ return false;
3147
+ if (started.sourceSnapshot !== undefined) {
3148
+ await this.finishSnapshotVerification(started, proc.exitCode === 0);
3149
+ }
3150
+ else if (proc.exitCode === 0) {
3151
+ this.verificationGate.recordVerification(started.revision, started.command);
3152
+ }
3153
+ else {
3154
+ this.verificationGate.recordFailedVerification(started.command, started.revision);
3155
+ }
3156
+ this.backgroundVerification.delete(id);
3157
+ return true;
3158
+ }
3159
+ getVerificationEvidence() {
3160
+ return this.verificationGate.evidence();
3161
+ }
3162
+ async finishSnapshotVerification(check, passed) {
3163
+ const after = check.sourceSnapshot
3164
+ ? await captureVerificationSnapshot(this.opts.cwd, [...this.hookFileEditCounts.keys()])
3165
+ : null;
3166
+ if (after === null || after !== check.sourceSnapshot) {
3167
+ this.verificationGate.requireFreshVerification(true, check.command);
3168
+ this.verificationGate.recordRejectedCheck(check.command, after === null
3169
+ ? "Workspace inputs could not be compared; run a read-only check after the build"
3170
+ : "Build changed workspace inputs; run checks against the changed source");
3171
+ if (!passed)
3172
+ this.verificationGate.recordFailedVerification(check.command);
3173
+ }
3174
+ else if (passed) {
3175
+ this.verificationGate.recordVerification(check.revision, check.command);
3176
+ }
3177
+ else {
3178
+ this.verificationGate.recordFailedVerification(check.command, check.revision);
3179
+ }
3180
+ }
3114
3181
  getVerificationProblem() {
3182
+ if ([...this.hookToolCalls.values()].some((call) => call.sourceSnapshot !== undefined)) {
3183
+ return "Unverified: a build is still running or its workspace comparison is pending.";
3184
+ }
3115
3185
  return (this.verificationGate.verificationProblem() ??
3116
3186
  (this.backgroundVerification.size > 0
3117
3187
  ? "Unverified: a background check is still running or its result has not been confirmed."