@kenkaiiii/ggcoder 5.22.0 → 5.22.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.
@@ -35,6 +35,7 @@ import { validateKenModelPref, effectiveKenModel } from "./core/ken-model.js";
35
35
  import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeKenTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
36
36
  import { AuthStorage } from "./core/auth-storage.js";
37
37
  import { cleanupToolOutputs } from "./tools/overflow.js";
38
+ import { readCappedBody } from "./utils/http-body.js";
38
39
  import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@kenkaiiii/gg-core";
39
40
  import { loginAnthropic } from "./core/oauth/anthropic.js";
40
41
  import { loginOpenAI } from "./core/oauth/openai.js";
@@ -240,6 +241,9 @@ async function prepareAttachments(cwd, attachments) {
240
241
  return out;
241
242
  }
242
243
  const FILE_SEARCH_LIMIT = 20;
244
+ // Upper bound on files walked per search (baseline #8 memory cap). Far above the
245
+ // 20-result output limit, so relevance/recency ranking is unaffected in practice.
246
+ const FILE_SEARCH_SCAN_CAP = 50_000;
243
247
  /** Score a candidate path against a lowercased query. Higher is better; a
244
248
  * negative score means "no match". Basename hits beat path hits; prefix beats
245
249
  * substring; shorter paths break ties. */
@@ -292,8 +296,13 @@ async function searchProjectFiles(cwd, rawQuery) {
292
296
  }
293
297
  const ig = ignore.default().add(gitignore);
294
298
  // `stats: true` gives mtime without a second stat pass, so the empty-query
