@kenkaiiii/ggcoder 5.6.0 → 5.6.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 +274 -53
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/agent-session-queue.test.d.ts +2 -0
- package/dist/core/agent-session-queue.test.d.ts.map +1 -0
- package/dist/core/agent-session-queue.test.js +122 -0
- package/dist/core/agent-session-queue.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +8 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autopilot-cycle.d.ts +67 -0
- package/dist/core/autopilot-cycle.d.ts.map +1 -0
- package/dist/core/autopilot-cycle.js +50 -0
- package/dist/core/autopilot-cycle.js.map +1 -0
- package/dist/core/autopilot-cycle.test.d.ts +2 -0
- package/dist/core/autopilot-cycle.test.d.ts.map +1 -0
- package/dist/core/autopilot-cycle.test.js +179 -0
- package/dist/core/autopilot-cycle.test.js.map +1 -0
- package/dist/core/autopilot-gate.d.ts +83 -0
- package/dist/core/autopilot-gate.d.ts.map +1 -0
- package/dist/core/autopilot-gate.js +96 -0
- package/dist/core/autopilot-gate.js.map +1 -0
- package/dist/core/autopilot-gate.test.d.ts +2 -0
- package/dist/core/autopilot-gate.test.d.ts.map +1 -0
- package/dist/core/autopilot-gate.test.js +159 -0
- package/dist/core/autopilot-gate.test.js.map +1 -0
- package/dist/core/ken-context.d.ts +17 -0
- package/dist/core/ken-context.d.ts.map +1 -1
- package/dist/core/ken-context.js +47 -6
- package/dist/core/ken-context.js.map +1 -1
- package/dist/core/ken-context.test.js +122 -1
- package/dist/core/ken-context.test.js.map +1 -1
- package/dist/core/ken-model.d.ts +46 -0
- package/dist/core/ken-model.d.ts.map +1 -0
- package/dist/core/ken-model.js +26 -0
- package/dist/core/ken-model.js.map +1 -0
- package/dist/core/ken-model.test.d.ts +2 -0
- package/dist/core/ken-model.test.d.ts.map +1 -0
- package/dist/core/ken-model.test.js +51 -0
- package/dist/core/ken-model.test.js.map +1 -0
- package/dist/core/ken-prompt.js +8 -4
- package/dist/core/ken-prompt.js.map +1 -1
- package/dist/core/ken-prompt.test.d.ts +2 -0
- package/dist/core/ken-prompt.test.d.ts.map +1 -0
- package/dist/core/ken-prompt.test.js +43 -0
- package/dist/core/ken-prompt.test.js.map +1 -0
- package/dist/core/speed-benchmark.test.js +2 -3
- package/dist/core/speed-benchmark.test.js.map +1 -1
- package/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -23,6 +23,9 @@ import { AgentSession } from "./core/agent-session.js";
|
|
|
23
23
|
import { buildKenSystemPrompt, buildKenAutopilotSystemPrompt } from "./core/ken-prompt.js";
|
|
24
24
|
import { buildKenDigest, buildKenAutopilotContext } from "./core/ken-context.js";
|
|
25
25
|
import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
|
|
26
|
+
import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, } from "./core/autopilot-gate.js";
|
|
27
|
+
import { driveAutopilotCycle } from "./core/autopilot-cycle.js";
|
|
28
|
+
import { validateKenModelPref, effectiveKenModel } from "./core/ken-model.js";
|
|
26
29
|
import { collectProjectContext } from "./system-prompt.js";
|
|
27
30
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
28
31
|
import { MOONSHOT_OAUTH_KEY, XIAOMI_CREDITS_KEY } from "@kenkaiiii/gg-core";
|
|
@@ -82,6 +85,7 @@ async function loadAppSettings() {
|
|
|
82
85
|
// model/thinking handlers below).
|
|
83
86
|
projectModels: raw.projectModels && typeof raw.projectModels === "object" ? raw.projectModels : undefined,
|
|
84
87
|
autopilot: raw.autopilot && typeof raw.autopilot === "object" ? raw.autopilot : undefined,
|
|
88
|
+
kenModels: raw.kenModels && typeof raw.kenModels === "object" ? raw.kenModels : undefined,
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
catch {
|
|
@@ -105,6 +109,24 @@ async function saveProjectModelPrefs(cwd, prefs) {
|
|
|
105
109
|
s.projectModels = { ...(s.projectModels ?? {}), [key]: prefs };
|
|
106
110
|
await saveAppSettings(s);
|
|
107
111
|
}
|
|
112
|
+
/** Read this project's persisted Ken model override, if any. */
|
|
113
|
+
async function loadKenModelPref(cwd) {
|
|
114
|
+
const s = await loadAppSettings();
|
|
115
|
+
return s.kenModels?.[projectModelKey(cwd)];
|
|
116
|
+
}
|
|
117
|
+
/** Persist (or with null, clear) this project's Ken model override via
|
|
118
|
+
* read-modify-write so the rest of the settings file is preserved. */
|
|
119
|
+
async function saveKenModelPref(cwd, pref) {
|
|
120
|
+
const s = await loadAppSettings();
|
|
121
|
+
const key = projectModelKey(cwd);
|
|
122
|
+
const next = { ...(s.kenModels ?? {}) };
|
|
123
|
+
if (pref)
|
|
124
|
+
next[key] = pref;
|
|
125
|
+
else
|
|
126
|
+
delete next[key];
|
|
127
|
+
s.kenModels = next;
|
|
128
|
+
await saveAppSettings(s);
|
|
129
|
+
}
|
|
108
130
|
/** Read this project's persisted autopilot flag (default off). */
|
|
109
131
|
async function loadAutopilot(cwd) {
|
|
110
132
|
const s = await loadAppSettings();
|
|
@@ -603,9 +625,11 @@ function lastAssistantText(messages) {
|
|
|
603
625
|
/**
|
|
604
626
|
* Assemble Ken's context digest for one `@Ken` question: project docs (up the
|
|
605
627
|
* tree) + git/env + the build session's compaction summary + recent activity.
|
|
606
|
-
* Prepended to the user's question as Ken's prompt body each turn.
|
|
628
|
+
* Prepended to the user's question as Ken's prompt body each turn. Workflow
|
|
629
|
+
* commands + autopilot-injected prompts are passed through so the digest
|
|
630
|
+
* labels them as what they are instead of user-authored asks.
|
|
607
631
|
*/
|
|
608
|
-
async function buildKenContext(buildSession, cwd, gitBranch, question) {
|
|
632
|
+
async function buildKenContext(buildSession, cwd, gitBranch, question, workflowCommands, injectedPrompts) {
|
|
609
633
|
const projectContext = await collectProjectContext(cwd).catch(() => []);
|
|
610
634
|
return buildKenDigest({
|
|
611
635
|
question,
|
|
@@ -613,6 +637,8 @@ async function buildKenContext(buildSession, cwd, gitBranch, question) {
|
|
|
613
637
|
cwd,
|
|
614
638
|
gitBranch,
|
|
615
639
|
messages: buildSession.getMessages(),
|
|
640
|
+
workflowCommands,
|
|
641
|
+
injectedPrompts,
|
|
616
642
|
});
|
|
617
643
|
}
|
|
618
644
|
/**
|
|
@@ -804,6 +830,23 @@ async function createSession(deps, opts) {
|
|
|
804
830
|
let autopilotCancelled = false;
|
|
805
831
|
// Hard cap on review→prompt→review rounds per user turn (loop safety).
|
|
806
832
|
const MAX_AUTOPILOT_ROUNDS = 3;
|
|
833
|
+
// Prompt bodies Autopilot Ken injected into the BUILD session this
|
|
834
|
+
// conversation. Passed into every Ken digest so injected prompts render as
|
|
835
|
+
// "Ken autopilot (injected)" instead of `**User:**` — otherwise multi-round
|
|
836
|
+
// cycles drift into Ken reviewing against his own last prompt. Cleared
|
|
837
|
+
// whenever the conversation resets (new session / plan accept / task run).
|
|
838
|
+
let injectedAutopilotPrompts = [];
|
|
839
|
+
// Workflow (prompt-template) commands: built-in + the project's custom
|
|
840
|
+
// `.gg/commands/*.md`. Used to gate autopilot off command turns and to label
|
|
841
|
+
// expanded templates in Ken's digests. Loaded fresh so a newly added custom
|
|
842
|
+
// command is picked up without a restart (mirrors GET /commands).
|
|
843
|
+
async function loadWorkflowCommandSpecs() {
|
|
844
|
+
const custom = await loadCustomCommands(cwd).catch(() => []);
|
|
845
|
+
return [
|
|
846
|
+
...PROMPT_COMMANDS.map((c) => ({ name: c.name, aliases: c.aliases, prompt: c.prompt })),
|
|
847
|
+
...custom.map((c) => ({ name: c.name, aliases: [], prompt: c.prompt })),
|
|
848
|
+
];
|
|
849
|
+
}
|
|
807
850
|
// ── Telegram serve (remote control via Telegram) ───────────
|
|
808
851
|
// A single embedded serve session lives in this sidecar process. Only the main
|
|
809
852
|
// window's home screen exposes the controls, so there's one bot per app.
|
|
@@ -819,6 +862,35 @@ async function createSession(deps, opts) {
|
|
|
819
862
|
let kenRunning = false;
|
|
820
863
|
let pendingKenModel = null;
|
|
821
864
|
const kenToolCallNames = new Map();
|
|
865
|
+
// Ken's per-project model override. null → Ken (chat + autopilot) follows GG
|
|
866
|
+
// Coder's model, including live switches (the historical behavior). Set → Ken
|
|
867
|
+
// is pinned to his own model and GG Coder switches no longer touch him. A
|
|
868
|
+
// stale persisted pin (model dropped from the registry / provider logged
|
|
869
|
+
// out) validates to null so Ken degrades to following instead of erroring.
|
|
870
|
+
let kenModelOverride = validateKenModelPref(await loadKenModelPref(cwd), {
|
|
871
|
+
modelExists: (id) => getModel(id) !== undefined,
|
|
872
|
+
providerConnected: () => true, // async auth checked below
|
|
873
|
+
});
|
|
874
|
+
if (kenModelOverride && !(await auth.hasProviderAuth(kenModelOverride.provider))) {
|
|
875
|
+
log("WARN", "app-sidecar", "ken model override provider not connected — following GG", {
|
|
876
|
+
provider: kenModelOverride.provider,
|
|
877
|
+
model: kenModelOverride.model,
|
|
878
|
+
});
|
|
879
|
+
kenModelOverride = null;
|
|
880
|
+
}
|
|
881
|
+
/** The model Ken uses next turn: the pin when set, else GG Coder's. */
|
|
882
|
+
function kenCurrentModel() {
|
|
883
|
+
if (kenModelOverride)
|
|
884
|
+
return kenModelOverride;
|
|
885
|
+
const st = session.getState();
|
|
886
|
+
return { provider: st.provider, model: st.model };
|
|
887
|
+
}
|
|
888
|
+
/** Footer payload: Ken's effective model + whether it's a pin. Merged into
|
|
889
|
+
* /state, the SSE ready frame, and every ken_model_change broadcast. */
|
|
890
|
+
function kenStatePayload() {
|
|
891
|
+
const st = session.getState();
|
|
892
|
+
return effectiveKenModel(kenModelOverride, { provider: st.provider, model: st.model });
|
|
893
|
+
}
|
|
822
894
|
async function syncKenModel(provider, model) {
|
|
823
895
|
if (kenRunning) {
|
|
824
896
|
pendingKenModel = { provider, model };
|
|
@@ -835,10 +907,10 @@ async function createSession(deps, opts) {
|
|
|
835
907
|
async function ensureKenSession() {
|
|
836
908
|
if (kenSession)
|
|
837
909
|
return kenSession;
|
|
838
|
-
const
|
|
910
|
+
const target = kenCurrentModel();
|
|
839
911
|
const ken = new AgentSession({
|
|
840
|
-
provider:
|
|
841
|
-
model:
|
|
912
|
+
provider: target.provider,
|
|
913
|
+
model: target.model,
|
|
842
914
|
cwd,
|
|
843
915
|
systemPrompt: buildKenSystemPrompt(),
|
|
844
916
|
allowedTools: KEN_ALLOWED_TOOLS,
|
|
@@ -869,7 +941,10 @@ async function createSession(deps, opts) {
|
|
|
869
941
|
broadcastError("ken_error", "ken error", d.error);
|
|
870
942
|
});
|
|
871
943
|
kenSession = ken;
|
|
872
|
-
log("INFO", "app-sidecar", "ken session ready", {
|
|
944
|
+
log("INFO", "app-sidecar", "ken session ready", {
|
|
945
|
+
provider: target.provider,
|
|
946
|
+
model: target.model,
|
|
947
|
+
});
|
|
873
948
|
return ken;
|
|
874
949
|
}
|
|
875
950
|
// ── Autopilot Ken (auto-reviewer) ──────────────────────────
|
|
@@ -898,10 +973,10 @@ async function createSession(deps, opts) {
|
|
|
898
973
|
async function ensureKenAutoSession() {
|
|
899
974
|
if (kenAutoSession)
|
|
900
975
|
return kenAutoSession;
|
|
901
|
-
const
|
|
976
|
+
const target = kenCurrentModel();
|
|
902
977
|
const ken = new AgentSession({
|
|
903
|
-
provider:
|
|
904
|
-
model:
|
|
978
|
+
provider: target.provider,
|
|
979
|
+
model: target.model,
|
|
905
980
|
cwd,
|
|
906
981
|
systemPrompt: buildKenAutopilotSystemPrompt(),
|
|
907
982
|
allowedTools: KEN_ALLOWED_TOOLS,
|
|
@@ -914,8 +989,8 @@ async function createSession(deps, opts) {
|
|
|
914
989
|
// runAutopilotReview try/catch as autopilot_error frames.
|
|
915
990
|
kenAutoSession = ken;
|
|
916
991
|
log("INFO", "app-sidecar", "ken autopilot session ready", {
|
|
917
|
-
provider:
|
|
918
|
-
model:
|
|
992
|
+
provider: target.provider,
|
|
993
|
+
model: target.model,
|
|
919
994
|
});
|
|
920
995
|
return ken;
|
|
921
996
|
}
|
|
@@ -980,7 +1055,9 @@ async function createSession(deps, opts) {
|
|
|
980
1055
|
// One review = prompt the kenAuto session with the review digest, read its
|
|
981
1056
|
// final assistant text, parse a verdict. Returns null on failure (surfaced as
|
|
982
1057
|
// an autopilot_error frame) so the cycle stops rather than looping blind.
|
|
983
|
-
|
|
1058
|
+
// `originalRequest` is the user prompt that started the turn under review —
|
|
1059
|
+
// pinned in the digest so it can't scroll out during multi-round cycles.
|
|
1060
|
+
async function runAutopilotReview(originalRequest) {
|
|
984
1061
|
autopilotReviewing = true;
|
|
985
1062
|
broadcast("autopilot_review_start", {});
|
|
986
1063
|
try {
|
|
@@ -991,6 +1068,9 @@ async function createSession(deps, opts) {
|
|
|
991
1068
|
cwd,
|
|
992
1069
|
gitBranch,
|
|
993
1070
|
messages: session.getMessages(),
|
|
1071
|
+
originalRequest,
|
|
1072
|
+
injectedPrompts: [...injectedAutopilotPrompts],
|
|
1073
|
+
workflowCommands: await loadWorkflowCommandSpecs(),
|
|
994
1074
|
});
|
|
995
1075
|
await ken.prompt(digest);
|
|
996
1076
|
return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
|
|
@@ -1009,50 +1089,101 @@ async function createSession(deps, opts) {
|
|
|
1009
1089
|
}
|
|
1010
1090
|
}
|
|
1011
1091
|
// Drive the review→prompt→review loop for one finished user turn. Only ever
|
|
1012
|
-
// called
|
|
1013
|
-
// task runner, resume, /ken, or
|
|
1014
|
-
//
|
|
1015
|
-
|
|
1092
|
+
// called after shouldStartAutopilotCycle approves the turn (POST /prompt or
|
|
1093
|
+
// the stranded-queue drain) — never from the task runner, resume, /ken, or
|
|
1094
|
+
// error paths, so there's no recursion and no guard tangle. The loop's
|
|
1095
|
+
// control flow lives in driveAutopilotCycle (core/autopilot-cycle.ts) so
|
|
1096
|
+
// every exit path is unit-tested; this only wires the real dependencies.
|
|
1097
|
+
async function runAutopilotCycle(originalRequest) {
|
|
1016
1098
|
if (!autopilot || autopilotCancelled)
|
|
1017
1099
|
return;
|
|
1018
1100
|
autopilotActive = true;
|
|
1019
1101
|
try {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1102
|
+
await driveAutopilotCycle({
|
|
1103
|
+
maxRounds: MAX_AUTOPILOT_ROUNDS,
|
|
1104
|
+
isCancelled: () => autopilotCancelled,
|
|
1105
|
+
// An injected run entering plan mode halts the cycle (autopilot_human
|
|
1106
|
+
// with the plan-hold reason) — Ken never prompts into a read-only
|
|
1107
|
+
// plan-mode session or answers the plan modal for the user.
|
|
1108
|
+
isPlanMode: () => session.getPlanMode(),
|
|
1109
|
+
// Lean context per user turn: wipe prior review history so each new
|
|
1110
|
+
// turn starts cheap, while within this cycle the few review messages
|
|
1111
|
+
// persist so Ken remembers what he already asked GG Coder to fix.
|
|
1112
|
+
resetReviewer: async () => {
|
|
1113
|
+
await kenAutoSession?.newSession().catch(() => { });
|
|
1114
|
+
},
|
|
1115
|
+
review: () => runAutopilotReview(originalRequest),
|
|
1116
|
+
// prompt → record the injected body (so later digests label it as
|
|
1117
|
+
// Ken's, not the user's), show a compact Ken-tinted marker (not the
|
|
1118
|
+
// prompt body), then feed GG Coder bracketed by runAgent so the run
|
|
1119
|
+
// streams normally; the shared finally never re-triggers autopilot,
|
|
1120
|
+
// so this can't recurse.
|
|
1121
|
+
onInjected: (body, round) => {
|
|
1122
|
+
injectedAutopilotPrompts.push(body);
|
|
1123
|
+
broadcast("autopilot_prompted", { round, body });
|
|
1124
|
+
},
|
|
1125
|
+
runPrompt: (body) => runAgent(body, () => session.prompt(body)),
|
|
1126
|
+
emit: (event) => broadcast(event.type, event.data),
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
finally {
|
|
1130
|
+
autopilotActive = false;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
// ── Stranded-queue drain ───────────────────────────────
|
|
1134
|
+
// A prompt POSTed while an autopilot cycle is between injected runs (build
|
|
1135
|
+
// idle, Ken reviewing) queues — but the queue only drains INTO a running
|
|
1136
|
+
// turn as steering. If the cycle ends without another run (ALL_CLEAR /
|
|
1137
|
+
// IGNORE / HUMAN / error), that message would sit stranded until the next
|
|
1138
|
+
// unrelated prompt, then land mislabeled as "concurrent steering" of an
|
|
1139
|
+
// unrelated run. Drain it here as a fresh turn of its own (with its own
|
|
1140
|
+
// gated review). Also covers the non-autopilot tail window: a message queued
|
|
1141
|
+
// after the run's last steering drain but before run_end.
|
|
1142
|
+
let drainingStrandedQueue = false;
|
|
1143
|
+
async function runStrandedQueue() {
|
|
1144
|
+
if (drainingStrandedQueue)
|
|
1145
|
+
return;
|
|
1146
|
+
drainingStrandedQueue = true;
|
|
1147
|
+
try {
|
|
1148
|
+
for (;;) {
|
|
1149
|
+
if (running || autopilotActive)
|
|
1032
1150
|
return;
|
|
1033
|
-
|
|
1034
|
-
if (
|
|
1035
|
-
// Nothing worth reviewing (small talk, a mechanical git op, etc.) —
|
|
1036
|
-
// stop the cycle silently, no marker at all.
|
|
1037
|
-
broadcast("autopilot_ignored", {});
|
|
1151
|
+
const next = session.takeNextQueuedMessage();
|
|
1152
|
+
if (!next)
|
|
1038
1153
|
return;
|
|
1154
|
+
broadcast("queued", { count: session.getQueuedCount() });
|
|
1155
|
+
if (!next.text.trim() && next.attachments.length === 0)
|
|
1156
|
+
continue;
|
|
1157
|
+
const workflowCommand = next.attachments.length === 0 &&
|
|
1158
|
+
isWorkflowCommandText(next.text, await loadWorkflowCommandSpecs());
|
|
1159
|
+
const assistantsBefore = countAssistantMessages(session.getMessages());
|
|
1160
|
+
await runAgent(next.text, async () => {
|
|
1161
|
+
if (next.attachments.length > 0) {
|
|
1162
|
+
await session.promptWithAttachments(next.text, next.attachments);
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
await session.prompt(next.text);
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
const decision = shouldStartAutopilotCycle({
|
|
1169
|
+
enabled: autopilot,
|
|
1170
|
+
cancelled: autopilotCancelled,
|
|
1171
|
+
planMode: session.getPlanMode(),
|
|
1172
|
+
workflowCommand,
|
|
1173
|
+
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1174
|
+
});
|
|
1175
|
+
if (decision.start) {
|
|
1176
|
+
await runAutopilotCycle(next.text);
|
|
1039
1177
|
}
|
|
1040
|
-
if (
|
|
1041
|
-
|
|
1042
|
-
|
|
1178
|
+
else if (autopilot) {
|
|
1179
|
+
log("INFO", "app-sidecar", "autopilot skipped (queued turn)", {
|
|
1180
|
+
reason: decision.reason,
|
|
1181
|
+
});
|
|
1043
1182
|
}
|
|
1044
|
-
// prompt → show a compact Ken-tinted marker (not the prompt body), then
|
|
1045
|
-
// feed GG Coder. Bracketed by runAgent so the run streams normally; the
|
|
1046
|
-
// shared finally no longer re-triggers autopilot, so this can't recurse.
|
|
1047
|
-
broadcast("autopilot_prompted", { round, body: verdict.body });
|
|
1048
|
-
await runAgent(verdict.body, () => session.prompt(verdict.body));
|
|
1049
|
-
if (autopilotCancelled)
|
|
1050
|
-
return;
|
|
1051
1183
|
}
|
|
1052
|
-
broadcast("autopilot_capped", { rounds: MAX_AUTOPILOT_ROUNDS });
|
|
1053
1184
|
}
|
|
1054
1185
|
finally {
|
|
1055
|
-
|
|
1186
|
+
drainingStrandedQueue = false;
|
|
1056
1187
|
}
|
|
1057
1188
|
}
|
|
1058
1189
|
// ── Task runner (project task list → sessions) ──────────────
|
|
@@ -1066,6 +1197,7 @@ async function createSession(deps, opts) {
|
|
|
1066
1197
|
return false;
|
|
1067
1198
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1068
1199
|
await session.newSession();
|
|
1200
|
+
injectedAutopilotPrompts = [];
|
|
1069
1201
|
titleGenerated = false;
|
|
1070
1202
|
broadcast("session_reset", {});
|
|
1071
1203
|
markTaskInProgress(cwd, task.id);
|
|
@@ -1173,6 +1305,7 @@ async function createSession(deps, opts) {
|
|
|
1173
1305
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1174
1306
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
1175
1307
|
autopilot,
|
|
1308
|
+
...kenStatePayload(),
|
|
1176
1309
|
...footerExtras(),
|
|
1177
1310
|
});
|
|
1178
1311
|
return;
|
|
@@ -1197,6 +1330,7 @@ async function createSession(deps, opts) {
|
|
|
1197
1330
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1198
1331
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
1199
1332
|
autopilot,
|
|
1333
|
+
...kenStatePayload(),
|
|
1200
1334
|
...footerExtras(),
|
|
1201
1335
|
},
|
|
1202
1336
|
})}\n\n`);
|
|
@@ -1537,6 +1671,13 @@ async function createSession(deps, opts) {
|
|
|
1537
1671
|
// Fresh user turn: clear any cancel flag left from a prior cycle so this
|
|
1538
1672
|
// turn's autopilot review can run.
|
|
1539
1673
|
autopilotCancelled = false;
|
|
1674
|
+
// Gate inputs captured around the run: whether this turn is a workflow
|
|
1675
|
+
// slash command (attachment prompts skip slash expansion entirely), and
|
|
1676
|
+
// how many assistant messages the run actually adds. Computed even when
|
|
1677
|
+
// autopilot is currently off — the toggle can flip ON mid-run, and the
|
|
1678
|
+
// gate reads the post-run value.
|
|
1679
|
+
const workflowCommand = attachments.length === 0 && isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
|
|
1680
|
+
const assistantsBefore = countAssistantMessages(session.getMessages());
|
|
1540
1681
|
await runAgent(text, async () => {
|
|
1541
1682
|
if (attachments.length > 0) {
|
|
1542
1683
|
// Persist each attachment under .gg/uploads so files are inspectable
|
|
@@ -1552,11 +1693,31 @@ async function createSession(deps, opts) {
|
|
|
1552
1693
|
await session.prompt(text);
|
|
1553
1694
|
}
|
|
1554
1695
|
});
|
|
1555
|
-
// After the user's run settles, kick off Ken's auto-review loop
|
|
1556
|
-
//
|
|
1557
|
-
//
|
|
1558
|
-
|
|
1559
|
-
|
|
1696
|
+
// After the user's run settles, kick off Ken's auto-review loop — but
|
|
1697
|
+
// only when the turn is actually reviewable (shouldStartAutopilotCycle):
|
|
1698
|
+
// workflow commands (/compare, /bullet-proof, …) end with reports or
|
|
1699
|
+
// A/B/C choices reserved for the USER; registry commands (/help) and
|
|
1700
|
+
// failed runs add no assistant work to judge; a turn that ended in plan
|
|
1701
|
+
// mode has a pending Accept/Reject modal Ken must not preempt. This is
|
|
1702
|
+
// the ONLY entry point into the cycle besides the stranded-queue drain —
|
|
1703
|
+
// it drives any follow-up GG Coder runs itself, so the shared runAgent
|
|
1704
|
+
// finally never recurses.
|
|
1705
|
+
const decision = shouldStartAutopilotCycle({
|
|
1706
|
+
enabled: autopilot,
|
|
1707
|
+
cancelled: autopilotCancelled,
|
|
1708
|
+
planMode: session.getPlanMode(),
|
|
1709
|
+
workflowCommand,
|
|
1710
|
+
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1711
|
+
});
|
|
1712
|
+
if (decision.start) {
|
|
1713
|
+
await runAutopilotCycle(text);
|
|
1714
|
+
}
|
|
1715
|
+
else if (autopilot) {
|
|
1716
|
+
log("INFO", "app-sidecar", "autopilot skipped", { reason: decision.reason });
|
|
1717
|
+
}
|
|
1718
|
+
// A prompt sent while Ken was reviewing (build idle) queued but had no
|
|
1719
|
+
// run to steer into — run it now as a fresh turn so it never strands.
|
|
1720
|
+
await runStrandedQueue();
|
|
1560
1721
|
});
|
|
1561
1722
|
return;
|
|
1562
1723
|
}
|
|
@@ -1587,7 +1748,7 @@ async function createSession(deps, opts) {
|
|
|
1587
1748
|
broadcast("ken_run_start", { text });
|
|
1588
1749
|
try {
|
|
1589
1750
|
const ken = await ensureKenSession();
|
|
1590
|
-
const digest = await buildKenContext(session, cwd, gitBranch, text);
|
|
1751
|
+
const digest = await buildKenContext(session, cwd, gitBranch, text, await loadWorkflowCommandSpecs(), injectedAutopilotPrompts);
|
|
1591
1752
|
await ken.prompt(digest);
|
|
1592
1753
|
// Record the turn against the BUILD session so it persists + survives
|
|
1593
1754
|
// resume (advisory custom entry, never an LLM message). Reply is Ken's
|
|
@@ -1785,8 +1946,12 @@ async function createSession(deps, opts) {
|
|
|
1785
1946
|
return;
|
|
1786
1947
|
}
|
|
1787
1948
|
await session.switchModel(target.provider, target.id);
|
|
1788
|
-
|
|
1789
|
-
|
|
1949
|
+
// Ken follows GG Coder's model only while un-pinned; a user-set Ken
|
|
1950
|
+
// override survives GG model switches untouched.
|
|
1951
|
+
if (!kenModelOverride) {
|
|
1952
|
+
await syncKenModel(target.provider, target.id);
|
|
1953
|
+
await syncKenAutoModel(target.provider, target.id);
|
|
1954
|
+
}
|
|
1790
1955
|
// Clamp the reasoning level to what the new model supports (mirrors the
|
|
1791
1956
|
// CLI): keep thinking on at the first supported tier if it was on but
|
|
1792
1957
|
// the prior level is unsupported here; leave it off if it was off.
|
|
@@ -1813,6 +1978,12 @@ async function createSession(deps, opts) {
|
|
|
1813
1978
|
// model_change is emitted by switchModel; follow with thinking_change so
|
|
1814
1979
|
// the footer toggle reflects the new model's supported levels.
|
|
1815
1980
|
broadcast("thinking_change", payload);
|
|
1981
|
+
// Un-pinned Ken just followed the switch — update his footer chip too.
|
|
1982
|
+
// When Ken is pinned, his effective model did not change, so skip the
|
|
1983
|
+
// no-op event (keeps footer/event tests from treating a GG switch as a
|
|
1984
|
+
// Ken switch).
|
|
1985
|
+
if (!kenModelOverride)
|
|
1986
|
+
broadcast("ken_model_change", kenStatePayload());
|
|
1816
1987
|
// The new model usually has a different context window — push extras so
|
|
1817
1988
|
// the footer's context meter rescales immediately.
|
|
1818
1989
|
broadcast("extras", footerExtras());
|
|
@@ -1820,6 +1991,54 @@ async function createSession(deps, opts) {
|
|
|
1820
1991
|
});
|
|
1821
1992
|
return;
|
|
1822
1993
|
}
|
|
1994
|
+
// Set or clear Ken's model pin. Body: { model: "<id>" } to pin, or
|
|
1995
|
+
// { model: null } / "" to clear (Ken resumes following GG Coder). Applies
|
|
1996
|
+
// to BOTH Ken sessions (chat + autopilot reviewer); a switch landing while
|
|
1997
|
+
// either is mid-run defers via the pending-model mechanics.
|
|
1998
|
+
if (method === "POST" && url === "/ken/model") {
|
|
1999
|
+
void readBody(req).then(async (raw) => {
|
|
2000
|
+
let modelId;
|
|
2001
|
+
try {
|
|
2002
|
+
const parsed = JSON.parse(raw).model;
|
|
2003
|
+
modelId = typeof parsed === "string" && parsed.trim() ? parsed.trim() : null;
|
|
2004
|
+
}
|
|
2005
|
+
catch {
|
|
2006
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (modelId === null) {
|
|
2010
|
+
// Clear the pin → follow GG Coder again, syncing both sessions back.
|
|
2011
|
+
kenModelOverride = null;
|
|
2012
|
+
await saveKenModelPref(cwd, null);
|
|
2013
|
+
const st = session.getState();
|
|
2014
|
+
await syncKenModel(st.provider, st.model);
|
|
2015
|
+
await syncKenAutoModel(st.provider, st.model);
|
|
2016
|
+
log("INFO", "app-sidecar", "ken model pin cleared — following GG", {
|
|
2017
|
+
provider: st.provider,
|
|
2018
|
+
model: st.model,
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
else {
|
|
2022
|
+
const target = getModel(modelId);
|
|
2023
|
+
if (!target) {
|
|
2024
|
+
json(res, 404, { error: `unknown model: ${modelId}` });
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
kenModelOverride = { provider: target.provider, model: target.id };
|
|
2028
|
+
await saveKenModelPref(cwd, kenModelOverride);
|
|
2029
|
+
await syncKenModel(target.provider, target.id);
|
|
2030
|
+
await syncKenAutoModel(target.provider, target.id);
|
|
2031
|
+
log("INFO", "app-sidecar", "ken model pinned", {
|
|
2032
|
+
provider: target.provider,
|
|
2033
|
+
model: target.id,
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
const payload = kenStatePayload();
|
|
2037
|
+
broadcast("ken_model_change", payload);
|
|
2038
|
+
json(res, 200, payload);
|
|
2039
|
+
});
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
1823
2042
|
if (method === "POST" && url === "/kill") {
|
|
1824
2043
|
void readBody(req).then(async (raw) => {
|
|
1825
2044
|
let id;
|
|
@@ -1891,6 +2110,7 @@ async function createSession(deps, opts) {
|
|
|
1891
2110
|
void session
|
|
1892
2111
|
.newSession()
|
|
1893
2112
|
.then(() => {
|
|
2113
|
+
injectedAutopilotPrompts = [];
|
|
1894
2114
|
broadcast("session_reset", {});
|
|
1895
2115
|
json(res, 200, { ok: true });
|
|
1896
2116
|
})
|
|
@@ -1924,6 +2144,7 @@ async function createSession(deps, opts) {
|
|
|
1924
2144
|
}
|
|
1925
2145
|
try {
|
|
1926
2146
|
await session.newSession();
|
|
2147
|
+
injectedAutopilotPrompts = [];
|
|
1927
2148
|
titleGenerated = false;
|
|
1928
2149
|
await session.setApprovedPlan(planPath);
|
|
1929
2150
|
broadcast("session_reset", {});
|