@kenkaiiii/ggcoder 5.6.0 → 5.6.2
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 +353 -67
- 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 +42 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +76 -3
- 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 +97 -0
- package/dist/core/autopilot-gate.d.ts.map +1 -0
- package/dist/core/autopilot-gate.js +196 -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 +270 -0
- package/dist/core/autopilot-gate.test.js.map +1 -0
- package/dist/core/autopilot-verdict.d.ts.map +1 -1
- package/dist/core/autopilot-verdict.js +41 -1
- package/dist/core/autopilot-verdict.js.map +1 -1
- package/dist/core/autopilot-verdict.test.js +16 -0
- package/dist/core/autopilot-verdict.test.js.map +1 -1
- package/dist/core/ken-context.d.ts +23 -2
- package/dist/core/ken-context.d.ts.map +1 -1
- package/dist/core/ken-context.js +47 -8
- package/dist/core/ken-context.js.map +1 -1
- package/dist/core/ken-context.test.js +120 -5
- 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 +2 -15
- package/dist/core/ken-prompt.d.ts.map +1 -1
- package/dist/core/ken-prompt.js +47 -14
- 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 +76 -0
- package/dist/core/ken-prompt.test.js.map +1 -0
- package/dist/core/session-manager.d.ts +18 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +31 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +71 -1
- package/dist/core/session-manager.test.js.map +1 -1
- 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,7 +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 {
|
|
26
|
+
import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
|
|
27
|
+
import { driveAutopilotCycle } from "./core/autopilot-cycle.js";
|
|
28
|
+
import { validateKenModelPref, effectiveKenModel } from "./core/ken-model.js";
|
|
27
29
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
28
30
|
import { MOONSHOT_OAUTH_KEY, XIAOMI_CREDITS_KEY } from "@kenkaiiii/gg-core";
|
|
29
31
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
@@ -82,6 +84,7 @@ async function loadAppSettings() {
|
|
|
82
84
|
// model/thinking handlers below).
|
|
83
85
|
projectModels: raw.projectModels && typeof raw.projectModels === "object" ? raw.projectModels : undefined,
|
|
84
86
|
autopilot: raw.autopilot && typeof raw.autopilot === "object" ? raw.autopilot : undefined,
|
|
87
|
+
kenModels: raw.kenModels && typeof raw.kenModels === "object" ? raw.kenModels : undefined,
|
|
85
88
|
};
|
|
86
89
|
}
|
|
87
90
|
catch {
|
|
@@ -105,6 +108,24 @@ async function saveProjectModelPrefs(cwd, prefs) {
|
|
|
105
108
|
s.projectModels = { ...(s.projectModels ?? {}), [key]: prefs };
|
|
106
109
|
await saveAppSettings(s);
|
|
107
110
|
}
|
|
111
|
+
/** Read this project's persisted Ken model override, if any. */
|
|
112
|
+
async function loadKenModelPref(cwd) {
|
|
113
|
+
const s = await loadAppSettings();
|
|
114
|
+
return s.kenModels?.[projectModelKey(cwd)];
|
|
115
|
+
}
|
|
116
|
+
/** Persist (or with null, clear) this project's Ken model override via
|
|
117
|
+
* read-modify-write so the rest of the settings file is preserved. */
|
|
118
|
+
async function saveKenModelPref(cwd, pref) {
|
|
119
|
+
const s = await loadAppSettings();
|
|
120
|
+
const key = projectModelKey(cwd);
|
|
121
|
+
const next = { ...(s.kenModels ?? {}) };
|
|
122
|
+
if (pref)
|
|
123
|
+
next[key] = pref;
|
|
124
|
+
else
|
|
125
|
+
delete next[key];
|
|
126
|
+
s.kenModels = next;
|
|
127
|
+
await saveAppSettings(s);
|
|
128
|
+
}
|
|
108
129
|
/** Read this project's persisted autopilot flag (default off). */
|
|
109
130
|
async function loadAutopilot(cwd) {
|
|
110
131
|
const s = await loadAppSettings();
|
|
@@ -601,18 +622,23 @@ function lastAssistantText(messages) {
|
|
|
601
622
|
return "";
|
|
602
623
|
}
|
|
603
624
|
/**
|
|
604
|
-
* Assemble Ken's context digest for one `@Ken` question:
|
|
605
|
-
*
|
|
606
|
-
*
|
|
625
|
+
* Assemble Ken's context digest for one `@Ken` question: git/env + the build
|
|
626
|
+
* session's compaction summary + recent activity. Prepended to the user's
|
|
627
|
+
* question as Ken's prompt body each turn. Project docs (CLAUDE.md/AGENTS.md)
|
|
628
|
+
* are NOT here — they're folded into Ken's cached system prompt once per
|
|
629
|
+
* session instead (see ken-prompt.ts), so they hit the provider prompt cache
|
|
630
|
+
* instead of being re-sent uncached on every question. Workflow commands +
|
|
631
|
+
* autopilot-injected prompts are passed through so the digest labels them as
|
|
632
|
+
* what they are instead of user-authored asks.
|
|
607
633
|
*/
|
|
608
|
-
|
|
609
|
-
const projectContext = await collectProjectContext(cwd).catch(() => []);
|
|
634
|
+
function buildKenContext(buildSession, cwd, gitBranch, question, workflowCommands, injectedPrompts) {
|
|
610
635
|
return buildKenDigest({
|
|
611
636
|
question,
|
|
612
|
-
projectContext,
|
|
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,16 +907,19 @@ 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
|
-
systemPrompt: buildKenSystemPrompt(),
|
|
915
|
+
systemPrompt: await buildKenSystemPrompt(cwd),
|
|
844
916
|
allowedTools: KEN_ALLOWED_TOOLS,
|
|
845
917
|
allowedMcpServers: KEN_ALLOWED_MCP_SERVERS,
|
|
846
918
|
transient: true,
|
|
847
919
|
signal: kenAbort.signal,
|
|
920
|
+
// Ken's bursty, spread-out turns (chat) outlast the default 5-min cache
|
|
921
|
+
// TTL regardless of the user's global speedProfile pick.
|
|
922
|
+
forceLongCacheRetention: true,
|
|
848
923
|
});
|
|
849
924
|
await ken.initialize();
|
|
850
925
|
// Bridge Ken's bus to the shared SSE fan-out with ken_-prefixed types so the
|
|
@@ -869,7 +944,10 @@ async function createSession(deps, opts) {
|
|
|
869
944
|
broadcastError("ken_error", "ken error", d.error);
|
|
870
945
|
});
|
|
871
946
|
kenSession = ken;
|
|
872
|
-
log("INFO", "app-sidecar", "ken session ready", {
|
|
947
|
+
log("INFO", "app-sidecar", "ken session ready", {
|
|
948
|
+
provider: target.provider,
|
|
949
|
+
model: target.model,
|
|
950
|
+
});
|
|
873
951
|
return ken;
|
|
874
952
|
}
|
|
875
953
|
// ── Autopilot Ken (auto-reviewer) ──────────────────────────
|
|
@@ -898,24 +976,27 @@ async function createSession(deps, opts) {
|
|
|
898
976
|
async function ensureKenAutoSession() {
|
|
899
977
|
if (kenAutoSession)
|
|
900
978
|
return kenAutoSession;
|
|
901
|
-
const
|
|
979
|
+
const target = kenCurrentModel();
|
|
902
980
|
const ken = new AgentSession({
|
|
903
|
-
provider:
|
|
904
|
-
model:
|
|
981
|
+
provider: target.provider,
|
|
982
|
+
model: target.model,
|
|
905
983
|
cwd,
|
|
906
|
-
systemPrompt: buildKenAutopilotSystemPrompt(),
|
|
984
|
+
systemPrompt: await buildKenAutopilotSystemPrompt(cwd),
|
|
907
985
|
allowedTools: KEN_ALLOWED_TOOLS,
|
|
908
986
|
allowedMcpServers: KEN_ALLOWED_MCP_SERVERS,
|
|
909
987
|
transient: true,
|
|
910
988
|
signal: kenAutoAbort.signal,
|
|
989
|
+
// Autopilot review rounds routinely span the injected GG Coder run
|
|
990
|
+
// (often >5 min) regardless of the user's global speedProfile pick.
|
|
991
|
+
forceLongCacheRetention: true,
|
|
911
992
|
});
|
|
912
993
|
await ken.initialize();
|
|
913
994
|
// Deliberately no bus bridge: the review is silent. Errors surface via the
|
|
914
995
|
// runAutopilotReview try/catch as autopilot_error frames.
|
|
915
996
|
kenAutoSession = ken;
|
|
916
997
|
log("INFO", "app-sidecar", "ken autopilot session ready", {
|
|
917
|
-
provider:
|
|
918
|
-
model:
|
|
998
|
+
provider: target.provider,
|
|
999
|
+
model: target.model,
|
|
919
1000
|
});
|
|
920
1001
|
return ken;
|
|
921
1002
|
}
|
|
@@ -980,17 +1061,20 @@ async function createSession(deps, opts) {
|
|
|
980
1061
|
// One review = prompt the kenAuto session with the review digest, read its
|
|
981
1062
|
// final assistant text, parse a verdict. Returns null on failure (surfaced as
|
|
982
1063
|
// an autopilot_error frame) so the cycle stops rather than looping blind.
|
|
983
|
-
|
|
1064
|
+
// `originalRequest` is the user prompt that started the turn under review —
|
|
1065
|
+
// pinned in the digest so it can't scroll out during multi-round cycles.
|
|
1066
|
+
async function runAutopilotReview(originalRequest) {
|
|
984
1067
|
autopilotReviewing = true;
|
|
985
1068
|
broadcast("autopilot_review_start", {});
|
|
986
1069
|
try {
|
|
987
1070
|
const ken = await ensureKenAutoSession();
|
|
988
|
-
const projectContext = await collectProjectContext(cwd).catch(() => []);
|
|
989
1071
|
const digest = buildKenAutopilotContext({
|
|
990
|
-
projectContext,
|
|
991
1072
|
cwd,
|
|
992
1073
|
gitBranch,
|
|
993
1074
|
messages: session.getMessages(),
|
|
1075
|
+
originalRequest,
|
|
1076
|
+
injectedPrompts: [...injectedAutopilotPrompts],
|
|
1077
|
+
workflowCommands: await loadWorkflowCommandSpecs(),
|
|
994
1078
|
});
|
|
995
1079
|
await ken.prompt(digest);
|
|
996
1080
|
return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
|
|
@@ -1009,50 +1093,123 @@ async function createSession(deps, opts) {
|
|
|
1009
1093
|
}
|
|
1010
1094
|
}
|
|
1011
1095
|
// Drive the review→prompt→review loop for one finished user turn. Only ever
|
|
1012
|
-
// called
|
|
1013
|
-
// task runner, resume, /ken, or
|
|
1014
|
-
//
|
|
1015
|
-
|
|
1096
|
+
// called after shouldStartAutopilotCycle approves the turn (POST /prompt or
|
|
1097
|
+
// the stranded-queue drain) — never from the task runner, resume, /ken, or
|
|
1098
|
+
// error paths, so there's no recursion and no guard tangle. The loop's
|
|
1099
|
+
// control flow lives in driveAutopilotCycle (core/autopilot-cycle.ts) so
|
|
1100
|
+
// every exit path is unit-tested; this only wires the real dependencies.
|
|
1101
|
+
async function runAutopilotCycle(originalRequest) {
|
|
1016
1102
|
if (!autopilot || autopilotCancelled)
|
|
1017
1103
|
return;
|
|
1018
1104
|
autopilotActive = true;
|
|
1019
1105
|
try {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1106
|
+
await driveAutopilotCycle({
|
|
1107
|
+
maxRounds: MAX_AUTOPILOT_ROUNDS,
|
|
1108
|
+
isCancelled: () => autopilotCancelled,
|
|
1109
|
+
// An injected run entering plan mode halts the cycle (autopilot_human
|
|
1110
|
+
// with the plan-hold reason) — Ken never prompts into a read-only
|
|
1111
|
+
// plan-mode session or answers the plan modal for the user.
|
|
1112
|
+
isPlanMode: () => session.getPlanMode(),
|
|
1113
|
+
// Lean context per user turn: wipe prior review history so each new
|
|
1114
|
+
// turn starts cheap, while within this cycle the few review messages
|
|
1115
|
+
// persist so Ken remembers what he already asked GG Coder to fix.
|
|
1116
|
+
resetReviewer: async () => {
|
|
1117
|
+
await kenAutoSession?.newSession().catch(() => { });
|
|
1118
|
+
},
|
|
1119
|
+
review: () => runAutopilotReview(originalRequest),
|
|
1120
|
+
// prompt → record the injected body (so later digests label it as
|
|
1121
|
+
// Ken's, not the user's), show a compact Ken-tinted marker (not the
|
|
1122
|
+
// prompt body), then feed GG Coder bracketed by runAgent so the run
|
|
1123
|
+
// streams normally; the shared finally never re-triggers autopilot,
|
|
1124
|
+
// so this can't recurse.
|
|
1125
|
+
onInjected: (body, round) => {
|
|
1126
|
+
injectedAutopilotPrompts.push(body);
|
|
1127
|
+
broadcast("autopilot_prompted", { round, body });
|
|
1128
|
+
void session.persistAutopilotMarker("prompted", { body });
|
|
1129
|
+
},
|
|
1130
|
+
runPrompt: (body) => runAgent(body, () => session.prompt(body)),
|
|
1131
|
+
emit: (event) => {
|
|
1132
|
+
broadcast(event.type, event.data);
|
|
1133
|
+
// Persist the terminal verdict marker so a resumed session renders the
|
|
1134
|
+
// same Ken bubble the live run showed instead of dropping it or
|
|
1135
|
+
// falling back to the raw verdict text (e.g. ALL_CLEAR).
|
|
1136
|
+
if (event.type === "autopilot_done") {
|
|
1137
|
+
void session.persistAutopilotMarker("done");
|
|
1138
|
+
}
|
|
1139
|
+
else if (event.type === "autopilot_human") {
|
|
1140
|
+
void session.persistAutopilotMarker("human", { reason: event.data.reason });
|
|
1141
|
+
}
|
|
1142
|
+
else if (event.type === "autopilot_capped") {
|
|
1143
|
+
void session.persistAutopilotMarker("capped");
|
|
1144
|
+
}
|
|
1145
|
+
// autopilot_ignored renders nothing live, so nothing is persisted either.
|
|
1146
|
+
},
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
finally {
|
|
1150
|
+
autopilotActive = false;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// ── Stranded-queue drain ───────────────────────────────
|
|
1154
|
+
// A prompt POSTed while an autopilot cycle is between injected runs (build
|
|
1155
|
+
// idle, Ken reviewing) queues — but the queue only drains INTO a running
|
|
1156
|
+
// turn as steering. If the cycle ends without another run (ALL_CLEAR /
|
|
1157
|
+
// IGNORE / HUMAN / error), that message would sit stranded until the next
|
|
1158
|
+
// unrelated prompt, then land mislabeled as "concurrent steering" of an
|
|
1159
|
+
// unrelated run. Drain it here as a fresh turn of its own (with its own
|
|
1160
|
+
// gated review). Also covers the non-autopilot tail window: a message queued
|
|
1161
|
+
// after the run's last steering drain but before run_end.
|
|
1162
|
+
let drainingStrandedQueue = false;
|
|
1163
|
+
async function runStrandedQueue() {
|
|
1164
|
+
if (drainingStrandedQueue)
|
|
1165
|
+
return;
|
|
1166
|
+
drainingStrandedQueue = true;
|
|
1167
|
+
try {
|
|
1168
|
+
for (;;) {
|
|
1169
|
+
if (running || autopilotActive)
|
|
1032
1170
|
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", {});
|
|
1171
|
+
const next = session.takeNextQueuedMessage();
|
|
1172
|
+
if (!next)
|
|
1038
1173
|
return;
|
|
1174
|
+
broadcast("queued", { count: session.getQueuedCount() });
|
|
1175
|
+
if (!next.text.trim() && next.attachments.length === 0)
|
|
1176
|
+
continue;
|
|
1177
|
+
const workflowCommand = next.attachments.length === 0 &&
|
|
1178
|
+
isWorkflowCommandText(next.text, await loadWorkflowCommandSpecs());
|
|
1179
|
+
const assistantsBefore = countAssistantMessages(session.getMessages());
|
|
1180
|
+
const messagesBefore = session.getMessages().length;
|
|
1181
|
+
await runAgent(next.text, async () => {
|
|
1182
|
+
if (next.attachments.length > 0) {
|
|
1183
|
+
await session.promptWithAttachments(next.text, next.attachments);
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
await session.prompt(next.text);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
const decision = shouldStartAutopilotCycle({
|
|
1190
|
+
enabled: autopilot,
|
|
1191
|
+
cancelled: autopilotCancelled,
|
|
1192
|
+
planMode: session.getPlanMode(),
|
|
1193
|
+
workflowCommand,
|
|
1194
|
+
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1195
|
+
// Skip the review API call outright for turns that only started a
|
|
1196
|
+
// background process (dev server/watcher), ran a read-only lookup, or
|
|
1197
|
+
// committed/pushed — Ken's autopilot contract already IGNOREs these,
|
|
1198
|
+
// so there's no reason to pay for that verdict.
|
|
1199
|
+
mechanicalOnly: isMechanicalOnlyTurn(extractTurnToolCalls(session.getMessages(), messagesBefore)),
|
|
1200
|
+
});
|
|
1201
|
+
if (decision.start) {
|
|
1202
|
+
await runAutopilotCycle(next.text);
|
|
1039
1203
|
}
|
|
1040
|
-
if (
|
|
1041
|
-
|
|
1042
|
-
|
|
1204
|
+
else if (autopilot) {
|
|
1205
|
+
log("INFO", "app-sidecar", "autopilot skipped (queued turn)", {
|
|
1206
|
+
reason: decision.reason,
|
|
1207
|
+
});
|
|
1043
1208
|
}
|
|
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
1209
|
}
|
|
1052
|
-
broadcast("autopilot_capped", { rounds: MAX_AUTOPILOT_ROUNDS });
|
|
1053
1210
|
}
|
|
1054
1211
|
finally {
|
|
1055
|
-
|
|
1212
|
+
drainingStrandedQueue = false;
|
|
1056
1213
|
}
|
|
1057
1214
|
}
|
|
1058
1215
|
// ── Task runner (project task list → sessions) ──────────────
|
|
@@ -1066,6 +1223,7 @@ async function createSession(deps, opts) {
|
|
|
1066
1223
|
return false;
|
|
1067
1224
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1068
1225
|
await session.newSession();
|
|
1226
|
+
injectedAutopilotPrompts = [];
|
|
1069
1227
|
titleGenerated = false;
|
|
1070
1228
|
broadcast("session_reset", {});
|
|
1071
1229
|
markTaskInProgress(cwd, task.id);
|
|
@@ -1173,6 +1331,7 @@ async function createSession(deps, opts) {
|
|
|
1173
1331
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1174
1332
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
1175
1333
|
autopilot,
|
|
1334
|
+
...kenStatePayload(),
|
|
1176
1335
|
...footerExtras(),
|
|
1177
1336
|
});
|
|
1178
1337
|
return;
|
|
@@ -1197,6 +1356,7 @@ async function createSession(deps, opts) {
|
|
|
1197
1356
|
supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
|
|
1198
1357
|
supportsVideo: getModel(st.model)?.supportsVideo ?? false,
|
|
1199
1358
|
autopilot,
|
|
1359
|
+
...kenStatePayload(),
|
|
1200
1360
|
...footerExtras(),
|
|
1201
1361
|
},
|
|
1202
1362
|
})}\n\n`);
|
|
@@ -1371,9 +1531,37 @@ async function createSession(deps, opts) {
|
|
|
1371
1531
|
history.push({ role: "assistant", text: turn.reply, ken: true });
|
|
1372
1532
|
}
|
|
1373
1533
|
};
|
|
1534
|
+
// Autopilot verdict markers to interleave, same anchor scheme as Ken
|
|
1535
|
+
// turns — each becomes a single assistant row the webview renders
|
|
1536
|
+
// exactly like the live `autopilot` item (never a raw verdict string).
|
|
1537
|
+
const autopilotByCount = new Map();
|
|
1538
|
+
for (const marker of session.getAutopilotMarkers()) {
|
|
1539
|
+
const list = autopilotByCount.get(marker.afterMessageCount) ?? [];
|
|
1540
|
+
list.push(marker);
|
|
1541
|
+
autopilotByCount.set(marker.afterMessageCount, list);
|
|
1542
|
+
}
|
|
1543
|
+
const flushAutopilot = (count) => {
|
|
1544
|
+
const markers = autopilotByCount.get(count);
|
|
1545
|
+
if (!markers)
|
|
1546
|
+
return;
|
|
1547
|
+
autopilotByCount.delete(count);
|
|
1548
|
+
for (const marker of markers) {
|
|
1549
|
+
history.push({
|
|
1550
|
+
role: "assistant",
|
|
1551
|
+
text: "",
|
|
1552
|
+
autopilot: {
|
|
1553
|
+
phase: marker.phase,
|
|
1554
|
+
...(marker.reason !== undefined ? { reason: marker.reason } : {}),
|
|
1555
|
+
...(marker.body !== undefined ? { body: marker.body } : {}),
|
|
1556
|
+
},
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1374
1560
|
let nonSystemCount = 0;
|
|
1375
|
-
// Turns recorded before any build message (anchor 0) render at
|
|
1561
|
+
// Turns/markers recorded before any build message (anchor 0) render at
|
|
1562
|
+
// the top.
|
|
1376
1563
|
flushKen(0);
|
|
1564
|
+
flushAutopilot(0);
|
|
1377
1565
|
for (const msg of messages) {
|
|
1378
1566
|
if (msg.role === "system")
|
|
1379
1567
|
continue;
|
|
@@ -1467,14 +1655,19 @@ async function createSession(deps, opts) {
|
|
|
1467
1655
|
});
|
|
1468
1656
|
}
|
|
1469
1657
|
}
|
|
1470
|
-
// Interleave any Ken turns recorded right after
|
|
1658
|
+
// Interleave any Ken turns / autopilot markers recorded right after
|
|
1659
|
+
// this message.
|
|
1471
1660
|
flushKen(nonSystemCount);
|
|
1661
|
+
flushAutopilot(nonSystemCount);
|
|
1472
1662
|
}
|
|
1473
|
-
// Flush remaining Ken turns whose anchor is at/after
|
|
1474
|
-
// (e.g.
|
|
1475
|
-
// count after compaction shrank the history) so none
|
|
1663
|
+
// Flush remaining Ken turns / autopilot markers whose anchor is at/after
|
|
1664
|
+
// the message count (e.g. recorded before any build message, or anchors
|
|
1665
|
+
// beyond the current count after compaction shrank the history) so none
|
|
1666
|
+
// are dropped.
|
|
1476
1667
|
for (const count of [...kenByCount.keys()].sort((a, b) => a - b))
|
|
1477
1668
|
flushKen(count);
|
|
1669
|
+
for (const count of [...autopilotByCount.keys()].sort((a, b) => a - b))
|
|
1670
|
+
flushAutopilot(count);
|
|
1478
1671
|
json(res, 200, { history });
|
|
1479
1672
|
})();
|
|
1480
1673
|
return;
|
|
@@ -1537,6 +1730,14 @@ async function createSession(deps, opts) {
|
|
|
1537
1730
|
// Fresh user turn: clear any cancel flag left from a prior cycle so this
|
|
1538
1731
|
// turn's autopilot review can run.
|
|
1539
1732
|
autopilotCancelled = false;
|
|
1733
|
+
// Gate inputs captured around the run: whether this turn is a workflow
|
|
1734
|
+
// slash command (attachment prompts skip slash expansion entirely), and
|
|
1735
|
+
// how many assistant messages the run actually adds. Computed even when
|
|
1736
|
+
// autopilot is currently off — the toggle can flip ON mid-run, and the
|
|
1737
|
+
// gate reads the post-run value.
|
|
1738
|
+
const workflowCommand = attachments.length === 0 && isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
|
|
1739
|
+
const assistantsBefore = countAssistantMessages(session.getMessages());
|
|
1740
|
+
const messagesBefore = session.getMessages().length;
|
|
1540
1741
|
await runAgent(text, async () => {
|
|
1541
1742
|
if (attachments.length > 0) {
|
|
1542
1743
|
// Persist each attachment under .gg/uploads so files are inspectable
|
|
@@ -1552,11 +1753,36 @@ async function createSession(deps, opts) {
|
|
|
1552
1753
|
await session.prompt(text);
|
|
1553
1754
|
}
|
|
1554
1755
|
});
|
|
1555
|
-
// After the user's run settles, kick off Ken's auto-review loop
|
|
1556
|
-
//
|
|
1557
|
-
//
|
|
1558
|
-
|
|
1559
|
-
|
|
1756
|
+
// After the user's run settles, kick off Ken's auto-review loop — but
|
|
1757
|
+
// only when the turn is actually reviewable (shouldStartAutopilotCycle):
|
|
1758
|
+
// workflow commands (/compare, /bullet-proof, …) end with reports or
|
|
1759
|
+
// A/B/C choices reserved for the USER; registry commands (/help) and
|
|
1760
|
+
// failed runs add no assistant work to judge; a turn that ended in plan
|
|
1761
|
+
// mode has a pending Accept/Reject modal Ken must not preempt. This is
|
|
1762
|
+
// the ONLY entry point into the cycle besides the stranded-queue drain —
|
|
1763
|
+
// it drives any follow-up GG Coder runs itself, so the shared runAgent
|
|
1764
|
+
// finally never recurses.
|
|
1765
|
+
const decision = shouldStartAutopilotCycle({
|
|
1766
|
+
enabled: autopilot,
|
|
1767
|
+
cancelled: autopilotCancelled,
|
|
1768
|
+
planMode: session.getPlanMode(),
|
|
1769
|
+
workflowCommand,
|
|
1770
|
+
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1771
|
+
// Skip the review API call outright for turns that only started a
|
|
1772
|
+
// background process (dev server/watcher), ran a read-only lookup, or
|
|
1773
|
+
// committed/pushed — Ken's autopilot contract already IGNOREs these,
|
|
1774
|
+
// so there's no reason to pay for that verdict.
|
|
1775
|
+
mechanicalOnly: isMechanicalOnlyTurn(extractTurnToolCalls(session.getMessages(), messagesBefore)),
|
|
1776
|
+
});
|
|
1777
|
+
if (decision.start) {
|
|
1778
|
+
await runAutopilotCycle(text);
|
|
1779
|
+
}
|
|
1780
|
+
else if (autopilot) {
|
|
1781
|
+
log("INFO", "app-sidecar", "autopilot skipped", { reason: decision.reason });
|
|
1782
|
+
}
|
|
1783
|
+
// A prompt sent while Ken was reviewing (build idle) queued but had no
|
|
1784
|
+
// run to steer into — run it now as a fresh turn so it never strands.
|
|
1785
|
+
await runStrandedQueue();
|
|
1560
1786
|
});
|
|
1561
1787
|
return;
|
|
1562
1788
|
}
|
|
@@ -1587,7 +1813,7 @@ async function createSession(deps, opts) {
|
|
|
1587
1813
|
broadcast("ken_run_start", { text });
|
|
1588
1814
|
try {
|
|
1589
1815
|
const ken = await ensureKenSession();
|
|
1590
|
-
const digest = await buildKenContext(session, cwd, gitBranch, text);
|
|
1816
|
+
const digest = await buildKenContext(session, cwd, gitBranch, text, await loadWorkflowCommandSpecs(), injectedAutopilotPrompts);
|
|
1591
1817
|
await ken.prompt(digest);
|
|
1592
1818
|
// Record the turn against the BUILD session so it persists + survives
|
|
1593
1819
|
// resume (advisory custom entry, never an LLM message). Reply is Ken's
|
|
@@ -1785,8 +2011,12 @@ async function createSession(deps, opts) {
|
|
|
1785
2011
|
return;
|
|
1786
2012
|
}
|
|
1787
2013
|
await session.switchModel(target.provider, target.id);
|
|
1788
|
-
|
|
1789
|
-
|
|
2014
|
+
// Ken follows GG Coder's model only while un-pinned; a user-set Ken
|
|
2015
|
+
// override survives GG model switches untouched.
|
|
2016
|
+
if (!kenModelOverride) {
|
|
2017
|
+
await syncKenModel(target.provider, target.id);
|
|
2018
|
+
await syncKenAutoModel(target.provider, target.id);
|
|
2019
|
+
}
|
|
1790
2020
|
// Clamp the reasoning level to what the new model supports (mirrors the
|
|
1791
2021
|
// CLI): keep thinking on at the first supported tier if it was on but
|
|
1792
2022
|
// the prior level is unsupported here; leave it off if it was off.
|
|
@@ -1813,6 +2043,12 @@ async function createSession(deps, opts) {
|
|
|
1813
2043
|
// model_change is emitted by switchModel; follow with thinking_change so
|
|
1814
2044
|
// the footer toggle reflects the new model's supported levels.
|
|
1815
2045
|
broadcast("thinking_change", payload);
|
|
2046
|
+
// Un-pinned Ken just followed the switch — update his footer chip too.
|
|
2047
|
+
// When Ken is pinned, his effective model did not change, so skip the
|
|
2048
|
+
// no-op event (keeps footer/event tests from treating a GG switch as a
|
|
2049
|
+
// Ken switch).
|
|
2050
|
+
if (!kenModelOverride)
|
|
2051
|
+
broadcast("ken_model_change", kenStatePayload());
|
|
1816
2052
|
// The new model usually has a different context window — push extras so
|
|
1817
2053
|
// the footer's context meter rescales immediately.
|
|
1818
2054
|
broadcast("extras", footerExtras());
|
|
@@ -1820,6 +2056,54 @@ async function createSession(deps, opts) {
|
|
|
1820
2056
|
});
|
|
1821
2057
|
return;
|
|
1822
2058
|
}
|
|
2059
|
+
// Set or clear Ken's model pin. Body: { model: "<id>" } to pin, or
|
|
2060
|
+
// { model: null } / "" to clear (Ken resumes following GG Coder). Applies
|
|
2061
|
+
// to BOTH Ken sessions (chat + autopilot reviewer); a switch landing while
|
|
2062
|
+
// either is mid-run defers via the pending-model mechanics.
|
|
2063
|
+
if (method === "POST" && url === "/ken/model") {
|
|
2064
|
+
void readBody(req).then(async (raw) => {
|
|
2065
|
+
let modelId;
|
|
2066
|
+
try {
|
|
2067
|
+
const parsed = JSON.parse(raw).model;
|
|
2068
|
+
modelId = typeof parsed === "string" && parsed.trim() ? parsed.trim() : null;
|
|
2069
|
+
}
|
|
2070
|
+
catch {
|
|
2071
|
+
json(res, 400, { error: "invalid JSON body" });
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
if (modelId === null) {
|
|
2075
|
+
// Clear the pin → follow GG Coder again, syncing both sessions back.
|
|
2076
|
+
kenModelOverride = null;
|
|
2077
|
+
await saveKenModelPref(cwd, null);
|
|
2078
|
+
const st = session.getState();
|
|
2079
|
+
await syncKenModel(st.provider, st.model);
|
|
2080
|
+
await syncKenAutoModel(st.provider, st.model);
|
|
2081
|
+
log("INFO", "app-sidecar", "ken model pin cleared — following GG", {
|
|
2082
|
+
provider: st.provider,
|
|
2083
|
+
model: st.model,
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
else {
|
|
2087
|
+
const target = getModel(modelId);
|
|
2088
|
+
if (!target) {
|
|
2089
|
+
json(res, 404, { error: `unknown model: ${modelId}` });
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
kenModelOverride = { provider: target.provider, model: target.id };
|
|
2093
|
+
await saveKenModelPref(cwd, kenModelOverride);
|
|
2094
|
+
await syncKenModel(target.provider, target.id);
|
|
2095
|
+
await syncKenAutoModel(target.provider, target.id);
|
|
2096
|
+
log("INFO", "app-sidecar", "ken model pinned", {
|
|
2097
|
+
provider: target.provider,
|
|
2098
|
+
model: target.id,
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
const payload = kenStatePayload();
|
|
2102
|
+
broadcast("ken_model_change", payload);
|
|
2103
|
+
json(res, 200, payload);
|
|
2104
|
+
});
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
1823
2107
|
if (method === "POST" && url === "/kill") {
|
|
1824
2108
|
void readBody(req).then(async (raw) => {
|
|
1825
2109
|
let id;
|
|
@@ -1891,6 +2175,7 @@ async function createSession(deps, opts) {
|
|
|
1891
2175
|
void session
|
|
1892
2176
|
.newSession()
|
|
1893
2177
|
.then(() => {
|
|
2178
|
+
injectedAutopilotPrompts = [];
|
|
1894
2179
|
broadcast("session_reset", {});
|
|
1895
2180
|
json(res, 200, { ok: true });
|
|
1896
2181
|
})
|
|
@@ -1924,6 +2209,7 @@ async function createSession(deps, opts) {
|
|
|
1924
2209
|
}
|
|
1925
2210
|
try {
|
|
1926
2211
|
await session.newSession();
|
|
2212
|
+
injectedAutopilotPrompts = [];
|
|
1927
2213
|
titleGenerated = false;
|
|
1928
2214
|
await session.setApprovedPlan(planPath);
|
|
1929
2215
|
broadcast("session_reset", {});
|