@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.
- package/dist/app-sidecar.js +7 -0
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/agent-session.d.ts +4 -0
- package/dist/core/agent-session.js +96 -26
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/event-bus.d.ts +4 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/ken-context.d.ts +5 -0
- package/dist/core/ken-context.js +9 -2
- package/dist/core/ken-context.js.map +1 -1
- package/dist/core/lsp/manager.d.ts +3 -1
- package/dist/core/lsp/manager.js +12 -3
- package/dist/core/lsp/manager.js.map +1 -1
- package/dist/core/verification-evidence.d.ts +2 -0
- package/dist/core/verification-evidence.js +17 -2
- package/dist/core/verification-evidence.js.map +1 -1
- package/dist/core/verification-gate.d.ts +5 -0
- package/dist/core/verification-gate.js +29 -0
- package/dist/core/verification-gate.js.map +1 -1
- package/dist/core/verification-snapshot.d.ts +7 -0
- package/dist/core/verification-snapshot.js +111 -0
- package/dist/core/verification-snapshot.js.map +1 -0
- package/package.json +4 -4
|
@@ -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
|
-
|
|
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, {
|
|
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
|
-
|
|
1168
|
-
|
|
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("
|
|
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("
|
|
1706
|
+
this.eventBus.emit("diagnostics", { text: diagnosticText });
|
|
1684
1707
|
this.refreshVerificationArmed();
|
|
1685
|
-
|
|
1686
|
-
|
|
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."
|