@kenkaiiii/ggcoder 5.7.0 → 5.8.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 +335 -39
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/agent-session.d.ts +20 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +94 -17
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autopilot-cycle.d.ts +52 -17
- package/dist/core/autopilot-cycle.d.ts.map +1 -1
- package/dist/core/autopilot-cycle.js +53 -13
- package/dist/core/autopilot-cycle.js.map +1 -1
- package/dist/core/autopilot-cycle.test.js +195 -10
- package/dist/core/autopilot-cycle.test.js.map +1 -1
- package/dist/core/autopilot-gate.d.ts +12 -0
- package/dist/core/autopilot-gate.d.ts.map +1 -1
- package/dist/core/autopilot-gate.js +10 -1
- package/dist/core/autopilot-gate.js.map +1 -1
- package/dist/core/autopilot-gate.test.js +27 -2
- package/dist/core/autopilot-gate.test.js.map +1 -1
- package/dist/core/ken-context.d.ts +20 -0
- package/dist/core/ken-context.d.ts.map +1 -1
- package/dist/core/ken-context.js +46 -2
- package/dist/core/ken-context.js.map +1 -1
- package/dist/core/ken-context.test.js +52 -5
- package/dist/core/ken-context.test.js.map +1 -1
- package/dist/core/ken-prompt.js +14 -4
- package/dist/core/ken-prompt.js.map +1 -1
- package/dist/core/ken-prompt.test.js +20 -3
- package/dist/core/ken-prompt.test.js.map +1 -1
- package/dist/core/session-history.d.ts +51 -0
- package/dist/core/session-history.d.ts.map +1 -0
- package/dist/core/session-history.js +145 -0
- package/dist/core/session-history.js.map +1 -0
- package/dist/core/session-history.test.d.ts +2 -0
- package/dist/core/session-history.test.d.ts.map +1 -0
- package/dist/core/session-history.test.js +95 -0
- package/dist/core/session-history.test.js.map +1 -0
- package/dist/core/session-manager.d.ts +17 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +37 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +70 -1
- package/dist/core/session-manager.test.js.map +1 -1
- package/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -22,11 +22,12 @@ import { formatError } from "@kenkaiiii/gg-ai";
|
|
|
22
22
|
import { runJsonMode } from "./modes/json-mode.js";
|
|
23
23
|
import { AgentSession } from "./core/agent-session.js";
|
|
24
24
|
import { buildKenSystemPrompt, buildKenAutopilotSystemPrompt } from "./core/ken-prompt.js";
|
|
25
|
-
import { buildKenDigest, buildKenAutopilotContext } from "./core/ken-context.js";
|
|
25
|
+
import { buildKenDigest, buildKenAutopilotContext, buildKenAutopilotPlanContext, } from "./core/ken-context.js";
|
|
26
26
|
import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
|
|
27
27
|
import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
|
|
28
28
|
import { driveAutopilotCycle } from "./core/autopilot-cycle.js";
|
|
29
29
|
import { validateKenModelPref, effectiveKenModel } from "./core/ken-model.js";
|
|
30
|
+
import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeKenTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
|
|
30
31
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
31
32
|
import { MOONSHOT_OAUTH_KEY, XIAOMI_CREDITS_KEY } from "@kenkaiiii/gg-core";
|
|
32
33
|
import { loginAnthropic } from "./core/oauth/anthropic.js";
|
|
@@ -811,6 +812,16 @@ async function createSession(deps, opts) {
|
|
|
811
812
|
...(f.statusCode != null ? { statusCode: f.statusCode } : {}),
|
|
812
813
|
...(f.resetsAt != null ? { resetsAt: f.resetsAt } : {}),
|
|
813
814
|
});
|
|
815
|
+
// Persist the error row (display-only marker) so a resumed session shows
|
|
816
|
+
// the same headline/message/guidance the live run did. Best-effort.
|
|
817
|
+
void session
|
|
818
|
+
.persistAppMarker("error", {
|
|
819
|
+
scope: type,
|
|
820
|
+
headline: f.headline,
|
|
821
|
+
...(f.message ? { message: f.message } : {}),
|
|
822
|
+
guidance: f.guidance,
|
|
823
|
+
})
|
|
824
|
+
.catch(() => { });
|
|
814
825
|
}
|
|
815
826
|
// The session file path to resume (passed by the daemon's POST /session);
|
|
816
827
|
// empty/unset starts a fresh session.
|
|
@@ -835,6 +846,8 @@ async function createSession(deps, opts) {
|
|
|
835
846
|
onEnterPlan: async (reason) => {
|
|
836
847
|
await session.setPlanMode(true);
|
|
837
848
|
broadcast("plan_enter", { reason: reason ?? "" });
|
|
849
|
+
// Persist the plan-mode banner so a resumed session still shows it.
|
|
850
|
+
void session.persistAppMarker("plan", { reason: reason ?? "" }).catch(() => { });
|
|
838
851
|
},
|
|
839
852
|
onExitPlan: async (planPath) => {
|
|
840
853
|
await session.setPlanMode(false);
|
|
@@ -847,6 +860,12 @@ async function createSession(deps, opts) {
|
|
|
847
860
|
catch {
|
|
848
861
|
content = "";
|
|
849
862
|
}
|
|
863
|
+
// Record the submitted plan so the autopilot gate can route this turn
|
|
864
|
+
// into a PLAN review instead of a stale work review (plan mode is
|
|
865
|
+
// already false here, so the gate's planMode check alone never catches
|
|
866
|
+
// a submission). setPendingPlan bumps planGeneration, which invalidates
|
|
867
|
+
// any in-flight Ken plan review racing a user action.
|
|
868
|
+
setPendingPlan(planPath, content);
|
|
850
869
|
broadcast("plan_exit", { planPath, content });
|
|
851
870
|
return "Plan submitted for user review. Wait for the user to approve, reject, or dismiss it before implementing.";
|
|
852
871
|
},
|
|
@@ -940,6 +959,28 @@ async function createSession(deps, opts) {
|
|
|
940
959
|
// cycles drift into Ken reviewing against his own last prompt. Cleared
|
|
941
960
|
// whenever the conversation resets (new session / plan accept / task run).
|
|
942
961
|
let injectedAutopilotPrompts = [];
|
|
962
|
+
// The plan GG Coder submitted via exit_plan that still awaits a decision
|
|
963
|
+
// (Ken's auto-review in autopilot, or the user's modal). Path + the content
|
|
964
|
+
// read at submission time (fallback if the file becomes unreadable).
|
|
965
|
+
let pendingPlanPath = null;
|
|
966
|
+
let pendingPlanContent = "";
|
|
967
|
+
// Bumped on EVERY pending-plan set/clear. Ken's plan review captures it
|
|
968
|
+
// before reviewing and re-checks it before acting on the verdict, so a user
|
|
969
|
+
// Accept/Reject racing an in-flight review always wins — the stale verdict
|
|
970
|
+
// is discarded silently.
|
|
971
|
+
let planGeneration = 0;
|
|
972
|
+
function setPendingPlan(planPath, content) {
|
|
973
|
+
pendingPlanPath = planPath;
|
|
974
|
+
pendingPlanContent = content;
|
|
975
|
+
planGeneration++;
|
|
976
|
+
}
|
|
977
|
+
function clearPendingPlan() {
|
|
978
|
+
if (pendingPlanPath === null)
|
|
979
|
+
return;
|
|
980
|
+
pendingPlanPath = null;
|
|
981
|
+
pendingPlanContent = "";
|
|
982
|
+
planGeneration++;
|
|
983
|
+
}
|
|
943
984
|
// Workflow (prompt-template) commands: built-in + the project's custom
|
|
944
985
|
// `.gg/commands/*.md`. Used to gate autopilot off command turns and to label
|
|
945
986
|
// expanded templates in Ken's digests. Loaded fresh so a newly added custom
|
|
@@ -1209,6 +1250,60 @@ async function createSession(deps, opts) {
|
|
|
1209
1250
|
await syncKenAutoModel(pending.provider, pending.model);
|
|
1210
1251
|
}
|
|
1211
1252
|
}
|
|
1253
|
+
// One PLAN review: like runAutopilotReview but the digest carries the
|
|
1254
|
+
// submitted plan's markdown (`## Plan under review`) and the plan-review
|
|
1255
|
+
// instruction — Ken judges the plan itself, not finished work. Returns null
|
|
1256
|
+
// on failure; a failure caused by the user's own action racing the review
|
|
1257
|
+
// (cancel or a manual Accept/Reject that bumped planGeneration) stays
|
|
1258
|
+
// SILENT — no autopilot_error — because the user's decision already won.
|
|
1259
|
+
async function runAutopilotPlanReview(originalRequest) {
|
|
1260
|
+
const planPath = pendingPlanPath;
|
|
1261
|
+
if (planPath === null)
|
|
1262
|
+
return null;
|
|
1263
|
+
const genAtStart = planGeneration;
|
|
1264
|
+
autopilotReviewing = true;
|
|
1265
|
+
broadcast("autopilot_review_start", {});
|
|
1266
|
+
try {
|
|
1267
|
+
const ken = await ensureKenAutoSession();
|
|
1268
|
+
// Re-read the plan file (the run may have revised it in place); fall
|
|
1269
|
+
// back to the content captured at exit_plan time.
|
|
1270
|
+
const planContent = await fs.readFile(planPath, "utf-8").catch(() => pendingPlanContent);
|
|
1271
|
+
const digest = buildKenAutopilotPlanContext({
|
|
1272
|
+
cwd,
|
|
1273
|
+
gitBranch,
|
|
1274
|
+
messages: session.getMessages(),
|
|
1275
|
+
originalRequest,
|
|
1276
|
+
injectedPrompts: [...injectedAutopilotPrompts],
|
|
1277
|
+
workflowCommands: await loadWorkflowCommandSpecs(),
|
|
1278
|
+
planContent,
|
|
1279
|
+
});
|
|
1280
|
+
await ken.prompt(digest);
|
|
1281
|
+
if (autopilotCancelled || planGeneration !== genAtStart)
|
|
1282
|
+
return null;
|
|
1283
|
+
return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
|
|
1284
|
+
}
|
|
1285
|
+
catch (err) {
|
|
1286
|
+
// User action mid-review (manual Accept aborts the kenAuto run): drop
|
|
1287
|
+
// the review silently — the user's decision supersedes Ken's.
|
|
1288
|
+
if (autopilotCancelled || planGeneration !== genAtStart)
|
|
1289
|
+
return null;
|
|
1290
|
+
broadcastError("autopilot_error", "autopilot plan review failed", err);
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
finally {
|
|
1294
|
+
autopilotReviewing = false;
|
|
1295
|
+
// Apply any model switch that landed mid-review.
|
|
1296
|
+
const pending = pendingKenAutoModel;
|
|
1297
|
+
pendingKenAutoModel = null;
|
|
1298
|
+
if (pending)
|
|
1299
|
+
await syncKenAutoModel(pending.provider, pending.model);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
// The prompt fed to the fresh session after a plan is accepted — the SAME
|
|
1303
|
+
// string the webview sends on a manual Accept (see PlanReviewModal's accept
|
|
1304
|
+
// handler in gg-app/src/App.tsx). Keep the two in lockstep so auto- and
|
|
1305
|
+
// manual approval produce identical implementation turns.
|
|
1306
|
+
const IMPLEMENT_PLAN_PROMPT = "The plan has been approved. Implement it now, following each step in order.";
|
|
1212
1307
|
// Drive the review→prompt→review loop for one finished user turn. Only ever
|
|
1213
1308
|
// called after shouldStartAutopilotCycle approves the turn (POST /prompt or
|
|
1214
1309
|
// the stranded-queue drain) — never from the task runner, resume, /ken, or
|
|
@@ -1219,14 +1314,53 @@ async function createSession(deps, opts) {
|
|
|
1219
1314
|
if (!autopilot || autopilotCancelled)
|
|
1220
1315
|
return;
|
|
1221
1316
|
autopilotActive = true;
|
|
1317
|
+
// Generation captured by the last plan review; acceptPlan re-checks it so
|
|
1318
|
+
// a user Accept/Reject landing mid-review always wins.
|
|
1319
|
+
let planGenAtReview = -1;
|
|
1222
1320
|
try {
|
|
1223
1321
|
await driveAutopilotCycle({
|
|
1224
|
-
|
|
1322
|
+
// A plan-pending cycle needs extra rounds: approve+implement and the
|
|
1323
|
+
// post-implement work review each consume one, so +2 keeps a real fix
|
|
1324
|
+
// round available.
|
|
1325
|
+
maxRounds: pendingPlanPath !== null ? MAX_AUTOPILOT_ROUNDS + 2 : MAX_AUTOPILOT_ROUNDS,
|
|
1225
1326
|
isCancelled: () => autopilotCancelled,
|
|
1226
|
-
// An injected run entering plan mode
|
|
1227
|
-
//
|
|
1228
|
-
// plan-mode session
|
|
1327
|
+
// An injected run entering plan mode WITHOUT submitting (enter_plan,
|
|
1328
|
+
// no exit_plan) halts the cycle — Ken never prompts into a read-only
|
|
1329
|
+
// plan-mode session. A submitted plan takes the planPending branch.
|
|
1229
1330
|
isPlanMode: () => session.getPlanMode(),
|
|
1331
|
+
planPending: () => pendingPlanPath !== null,
|
|
1332
|
+
reviewPlan: async () => {
|
|
1333
|
+
planGenAtReview = planGeneration;
|
|
1334
|
+
return runAutopilotPlanReview(originalRequest);
|
|
1335
|
+
},
|
|
1336
|
+
// Auto-accept: the inlined POST /plan/accept body. Returns false when
|
|
1337
|
+
// the plan generation moved since the review (user acted) — the cycle
|
|
1338
|
+
// exits silently and the user's action stands.
|
|
1339
|
+
acceptPlan: async () => {
|
|
1340
|
+
if (pendingPlanPath === null || planGeneration !== planGenAtReview)
|
|
1341
|
+
return false;
|
|
1342
|
+
const planPath = pendingPlanPath;
|
|
1343
|
+
try {
|
|
1344
|
+
await session.newSession();
|
|
1345
|
+
injectedAutopilotPrompts = [];
|
|
1346
|
+
titleGenerated = false;
|
|
1347
|
+
await session.setApprovedPlan(planPath);
|
|
1348
|
+
}
|
|
1349
|
+
catch (err) {
|
|
1350
|
+
broadcastError("autopilot_error", "autopilot plan accept failed", err);
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
clearPendingPlan();
|
|
1354
|
+
// Ordering is load-bearing: the webview reads its still-open plan
|
|
1355
|
+
// modal state (step count) on autopilot_plan_accepted, and
|
|
1356
|
+
// session_reset clears it — accepted must land first.
|
|
1357
|
+
broadcast("autopilot_plan_accepted", {});
|
|
1358
|
+
broadcast("session_reset", {});
|
|
1359
|
+
// Persisted into the NEW session so a resume shows the marker.
|
|
1360
|
+
void session.persistAutopilotMarker("plan_approved");
|
|
1361
|
+
return true;
|
|
1362
|
+
},
|
|
1363
|
+
runImplement: () => runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(IMPLEMENT_PLAN_PROMPT)),
|
|
1230
1364
|
// Lean context per user turn: wipe prior review history so each new
|
|
1231
1365
|
// turn starts cheap, while within this cycle the few review messages
|
|
1232
1366
|
// persist so Ken remembers what he already asked GG Coder to fix.
|
|
@@ -1240,20 +1374,34 @@ async function createSession(deps, opts) {
|
|
|
1240
1374
|
// streams normally; the shared finally never re-triggers autopilot,
|
|
1241
1375
|
// so this can't recurse.
|
|
1242
1376
|
onInjected: (body, round) => {
|
|
1377
|
+
// A revision injection supersedes the pending plan — if the run
|
|
1378
|
+
// resubmits via exit_plan, onExitPlan re-sets it (no-op for work-
|
|
1379
|
+
// branch injections, where nothing is pending).
|
|
1380
|
+
clearPendingPlan();
|
|
1243
1381
|
injectedAutopilotPrompts.push(body);
|
|
1244
1382
|
broadcast("autopilot_prompted", { round, body });
|
|
1245
1383
|
void session.persistAutopilotMarker("prompted", { body });
|
|
1246
1384
|
},
|
|
1247
1385
|
runPrompt: (body) => runAgent(body, () => session.prompt(body)),
|
|
1248
1386
|
emit: (event) => {
|
|
1249
|
-
broadcast(event.type, event.data);
|
|
1250
1387
|
// Persist the terminal verdict marker so a resumed session renders the
|
|
1251
1388
|
// same Ken bubble the live run showed instead of dropping it or
|
|
1252
1389
|
// falling back to the raw verdict text (e.g. ALL_CLEAR).
|
|
1253
1390
|
if (event.type === "autopilot_done") {
|
|
1391
|
+
// Broadcast the SAME copySeed the persisted marker will produce on
|
|
1392
|
+
// resume, so the live all-clear wording matches the resumed one
|
|
1393
|
+
// (computed before persist — same synchronous message count).
|
|
1394
|
+
const seed = autopilotMarkerCopySeed({
|
|
1395
|
+
version: 1,
|
|
1396
|
+
phase: "done",
|
|
1397
|
+
afterMessageCount: session.getMessages().filter((m) => m.role !== "system").length,
|
|
1398
|
+
});
|
|
1399
|
+
broadcast(event.type, { ...event.data, copySeed: seed });
|
|
1254
1400
|
void session.persistAutopilotMarker("done");
|
|
1401
|
+
return;
|
|
1255
1402
|
}
|
|
1256
|
-
|
|
1403
|
+
broadcast(event.type, event.data);
|
|
1404
|
+
if (event.type === "autopilot_human") {
|
|
1257
1405
|
void session.persistAutopilotMarker("human", { reason: event.data.reason });
|
|
1258
1406
|
}
|
|
1259
1407
|
else if (event.type === "autopilot_capped") {
|
|
@@ -1291,6 +1439,9 @@ async function createSession(deps, opts) {
|
|
|
1291
1439
|
broadcast("queued", { count: session.getQueuedCount() });
|
|
1292
1440
|
if (!next.text.trim() && next.attachments.length === 0)
|
|
1293
1441
|
continue;
|
|
1442
|
+
// A queued message draining as a fresh turn supersedes any pending
|
|
1443
|
+
// plan, exactly like a direct POST /prompt turn.
|
|
1444
|
+
clearPendingPlan();
|
|
1294
1445
|
const workflowCommand = next.attachments.length === 0 &&
|
|
1295
1446
|
isWorkflowCommandText(next.text, await loadWorkflowCommandSpecs());
|
|
1296
1447
|
const assistantsBefore = countAssistantMessages(session.getMessages());
|
|
@@ -1307,6 +1458,9 @@ async function createSession(deps, opts) {
|
|
|
1307
1458
|
enabled: autopilot,
|
|
1308
1459
|
cancelled: autopilotCancelled,
|
|
1309
1460
|
planMode: session.getPlanMode(),
|
|
1461
|
+
// A submitted plan (exit_plan fired) routes into the PLAN review
|
|
1462
|
+
// branch — the cycle reviews the plan itself instead of skipping.
|
|
1463
|
+
planPending: pendingPlanPath !== null,
|
|
1310
1464
|
workflowCommand,
|
|
1311
1465
|
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1312
1466
|
// Skip the review API call outright for turns that only started a
|
|
@@ -1316,6 +1470,9 @@ async function createSession(deps, opts) {
|
|
|
1316
1470
|
mechanicalOnly: isMechanicalOnlyTurn(extractTurnToolCalls(session.getMessages(), messagesBefore)),
|
|
1317
1471
|
});
|
|
1318
1472
|
if (decision.start) {
|
|
1473
|
+
log("INFO", "app-sidecar", "autopilot cycle starting (queued turn)", {
|
|
1474
|
+
kind: decision.kind,
|
|
1475
|
+
});
|
|
1319
1476
|
await runAutopilotCycle(next.text);
|
|
1320
1477
|
}
|
|
1321
1478
|
else if (autopilot) {
|
|
@@ -1341,11 +1498,14 @@ async function createSession(deps, opts) {
|
|
|
1341
1498
|
// Fresh session per task so one task's context never bleeds into the next.
|
|
1342
1499
|
await session.newSession();
|
|
1343
1500
|
injectedAutopilotPrompts = [];
|
|
1501
|
+
clearPendingPlan();
|
|
1344
1502
|
titleGenerated = false;
|
|
1345
1503
|
broadcast("session_reset", {});
|
|
1346
1504
|
markTaskInProgress(cwd, task.id);
|
|
1347
1505
|
broadcast("tasks_list", { tasks: loadTasksSync(cwd) });
|
|
1348
1506
|
broadcast("task_start", { id: task.id, title: task.title });
|
|
1507
|
+
// Persist the task header so a resumed task session shows what ran.
|
|
1508
|
+
void session.persistAppMarker("task", { title: task.title }).catch(() => { });
|
|
1349
1509
|
const shortId = task.id.slice(0, 8);
|
|
1350
1510
|
const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
|
|
1351
1511
|
`tasks({ action: "done", id: "${shortId}" })`;
|
|
@@ -1636,8 +1796,11 @@ async function createSession(deps, opts) {
|
|
|
1636
1796
|
// they were recorded after, so each lands right after that message. A
|
|
1637
1797
|
// turn becomes two wire rows: the `@Ken` question (user) + Ken's reply
|
|
1638
1798
|
// (assistant), both flagged `ken` so the webview tints them.
|
|
1799
|
+
// Deduped; stale anchors are clamped to the last message (Ken turns
|
|
1800
|
+
// carry real conversation, so they render at the end instead of
|
|
1801
|
+
// vanishing).
|
|
1639
1802
|
const kenByCount = new Map();
|
|
1640
|
-
for (const turn of session.getKenTurns()) {
|
|
1803
|
+
for (const turn of normalizeKenTurnsForHistory(session.getKenTurns(), messages.filter((m) => m.role !== "system").length)) {
|
|
1641
1804
|
const list = kenByCount.get(turn.afterMessageCount) ?? [];
|
|
1642
1805
|
list.push(turn);
|
|
1643
1806
|
kenByCount.set(turn.afterMessageCount, list);
|
|
@@ -1655,8 +1818,12 @@ async function createSession(deps, opts) {
|
|
|
1655
1818
|
// Autopilot verdict markers to interleave, same anchor scheme as Ken
|
|
1656
1819
|
// turns — each becomes a single assistant row the webview renders
|
|
1657
1820
|
// exactly like the live `autopilot` item (never a raw verdict string).
|
|
1821
|
+
// Compact/continuation rewrites can carry old markers whose original
|
|
1822
|
+
// afterMessageCount is beyond the restored message list; dropping those
|
|
1823
|
+
// prevents stale all-clear bubbles from bunching at the bottom on resume.
|
|
1824
|
+
const restoredMessageCount = messages.filter((m) => m.role !== "system").length;
|
|
1658
1825
|
const autopilotByCount = new Map();
|
|
1659
|
-
for (const marker of session.getAutopilotMarkers()) {
|
|
1826
|
+
for (const marker of normalizeAutopilotMarkersForHistory(session.getAutopilotMarkers(), restoredMessageCount)) {
|
|
1660
1827
|
const list = autopilotByCount.get(marker.afterMessageCount) ?? [];
|
|
1661
1828
|
list.push(marker);
|
|
1662
1829
|
autopilotByCount.set(marker.afterMessageCount, list);
|
|
@@ -1674,15 +1841,77 @@ async function createSession(deps, opts) {
|
|
|
1674
1841
|
phase: marker.phase,
|
|
1675
1842
|
...(marker.reason !== undefined ? { reason: marker.reason } : {}),
|
|
1676
1843
|
...(marker.body !== undefined ? { body: marker.body } : {}),
|
|
1844
|
+
copySeed: marker.copySeed,
|
|
1677
1845
|
},
|
|
1678
1846
|
});
|
|
1679
1847
|
}
|
|
1680
1848
|
};
|
|
1849
|
+
// App transcript markers (plan banner / task header / error rows /
|
|
1850
|
+
// user-bubble hints), same anchor scheme. user_hint markers don't
|
|
1851
|
+
// become rows — they decorate the user row at their anchor instead.
|
|
1852
|
+
const appMarkersByCount = new Map();
|
|
1853
|
+
const userHintByCount = new Map();
|
|
1854
|
+
// Compaction-count markers pair with compacted summary rows in file
|
|
1855
|
+
// order (FIFO), not by anchor — the summary user message is what
|
|
1856
|
+
// positions the notice.
|
|
1857
|
+
const compactionCounts = [];
|
|
1858
|
+
for (const marker of normalizeAppMarkersForHistory(session.getAppMarkers(), restoredMessageCount)) {
|
|
1859
|
+
if (marker.kind === "user_hint") {
|
|
1860
|
+
userHintByCount.set(marker.afterMessageCount, marker.data);
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
if (marker.kind === "compaction") {
|
|
1864
|
+
const d = marker.data;
|
|
1865
|
+
if (typeof d.originalCount === "number" && typeof d.newCount === "number") {
|
|
1866
|
+
compactionCounts.push({ originalCount: d.originalCount, newCount: d.newCount });
|
|
1867
|
+
}
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
const list = appMarkersByCount.get(marker.afterMessageCount) ?? [];
|
|
1871
|
+
list.push(marker);
|
|
1872
|
+
appMarkersByCount.set(marker.afterMessageCount, list);
|
|
1873
|
+
}
|
|
1874
|
+
const flushAppMarkers = (count) => {
|
|
1875
|
+
const markers = appMarkersByCount.get(count);
|
|
1876
|
+
if (!markers)
|
|
1877
|
+
return;
|
|
1878
|
+
appMarkersByCount.delete(count);
|
|
1879
|
+
for (const marker of markers) {
|
|
1880
|
+
const d = marker.data;
|
|
1881
|
+
if (marker.kind === "plan") {
|
|
1882
|
+
history.push({
|
|
1883
|
+
role: "assistant",
|
|
1884
|
+
text: "",
|
|
1885
|
+
plan: { reason: typeof d.reason === "string" ? d.reason : "" },
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
else if (marker.kind === "task") {
|
|
1889
|
+
history.push({
|
|
1890
|
+
role: "assistant",
|
|
1891
|
+
text: "",
|
|
1892
|
+
task: { title: typeof d.title === "string" ? d.title : "" },
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
else if (marker.kind === "error" && typeof d.headline === "string") {
|
|
1896
|
+
history.push({
|
|
1897
|
+
role: "assistant",
|
|
1898
|
+
text: "",
|
|
1899
|
+
error: {
|
|
1900
|
+
scope: typeof d.scope === "string" ? d.scope : "error",
|
|
1901
|
+
headline: d.headline,
|
|
1902
|
+
...(typeof d.message === "string" ? { message: d.message } : {}),
|
|
1903
|
+
...(typeof d.guidance === "string" ? { guidance: d.guidance } : {}),
|
|
1904
|
+
},
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1681
1909
|
let nonSystemCount = 0;
|
|
1682
1910
|
// Turns/markers recorded before any build message (anchor 0) render at
|
|
1683
1911
|
// the top.
|
|
1684
1912
|
flushKen(0);
|
|
1685
1913
|
flushAutopilot(0);
|
|
1914
|
+
flushAppMarkers(0);
|
|
1686
1915
|
for (const msg of messages) {
|
|
1687
1916
|
if (msg.role === "system")
|
|
1688
1917
|
continue;
|
|
@@ -1732,30 +1961,54 @@ async function createSession(deps, opts) {
|
|
|
1732
1961
|
}
|
|
1733
1962
|
continue;
|
|
1734
1963
|
}
|
|
1735
|
-
// User or assistant message —
|
|
1736
|
-
//
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1964
|
+
// User or assistant message — text/hook/command/compacted extraction,
|
|
1965
|
+
// plus sub-agent group detection for assistant tool_calls.
|
|
1966
|
+
if (msg.role === "user") {
|
|
1967
|
+
// Rebuild the live bubble: strip the steering wrapper, drop
|
|
1968
|
+
// attachment/file notes the model saw but the bubble never showed.
|
|
1969
|
+
const restored = restoreUserRow(msg.content);
|
|
1970
|
+
const text = restored.text;
|
|
1971
|
+
const hook = detectHookKind(text);
|
|
1972
|
+
const compacted = !hook && text.startsWith("[Previous conversation summary]");
|
|
1973
|
+
const command = !hook && !compacted ? detectPromptCommand(text, commandCandidates) : null;
|
|
1974
|
+
if (text.trim() || restored.images.length > 0) {
|
|
1975
|
+
const hint = userHintByCount.get(nonSystemCount);
|
|
1976
|
+
history.push({
|
|
1977
|
+
role: "user",
|
|
1978
|
+
text: command ?? text,
|
|
1979
|
+
images: restored.images,
|
|
1980
|
+
hook,
|
|
1981
|
+
command: command !== null,
|
|
1982
|
+
compacted,
|
|
1983
|
+
// Markers accumulate across continuation files (each rewrite
|
|
1984
|
+
// re-persists prior ones) but only the LATEST summary row
|
|
1985
|
+
// survives compaction — so consume from the newest end.
|
|
1986
|
+
...(compacted && compactionCounts.length > 0
|
|
1987
|
+
? { compactionCounts: compactionCounts.pop() }
|
|
1988
|
+
: {}),
|
|
1989
|
+
...(hint?.kenSent === true ? { kenSent: true } : {}),
|
|
1990
|
+
...(Array.isArray(hint?.enhancements) ? { enhancements: hint.enhancements } : {}),
|
|
1991
|
+
});
|
|
1992
|
+
// Live showed the video-capability warning right after the bubble.
|
|
1993
|
+
if (restored.videoWarning) {
|
|
1994
|
+
history.push({ role: "assistant", text: "", infoKind: "video_warning" });
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
else {
|
|
1999
|
+
// Assistant: one wire row per persisted text block — live streaming
|
|
2000
|
+
// splits bubbles at server_tool_call boundaries, and the persisted
|
|
2001
|
+
// content keeps those blocks separate.
|
|
2002
|
+
for (const blockText of restoreAssistantTexts(msg.content)) {
|
|
2003
|
+
history.push({
|
|
2004
|
+
role: "assistant",
|
|
2005
|
+
text: blockText,
|
|
2006
|
+
images: [],
|
|
2007
|
+
hook: null,
|
|
2008
|
+
command: false,
|
|
2009
|
+
compacted: false,
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
1759
2012
|
}
|
|
1760
2013
|
// Assistant tool_call blocks: detect sub-agent delegations.
|
|
1761
2014
|
if (msg.role === "assistant" && typeof msg.content !== "string") {
|
|
@@ -1776,19 +2029,21 @@ async function createSession(deps, opts) {
|
|
|
1776
2029
|
});
|
|
1777
2030
|
}
|
|
1778
2031
|
}
|
|
1779
|
-
// Interleave any Ken turns / autopilot markers recorded right
|
|
1780
|
-
// this message.
|
|
2032
|
+
// Interleave any Ken turns / autopilot / app markers recorded right
|
|
2033
|
+
// after this message.
|
|
1781
2034
|
flushKen(nonSystemCount);
|
|
1782
2035
|
flushAutopilot(nonSystemCount);
|
|
2036
|
+
flushAppMarkers(nonSystemCount);
|
|
1783
2037
|
}
|
|
1784
|
-
// Flush remaining Ken turns
|
|
1785
|
-
//
|
|
1786
|
-
//
|
|
1787
|
-
// are dropped.
|
|
2038
|
+
// Flush remaining Ken turns whose anchor is at/after the message count so
|
|
2039
|
+
// none are dropped. Autopilot/app markers beyond the restored message
|
|
2040
|
+
// count were already filtered above; any remaining marker here is valid.
|
|
1788
2041
|
for (const count of [...kenByCount.keys()].sort((a, b) => a - b))
|
|
1789
2042
|
flushKen(count);
|
|
1790
2043
|
for (const count of [...autopilotByCount.keys()].sort((a, b) => a - b))
|
|
1791
2044
|
flushAutopilot(count);
|
|
2045
|
+
for (const count of [...appMarkersByCount.keys()].sort((a, b) => a - b))
|
|
2046
|
+
flushAppMarkers(count);
|
|
1792
2047
|
json(res, 200, { history });
|
|
1793
2048
|
})();
|
|
1794
2049
|
return;
|
|
@@ -1821,10 +2076,12 @@ async function createSession(deps, opts) {
|
|
|
1821
2076
|
void readBody(req).then(async (raw) => {
|
|
1822
2077
|
let text;
|
|
1823
2078
|
let attachments;
|
|
2079
|
+
let meta;
|
|
1824
2080
|
try {
|
|
1825
2081
|
const body = JSON.parse(raw);
|
|
1826
2082
|
text = body.text ?? "";
|
|
1827
2083
|
attachments = Array.isArray(body.attachments) ? body.attachments : [];
|
|
2084
|
+
meta = typeof body.meta === "object" && body.meta !== null ? body.meta : undefined;
|
|
1828
2085
|
}
|
|
1829
2086
|
catch {
|
|
1830
2087
|
json(res, 400, { error: "invalid JSON body" });
|
|
@@ -1848,9 +2105,25 @@ async function createSession(deps, opts) {
|
|
|
1848
2105
|
return;
|
|
1849
2106
|
}
|
|
1850
2107
|
json(res, 202, { accepted: true });
|
|
2108
|
+
// Webview display hint for this prompt's user bubble (kenSent shimmer
|
|
2109
|
+
// label / enhancer highlight segments). Anchored +1 so it attaches to
|
|
2110
|
+
// the user message the prompt below is about to push. Queued prompts
|
|
2111
|
+
// skip this (their position in the run is unpredictable).
|
|
2112
|
+
if (meta && (meta.kenSent === true || Array.isArray(meta.enhancements))) {
|
|
2113
|
+
void session
|
|
2114
|
+
.persistAppMarker("user_hint", {
|
|
2115
|
+
...(meta.kenSent === true ? { kenSent: true } : {}),
|
|
2116
|
+
...(Array.isArray(meta.enhancements) ? { enhancements: meta.enhancements } : {}),
|
|
2117
|
+
}, 1)
|
|
2118
|
+
.catch(() => { });
|
|
2119
|
+
}
|
|
1851
2120
|
// Fresh user turn: clear any cancel flag left from a prior cycle so this
|
|
1852
2121
|
// turn's autopilot review can run.
|
|
1853
2122
|
autopilotCancelled = false;
|
|
2123
|
+
// A typed message while a plan modal/review is pending (reject,
|
|
2124
|
+
// feedback, anything) supersedes the pending plan — the bump also
|
|
2125
|
+
// invalidates any in-flight Ken plan review.
|
|
2126
|
+
clearPendingPlan();
|
|
1854
2127
|
// Gate inputs captured around the run: whether this turn is a workflow
|
|
1855
2128
|
// slash command (attachment prompts skip slash expansion entirely), and
|
|
1856
2129
|
// how many assistant messages the run actually adds. Computed even when
|
|
@@ -1887,6 +2160,9 @@ async function createSession(deps, opts) {
|
|
|
1887
2160
|
enabled: autopilot,
|
|
1888
2161
|
cancelled: autopilotCancelled,
|
|
1889
2162
|
planMode: session.getPlanMode(),
|
|
2163
|
+
// A submitted plan (exit_plan fired) routes into the PLAN review
|
|
2164
|
+
// branch — the cycle reviews the plan itself instead of skipping.
|
|
2165
|
+
planPending: pendingPlanPath !== null,
|
|
1890
2166
|
workflowCommand,
|
|
1891
2167
|
assistantMessagesAdded: countAssistantMessages(session.getMessages()) - assistantsBefore,
|
|
1892
2168
|
// Skip the review API call outright for turns that only started a
|
|
@@ -1896,6 +2172,7 @@ async function createSession(deps, opts) {
|
|
|
1896
2172
|
mechanicalOnly: isMechanicalOnlyTurn(extractTurnToolCalls(session.getMessages(), messagesBefore)),
|
|
1897
2173
|
});
|
|
1898
2174
|
if (decision.start) {
|
|
2175
|
+
log("INFO", "app-sidecar", "autopilot cycle starting", { kind: decision.kind });
|
|
1899
2176
|
await runAutopilotCycle(text);
|
|
1900
2177
|
}
|
|
1901
2178
|
else if (autopilot) {
|
|
@@ -2298,6 +2575,7 @@ async function createSession(deps, opts) {
|
|
|
2298
2575
|
.newSession()
|
|
2299
2576
|
.then(() => {
|
|
2300
2577
|
injectedAutopilotPrompts = [];
|
|
2578
|
+
clearPendingPlan();
|
|
2301
2579
|
broadcast("session_reset", {});
|
|
2302
2580
|
json(res, 200, { ok: true });
|
|
2303
2581
|
})
|
|
@@ -2329,6 +2607,24 @@ async function createSession(deps, opts) {
|
|
|
2329
2607
|
json(res, 409, { error: "cannot accept a plan while the agent is running" });
|
|
2330
2608
|
return;
|
|
2331
2609
|
}
|
|
2610
|
+
// Manual accept, possibly racing Ken's autopilot plan review: the user
|
|
2611
|
+
// always wins. Bump the plan generation (invalidates any in-flight
|
|
2612
|
+
// review's verdict), stop the cycle, abort a mid-prompt review on the
|
|
2613
|
+
// kenAuto session, and clear the spinner — autopilot_ignored renders
|
|
2614
|
+
// nothing, so no stale "approve or reject" bubble ever lands. The
|
|
2615
|
+
// webview's follow-up "implement" /prompt arrives as a fresh turn
|
|
2616
|
+
// (resetting autopilotCancelled), so the implementation still gets its
|
|
2617
|
+
// normal post-run review; if it lands while the cycle is winding down
|
|
2618
|
+
// it queues and runStrandedQueue drains it as a fresh turn.
|
|
2619
|
+
clearPendingPlan();
|
|
2620
|
+
autopilotCancelled = true;
|
|
2621
|
+
kenAutoAbort.abort();
|
|
2622
|
+
kenAutoAbort = new AbortController();
|
|
2623
|
+
kenAutoSession?.setSignal(kenAutoAbort.signal);
|
|
2624
|
+
if (autopilotReviewing) {
|
|
2625
|
+
autopilotReviewing = false;
|
|
2626
|
+
broadcast("autopilot_ignored", {});
|
|
2627
|
+
}
|
|
2332
2628
|
try {
|
|
2333
2629
|
await session.newSession();
|
|
2334
2630
|
injectedAutopilotPrompts = [];
|