@kenkaiiii/ggcoder 5.5.1 → 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 -47
- 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/autopilot-verdict.d.ts +14 -2
- package/dist/core/autopilot-verdict.d.ts.map +1 -1
- package/dist/core/autopilot-verdict.js +19 -2
- package/dist/core/autopilot-verdict.js.map +1 -1
- package/dist/core/autopilot-verdict.test.js +10 -0
- package/dist/core/autopilot-verdict.test.js.map +1 -1
- 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.d.ts +3 -3
- package/dist/core/ken-prompt.js +19 -7
- 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,44 +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
|
-
|
|
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)
|
|
1029
1150
|
return;
|
|
1030
|
-
|
|
1031
|
-
|
|
1151
|
+
const next = session.takeNextQueuedMessage();
|
|
1152
|
+
if (!next)
|
|
1032
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);
|
|
1033
1177
|
}
|
|
1034
|
-
if (
|
|
1035
|
-
|
|
1036
|
-
|
|
1178
|
+
else if (autopilot) {
|
|
1179
|
+
log("INFO", "app-sidecar", "autopilot skipped (queued turn)", {
|
|
1180
|
+
reason: decision.reason,
|
|
1181
|
+
});
|
|
1037
1182
|
}
|
|
1038
|
-
// prompt → show a compact Ken-tinted marker (not the prompt body), then
|
|
1039
|
-
// feed GG Coder. Bracketed by runAgent so the run streams normally; the
|
|
1040
|
-
// shared finally no longer re-triggers autopilot, so this can't recurse.
|
|
1041
|
-
broadcast("autopilot_prompted", { round, body: verdict.body });
|
|
1042
|
-
await runAgent(verdict.body, () => session.prompt(verdict.body));
|
|
1043
|
-
if (autopilotCancelled)
|
|
1044
|
-
return;
|
|
1045
1183
|
}
|
|
1046
|
-
broadcast("autopilot_capped", { rounds: MAX_AUTOPILOT_ROUNDS });
|
|
1047
1184
|
}
|
|
1048
1185
|
finally {
|
|
1049
|
-
|
|
1186
|
+
drainingStrandedQueue = false;
|
|
1050
1187
|
}
|
|
1051
1188
|
}
|
|
1052
1189
|
// ── Task runner (project task list → sessions) ──────────────
|
|
@@ -1060,6 +1197,7 @@ async function createSession(deps, opts) {
|
|
|
1060
1197
|
return false;
|
|
1061
1198
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1062
1199
|
await session.newSession();
|
|
1200
|
+
injectedAutopilotPrompts = [];
|
|
1063
1201
|
titleGenerated = false;
|
|
1064
1202
|
broadcast("session_reset", {});
|
|
1065
1203
|
markTaskInProgress(cwd, task.id);
|
|
@@ -1167,6 +1305,7 @@ async function createSession(deps, opts) {
|
|
|
1167
1305
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1168
1306
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
1169
1307
|
autopilot,
|
|
1308
|
+
...kenStatePayload(),
|
|
1170
1309
|
...footerExtras(),
|
|
1171
1310
|
});
|
|
1172
1311
|
return;
|
|
@@ -1191,6 +1330,7 @@ async function createSession(deps, opts) {
|
|
|
1191
1330
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1192
1331
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
1193
1332
|
autopilot,
|
|
1333
|
+
...kenStatePayload(),
|
|
1194
1334
|
...footerExtras(),
|
|
1195
1335
|
},
|
|
1196
1336
|
})}\n\n`);
|
|
@@ -1531,6 +1671,13 @@ async function createSession(deps, opts) {
|
|
|
1531
1671
|
// Fresh user turn: clear any cancel flag left from a prior cycle so this
|
|
1532
1672
|
// turn's autopilot review can run.
|
|
1533
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());
|
|
1534
1681
|
await runAgent(text, async () => {
|
|
1535
1682
|
if (attachments.length > 0) {
|
|
1536
1683
|
// Persist each attachment under .gg/uploads so files are inspectable
|
|
@@ -1546,11 +1693,31 @@ async function createSession(deps, opts) {
|
|
|
1546
1693
|
await session.prompt(text);
|
|
1547
1694
|
}
|
|
1548
1695
|
});
|
|
1549
|
-
// After the user's run settles, kick off Ken's auto-review loop
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
|
|
1553
|
-
|
|
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();
|
|
1554
1721
|
});
|
|
1555
1722
|
return;
|
|
1556
1723
|
}
|
|
@@ -1581,7 +1748,7 @@ async function createSession(deps, opts) {
|
|
|
1581
1748
|
broadcast("ken_run_start", { text });
|
|
1582
1749
|
try {
|
|
1583
1750
|
const ken = await ensureKenSession();
|
|
1584
|
-
const digest = await buildKenContext(session, cwd, gitBranch, text);
|
|
1751
|
+
const digest = await buildKenContext(session, cwd, gitBranch, text, await loadWorkflowCommandSpecs(), injectedAutopilotPrompts);
|
|
1585
1752
|
await ken.prompt(digest);
|
|
1586
1753
|
// Record the turn against the BUILD session so it persists + survives
|
|
1587
1754
|
// resume (advisory custom entry, never an LLM message). Reply is Ken's
|
|
@@ -1779,8 +1946,12 @@ async function createSession(deps, opts) {
|
|
|
1779
1946
|
return;
|
|
1780
1947
|
}
|
|
1781
1948
|
await session.switchModel(target.provider, target.id);
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
+
}
|
|
1784
1955
|
// Clamp the reasoning level to what the new model supports (mirrors the
|
|
1785
1956
|
// CLI): keep thinking on at the first supported tier if it was on but
|
|
1786
1957
|
// the prior level is unsupported here; leave it off if it was off.
|
|
@@ -1807,6 +1978,12 @@ async function createSession(deps, opts) {
|
|
|
1807
1978
|
// model_change is emitted by switchModel; follow with thinking_change so
|
|
1808
1979
|
// the footer toggle reflects the new model's supported levels.
|
|
1809
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());
|
|
1810
1987
|
// The new model usually has a different context window — push extras so
|
|
1811
1988
|
// the footer's context meter rescales immediately.
|
|
1812
1989
|
broadcast("extras", footerExtras());
|
|
@@ -1814,6 +1991,54 @@ async function createSession(deps, opts) {
|
|
|
1814
1991
|
});
|
|
1815
1992
|
return;
|
|
1816
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
|
+
}
|
|
1817
2042
|
if (method === "POST" && url === "/kill") {
|
|
1818
2043
|
void readBody(req).then(async (raw) => {
|
|
1819
2044
|
let id;
|
|
@@ -1885,6 +2110,7 @@ async function createSession(deps, opts) {
|
|
|
1885
2110
|
void session
|
|
1886
2111
|
.newSession()
|
|
1887
2112
|
.then(() => {
|
|
2113
|
+
injectedAutopilotPrompts = [];
|
|
1888
2114
|
broadcast("session_reset", {});
|
|
1889
2115
|
json(res, 200, { ok: true });
|
|
1890
2116
|
})
|
|
@@ -1918,6 +2144,7 @@ async function createSession(deps, opts) {
|
|
|
1918
2144
|
}
|
|
1919
2145
|
try {
|
|
1920
2146
|
await session.newSession();
|
|
2147
|
+
injectedAutopilotPrompts = [];
|
|
1921
2148
|
titleGenerated = false;
|
|
1922
2149
|
await session.setApprovedPlan(planPath);
|
|
1923
2150
|
broadcast("session_reset", {});
|