295
- // "recent files" path is a single walk.
296
- const entries = await fg.default("**/*", {
299
+ // "recent files" path is a single walk. Stream + hard scan cap (baseline #8):
300
+ // collecting the full result array retained ~0.9 MB per 1k files (18.5 MB at
301
+ // 20k), unbounded. Bail after FILE_SEARCH_SCAN_CAP entries so a giant repo
302
+ // can't balloon RSS; the newest/most-relevant matches still surface because
303
+ // the cap is far above the 20-result output limit.
304
+ const entries = [];
305
+ const scanStream = fg.default.stream("**/*", {
297
306
  cwd,
298
307
  dot: false,
299
308
  onlyFiles: true,
@@ -302,6 +311,17 @@ async function searchProjectFiles(cwd, rawQuery) {
302
311
  followSymbolicLinks: false,
303
312
  stats: true,
304
313
  });
314
+ for await (const entry of scanStream) {
315
+ entries.push(entry);
316
+ if (entries.length >= FILE_SEARCH_SCAN_CAP) {
317
+ scanStream.destroy();
318
+ log("DEBUG", "app-sidecar", "file search scan cap hit", {
319
+ cap: String(FILE_SEARCH_SCAN_CAP),
320
+ cwd,
321
+ });
322
+ break;
323
+ }
324
+ }
305
325
  const files = entries.filter((e) => !ig.ignores(e.path));
306
326
  if (!query) {
307
327
  return files
@@ -469,13 +489,8 @@ async function runJsonModeIfRequested() {
469
489
  // ── Daemon-level HTTP helpers (shared by the session-management routes) ─────
470
490
  // The per-session route table has its own local copies; these serve the
471
491
  // daemon's own POST /session / DELETE /session routes.
472
- function daemonReadBody(req) {
473
- return new Promise((resolve, reject) => {
474
- const chunks = [];
475
- req.on("data", (c) => chunks.push(c));
476
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
477
- req.on("error", reject);
478
- });
492
+ function daemonReadBody(req, res) {
493
+ return readCappedBody(req, res);
479
494
  }
480
495
  function daemonJson(res, status, body) {
481
496
  res.writeHead(status, {
@@ -679,7 +694,9 @@ async function main() {
679
694
  // ── Daemon-level routes (session lifecycle) ──────────────────────────
680
695
  // Create a session for a window: { mode?, cwd, sessionPath? } → { sessionId }.
681
696
  if (method === "POST" && url === "/session") {
682
- void daemonReadBody(req).then(async (raw) => {
697
+ void daemonReadBody(req, res).then(async (raw) => {
698
+ if (raw === null)
699
+ return;
683
700
  let body = {};
684
701
  try {
685
702
  body = raw ? JSON.parse(raw) : {};
@@ -777,6 +794,8 @@ async function main() {
777
794
  // Radio playback is app-wide (one stream across all windows), so it stops
778
795
  // at the daemon level, not per session.
779
796
  stopRadio();
797
+ // Close the ~/.gg progress fs.watch handle (baseline #8 leak fix).
798
+ progress.dispose();
780
799
  await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
781
800
  server.close();
782
801
  process.exit(0);
@@ -914,6 +933,7 @@ async function createProgressManager(agentDir, broadcastAll) {
914
933
  // Watch ~/.gg (dir watch survives the atomic tmp+rename) for progress.json
915
934
  // writes from other daemon processes; debounce, reload read-only, dedupe by nonce.
916
935
  let watchDebounce = null;
936
+ let progressWatcher = null;
917
937
  try {
918
938
  const watcher = fsWatch(agentDir, (_event, filename) => {
919
939
  if (filename !== "progress.json")
@@ -935,6 +955,7 @@ async function createProgressManager(agentDir, broadcastAll) {
935
955
  }, 150);
936
956
  });
937
957
  watcher.unref();
958
+ progressWatcher = watcher;
938
959
  }
939
960
  catch (err) {
940
961
  captureSidecarError(err, "app-sidecar.progress.watch");
@@ -942,7 +963,22 @@ async function createProgressManager(agentDir, broadcastAll) {
942
963
  message: err instanceof Error ? err.message : String(err),
943
964
  });
944
965
  }
945
- return { snapshot, awardRun };
966
+ // Dispose closes the fs.watch handle (baseline #8: it was previously never
967
+ // closed — a per-daemon leak) and clears any pending debounce timer.
968
+ function dispose() {
969
+ if (watchDebounce) {
970
+ clearTimeout(watchDebounce);
971
+ watchDebounce = null;
972
+ }
973
+ try {
974
+ progressWatcher?.close();
975
+ }
976
+ catch {
977
+ // Already closed / never opened — nothing to do.
978
+ }
979
+ progressWatcher = null;
980
+ }
981
+ return { snapshot, awardRun, dispose };
946
982
  }
947
983
  /**
948
984
  * Build one in-process agent session: its AgentSession, SSE client set, event
@@ -2057,13 +2093,8 @@ async function createSession(deps, opts) {
2057
2093
  gitPoll.unref?.();
2058
2094
  };
2059
2095
  scheduleGitPoll(5000);
2060
- function readBody(req) {
2061
- return new Promise((resolve, reject) => {
2062
- const chunks = [];
2063
- req.on("data", (c) => chunks.push(c));
2064
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
2065
- req.on("error", reject);
2066
- });
2096
+ function readBody(req, res) {
2097
+ return readCappedBody(req, res);
2067
2098
  }
2068
2099
  function json(res, status, body) {
2069
2100
  const payload = JSON.stringify(redactValue(body, { secrets: environmentSecrets(process.env) }));
@@ -2204,7 +2235,9 @@ async function createSession(deps, opts) {
2204
2235
  return;
2205
2236
  }
2206
2237
  if (method === "POST" && url === "/settings") {
2207
- void readBody(req).then(async (raw) => {
2238
+ void readBody(req, res).then(async (raw) => {
2239
+ if (raw === null)
2240
+ return;
2208
2241
  let projectsRoot;
2209
2242
  try {
2210
2243
  projectsRoot = JSON.parse(raw).projectsRoot ?? "";
@@ -2227,7 +2260,9 @@ async function createSession(deps, opts) {
2227
2260
  return;
2228
2261
  }
2229
2262
  if (method === "POST" && url === "/create-project") {
2230
- void readBody(req).then(async (raw) => {
2263
+ void readBody(req, res).then(async (raw) => {
2264
+ if (raw === null)
2265
+ return;
2231
2266
  let name;
2232
2267
  try {
2233
2268
  name = JSON.parse(raw).name ?? "";
@@ -2640,7 +2675,9 @@ async function createSession(deps, opts) {
2640
2675
  return;
2641
2676
  }
2642
2677
  if (method === "POST" && url === "/prompt") {
2643
- void readBody(req).then(async (raw) => {
2678
+ void readBody(req, res).then(async (raw) => {
2679
+ if (raw === null)
2680
+ return;
2644
2681
  let text;
2645
2682
  let attachments;
2646
2683
  let meta;
@@ -2767,7 +2804,9 @@ async function createSession(deps, opts) {
2767
2804
  json(res, 404, { error: "Ken is not available in GG Chat." });
2768
2805
  return;
2769
2806
  }
2770
- void readBody(req).then(async (raw) => {
2807
+ void readBody(req, res).then(async (raw) => {
2808
+ if (raw === null)
2809
+ return;
2771
2810
  let text;
2772
2811
  try {
2773
2812
  text = JSON.parse(raw).text ?? "";
@@ -2826,7 +2865,9 @@ async function createSession(deps, opts) {
2826
2865
  json(res, 404, { error: "Autopilot is not available in GG Chat." });
2827
2866
  return;
2828
2867
  }
2829
- void readBody(req).then(async (raw) => {
2868
+ void readBody(req, res).then(async (raw) => {
2869
+ if (raw === null)
2870
+ return;
2830
2871
  let enabled;
2831
2872
  try {
2832
2873
  enabled = Boolean(JSON.parse(raw).enabled);
@@ -2847,7 +2888,9 @@ async function createSession(deps, opts) {
2847
2888
  return;
2848
2889
  }
2849
2890
  if (method === "POST" && url === "/enhance") {
2850
- void readBody(req).then(async (raw) => {
2891
+ void readBody(req, res).then(async (raw) => {
2892
+ if (raw === null)
2893
+ return;
2851
2894
  let text;
2852
2895
  try {
2853
2896
  text = JSON.parse(raw).text ?? "";
@@ -2896,7 +2939,9 @@ async function createSession(deps, opts) {
2896
2939
  return;
2897
2940
  }
2898
2941
  if (method === "POST" && url === "/radio/volume") {
2899
- void readBody(req).then((raw) => {
2942
+ void readBody(req, res).then((raw) => {
2943
+ if (raw === null)
2944
+ return;
2900
2945
  let volume;
2901
2946
  try {
2902
2947
  volume = Number(JSON.parse(raw).volume);
@@ -2919,7 +2964,9 @@ async function createSession(deps, opts) {
2919
2964
  return;
2920
2965
  }
2921
2966
  if (method === "POST" && url === "/radio") {
2922
- void readBody(req).then((raw) => {
2967
+ void readBody(req, res).then((raw) => {
2968
+ if (raw === null)
2969
+ return;
2923
2970
  let station;
2924
2971
  try {
2925
2972
  station = JSON.parse(raw).station ?? "";
@@ -2943,7 +2990,9 @@ async function createSession(deps, opts) {
2943
2990
  return;
2944
2991
  }
2945
2992
  if (method === "POST" && url === "/tasks/run") {
2946
- void readBody(req).then((raw) => {
2993
+ void readBody(req, res).then((raw) => {
2994
+ if (raw === null)
2995
+ return;
2947
2996
  let id;
2948
2997
  let all;
2949
2998
  try {
@@ -2965,7 +3014,9 @@ async function createSession(deps, opts) {
2965
3014
  return;
2966
3015
  }
2967
3016
  if (method === "POST" && url === "/tasks/delete") {
2968
- void readBody(req).then((raw) => {
3017
+ void readBody(req, res).then((raw) => {
3018
+ if (raw === null)
3019
+ return;
2969
3020
  let id;
2970
3021
  try {
2971
3022
  id = JSON.parse(raw).id ?? "";
@@ -3003,7 +3054,9 @@ async function createSession(deps, opts) {
3003
3054
  return;
3004
3055
  }
3005
3056
  if (method === "POST" && url === "/model") {
3006
- void readBody(req).then(async (raw) => {
3057
+ void readBody(req, res).then(async (raw) => {
3058
+ if (raw === null)
3059
+ return;
3007
3060
  let modelId;
3008
3061
  try {
3009
3062
  modelId = JSON.parse(raw).model ?? "";
@@ -3072,7 +3125,9 @@ async function createSession(deps, opts) {
3072
3125
  // to BOTH Ken sessions (chat + autopilot reviewer); a switch landing while
3073
3126
  // either is mid-run defers via the pending-model mechanics.
3074
3127
  if (method === "POST" && url === "/ken/model") {
3075
- void readBody(req).then(async (raw) => {
3128
+ void readBody(req, res).then(async (raw) => {
3129
+ if (raw === null)
3130
+ return;
3076
3131
  let modelId;
3077
3132
  try {
3078
3133
  const parsed = JSON.parse(raw).model;
@@ -3116,7 +3171,9 @@ async function createSession(deps, opts) {
3116
3171
  return;
3117
3172
  }
3118
3173
  if (method === "POST" && url === "/kill") {
3119
- void readBody(req).then(async (raw) => {
3174
+ void readBody(req, res).then(async (raw) => {
3175
+ if (raw === null)
3176
+ return;
3120
3177
  let id;
3121
3178
  try {
3122
3179
  id = JSON.parse(raw).id ?? "";
@@ -3237,7 +3294,9 @@ async function createSession(deps, opts) {
3237
3294
  // 3. session_reset tells the webview to clear its transcript; it then runs
3238
3295
  // the "implement it now" prompt in the clean session.
3239
3296
  if (method === "POST" && url === "/plan/accept") {
3240
- void readBody(req).then(async (raw) => {
3297
+ void readBody(req, res).then(async (raw) => {
3298
+ if (raw === null)
3299
+ return;
3241
3300
  let planPath;
3242
3301
  try {
3243
3302
  planPath = JSON.parse(raw).planPath || undefined;
@@ -3289,7 +3348,9 @@ async function createSession(deps, opts) {
3289
3348
  return;
3290
3349
  }
3291
3350
  if (method === "POST" && url === "/auth/apikey") {
3292
- void readBody(req).then(async (raw) => {
3351
+ void readBody(req, res).then(async (raw) => {
3352
+ if (raw === null)
3353
+ return;
3293
3354
  let provider = "";
3294
3355
  let key;
3295
3356
  let variant;
@@ -3332,7 +3393,9 @@ async function createSession(deps, opts) {
3332
3393
  return;
3333
3394
  }
3334
3395
  if (method === "POST" && url === "/auth/oauth/start") {
3335
- void readBody(req).then((raw) => {
3396
+ void readBody(req, res).then((raw) => {
3397
+ if (raw === null)
3398
+ return;
3336
3399
  let provider = "";
3337
3400
  try {
3338
3401
  provider = JSON.parse(raw).provider ?? "";
@@ -3389,7 +3452,9 @@ async function createSession(deps, opts) {
3389
3452
  return;
3390
3453
  }
3391
3454
  if (method === "POST" && url === "/auth/oauth/code") {
3392
- void readBody(req).then((raw) => {
3455
+ void readBody(req, res).then((raw) => {
3456
+ if (raw === null)
3457
+ return;
3393
3458
  let code;
3394
3459
  try {
3395
3460
  code = JSON.parse(raw).code ?? "";
@@ -3409,7 +3474,9 @@ async function createSession(deps, opts) {
3409
3474
  return;
3410
3475
  }
3411
3476
  if (method === "POST" && url === "/auth/logout") {
3412
- void readBody(req).then(async (raw) => {
3477
+ void readBody(req, res).then(async (raw) => {
3478
+ if (raw === null)
3479
+ return;
3413
3480
  let provider;
3414
3481
  try {
3415
3482
  provider = JSON.parse(raw).provider ?? "";
@@ -3448,7 +3515,9 @@ async function createSession(deps, opts) {
3448
3515
  return;
3449
3516
  }
3450
3517
  if (method === "POST" && url === "/telegram") {
3451
- void readBody(req).then(async (raw) => {
3518
+ void readBody(req, res).then(async (raw) => {
3519
+ if (raw === null)
3520
+ return;
3452
3521
  let botTokenInput;
3453
3522
  let userIdInput;
3454
3523
  try {
@@ -3552,7 +3621,9 @@ async function createSession(deps, opts) {
3552
3621
  return;
3553
3622
  }
3554
3623
  if (method === "POST" && url === "/mcp/add") {
3555
- void readBody(req).then(async (raw) => {
3624
+ void readBody(req, res).then(async (raw) => {
3625
+ if (raw === null)
3626
+ return;
3556
3627
  let line;
3557
3628
  let scopeValue;
3558
3629
  let bodyCwd;
@@ -3609,7 +3680,9 @@ async function createSession(deps, opts) {
3609
3680
  return;
3610
3681
  }
3611
3682
  if (method === "POST" && url === "/mcp/remove") {
3612
- void readBody(req).then(async (raw) => {
3683
+ void readBody(req, res).then(async (raw) => {
3684
+ if (raw === null)
3685
+ return;
3613
3686
  let name;
3614
3687
  let scopeValue;
3615
3688
  let bodyCwd;
@@ -3646,7 +3719,9 @@ async function createSession(deps, opts) {
3646
3719
  // `mcp_auth_error`. Responds 202 immediately and runs the flow in the
3647
3720
  // background (the browser round-trip can take a while).
3648
3721
  if (method === "POST" && url === "/mcp/login") {
3649
- void readBody(req).then(async (raw) => {
3722
+ void readBody(req, res).then(async (raw) => {
3723
+ if (raw === null)
3724
+ return;
3650
3725
  let name;
3651
3726
  let scopeValue;
3652
3727
  let bodyCwd;