mobbdev 1.4.44 → 1.4.46

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.
@@ -4405,6 +4405,8 @@ var ScanContext = {
4405
4405
  };
4406
4406
 
4407
4407
  // src/utils/proxy.ts
4408
+ import { Agent as HttpAgent } from "http";
4409
+ import { Agent as HttpsAgent } from "https";
4408
4410
  import fetchOrig from "cross-fetch";
4409
4411
  import Debug2 from "debug";
4410
4412
  import { HttpsProxyAgent } from "https-proxy-agent";
@@ -4418,31 +4420,33 @@ function httpToWsUrl(url) {
4418
4420
 
4419
4421
  // src/utils/proxy.ts
4420
4422
  var debug2 = Debug2("mobbdev:proxy");
4423
+ var UPLOAD_MAX_SOCKETS = 8;
4424
+ var uploadHttpAgent;
4425
+ var uploadHttpsAgent;
4426
+ var uploadProxyAgents = /* @__PURE__ */ new Map();
4421
4427
  function getHttpProxy() {
4422
4428
  return process.env["HTTPS_PROXY"] || process.env["HTTP_PROXY"] || "";
4423
4429
  }
4424
4430
  function getHttpProxyOnly() {
4425
4431
  return process.env["HTTP_PROXY"] || "";
4426
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
+ }
4427
4447
  function getProxyAgent(url) {
4428
4448
  try {
4429
- const parsedUrl = new URL(url);
4430
- const hostname = parsedUrl.hostname.toLowerCase();
4431
- if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]") {
4432
- debug2("Skipping proxy for localhost URL: %s", url);
4433
- return void 0;
4434
- }
4435
- const noProxy = process.env["NO_PROXY"] || process.env["no_proxy"];
4436
- if (noProxy) {
4437
- const noProxyList = noProxy.split(",").map((h) => h.trim().toLowerCase());
4438
- if (noProxyList.includes(hostname) || noProxyList.includes("*")) {
4439
- debug2("Skipping proxy due to NO_PROXY for: %s", url);
4440
- return void 0;
4441
- }
4442
- }
4443
- const isHttp = parsedUrl.protocol === "http:";
4444
- const isHttps = parsedUrl.protocol === "https:";
4445
- const proxy = isHttps ? getHttpProxy() : isHttp ? getHttpProxyOnly() : null;
4449
+ const { proxy } = resolveProxyForUrl(url);
4446
4450
  if (proxy) {
4447
4451
  debug2("Using proxy %s for %s", proxy, url);
4448
4452
  return new HttpsProxyAgent(proxy);
@@ -4452,6 +4456,39 @@ function getProxyAgent(url) {
4452
4456
  }
4453
4457
  return void 0;
4454
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
+ }
4455
4492
  var fetchWithProxy = (url, options = {}) => {
4456
4493
  try {
4457
4494
  const agent = getProxyAgent(url.toString());
@@ -5659,7 +5696,7 @@ async function uploadFile({
5659
5696
  )
5660
5697
  );
5661
5698
  }
5662
- const agent = getProxyAgent(url);
5699
+ const agent = getUploadAgent(url);
5663
5700
  const effectiveSignal = signal ?? AbortSignal.timeout(computeUploadTimeoutMs(file));
5664
5701
  const response = await fetch3(url, {
5665
5702
  method: "POST",
package/dist/index.mjs CHANGED
@@ -11371,6 +11371,8 @@ __publicField(_ReportDigestError, "defaultMessage", "\u{1F575}\uFE0F\u200D\u2642
11371
11371
  var ReportDigestError = _ReportDigestError;
11372
11372
 
11373
11373
  // src/utils/proxy.ts
11374
+ import { Agent as HttpAgent } from "http";
11375
+ import { Agent as HttpsAgent } from "https";
11374
11376
  import fetchOrig from "cross-fetch";
11375
11377
  import Debug5 from "debug";
11376
11378
  import { HttpsProxyAgent } from "https-proxy-agent";
@@ -11384,31 +11386,33 @@ function httpToWsUrl(url) {
11384
11386
 
11385
11387
  // src/utils/proxy.ts
11386
11388
  var debug6 = Debug5("mobbdev:proxy");
11389
+ var UPLOAD_MAX_SOCKETS = 8;
11390
+ var uploadHttpAgent;
11391
+ var uploadHttpsAgent;
11392
+ var uploadProxyAgents = /* @__PURE__ */ new Map();
11387
11393
  function getHttpProxy() {
11388
11394
  return process.env["HTTPS_PROXY"] || process.env["HTTP_PROXY"] || "";
11389
11395
  }
11390
11396
  function getHttpProxyOnly() {
11391
11397
  return process.env["HTTP_PROXY"] || "";
11392
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
+ }
11393
11413
  function getProxyAgent(url) {
11394
11414
  try {
11395
- const parsedUrl = new URL(url);
11396
- const hostname = parsedUrl.hostname.toLowerCase();
11397
- if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]") {
11398
- debug6("Skipping proxy for localhost URL: %s", url);
11399
- return void 0;
11400
- }
11401
- const noProxy = process.env["NO_PROXY"] || process.env["no_proxy"];
11402
- if (noProxy) {
11403
- const noProxyList = noProxy.split(",").map((h) => h.trim().toLowerCase());
11404
- if (noProxyList.includes(hostname) || noProxyList.includes("*")) {
11405
- debug6("Skipping proxy due to NO_PROXY for: %s", url);
11406
- return void 0;
11407
- }
11408
- }
11409
- const isHttp = parsedUrl.protocol === "http:";
11410
- const isHttps = parsedUrl.protocol === "https:";
11411
- const proxy = isHttps ? getHttpProxy() : isHttp ? getHttpProxyOnly() : null;
11415
+ const { proxy } = resolveProxyForUrl(url);
11412
11416
  if (proxy) {
11413
11417
  debug6("Using proxy %s for %s", proxy, url);
11414
11418
  return new HttpsProxyAgent(proxy);
@@ -11418,6 +11422,39 @@ function getProxyAgent(url) {
11418
11422
  }
11419
11423
  return void 0;
11420
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
+ }
11421
11458
  var fetchWithProxy = (url, options = {}) => {
11422
11459
  try {
11423
11460
  const agent = getProxyAgent(url.toString());
@@ -12301,6 +12338,16 @@ var S3UploadError = class extends Error {
12301
12338
  this.name = "S3UploadError";
12302
12339
  }
12303
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
+ }
12304
12351
  function parseS3ErrorBody(body) {
12305
12352
  return {
12306
12353
  code: body.match(/<Code>([^<]+)<\/Code>/)?.[1],
@@ -12349,7 +12396,7 @@ async function uploadFile({
12349
12396
  )
12350
12397
  );
12351
12398
  }
12352
- const agent = getProxyAgent(url);
12399
+ const agent = getUploadAgent(url);
12353
12400
  const effectiveSignal = signal ?? AbortSignal.timeout(computeUploadTimeoutMs(file));
12354
12401
  const response = await fetch3(url, {
12355
12402
  method: "POST",
@@ -15950,7 +15997,179 @@ import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
15950
15997
  import * as os8 from "os";
15951
15998
  import path24 from "path";
15952
15999
  import { setTimeout as sleep4 } from "timers/promises";
15953
- 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
+ }
15954
16173
 
15955
16174
  // src/features/analysis/skill_quarantine/runQuarantineCheck.ts
15956
16175
  import { stat as stat3 } from "fs/promises";
@@ -16228,8 +16447,8 @@ var SCAN_PATHS = {
16228
16447
  // src/features/analysis/context_file_scanner.ts
16229
16448
  var MAX_CONTEXT_FILE_SIZE = 20 * 1024 * 1024;
16230
16449
  var SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
16231
- var FAILURE_BACKOFF_BASE_MS = 60 * 1e3;
16232
- 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;
16233
16452
  var sessionMtimes = /* @__PURE__ */ new Map();
16234
16453
  function markContextFilesUploaded(sessionId, files, skills) {
16235
16454
  const entry = getOrCreateSessionEntry(sessionId);
@@ -16251,8 +16470,8 @@ function markContextUploadFailed(sessionId, files, skills) {
16251
16470
  const recordFailure = (key) => {
16252
16471
  const attempts = (entry.failures.get(key)?.attempts ?? 0) + 1;
16253
16472
  const delay3 = Math.min(
16254
- FAILURE_BACKOFF_BASE_MS * 2 ** (attempts - 1),
16255
- FAILURE_BACKOFF_MAX_MS
16473
+ FAILURE_BACKOFF_BASE_MS2 * 2 ** (attempts - 1),
16474
+ FAILURE_BACKOFF_MAX_MS2
16256
16475
  );
16257
16476
  entry.failures.set(key, { attempts, nextRetryAt: now + delay3 });
16258
16477
  };
@@ -17367,7 +17586,12 @@ var DAEMON_POLL_INTERVAL_MS = (() => {
17367
17586
  var HEARTBEAT_STALE_MS = 3e4;
17368
17587
  var TRANSCRIPT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
17369
17588
  var DAEMON_CHUNK_SIZE = 50;
17370
- 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;
17371
17595
 
17372
17596
  // src/features/claude_code/daemon_pid_file.ts
17373
17597
  function getMobbdevDir() {
@@ -17451,7 +17675,7 @@ import { promisify } from "util";
17451
17675
  import { setTimeout as sleep3 } from "timers/promises";
17452
17676
  import pLimit6 from "p-limit";
17453
17677
  init_client_generates();
17454
- var UPLOAD_CONCURRENCY = 5;
17678
+ var UPLOAD_CONCURRENCY = 1;
17455
17679
  var UPLOAD_MAX_ATTEMPTS = 2;
17456
17680
  var UPLOAD_RETRY_BASE_MS = 250;
17457
17681
  async function uploadWithRetry(args) {
@@ -17469,6 +17693,29 @@ async function uploadWithRetry(args) {
17469
17693
  }
17470
17694
  }
17471
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
+ }
17472
17719
  async function uploadContextRecords(opts) {
17473
17720
  const {
17474
17721
  processedFiles,
@@ -17483,6 +17730,7 @@ async function uploadContextRecords(opts) {
17483
17730
  branch,
17484
17731
  commitSha,
17485
17732
  clientVersion,
17733
+ ledger = NOOP_UPLOAD_LEDGER,
17486
17734
  onFileError,
17487
17735
  onSkillError
17488
17736
  } = opts;
@@ -17491,82 +17739,97 @@ async function uploadContextRecords(opts) {
17491
17739
  const uploadedSkillGroups = [];
17492
17740
  const failedFiles = [];
17493
17741
  const failedSkillGroups = [];
17494
- const limit = pLimit6(UPLOAD_CONCURRENCY);
17742
+ let dedupedCount = 0;
17743
+ let suppressedCount = 0;
17495
17744
  const extraFields = {
17496
17745
  ...repositoryUrl !== void 0 && { repositoryUrl },
17497
17746
  ...branch !== void 0 && { branch },
17498
17747
  ...commitSha !== void 0 && { commitSha },
17499
17748
  ...clientVersion !== void 0 && { clientVersion }
17500
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
+ };
17501
17800
  const tasks = [
17502
17801
  ...processedFiles.map(
17503
- (pf) => limit(async () => {
17504
- const s3Key = `${keyPrefix}ctx-${pf.md5}.bin`;
17505
- try {
17506
- await uploadWithRetry({
17507
- file: Buffer.from(pf.sanitizedContent, "utf-8"),
17508
- url,
17509
- uploadKey: s3Key,
17510
- uploadFields
17511
- });
17512
- } catch (err) {
17513
- failedFiles.push(pf.entry);
17514
- onFileError?.(pf.entry.name, err);
17515
- return;
17516
- }
17517
- records.push({
17518
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17519
- platform: platform2,
17520
- recordId: `ctx:${sessionId}:${pf.md5}`,
17521
- recordTimestamp: now,
17522
- blameType: "CHAT" /* Chat */,
17523
- rawDataS3Key: s3Key,
17524
- ...extraFields,
17525
- context: {
17526
- md5: pf.md5,
17527
- category: pf.entry.category,
17528
- name: pf.entry.name,
17529
- sizeBytes: pf.sizeBytes,
17530
- filePath: pf.entry.path,
17531
- sessionId
17532
- }
17533
- });
17534
- 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)
17535
17816
  })
17536
17817
  ),
17537
17818
  ...processedSkills.map(
17538
- (ps) => limit(async () => {
17539
- const s3Key = `${keyPrefix}skill-${ps.md5}.zip`;
17540
- try {
17541
- await uploadWithRetry({
17542
- file: ps.zipBuffer,
17543
- url,
17544
- uploadKey: s3Key,
17545
- uploadFields
17546
- });
17547
- } catch (err) {
17548
- failedSkillGroups.push(ps.group);
17549
- onSkillError?.(ps.group.name, err);
17550
- return;
17551
- }
17552
- records.push({
17553
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17554
- platform: platform2,
17555
- recordId: `ctx:${sessionId}:${ps.md5}`,
17556
- recordTimestamp: now,
17557
- blameType: "CHAT" /* Chat */,
17558
- rawDataS3Key: s3Key,
17559
- ...extraFields,
17560
- context: {
17561
- md5: ps.md5,
17562
- category: SKILL_CATEGORY,
17563
- name: ps.group.name,
17564
- sizeBytes: ps.sizeBytes,
17565
- filePath: ps.group.skillPath,
17566
- sessionId
17567
- }
17568
- });
17569
- 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)
17570
17833
  })
17571
17834
  )
17572
17835
  ];
@@ -17576,7 +17839,10 @@ async function uploadContextRecords(opts) {
17576
17839
  uploadedFiles,
17577
17840
  uploadedSkillGroups,
17578
17841
  failedFiles,
17579
- failedSkillGroups
17842
+ failedSkillGroups,
17843
+ dedupedCount,
17844
+ suppressedCount,
17845
+ breakerOpen: ledger.isBreakerOpen(Date.now())
17580
17846
  };
17581
17847
  }
17582
17848
  async function runContextFileUploadPipeline(opts) {
@@ -17592,6 +17858,7 @@ async function runContextFileUploadPipeline(opts) {
17592
17858
  branch,
17593
17859
  commitSha,
17594
17860
  clientVersion,
17861
+ ledger,
17595
17862
  submitRecords,
17596
17863
  onFileError,
17597
17864
  onSkillError
@@ -17608,7 +17875,10 @@ async function runContextFileUploadPipeline(opts) {
17608
17875
  uploadedFiles,
17609
17876
  uploadedSkillGroups,
17610
17877
  failedFiles,
17611
- failedSkillGroups
17878
+ failedSkillGroups,
17879
+ dedupedCount,
17880
+ suppressedCount,
17881
+ breakerOpen
17612
17882
  } = await uploadContextRecords({
17613
17883
  processedFiles,
17614
17884
  processedSkills,
@@ -17622,20 +17892,28 @@ async function runContextFileUploadPipeline(opts) {
17622
17892
  branch,
17623
17893
  commitSha,
17624
17894
  clientVersion,
17895
+ ledger,
17625
17896
  onFileError,
17626
17897
  onSkillError
17627
17898
  });
17628
17899
  if (failedFiles.length > 0 || failedSkillGroups.length > 0) {
17629
17900
  markContextUploadFailed(sessionId, failedFiles, failedSkillGroups);
17630
17901
  }
17902
+ const summary = {
17903
+ dedupedCount,
17904
+ suppressedCount,
17905
+ failedCount: failedFiles.length + failedSkillGroups.length,
17906
+ breakerOpen
17907
+ };
17631
17908
  if (records.length === 0) {
17632
- return { fileCount: 0, skillCount: 0 };
17909
+ return { fileCount: 0, skillCount: 0, ...summary };
17633
17910
  }
17634
17911
  await submitRecords(records);
17635
17912
  markContextFilesUploaded(sessionId, uploadedFiles, uploadedSkillGroups);
17636
17913
  return {
17637
17914
  fileCount: uploadedFiles.length,
17638
- skillCount: uploadedSkillGroups.length
17915
+ skillCount: uploadedSkillGroups.length,
17916
+ ...summary
17639
17917
  };
17640
17918
  }
17641
17919
 
@@ -17643,7 +17921,7 @@ async function runContextFileUploadPipeline(opts) {
17643
17921
  init_client_generates();
17644
17922
 
17645
17923
  // src/utils/shared-logger/create-logger.ts
17646
- import Configstore2 from "configstore";
17924
+ import Configstore3 from "configstore";
17647
17925
  import pino from "pino";
17648
17926
 
17649
17927
  // src/utils/shared-logger/configstore-stream.ts
@@ -17870,7 +18148,7 @@ function createLogger(config2) {
17870
18148
  } = config2;
17871
18149
  let csStream = null;
17872
18150
  if (enableConfigstore) {
17873
- const store = new Configstore2(namespace, {});
18151
+ const store = new Configstore3(namespace, {});
17874
18152
  csStream = createConfigstoreStream(store, {
17875
18153
  buffered,
17876
18154
  scopePath,
@@ -17979,7 +18257,7 @@ function createLogger(config2) {
17979
18257
 
17980
18258
  // src/features/claude_code/hook_logger.ts
17981
18259
  var DD_RUM_TOKEN = true ? "pubf59c0182545bfb4c299175119f1abf9b" : "";
17982
- var CLI_VERSION = true ? "1.4.44" : "unknown";
18260
+ var CLI_VERSION = true ? "1.4.46" : "unknown";
17983
18261
  var NAMESPACE = "mobbdev-claude-code-hook-logs";
17984
18262
  var claudeCodeVersion;
17985
18263
  function buildDdTags() {
@@ -18550,7 +18828,7 @@ async function processTranscript(input, sessionStore, log2, maxEntries = DAEMON_
18550
18828
  errors: entries.length
18551
18829
  };
18552
18830
  }
18553
- async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2) {
18831
+ async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2, ledger) {
18554
18832
  const { regularFiles, skillGroups } = await scanContextFiles(
18555
18833
  cwd,
18556
18834
  "claude-code",
@@ -18572,6 +18850,7 @@ async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2) {
18572
18850
  );
18573
18851
  return;
18574
18852
  }
18853
+ const uploadFailures = [];
18575
18854
  const pipelineResult = await runContextFileUploadPipeline({
18576
18855
  processedFiles,
18577
18856
  processedSkills,
@@ -18580,20 +18859,15 @@ async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2) {
18580
18859
  url,
18581
18860
  uploadFieldsJSON,
18582
18861
  keyPrefix,
18862
+ ledger,
18583
18863
  submitRecords: async (records) => {
18584
18864
  const r = await prepareAndSendTracyRecords(gqlClient, records, cwd);
18585
18865
  if (!r.ok) {
18586
18866
  throw new Error(r.errors?.join(", ") ?? "batch upload failed");
18587
18867
  }
18588
18868
  },
18589
- onFileError: (name, err) => log2.error(
18590
- { data: { sessionId, name, err } },
18591
- "Failed to upload context file to S3"
18592
- ),
18593
- onSkillError: (name, err) => log2.error(
18594
- { data: { sessionId, name, err } },
18595
- "Failed to upload skill zip to S3"
18596
- )
18869
+ onFileError: (name, err) => uploadFailures.push({ name, err }),
18870
+ onSkillError: (name, err) => uploadFailures.push({ name, err })
18597
18871
  });
18598
18872
  if (pipelineResult === null) {
18599
18873
  log2.error(
@@ -18602,19 +18876,48 @@ async function uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2) {
18602
18876
  );
18603
18877
  return;
18604
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
+ }
18605
18898
  if (pipelineResult.fileCount > 0 || pipelineResult.skillCount > 0) {
18606
18899
  log2.info(
18607
18900
  {
18608
18901
  data: {
18609
18902
  sessionId,
18610
18903
  fileCount: pipelineResult.fileCount,
18611
- skillCount: pipelineResult.skillCount
18904
+ skillCount: pipelineResult.skillCount,
18905
+ dedupedCount: pipelineResult.dedupedCount
18612
18906
  }
18613
18907
  },
18614
18908
  "Uploaded context files and skills for session"
18615
18909
  );
18616
18910
  }
18617
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
+ }
18618
18921
 
18619
18922
  // src/features/claude_code/install_hook.ts
18620
18923
  import fs14 from "fs";
@@ -18953,9 +19256,18 @@ async function startDaemon() {
18953
19256
  hookLog.info("Daemon starting");
18954
19257
  pruneHookLogFile();
18955
19258
  const pidFile = await acquirePidFile();
19259
+ const uploadLedger = new ConfigstoreUploadLedger();
19260
+ const inFlightContextUploads = /* @__PURE__ */ new Set();
18956
19261
  async function gracefulExit(code, reason) {
18957
19262
  hookLog.info({ data: { code } }, `Daemon exiting: ${reason}`);
18958
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();
18959
19271
  await flushDdLogs();
18960
19272
  process.exit(code);
18961
19273
  }
@@ -19045,14 +19357,20 @@ async function startDaemon() {
19045
19357
  }
19046
19358
  }
19047
19359
  const now = Date.now();
19048
- if (now - lastContextScanMs >= CONTEXT_SCAN_INTERVAL_MS) {
19360
+ if (!shuttingDown && now - lastContextScanMs >= CONTEXT_SCAN_INTERVAL_MS) {
19049
19361
  lastContextScanMs = now;
19050
19362
  for (const { sessionId, cwd } of sessionCwdCache.values()) {
19051
19363
  const log2 = createScopedHookLog(cwd, { daemonMode: true });
19052
- uploadContextFilesIfNeeded(sessionId, cwd, gqlClient, log2).catch(
19053
- (err) => log2.warn({ err }, "Context file scan failed")
19054
- );
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);
19055
19372
  }
19373
+ uploadLedger.flush();
19056
19374
  }
19057
19375
  if (cleanupConfigDir) {
19058
19376
  await cleanupStaleSessions(cleanupConfigDir);
@@ -19222,7 +19540,7 @@ async function tryAutoUpgradeHooks() {
19222
19540
  var HOOK_LOG_MAX_SCOPE_KEYS = 20;
19223
19541
  var HOOK_LOG_MAX_ENTRIES_PER_KEY = 200;
19224
19542
  function pruneHookLogFile() {
19225
- const logFilePath = new Configstore3("mobbdev-claude-code-hook-logs").path;
19543
+ const logFilePath = new Configstore4("mobbdev-claude-code-hook-logs").path;
19226
19544
  try {
19227
19545
  const raw = readFileSync(logFilePath, "utf-8");
19228
19546
  const data = JSON.parse(raw);
@@ -19354,7 +19672,7 @@ import {
19354
19672
  } from "@modelcontextprotocol/sdk/types.js";
19355
19673
 
19356
19674
  // src/mcp/Logger.ts
19357
- import Configstore4 from "configstore";
19675
+ import Configstore5 from "configstore";
19358
19676
 
19359
19677
  // src/mcp/services/WorkspaceService.ts
19360
19678
  var WorkspaceService = class {
@@ -19440,7 +19758,7 @@ var Logger = class {
19440
19758
  __publicField(this, "lastKnownPath", null);
19441
19759
  this.host = WorkspaceService.getHost();
19442
19760
  this.unknownPathSuffix = Math.floor(1e3 + Math.random() * 9e3).toString();
19443
- this.mobbConfigStore = new Configstore4("mobb-logs", {});
19761
+ this.mobbConfigStore = new Configstore5("mobb-logs", {});
19444
19762
  this.mobbConfigStore.set("version", packageJson.version);
19445
19763
  }
19446
19764
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "1.4.44",
3
+ "version": "1.4.46",
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",