mobbdev 1.4.43 → 1.4.45

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.
@@ -1274,9 +1274,11 @@ var init_shared = __esm({
1274
1274
  z.object({
1275
1275
  scmSubmitFixRequest: z.object({
1276
1276
  submitFixRequest: z.object({
1277
+ // note: nullable at runtime (e.g. deleted or permission-filtered user)
1278
+ // even though the generated schema says otherwise
1277
1279
  createdByUser: z.object({
1278
1280
  email: z.string()
1279
- }),
1281
+ }).nullable().transform((user) => user ?? { email: "" }),
1280
1282
  targetBranchName: z.string().default("")
1281
1283
  }),
1282
1284
  prUrl: z.string().nullable(),
@@ -4403,6 +4405,8 @@ var ScanContext = {
4403
4405
  };
4404
4406
 
4405
4407
  // src/utils/proxy.ts
4408
+ import { Agent as HttpAgent } from "http";
4409
+ import { Agent as HttpsAgent } from "https";
4406
4410
  import fetchOrig from "cross-fetch";
4407
4411
  import Debug2 from "debug";
4408
4412
  import { HttpsProxyAgent } from "https-proxy-agent";
@@ -4416,31 +4420,33 @@ function httpToWsUrl(url) {
4416
4420
 
4417
4421
  // src/utils/proxy.ts
4418
4422
  var debug2 = Debug2("mobbdev:proxy");
4423
+ var UPLOAD_MAX_SOCKETS = 8;
4424
+ var uploadHttpAgent;
4425
+ var uploadHttpsAgent;
4426
+ var uploadProxyAgents = /* @__PURE__ */ new Map();
4419
4427
  function getHttpProxy() {
4420
4428
  return process.env["HTTPS_PROXY"] || process.env["HTTP_PROXY"] || "";
4421
4429
  }
4422
4430
  function getHttpProxyOnly() {
4423
4431
  return process.env["HTTP_PROXY"] || "";
4424
4432
  }
4433
+ function resolveProxyForUrl(url) {
4434
+ const parsedUrl = new URL(url);
4435
+ const hostname = parsedUrl.hostname.toLowerCase();
4436
+ const isHttp = parsedUrl.protocol === "http:";
4437
+ const isHttps = parsedUrl.protocol === "https:";
4438
+ const isLocal = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
4439
+ const noProxy = process.env["NO_PROXY"] || process.env["no_proxy"];
4440
+ const noProxyMatch = noProxy?.split(",").map((h) => h.trim().toLowerCase()).some((h) => h === hostname || h === "*") ?? false;
4441
+ if (isLocal || noProxyMatch) {
4442
+ return { proxy: "", isHttp, isHttps };
4443
+ }
4444
+ const proxy = isHttps ? getHttpProxy() : isHttp ? getHttpProxyOnly() : "";
4445
+ return { proxy, isHttp, isHttps };
4446
+ }
4425
4447
  function getProxyAgent(url) {
4426
4448
  try {
4427
- const parsedUrl = new URL(url);
4428
- const hostname = parsedUrl.hostname.toLowerCase();
4429
- if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]") {
4430
- debug2("Skipping proxy for localhost URL: %s", url);
4431
- return void 0;
4432
- }
4433
- const noProxy = process.env["NO_PROXY"] || process.env["no_proxy"];
4434
- if (noProxy) {
4435
- const noProxyList = noProxy.split(",").map((h) => h.trim().toLowerCase());
4436
- if (noProxyList.includes(hostname) || noProxyList.includes("*")) {
4437
- debug2("Skipping proxy due to NO_PROXY for: %s", url);
4438
- return void 0;
4439
- }
4440
- }
4441
- const isHttp = parsedUrl.protocol === "http:";
4442
- const isHttps = parsedUrl.protocol === "https:";
4443
- const proxy = isHttps ? getHttpProxy() : isHttp ? getHttpProxyOnly() : null;
4449
+ const { proxy } = resolveProxyForUrl(url);
4444
4450
  if (proxy) {
4445
4451
  debug2("Using proxy %s for %s", proxy, url);
4446
4452
  return new HttpsProxyAgent(proxy);
@@ -4450,6 +4456,39 @@ function getProxyAgent(url) {
4450
4456
  }
4451
4457
  return void 0;
4452
4458
  }
4459
+ function getUploadAgent(url) {
4460
+ try {
4461
+ const { proxy, isHttp, isHttps } = resolveProxyForUrl(url);
4462
+ if (proxy) {
4463
+ let agent = uploadProxyAgents.get(proxy);
4464
+ if (!agent) {
4465
+ agent = new HttpsProxyAgent(proxy, {
4466
+ keepAlive: true,
4467
+ maxSockets: UPLOAD_MAX_SOCKETS
4468
+ });
4469
+ uploadProxyAgents.set(proxy, agent);
4470
+ }
4471
+ return agent;
4472
+ }
4473
+ if (isHttps) {
4474
+ uploadHttpsAgent ?? (uploadHttpsAgent = new HttpsAgent({
4475
+ keepAlive: true,
4476
+ maxSockets: UPLOAD_MAX_SOCKETS
4477
+ }));
4478
+ return uploadHttpsAgent;
4479
+ }
4480
+ if (isHttp) {
4481
+ uploadHttpAgent ?? (uploadHttpAgent = new HttpAgent({
4482
+ keepAlive: true,
4483
+ maxSockets: UPLOAD_MAX_SOCKETS
4484
+ }));
4485
+ return uploadHttpAgent;
4486
+ }
4487
+ } catch (err) {
4488
+ debug2(`No upload agent for ${url}. Reason: ${err.message}`);
4489
+ }
4490
+ return void 0;
4491
+ }
4453
4492
  var fetchWithProxy = (url, options = {}) => {
4454
4493
  try {
4455
4494
  const agent = getProxyAgent(url.toString());
@@ -5657,7 +5696,7 @@ async function uploadFile({
5657
5696
  )
5658
5697
  );
5659
5698
  }
5660
- const agent = getProxyAgent(url);
5699
+ const agent = getUploadAgent(url);
5661
5700
  const effectiveSignal = signal ?? AbortSignal.timeout(computeUploadTimeoutMs(file));
5662
5701
  const response = await fetch3(url, {
5663
5702
  method: "POST",
package/dist/index.mjs CHANGED
@@ -1376,9 +1376,11 @@ var init_shared = __esm({
1376
1376
  z2.object({
1377
1377
  scmSubmitFixRequest: z2.object({
1378
1378
  submitFixRequest: z2.object({
1379
+ // note: nullable at runtime (e.g. deleted or permission-filtered user)
1380
+ // even though the generated schema says otherwise
1379
1381
  createdByUser: z2.object({
1380
1382
  email: z2.string()
1381
- }),
1383
+ }).nullable().transform((user) => user ?? { email: "" }),
1382
1384
  targetBranchName: z2.string().default("")
1383
1385
  }),
1384
1386
  prUrl: z2.string().nullable(),
@@ -11369,6 +11371,8 @@ __publicField(_ReportDigestError, "defaultMessage", "\u{1F575}\uFE0F\u200D\u2642
11369
11371
  var ReportDigestError = _ReportDigestError;
11370
11372
 
11371
11373
  // src/utils/proxy.ts
11374
+ import { Agent as HttpAgent } from "http";
11375
+ import { Agent as HttpsAgent } from "https";
11372
11376
  import fetchOrig from "cross-fetch";
11373
11377
  import Debug5 from "debug";
11374
11378
  import { HttpsProxyAgent } from "https-proxy-agent";
@@ -11382,31 +11386,33 @@ function httpToWsUrl(url) {
11382
11386
 
11383
11387
  // src/utils/proxy.ts
11384
11388
  var debug6 = Debug5("mobbdev:proxy");
11389
+ var UPLOAD_MAX_SOCKETS = 8;
11390
+ var uploadHttpAgent;
11391
+ var uploadHttpsAgent;
11392
+ var uploadProxyAgents = /* @__PURE__ */ new Map();
11385
11393
  function getHttpProxy() {
11386
11394
  return process.env["HTTPS_PROXY"] || process.env["HTTP_PROXY"] || "";
11387
11395
  }
11388
11396
  function getHttpProxyOnly() {
11389
11397
  return process.env["HTTP_PROXY"] || "";
11390
11398
  }
11399
+ function resolveProxyForUrl(url) {
11400
+ const parsedUrl = new URL(url);
11401
+ const hostname = parsedUrl.hostname.toLowerCase();
11402
+ const isHttp = parsedUrl.protocol === "http:";
11403
+ const isHttps = parsedUrl.protocol === "https:";
11404
+ const isLocal = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
11405
+ const noProxy = process.env["NO_PROXY"] || process.env["no_proxy"];
11406
+ const noProxyMatch = noProxy?.split(",").map((h) => h.trim().toLowerCase()).some((h) => h === hostname || h === "*") ?? false;
11407
+ if (isLocal || noProxyMatch) {
11408
+ return { proxy: "", isHttp, isHttps };
11409
+ }
11410
+ const proxy = isHttps ? getHttpProxy() : isHttp ? getHttpProxyOnly() : "";
11411
+ return { proxy, isHttp, isHttps };
11412
+ }
11391
11413
  function getProxyAgent(url) {
11392
11414
  try {
11393
- const parsedUrl = new URL(url);
11394
- const hostname = parsedUrl.hostname.toLowerCase();
11395
- if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]") {
11396
- debug6("Skipping proxy for localhost URL: %s", url);
11397
- return void 0;
11398
- }
11399
- const noProxy = process.env["NO_PROXY"] || process.env["no_proxy"];
11400
- if (noProxy) {
11401
- const noProxyList = noProxy.split(",").map((h) => h.trim().toLowerCase());
11402
- if (noProxyList.includes(hostname) || noProxyList.includes("*")) {
11403
- debug6("Skipping proxy due to NO_PROXY for: %s", url);
11404
- return void 0;
11405
- }
11406
- }
11407
- const isHttp = parsedUrl.protocol === "http:";
11408
- const isHttps = parsedUrl.protocol === "https:";
11409
- const proxy = isHttps ? getHttpProxy() : isHttp ? getHttpProxyOnly() : null;
11415
+ const { proxy } = resolveProxyForUrl(url);
11410
11416
  if (proxy) {
11411
11417
  debug6("Using proxy %s for %s", proxy, url);
11412
11418
  return new HttpsProxyAgent(proxy);
@@ -11416,6 +11422,39 @@ function getProxyAgent(url) {
11416
11422
  }
11417
11423
  return void 0;
11418
11424
  }
11425
+ function getUploadAgent(url) {
11426
+ try {
11427
+ const { proxy, isHttp, isHttps } = resolveProxyForUrl(url);
11428
+ if (proxy) {
11429
+ let agent = uploadProxyAgents.get(proxy);
11430
+ if (!agent) {
11431
+ agent = new HttpsProxyAgent(proxy, {
11432
+ keepAlive: true,
11433
+ maxSockets: UPLOAD_MAX_SOCKETS
11434
+ });
11435
+ uploadProxyAgents.set(proxy, agent);
11436
+ }
11437
+ return agent;
11438
+ }
11439
+ if (isHttps) {
11440
+ uploadHttpsAgent ?? (uploadHttpsAgent = new HttpsAgent({
11441
+ keepAlive: true,
11442
+ maxSockets: UPLOAD_MAX_SOCKETS
11443
+ }));
11444
+ return uploadHttpsAgent;
11445
+ }
11446
+ if (isHttp) {
11447
+ uploadHttpAgent ?? (uploadHttpAgent = new HttpAgent({
11448
+ keepAlive: true,
11449
+ maxSockets: UPLOAD_MAX_SOCKETS
11450
+ }));
11451
+ return uploadHttpAgent;
11452
+ }
11453
+ } catch (err) {
11454
+ debug6(`No upload agent for ${url}. Reason: ${err.message}`);
11455
+ }
11456
+ return void 0;
11457
+ }
11419
11458
  var fetchWithProxy = (url, options = {}) => {
11420
11459
  try {
11421
11460
  const agent = getProxyAgent(url.toString());
@@ -12299,6 +12338,16 @@ var S3UploadError = class extends Error {
12299
12338
  this.name = "S3UploadError";
12300
12339
  }
12301
12340
  };
12341
+ var ACTIONABLE_S3_CODES = /* @__PURE__ */ new Set([
12342
+ "AccessDenied",
12343
+ "ExpiredToken",
12344
+ "InvalidAccessKeyId",
12345
+ "SignatureDoesNotMatch",
12346
+ "TokenRefreshRequired"
12347
+ ]);
12348
+ function isActionableS3Error(err) {
12349
+ return err instanceof S3UploadError && err.s3Code !== void 0 && ACTIONABLE_S3_CODES.has(err.s3Code);
12350
+ }
12302
12351
  function parseS3ErrorBody(body) {
12303
12352
  return {
12304
12353
  code: body.match(/<Code>([^<]+)<\/Code>/)?.[1],
@@ -12347,7 +12396,7 @@ async function uploadFile({
12347
12396
  )
12348
12397
  );
12349
12398
  }
12350
- const agent = getProxyAgent(url);
12399
+ const agent = getUploadAgent(url);
12351
12400
  const effectiveSignal = signal ?? AbortSignal.timeout(computeUploadTimeoutMs(file));
12352
12401
  const response = await fetch3(url, {
12353
12402
  method: "POST",
@@ -15948,7 +15997,179 @@ import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
15948
15997
  import * as os8 from "os";
15949
15998
  import path24 from "path";
15950
15999
  import { setTimeout as sleep4 } from "timers/promises";
15951
- import Configstore3 from "configstore";
16000
+ import Configstore4 from "configstore";
16001
+
16002
+ // src/features/analysis/context_upload_ledger.ts
16003
+ import Configstore2 from "configstore";
16004
+ var UPLOADED_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
16005
+ var FAILURE_BACKOFF_BASE_MS = 60 * 1e3;
16006
+ var FAILURE_BACKOFF_MAX_MS = 60 * 60 * 1e3;
16007
+ var FAILURE_LOG_WINDOW_MS = 30 * 60 * 1e3;
16008
+ var BREAKER_FAILURE_THRESHOLD = 8;
16009
+ var BREAKER_FAILURE_WINDOW_MS = 5 * 60 * 1e3;
16010
+ var BREAKER_BASE_COOLDOWN_MS = 30 * 1e3;
16011
+ var BREAKER_MAX_COOLDOWN_MS = 10 * 60 * 1e3;
16012
+ var MAX_LEDGER_ENTRIES = 5e3;
16013
+ var LEDGER_NAMESPACE = "mobbdev-context-upload-ledger";
16014
+ var ENTRIES_KEY = "entries";
16015
+ var BREAKER_KEY = "breaker";
16016
+ var NOOP_UPLOAD_LEDGER = {
16017
+ isUploaded: () => false,
16018
+ isBackedOff: () => false,
16019
+ isBreakerOpen: () => false,
16020
+ onSuccess: () => {
16021
+ },
16022
+ onFailure: () => true,
16023
+ flush: () => {
16024
+ }
16025
+ };
16026
+ var ConfigstoreUploadLedger = class {
16027
+ constructor(store) {
16028
+ __publicField(this, "store");
16029
+ __publicField(this, "entries");
16030
+ __publicField(this, "breaker");
16031
+ __publicField(this, "dirty", false);
16032
+ this.store = store ?? new Configstore2(LEDGER_NAMESPACE);
16033
+ this.entries = loadEntries(this.store);
16034
+ this.breaker = loadBreaker(this.store);
16035
+ }
16036
+ isUploaded(md5, now) {
16037
+ const entry = this.entries.get(md5);
16038
+ if (!entry?.uploadedAt) {
16039
+ return false;
16040
+ }
16041
+ if (now - entry.uploadedAt > UPLOADED_TTL_MS) {
16042
+ return false;
16043
+ }
16044
+ entry.touchedAt = now;
16045
+ this.dirty = true;
16046
+ return true;
16047
+ }
16048
+ isBackedOff(md5, now) {
16049
+ const entry = this.entries.get(md5);
16050
+ return entry?.nextRetryAt !== void 0 && entry.nextRetryAt > now;
16051
+ }
16052
+ isBreakerOpen(now) {
16053
+ return this.breaker.openUntil > now;
16054
+ }
16055
+ onSuccess(md5, now) {
16056
+ const entry = this.getOrCreate(md5, now);
16057
+ entry.uploadedAt = now;
16058
+ entry.touchedAt = now;
16059
+ delete entry.attempts;
16060
+ delete entry.nextRetryAt;
16061
+ delete entry.lastLoggedAt;
16062
+ this.breaker.consecutiveFailures = 0;
16063
+ this.breaker.lastFailureAt = 0;
16064
+ this.breaker.trips = 0;
16065
+ this.breaker.openUntil = 0;
16066
+ this.dirty = true;
16067
+ }
16068
+ onFailure(md5, now) {
16069
+ const entry = this.getOrCreate(md5, now);
16070
+ const attempts = (entry.attempts ?? 0) + 1;
16071
+ const delay3 = Math.min(
16072
+ FAILURE_BACKOFF_BASE_MS * 2 ** (attempts - 1),
16073
+ FAILURE_BACKOFF_MAX_MS
16074
+ );
16075
+ entry.attempts = attempts;
16076
+ entry.nextRetryAt = now + delay3;
16077
+ entry.touchedAt = now;
16078
+ if (now - this.breaker.lastFailureAt > BREAKER_FAILURE_WINDOW_MS) {
16079
+ this.breaker.consecutiveFailures = 0;
16080
+ }
16081
+ this.breaker.lastFailureAt = now;
16082
+ this.breaker.consecutiveFailures += 1;
16083
+ if (this.breaker.consecutiveFailures >= BREAKER_FAILURE_THRESHOLD && this.breaker.openUntil <= now) {
16084
+ const cooldown = Math.min(
16085
+ BREAKER_BASE_COOLDOWN_MS * 2 ** this.breaker.trips,
16086
+ BREAKER_MAX_COOLDOWN_MS
16087
+ );
16088
+ this.breaker.trips += 1;
16089
+ this.breaker.openUntil = now + cooldown;
16090
+ }
16091
+ const shouldLog = entry.lastLoggedAt === void 0 || now - entry.lastLoggedAt >= FAILURE_LOG_WINDOW_MS;
16092
+ if (shouldLog) {
16093
+ entry.lastLoggedAt = now;
16094
+ }
16095
+ this.dirty = true;
16096
+ return shouldLog;
16097
+ }
16098
+ flush() {
16099
+ if (!this.dirty) {
16100
+ return;
16101
+ }
16102
+ this.evictIfNeeded();
16103
+ try {
16104
+ this.store.set(ENTRIES_KEY, Object.fromEntries(this.entries));
16105
+ this.store.set(BREAKER_KEY, this.breaker);
16106
+ this.dirty = false;
16107
+ } catch {
16108
+ }
16109
+ }
16110
+ getOrCreate(md5, now) {
16111
+ let entry = this.entries.get(md5);
16112
+ if (!entry) {
16113
+ entry = { touchedAt: now };
16114
+ this.entries.set(md5, entry);
16115
+ }
16116
+ return entry;
16117
+ }
16118
+ /** Evict least-recently-touched entries when over the cap. */
16119
+ evictIfNeeded() {
16120
+ if (this.entries.size <= MAX_LEDGER_ENTRIES) {
16121
+ return;
16122
+ }
16123
+ const sorted = [...this.entries.entries()].sort(
16124
+ (a, b) => a[1].touchedAt - b[1].touchedAt
16125
+ );
16126
+ const removeCount = this.entries.size - MAX_LEDGER_ENTRIES;
16127
+ for (let i = 0; i < removeCount; i++) {
16128
+ this.entries.delete(sorted[i][0]);
16129
+ }
16130
+ }
16131
+ };
16132
+ function finiteOrUndefined(value) {
16133
+ const n = Number(value);
16134
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
16135
+ }
16136
+ function loadEntries(store) {
16137
+ const map = /* @__PURE__ */ new Map();
16138
+ try {
16139
+ const raw = store.get(ENTRIES_KEY);
16140
+ if (raw && typeof raw === "object") {
16141
+ for (const [md5, entry] of Object.entries(raw)) {
16142
+ if (!entry || typeof entry !== "object") {
16143
+ continue;
16144
+ }
16145
+ map.set(md5, {
16146
+ uploadedAt: finiteOrUndefined(entry.uploadedAt),
16147
+ attempts: finiteOrUndefined(entry.attempts),
16148
+ nextRetryAt: finiteOrUndefined(entry.nextRetryAt),
16149
+ lastLoggedAt: finiteOrUndefined(entry.lastLoggedAt),
16150
+ touchedAt: finiteOrUndefined(entry.touchedAt) ?? 0
16151
+ });
16152
+ }
16153
+ }
16154
+ } catch {
16155
+ }
16156
+ return map;
16157
+ }
16158
+ function loadBreaker(store) {
16159
+ try {
16160
+ const raw = store.get(BREAKER_KEY);
16161
+ if (raw && typeof raw === "object") {
16162
+ return {
16163
+ consecutiveFailures: finiteOrUndefined(raw.consecutiveFailures) ?? 0,
16164
+ lastFailureAt: finiteOrUndefined(raw.lastFailureAt) ?? 0,
16165
+ trips: finiteOrUndefined(raw.trips) ?? 0,
16166
+ openUntil: finiteOrUndefined(raw.openUntil) ?? 0
16167
+ };
16168
+ }
16169
+ } catch {
16170
+ }
16171
+ return { consecutiveFailures: 0, lastFailureAt: 0, trips: 0, openUntil: 0 };
16172
+ }
15952
16173
 
15953
16174
  // src/features/analysis/skill_quarantine/runQuarantineCheck.ts
15954
16175
  import { stat as stat3 } from "fs/promises";
@@ -16226,8 +16447,8 @@ var SCAN_PATHS = {
16226
16447
  // src/features/analysis/context_file_scanner.ts
16227
16448
  var MAX_CONTEXT_FILE_SIZE = 20 * 1024 * 1024;
16228
16449
  var SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
16229
- var FAILURE_BACKOFF_BASE_MS = 60 * 1e3;
16230
- var FAILURE_BACKOFF_MAX_MS = 60 * 60 * 1e3;
16450
+ var FAILURE_BACKOFF_BASE_MS2 = 60 * 1e3;
16451
+ var FAILURE_BACKOFF_MAX_MS2 = 60 * 60 * 1e3;
16231
16452
  var sessionMtimes = /* @__PURE__ */ new Map();
16232
16453
  function markContextFilesUploaded(sessionId, files, skills) {
16233
16454
  const entry = getOrCreateSessionEntry(sessionId);
@@ -16249,8 +16470,8 @@ function markContextUploadFailed(sessionId, files, skills) {
16249
16470
  const recordFailure = (key) => {
16250
16471
  const attempts = (entry.failures.get(key)?.attempts ?? 0) + 1;
16251
16472
  const delay3 = Math.min(
16252
- FAILURE_BACKOFF_BASE_MS * 2 ** (attempts - 1),
16253
- FAILURE_BACKOFF_MAX_MS
16473
+ FAILURE_BACKOFF_BASE_MS2 * 2 ** (attempts - 1),
16474
+ FAILURE_BACKOFF_MAX_MS2
16254
16475
  );
16255
16476
  entry.failures.set(key, { attempts, nextRetryAt: now + delay3 });
16256
16477
  };
@@ -17365,7 +17586,12 @@ var DAEMON_POLL_INTERVAL_MS = (() => {
17365
17586
  var HEARTBEAT_STALE_MS = 3e4;
17366
17587
  var TRANSCRIPT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
17367
17588
  var DAEMON_CHUNK_SIZE = 50;
17368
- var CONTEXT_SCAN_INTERVAL_MS = 5e3;
17589
+ var CONTEXT_SCAN_INTERVAL_MS = (() => {
17590
+ const raw = Number(process.env["MOBB_CONTEXT_SCAN_INTERVAL_MS"]);
17591
+ if (!Number.isFinite(raw) || raw <= 0) return 15 * 6e4;
17592
+ return Math.min(Math.max(raw, 1e4), 60 * 6e4);
17593
+ })();
17594
+ var DAEMON_SHUTDOWN_DRAIN_MS = 6e3;
17369
17595
 
17370
17596
  // src/features/claude_code/daemon_pid_file.ts
17371
17597
  function getMobbdevDir() {
@@ -17449,7 +17675,7 @@ import { promisify } from "util";
17449
17675
  import { setTimeout as sleep3 } from "timers/promises";
17450
17676
  import pLimit6 from "p-limit";
17451
17677
  init_client_generates();
17452
- var UPLOAD_CONCURRENCY = 5;
17678
+ var UPLOAD_CONCURRENCY = 1;
17453
17679
  var UPLOAD_MAX_ATTEMPTS = 2;
17454
17680
  var UPLOAD_RETRY_BASE_MS = 250;
17455
17681
  async function uploadWithRetry(args) {
@@ -17467,6 +17693,29 @@ async function uploadWithRetry(args) {
17467
17693
  }
17468
17694
  }
17469
17695
  }
17696
+ var uploadLimit = pLimit6(UPLOAD_CONCURRENCY);
17697
+ var inFlightUploads = /* @__PURE__ */ new Map();
17698
+ async function coalescedUpload(s3Key, run2) {
17699
+ const existing = inFlightUploads.get(s3Key);
17700
+ if (existing) {
17701
+ try {
17702
+ await existing;
17703
+ return { leader: false, ok: true };
17704
+ } catch (err) {
17705
+ return { leader: false, ok: false, err };
17706
+ }
17707
+ }
17708
+ const p = uploadLimit(run2);
17709
+ inFlightUploads.set(s3Key, p);
17710
+ try {
17711
+ await p;
17712
+ return { leader: true, ok: true };
17713
+ } catch (err) {
17714
+ return { leader: true, ok: false, err };
17715
+ } finally {
17716
+ inFlightUploads.delete(s3Key);
17717
+ }
17718
+ }
17470
17719
  async function uploadContextRecords(opts) {
17471
17720
  const {
17472
17721
  processedFiles,
@@ -17481,6 +17730,7 @@ async function uploadContextRecords(opts) {
17481
17730
  branch,
17482
17731
  commitSha,
17483
17732
  clientVersion,
17733
+ ledger = NOOP_UPLOAD_LEDGER,
17484
17734
  onFileError,
17485
17735
  onSkillError
17486
17736
  } = opts;
@@ -17489,82 +17739,97 @@ async function uploadContextRecords(opts) {
17489
17739
  const uploadedSkillGroups = [];
17490
17740
  const failedFiles = [];
17491
17741
  const failedSkillGroups = [];
17492
- const limit = pLimit6(UPLOAD_CONCURRENCY);
17742
+ let dedupedCount = 0;
17743
+ let suppressedCount = 0;
17493
17744
  const extraFields = {
17494
17745
  ...repositoryUrl !== void 0 && { repositoryUrl },
17495
17746
  ...branch !== void 0 && { branch },
17496
17747
  ...commitSha !== void 0 && { commitSha },
17497
17748
  ...clientVersion !== void 0 && { clientVersion }
17498
17749
  };
17750
+ const processItem = async (item) => {
17751
+ const nowMs = Date.now();
17752
+ if (ledger.isUploaded(item.md5, nowMs)) {
17753
+ dedupedCount++;
17754
+ } else if (ledger.isBreakerOpen(nowMs) || ledger.isBackedOff(item.md5, nowMs)) {
17755
+ suppressedCount++;
17756
+ return;
17757
+ } else {
17758
+ const { leader, ok, err } = await coalescedUpload(
17759
+ item.s3Key,
17760
+ () => uploadWithRetry({
17761
+ file: item.getPayload(),
17762
+ url,
17763
+ uploadKey: item.s3Key,
17764
+ uploadFields
17765
+ })
17766
+ );
17767
+ if (!ok) {
17768
+ item.markFailed();
17769
+ if (leader) {
17770
+ const logByWindow = ledger.onFailure(item.md5, Date.now());
17771
+ if (logByWindow || isActionableS3Error(err)) {
17772
+ item.onError?.(item.errorName, err);
17773
+ }
17774
+ }
17775
+ return;
17776
+ }
17777
+ if (leader) {
17778
+ ledger.onSuccess(item.md5, Date.now());
17779
+ }
17780
+ }
17781
+ records.push({
17782
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17783
+ platform: platform2,
17784
+ recordId: `ctx:${sessionId}:${item.md5}`,
17785
+ recordTimestamp: now,
17786
+ blameType: "CHAT" /* Chat */,
17787
+ rawDataS3Key: item.s3Key,
17788
+ ...extraFields,
17789
+ context: {
17790
+ md5: item.md5,
17791
+ category: item.context.category,
17792
+ name: item.context.name,
17793
+ sizeBytes: item.context.sizeBytes,
17794
+ filePath: item.context.filePath,
17795
+ sessionId
17796
+ }
17797
+ });
17798
+ item.markUploaded();
17799
+ };
17499
17800
  const tasks = [
17500
17801
  ...processedFiles.map(
17501
- (pf) => limit(async () => {
17502
- const s3Key = `${keyPrefix}ctx-${pf.md5}.bin`;
17503
- try {
17504
- await uploadWithRetry({
17505
- file: Buffer.from(pf.sanitizedContent, "utf-8"),
17506
- url,
17507
- uploadKey: s3Key,
17508
- uploadFields
17509
- });
17510
- } catch (err) {
17511
- failedFiles.push(pf.entry);
17512
- onFileError?.(pf.entry.name, err);
17513
- return;
17514
- }
17515
- records.push({
17516
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17517
- platform: platform2,
17518
- recordId: `ctx:${sessionId}:${pf.md5}`,
17519
- recordTimestamp: now,
17520
- blameType: "CHAT" /* Chat */,
17521
- rawDataS3Key: s3Key,
17522
- ...extraFields,
17523
- context: {
17524
- md5: pf.md5,
17525
- category: pf.entry.category,
17526
- name: pf.entry.name,
17527
- sizeBytes: pf.sizeBytes,
17528
- filePath: pf.entry.path,
17529
- sessionId
17530
- }
17531
- });
17532
- uploadedFiles.push(pf.entry);
17802
+ (pf) => processItem({
17803
+ md5: pf.md5,
17804
+ s3Key: `${keyPrefix}ctx-${pf.md5}.bin`,
17805
+ getPayload: () => Buffer.from(pf.sanitizedContent, "utf-8"),
17806
+ context: {
17807
+ category: pf.entry.category,
17808
+ name: pf.entry.name,
17809
+ sizeBytes: pf.sizeBytes,
17810
+ filePath: pf.entry.path
17811
+ },
17812
+ errorName: pf.entry.name,
17813
+ onError: onFileError,
17814
+ markUploaded: () => uploadedFiles.push(pf.entry),
17815
+ markFailed: () => failedFiles.push(pf.entry)
17533
17816
  })
17534
17817
  ),
17535
17818
  ...processedSkills.map(
17536
- (ps) => limit(async () => {
17537
- const s3Key = `${keyPrefix}skill-${ps.md5}.zip`;
17538
- try {
17539
- await uploadWithRetry({
17540
- file: ps.zipBuffer,
17541
- url,
17542
- uploadKey: s3Key,
17543
- uploadFields
17544
- });
17545
- } catch (err) {
17546
- failedSkillGroups.push(ps.group);
17547
- onSkillError?.(ps.group.name, err);
17548
- return;
17549
- }
17550
- records.push({
17551
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17552
- platform: platform2,
17553
- recordId: `ctx:${sessionId}:${ps.md5}`,
17554
- recordTimestamp: now,
17555
- blameType: "CHAT" /* Chat */,
17556
- rawDataS3Key: s3Key,
17557
- ...extraFields,
17558
- context: {
17559
- md5: ps.md5,
17560
- category: SKILL_CATEGORY,
17561
- name: ps.group.name,
17562
- sizeBytes: ps.sizeBytes,
17563
- filePath: ps.group.skillPath,
17564
- sessionId
17565
- }
17566
- });
17567
- uploadedSkillGroups.push(ps.group);
17819
+ (ps) => processItem({
17820
+ md5: ps.md5,
17821
+ s3Key: `${keyPrefix}skill-${ps.md5}.zip`,
17822
+ getPayload: () => ps.zipBuffer,
17823
+ context: {
17824
+ category: SKILL_CATEGORY,
17825
+ name: ps.group.name,
17826
+ sizeBytes: ps.sizeBytes,
17827
+ filePath: ps.group.skillPath
17828
+ },
17829
+ errorName: ps.group.name,
17830
+ onError: onSkillError,
17831
+ markUploaded: () => uploadedSkillGroups.push(ps.group),
17832
+ markFailed: () => failedSkillGroups.push(ps.group)
17568
17833
  })
17569
17834
  )
17570
17835
  ];
@@ -17574,7 +17839,10 @@ async function uploadContextRecords(opts) {
17574
17839
  uploadedFiles,
17575
17840
  uploadedSkillGroups,
17576
17841
  failedFiles,
17577
- failedSkillGroups
17842
+ failedSkillGroups,
17843
+ dedupedCount,
17844
+ suppressedCount,
17845
+ breakerOpen: ledger.isBreakerOpen(Date.now())
17578
17846
  };
17579
17847
  }
17580
17848
  async function runContextFileUploadPipeline(opts) {
@@ -17590,6 +17858,7 @@ async function runContextFileUploadPipeline(opts) {
17590
17858
  branch,
17591
17859
  commitSha,
17592
17860
  clientVersion,
17861
+ ledger,
17593
17862
  submitRecords,
17594
17863
  onFileError,
17595
17864
  onSkillError
@@ -17606,7 +17875,10 @@ async function runContextFileUploadPipeline(opts) {
17606
17875
  uploadedFiles,
17607
17876
  uploadedSkillGroups,
17608
17877
  failedFiles,
17609
- failedSkillGroups
17878
+ failedSkillGroups,
17879
+ dedupedCount,
17880
+ suppressedCount,
17881
+ breakerOpen
17610
17882
  } = await uploadContextRecords({
17611
17883
  processedFiles,
17612
17884
  processedSkills,
@@ -17620,20 +17892,28 @@ async function runContextFileUploadPipeline(opts) {
17620
17892
  branch,
17621
17893
  commitSha,
17622
17894
  clientVersion,
17895
+ ledger,
17623
17896
  onFileError,
17624
17897
  onSkillError
17625
17898
  });
17626
17899
  if (failedFiles.length > 0 || failedSkillGroups.length > 0) {
17627
17900
  markContextUploadFailed(sessionId, failedFiles, failedSkillGroups);
17628
17901
  }
17902
+ const summary = {
17903
+ dedupedCount,
17904
+ suppressedCount,
17905
+ failedCount: failedFiles.length + failedSkillGroups.length,
17906
+ breakerOpen
17907
+ };
17629
17908
  if (records.length === 0) {
17630
- return { fileCount: 0, skillCount: 0 };
17909
+ return { fileCount: 0, skillCount: 0, ...summary };
17631
17910
  }
17632
17911
  await submitRecords(records);
17633
17912
  markContextFilesUploaded(sessionId, uploadedFiles, uploadedSkillGroups);
17634
17913
  return {
17635
17914
  fileCount: uploadedFiles.length,
17636
- skillCount: uploadedSkillGroups.length
17915
+ skillCount: uploadedSkillGroups.length,
17916
+ ...summary
17637
17917
  };
17638
17918
  }
17639
17919
 
@@ -17641,7 +17921,7 @@ async function runContextFileUploadPipeline(opts) {
17641
17921
  init_client_generates();
17642
17922
 
17643
17923
  // src/utils/shared-logger/create-logger.ts
17644
- import Configstore2 from "configstore";
17924
+ import Configstore3 from "configstore";
17645
17925
  import pino from "pino";
17646
17926
 
17647
17927
  // src/utils/shared-logger/configstore-stream.ts
@@ -17868,7 +18148,7 @@ function createLogger(config2) {
17868
18148
  } = config2;
17869
18149
  let csStream = null;
17870
18150
  if (enableConfigstore) {
17871
- const store = new Configstore2(namespace, {});
18151
+ const store = new Configstore3(namespace, {});
17872
18152
  csStream = createConfigstoreStream(store, {
17873
18153
  buffered,
17874
18154
  scopePath,
@@ -17977,7 +18257,7 @@ function createLogger(config2) {
17977
18257
 
17978
18258
  // src/features/claude_code/hook_logger.ts
17979
18259
  var DD_RUM_TOKEN = true ? "pubf59c0182545bfb4c299175119f1abf9b" : "";
17980
- var CLI_VERSION = true ? "1.4.43" : "unknown";
18260
+ var CLI_VERSION = true ? "1.4.45" : "unknown";
17981
18261
  var NAMESPACE = "mobbdev-claude-code-hook-logs";
17982
18262
  var claudeCodeVersion;
17983
18263
  function buildDdTags() {
@@ -18548,7 +18828,7 @@ async function processTranscript(input, sessionStore, log2, maxEntries = DAEMON_
18548
18828
  errors: entries.length
18549
18829
  };
18550
18830
  }
18551
- async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2) {
18831
+ async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2, ledger) {
18552
18832
  const { regularFiles, skillGroups } = await scanContextFiles(
18553
18833
  cwd,
18554
18834
  "claude-code",
@@ -18570,6 +18850,7 @@ async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2) {
18570
18850
  );
18571
18851
  return;
18572
18852
  }
18853
+ const uploadFailures = [];
18573
18854
  const pipelineResult = await runContextFileUploadPipeline({
18574
18855
  processedFiles,
18575
18856
  processedSkills,
@@ -18578,20 +18859,15 @@ async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2) {
18578
18859
  url,
18579
18860
  uploadFieldsJSON,
18580
18861
  keyPrefix,
18862
+ ledger,
18581
18863
  submitRecords: async (records) => {
18582
18864
  const r = await prepareAndSendTracyRecords(gqlClient, records, cwd);
18583
18865
  if (!r.ok) {
18584
18866
  throw new Error(r.errors?.join(", ") ?? "batch upload failed");
18585
18867
  }
18586
18868
  },
18587
- onFileError: (name, err) => log2.error(
18588
- { data: { sessionId, name, err } },
18589
- "Failed to upload context file to S3"
18590
- ),
18591
- onSkillError: (name, err) => log2.error(
18592
- { data: { sessionId, name, err } },
18593
- "Failed to upload skill zip to S3"
18594
- )
18869
+ onFileError: (name, err) => uploadFailures.push({ name, err }),
18870
+ onSkillError: (name, err) => uploadFailures.push({ name, err })
18595
18871
  });
18596
18872
  if (pipelineResult === null) {
18597
18873
  log2.error(
@@ -18600,19 +18876,48 @@ async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2) {
18600
18876
  );
18601
18877
  return;
18602
18878
  }
18879
+ if (uploadFailures.length > 0) {
18880
+ const hasActionableS3Error = uploadFailures.some(
18881
+ (f) => isActionableS3Error(f.err)
18882
+ );
18883
+ const payload = {
18884
+ sessionId,
18885
+ failedCount: pipelineResult.failedCount,
18886
+ suppressedCount: pipelineResult.suppressedCount,
18887
+ dedupedCount: pipelineResult.dedupedCount,
18888
+ breakerOpen: pipelineResult.breakerOpen,
18889
+ errTypes: summarizeUploadErrTypes(uploadFailures.map((f) => f.err)),
18890
+ sample: uploadFailures.slice(0, 3).map((f) => f.name)
18891
+ };
18892
+ if (hasActionableS3Error) {
18893
+ log2.error({ data: payload }, "Context/skill S3 upload failures");
18894
+ } else {
18895
+ log2.warn({ data: payload }, "Context/skill upload failures (transient)");
18896
+ }
18897
+ }
18603
18898
  if (pipelineResult.fileCount > 0 || pipelineResult.skillCount > 0) {
18604
18899
  log2.info(
18605
18900
  {
18606
18901
  data: {
18607
18902
  sessionId,
18608
18903
  fileCount: pipelineResult.fileCount,
18609
- skillCount: pipelineResult.skillCount
18904
+ skillCount: pipelineResult.skillCount,
18905
+ dedupedCount: pipelineResult.dedupedCount
18610
18906
  }
18611
18907
  },
18612
18908
  "Uploaded context files and skills for session"
18613
18909
  );
18614
18910
  }
18615
18911
  }
18912
+ function summarizeUploadErrTypes(errs) {
18913
+ const counts = {};
18914
+ for (const err of errs) {
18915
+ const e = err;
18916
+ const key = e?.type ?? e?.code ?? e?.name ?? "unknown";
18917
+ counts[key] = (counts[key] ?? 0) + 1;
18918
+ }
18919
+ return counts;
18920
+ }
18616
18921
 
18617
18922
  // src/features/claude_code/install_hook.ts
18618
18923
  import fs14 from "fs";
@@ -18951,9 +19256,18 @@ async function startDaemon() {
18951
19256
  hookLog.info("Daemon starting");
18952
19257
  pruneHookLogFile();
18953
19258
  const pidFile = await acquirePidFile();
19259
+ const uploadLedger = new ConfigstoreUploadLedger();
19260
+ const inFlightContextUploads = /* @__PURE__ */ new Set();
18954
19261
  async function gracefulExit(code, reason) {
18955
19262
  hookLog.info({ data: { code } }, `Daemon exiting: ${reason}`);
18956
19263
  pidFile.remove();
19264
+ if (inFlightContextUploads.size > 0) {
19265
+ await Promise.race([
19266
+ Promise.allSettled([...inFlightContextUploads]),
19267
+ sleep4(DAEMON_SHUTDOWN_DRAIN_MS)
19268
+ ]);
19269
+ }
19270
+ uploadLedger.flush();
18957
19271
  await flushDdLogs();
18958
19272
  process.exit(code);
18959
19273
  }
@@ -19043,14 +19357,20 @@ async function startDaemon() {
19043
19357
  }
19044
19358
  }
19045
19359
  const now = Date.now();
19046
- if (now - lastContextScanMs >= CONTEXT_SCAN_INTERVAL_MS) {
19360
+ if (!shuttingDown && now - lastContextScanMs >= CONTEXT_SCAN_INTERVAL_MS) {
19047
19361
  lastContextScanMs = now;
19048
19362
  for (const { sessionId, cwd } of sessionCwdCache.values()) {
19049
19363
  const log2 = createScopedHookLog(cwd, { daemonMode: true });
19050
- uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2).catch(
19051
- (err) => log2.warn({ err }, "Context file scan failed")
19052
- );
19364
+ const p = uploadContextFilesIfNeeded(
19365
+ sessionId,
19366
+ cwd,
19367
+ gqlClient,
19368
+ log2,
19369
+ uploadLedger
19370
+ ).catch((err) => log2.warn({ err }, "Context file scan failed")).finally(() => inFlightContextUploads.delete(p));
19371
+ inFlightContextUploads.add(p);
19053
19372
  }
19373
+ uploadLedger.flush();
19054
19374
  }
19055
19375
  if (cleanupConfigDir) {
19056
19376
  await cleanupStaleSessions(cleanupConfigDir);
@@ -19220,7 +19540,7 @@ async function tryAutoUpgradeHooks() {
19220
19540
  var HOOK_LOG_MAX_SCOPE_KEYS = 20;
19221
19541
  var HOOK_LOG_MAX_ENTRIES_PER_KEY = 200;
19222
19542
  function pruneHookLogFile() {
19223
- const logFilePath = new Configstore3("mobbdev-claude-code-hook-logs").path;
19543
+ const logFilePath = new Configstore4("mobbdev-claude-code-hook-logs").path;
19224
19544
  try {
19225
19545
  const raw = readFileSync(logFilePath, "utf-8");
19226
19546
  const data = JSON.parse(raw);
@@ -19352,7 +19672,7 @@ import {
19352
19672
  } from "@modelcontextprotocol/sdk/types.js";
19353
19673
 
19354
19674
  // src/mcp/Logger.ts
19355
- import Configstore4 from "configstore";
19675
+ import Configstore5 from "configstore";
19356
19676
 
19357
19677
  // src/mcp/services/WorkspaceService.ts
19358
19678
  var WorkspaceService = class {
@@ -19438,7 +19758,7 @@ var Logger = class {
19438
19758
  __publicField(this, "lastKnownPath", null);
19439
19759
  this.host = WorkspaceService.getHost();
19440
19760
  this.unknownPathSuffix = Math.floor(1e3 + Math.random() * 9e3).toString();
19441
- this.mobbConfigStore = new Configstore4("mobb-logs", {});
19761
+ this.mobbConfigStore = new Configstore5("mobb-logs", {});
19442
19762
  this.mobbConfigStore.set("version", packageJson.version);
19443
19763
  }
19444
19764
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "1.4.43",
3
+ "version": "1.4.45",
4
4
  "description": "Automated secure code remediation tool",
5
5
  "repository": "git+https://github.com/mobb-dev/bugsy.git",
6
6
  "main": "dist/index.mjs",