mobbdev 1.4.27 → 1.4.29

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.
@@ -221,13 +221,18 @@ type RepoState = {
221
221
  * `GitService.getCurrentRepoState()` so detached-HEAD handling and SHA
222
222
  * normalization stay in lockstep across the two clients.
223
223
  *
224
- * Never throws non-existent dirs, missing git binaries, and unrecognized
225
- * remotes all resolve to nulls so the daemon hot path can rely on a value.
224
+ * The repository URL is the canonical form (parseScmURL().canonicalUrl) for any
225
+ * SCM host, including self-hosted / enterprise remotes (scmType "Unknown"); only
226
+ * an absent or genuinely unparseable remote resolves to `repositoryUrl: null`.
227
+ *
228
+ * Never throws — a non-existent dir, missing git binary, or absent/unparseable
229
+ * remote resolves to null so the daemon hot path can rely on a value.
226
230
  */
227
231
  declare function readRepoState(workingDir?: string): Promise<RepoState>;
228
232
  /**
229
- * Gets the normalized GitHub repository URL from the current working directory.
230
- * Returns null if not in a git repository or if not a GitHub repository.
233
+ * Gets the canonical remote repository URL for the working directory's repo, for
234
+ * any SCM host (GitHub, GitLab, Azure DevOps, Bitbucket, and self-hosted /
235
+ * enterprise). Returns null when there is no remote or it can't be parsed.
231
236
  */
232
237
  declare function getRepositoryUrl(workingDir?: string): Promise<string | null>;
233
238
  /**
@@ -338,6 +338,7 @@ var init_client_generates = __esm({
338
338
  IssueType_Enum2["Pt"] = "PT";
339
339
  IssueType_Enum2["RaceConditionFormatFlaw"] = "RACE_CONDITION_FORMAT_FLAW";
340
340
  IssueType_Enum2["Redos"] = "REDOS";
341
+ IssueType_Enum2["RedundantCondition"] = "REDUNDANT_CONDITION";
341
342
  IssueType_Enum2["RedundantNilErrorCheck"] = "REDUNDANT_NIL_ERROR_CHECK";
342
343
  IssueType_Enum2["RegexInjection"] = "REGEX_INJECTION";
343
344
  IssueType_Enum2["RegexMissingTimeout"] = "REGEX_MISSING_TIMEOUT";
@@ -1919,6 +1920,7 @@ var init_getIssueType = __esm({
1919
1920
  ["RETURN_IN_INIT" /* ReturnInInit */]: "Return in Init",
1920
1921
  ["ACTION_NOT_PINNED_TO_COMMIT_SHA" /* ActionNotPinnedToCommitSha */]: "Action Not Pinned to Commit Sha",
1921
1922
  ["DJANGO_BLANK_FIELD_NEEDS_NULL_OR_DEFAULT" /* DjangoBlankFieldNeedsNullOrDefault */]: "Django Blank Field Needs Null or Default",
1923
+ ["REDUNDANT_CONDITION" /* RedundantCondition */]: "Insider Threat: Redundant Condition",
1922
1924
  ["REDUNDANT_NIL_ERROR_CHECK" /* RedundantNilErrorCheck */]: "Redundant Nil Error Check",
1923
1925
  ["MISSING_WORKFLOW_PERMISSIONS" /* MissingWorkflowPermissions */]: "Missing Workflow Permissions",
1924
1926
  ["EXCESSIVE_SECRETS_EXPOSURE" /* ExcessiveSecretsExposure */]: "Excessive Secrets Exposure",
@@ -5168,6 +5170,7 @@ var fixDetailsData = {
5168
5170
  ["RETURN_IN_INIT" /* ReturnInInit */]: void 0,
5169
5171
  ["ACTION_NOT_PINNED_TO_COMMIT_SHA" /* ActionNotPinnedToCommitSha */]: void 0,
5170
5172
  ["DJANGO_BLANK_FIELD_NEEDS_NULL_OR_DEFAULT" /* DjangoBlankFieldNeedsNullOrDefault */]: void 0,
5173
+ ["REDUNDANT_CONDITION" /* RedundantCondition */]: void 0,
5171
5174
  ["REDUNDANT_NIL_ERROR_CHECK" /* RedundantNilErrorCheck */]: void 0,
5172
5175
  ["MISSING_WORKFLOW_PERMISSIONS" /* MissingWorkflowPermissions */]: void 0,
5173
5176
  ["EXCESSIVE_SECRETS_EXPOSURE" /* ExcessiveSecretsExposure */]: void 0,
@@ -5514,9 +5517,19 @@ Provide the absolute directory that contains the executable (e.g. \`/usr/bin\`);
5514
5517
  }
5515
5518
  };
5516
5519
 
5520
+ // src/features/analysis/scm/shared/src/storedQuestionData/cpp/pathManipulation.ts
5521
+ var pathManipulation = {
5522
+ baseDirectory: {
5523
+ content: ({ expression }) => `Which directory must \`${expression}\` stay within? Enter the absolute base directory file access is restricted to.`,
5524
+ description: ({ expression }) => `The fix canonicalizes \`${expression}\` (resolving \`.\`, \`..\`, and symlinks) and verifies the result stays inside this base directory. Provide the **absolute** root the application is allowed to read or write under, for example \`/var/app/data\`. The directory must exist at runtime. A path that resolves outside it is rejected at runtime.`,
5525
+ guidance: (_) => "You **must** set this to an absolute path that exists at runtime and that your application confines file access to. If you leave it unset (or give a relative path), the generated fix is emitted with a placeholder identifier (`MOBB_SET_ALLOWED_BASE_DIR`) that **deliberately does not compile**, so a missing security boundary cannot be overlooked \u2014 replace it with your absolute base directory. A relative path would otherwise resolve against the process working directory (moving the boundary). At runtime the guard aborts (C: `exit(EXIT_FAILURE)`) / throws (C++: `std::runtime_error`) on a path that escapes the base. Pick the narrowest directory that still contains every legitimate file the code opens."
5526
+ }
5527
+ };
5528
+
5517
5529
  // src/features/analysis/scm/shared/src/storedQuestionData/cpp/index.ts
5518
5530
  var vulnerabilities11 = {
5519
- ["CMDi" /* CmDi */]: commandInjection
5531
+ ["CMDi" /* CmDi */]: commandInjection,
5532
+ ["PT" /* Pt */]: pathManipulation
5520
5533
  };
5521
5534
  var cpp_default = vulnerabilities11;
5522
5535
 
@@ -7829,6 +7842,19 @@ import Debug8 from "debug";
7829
7842
  import Debug7 from "debug";
7830
7843
  import fetch3, { File, fileFrom, FormData } from "node-fetch";
7831
7844
  var debug8 = Debug7("mobbdev:upload-file");
7845
+ var MIN_UPLOAD_TIMEOUT_MS = 1e4;
7846
+ var MAX_UPLOAD_TIMEOUT_MS = 6e4;
7847
+ var SLOW_LINK_BYTES_PER_MS = 512;
7848
+ function computeUploadTimeoutMs(file) {
7849
+ if (typeof file === "string") {
7850
+ return MAX_UPLOAD_TIMEOUT_MS;
7851
+ }
7852
+ const sizeAllowanceMs = Math.ceil(file.byteLength / SLOW_LINK_BYTES_PER_MS);
7853
+ return Math.min(
7854
+ MAX_UPLOAD_TIMEOUT_MS,
7855
+ MIN_UPLOAD_TIMEOUT_MS + sizeAllowanceMs
7856
+ );
7857
+ }
7832
7858
  var S3UploadError = class extends Error {
7833
7859
  constructor(status, s3Code, s3Message) {
7834
7860
  super(`Failed to upload the file: ${status}`);
@@ -7887,11 +7913,12 @@ async function uploadFile({
7887
7913
  );
7888
7914
  }
7889
7915
  const agent = getProxyAgent(url);
7916
+ const effectiveSignal = signal ?? AbortSignal.timeout(computeUploadTimeoutMs(file));
7890
7917
  const response = await fetch3(url, {
7891
7918
  method: "POST",
7892
7919
  body: form,
7893
7920
  agent,
7894
- signal
7921
+ signal: effectiveSignal
7895
7922
  });
7896
7923
  if (!response.ok) {
7897
7924
  let bodyText = "";
@@ -8468,8 +8495,7 @@ async function readRepoState(workingDir) {
8468
8495
  const repoStatePromise = gitService.getCurrentRepoState().catch(() => ({ branch: null, commitSha: null }));
8469
8496
  const repositoryUrlPromise = gitService.getRemoteUrl().then((url) => {
8470
8497
  if (!url) return null;
8471
- const parsed = parseScmURL(url);
8472
- return parsed?.scmType && parsed.scmType !== "Unknown" ? url : null;
8498
+ return parseScmURL(url)?.canonicalUrl ?? null;
8473
8499
  }).catch(() => null);
8474
8500
  const [{ branch, commitSha }, repositoryUrl] = await Promise.all([
8475
8501
  repoStatePromise,
package/dist/index.mjs CHANGED
@@ -338,6 +338,7 @@ var init_client_generates = __esm({
338
338
  IssueType_Enum2["Pt"] = "PT";
339
339
  IssueType_Enum2["RaceConditionFormatFlaw"] = "RACE_CONDITION_FORMAT_FLAW";
340
340
  IssueType_Enum2["Redos"] = "REDOS";
341
+ IssueType_Enum2["RedundantCondition"] = "REDUNDANT_CONDITION";
341
342
  IssueType_Enum2["RedundantNilErrorCheck"] = "REDUNDANT_NIL_ERROR_CHECK";
342
343
  IssueType_Enum2["RegexInjection"] = "REGEX_INJECTION";
343
344
  IssueType_Enum2["RegexMissingTimeout"] = "REGEX_MISSING_TIMEOUT";
@@ -1584,6 +1585,7 @@ var init_getIssueType = __esm({
1584
1585
  ["RETURN_IN_INIT" /* ReturnInInit */]: "Return in Init",
1585
1586
  ["ACTION_NOT_PINNED_TO_COMMIT_SHA" /* ActionNotPinnedToCommitSha */]: "Action Not Pinned to Commit Sha",
1586
1587
  ["DJANGO_BLANK_FIELD_NEEDS_NULL_OR_DEFAULT" /* DjangoBlankFieldNeedsNullOrDefault */]: "Django Blank Field Needs Null or Default",
1588
+ ["REDUNDANT_CONDITION" /* RedundantCondition */]: "Insider Threat: Redundant Condition",
1587
1589
  ["REDUNDANT_NIL_ERROR_CHECK" /* RedundantNilErrorCheck */]: "Redundant Nil Error Check",
1588
1590
  ["MISSING_WORKFLOW_PERMISSIONS" /* MissingWorkflowPermissions */]: "Missing Workflow Permissions",
1589
1591
  ["EXCESSIVE_SECRETS_EXPOSURE" /* ExcessiveSecretsExposure */]: "Excessive Secrets Exposure",
@@ -4873,6 +4875,7 @@ var fixDetailsData = {
4873
4875
  ["RETURN_IN_INIT" /* ReturnInInit */]: void 0,
4874
4876
  ["ACTION_NOT_PINNED_TO_COMMIT_SHA" /* ActionNotPinnedToCommitSha */]: void 0,
4875
4877
  ["DJANGO_BLANK_FIELD_NEEDS_NULL_OR_DEFAULT" /* DjangoBlankFieldNeedsNullOrDefault */]: void 0,
4878
+ ["REDUNDANT_CONDITION" /* RedundantCondition */]: void 0,
4876
4879
  ["REDUNDANT_NIL_ERROR_CHECK" /* RedundantNilErrorCheck */]: void 0,
4877
4880
  ["MISSING_WORKFLOW_PERMISSIONS" /* MissingWorkflowPermissions */]: void 0,
4878
4881
  ["EXCESSIVE_SECRETS_EXPOSURE" /* ExcessiveSecretsExposure */]: void 0,
@@ -5307,9 +5310,19 @@ Provide the absolute directory that contains the executable (e.g. \`/usr/bin\`);
5307
5310
  }
5308
5311
  };
5309
5312
 
5313
+ // src/features/analysis/scm/shared/src/storedQuestionData/cpp/pathManipulation.ts
5314
+ var pathManipulation = {
5315
+ baseDirectory: {
5316
+ content: ({ expression }) => `Which directory must \`${expression}\` stay within? Enter the absolute base directory file access is restricted to.`,
5317
+ description: ({ expression }) => `The fix canonicalizes \`${expression}\` (resolving \`.\`, \`..\`, and symlinks) and verifies the result stays inside this base directory. Provide the **absolute** root the application is allowed to read or write under, for example \`/var/app/data\`. The directory must exist at runtime. A path that resolves outside it is rejected at runtime.`,
5318
+ guidance: (_) => "You **must** set this to an absolute path that exists at runtime and that your application confines file access to. If you leave it unset (or give a relative path), the generated fix is emitted with a placeholder identifier (`MOBB_SET_ALLOWED_BASE_DIR`) that **deliberately does not compile**, so a missing security boundary cannot be overlooked \u2014 replace it with your absolute base directory. A relative path would otherwise resolve against the process working directory (moving the boundary). At runtime the guard aborts (C: `exit(EXIT_FAILURE)`) / throws (C++: `std::runtime_error`) on a path that escapes the base. Pick the narrowest directory that still contains every legitimate file the code opens."
5319
+ }
5320
+ };
5321
+
5310
5322
  // src/features/analysis/scm/shared/src/storedQuestionData/cpp/index.ts
5311
5323
  var vulnerabilities11 = {
5312
- ["CMDi" /* CmDi */]: commandInjection
5324
+ ["CMDi" /* CmDi */]: commandInjection,
5325
+ ["PT" /* Pt */]: pathManipulation
5313
5326
  };
5314
5327
  var cpp_default = vulnerabilities11;
5315
5328
 
@@ -14209,6 +14222,41 @@ init_urlParser2();
14209
14222
  import Debug8 from "debug";
14210
14223
  import fetch3, { File, fileFrom, FormData } from "node-fetch";
14211
14224
  var debug9 = Debug8("mobbdev:upload-file");
14225
+ var MIN_UPLOAD_TIMEOUT_MS = 1e4;
14226
+ var MAX_UPLOAD_TIMEOUT_MS = 6e4;
14227
+ var SLOW_LINK_BYTES_PER_MS = 512;
14228
+ function computeUploadTimeoutMs(file) {
14229
+ if (typeof file === "string") {
14230
+ return MAX_UPLOAD_TIMEOUT_MS;
14231
+ }
14232
+ const sizeAllowanceMs = Math.ceil(file.byteLength / SLOW_LINK_BYTES_PER_MS);
14233
+ return Math.min(
14234
+ MAX_UPLOAD_TIMEOUT_MS,
14235
+ MIN_UPLOAD_TIMEOUT_MS + sizeAllowanceMs
14236
+ );
14237
+ }
14238
+ var TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set([
14239
+ "ECONNRESET",
14240
+ "ETIMEDOUT",
14241
+ "ECONNREFUSED",
14242
+ "EPIPE",
14243
+ "EAI_AGAIN",
14244
+ "ENETUNREACH",
14245
+ "ENOTFOUND"
14246
+ ]);
14247
+ function isTransientUploadError(err) {
14248
+ if (err instanceof S3UploadError) {
14249
+ return false;
14250
+ }
14251
+ if (typeof err !== "object" || err === null) {
14252
+ return false;
14253
+ }
14254
+ const e = err;
14255
+ if (e.code !== void 0 && TRANSIENT_ERROR_CODES.has(e.code)) {
14256
+ return true;
14257
+ }
14258
+ return e.type === "system" || e.name === "AbortError" || e.name === "TimeoutError";
14259
+ }
14212
14260
  var S3UploadError = class extends Error {
14213
14261
  constructor(status, s3Code, s3Message) {
14214
14262
  super(`Failed to upload the file: ${status}`);
@@ -14267,11 +14315,12 @@ async function uploadFile({
14267
14315
  );
14268
14316
  }
14269
14317
  const agent = getProxyAgent(url);
14318
+ const effectiveSignal = signal ?? AbortSignal.timeout(computeUploadTimeoutMs(file));
14270
14319
  const response = await fetch3(url, {
14271
14320
  method: "POST",
14272
14321
  body: form,
14273
14322
  agent,
14274
- signal
14323
+ signal: effectiveSignal
14275
14324
  });
14276
14325
  if (!response.ok) {
14277
14326
  let bodyText = "";
@@ -14653,8 +14702,7 @@ async function readRepoState(workingDir) {
14653
14702
  const repoStatePromise = gitService.getCurrentRepoState().catch(() => ({ branch: null, commitSha: null }));
14654
14703
  const repositoryUrlPromise = gitService.getRemoteUrl().then((url) => {
14655
14704
  if (!url) return null;
14656
- const parsed = parseScmURL(url);
14657
- return parsed?.scmType && parsed.scmType !== "Unknown" ? url : null;
14705
+ return parseScmURL(url)?.canonicalUrl ?? null;
14658
14706
  }).catch(() => null);
14659
14707
  const [{ branch, commitSha }, repositoryUrl] = await Promise.all([
14660
14708
  repoStatePromise,
@@ -17865,7 +17913,7 @@ import { spawn } from "child_process";
17865
17913
  import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
17866
17914
  import * as os8 from "os";
17867
17915
  import path24 from "path";
17868
- import { setTimeout as sleep3 } from "timers/promises";
17916
+ import { setTimeout as sleep4 } from "timers/promises";
17869
17917
  import Configstore3 from "configstore";
17870
17918
 
17871
17919
  // src/features/analysis/skill_quarantine/runQuarantineCheck.ts
@@ -18144,23 +18192,61 @@ var SCAN_PATHS = {
18144
18192
  // src/features/analysis/context_file_scanner.ts
18145
18193
  var MAX_CONTEXT_FILE_SIZE = 20 * 1024 * 1024;
18146
18194
  var SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
18195
+ var FAILURE_BACKOFF_BASE_MS = 60 * 1e3;
18196
+ var FAILURE_BACKOFF_MAX_MS = 60 * 60 * 1e3;
18147
18197
  var sessionMtimes = /* @__PURE__ */ new Map();
18148
18198
  function markContextFilesUploaded(sessionId, files, skills) {
18149
- let entry = sessionMtimes.get(sessionId);
18150
- if (!entry) {
18151
- entry = { files: /* @__PURE__ */ new Map(), skills: /* @__PURE__ */ new Map(), lastUpdatedAt: Date.now() };
18152
- sessionMtimes.set(sessionId, entry);
18153
- }
18199
+ const entry = getOrCreateSessionEntry(sessionId);
18154
18200
  for (const f of files) {
18155
18201
  entry.files.set(f.path, f.mtimeMs);
18202
+ entry.failures.delete(f.path);
18156
18203
  }
18157
18204
  if (skills) {
18158
18205
  for (const sg of skills) {
18159
18206
  entry.skills.set(sg.sessionKey, sg.maxMtimeMs);
18207
+ entry.failures.delete(sg.sessionKey);
18160
18208
  }
18161
18209
  }
18162
18210
  entry.lastUpdatedAt = Date.now();
18163
18211
  }
18212
+ function markContextUploadFailed(sessionId, files, skills) {
18213
+ const entry = getOrCreateSessionEntry(sessionId);
18214
+ const now = Date.now();
18215
+ const recordFailure = (key) => {
18216
+ const attempts = (entry.failures.get(key)?.attempts ?? 0) + 1;
18217
+ const delay3 = Math.min(
18218
+ FAILURE_BACKOFF_BASE_MS * 2 ** (attempts - 1),
18219
+ FAILURE_BACKOFF_MAX_MS
18220
+ );
18221
+ entry.failures.set(key, { attempts, nextRetryAt: now + delay3 });
18222
+ };
18223
+ for (const f of files) {
18224
+ recordFailure(f.path);
18225
+ }
18226
+ if (skills) {
18227
+ for (const sg of skills) {
18228
+ recordFailure(sg.sessionKey);
18229
+ }
18230
+ }
18231
+ entry.lastUpdatedAt = now;
18232
+ }
18233
+ function getOrCreateSessionEntry(sessionId) {
18234
+ let entry = sessionMtimes.get(sessionId);
18235
+ if (!entry) {
18236
+ entry = {
18237
+ files: /* @__PURE__ */ new Map(),
18238
+ skills: /* @__PURE__ */ new Map(),
18239
+ failures: /* @__PURE__ */ new Map(),
18240
+ lastUpdatedAt: Date.now()
18241
+ };
18242
+ sessionMtimes.set(sessionId, entry);
18243
+ }
18244
+ return entry;
18245
+ }
18246
+ function isInUploadBackoff(entry, key, now) {
18247
+ const failure = entry.failures.get(key);
18248
+ return failure !== void 0 && failure.nextRetryAt > now;
18249
+ }
18164
18250
  var COPILOT_CUSTOM_LOCATION_SETTINGS = [
18165
18251
  {
18166
18252
  key: "chat.agentSkillsLocations",
@@ -18520,6 +18606,9 @@ async function scanContextFiles(workspaceRoot, platform2, sessionId) {
18520
18606
  if (prevMtime !== void 0 && fileStat.mtimeMs <= prevMtime) {
18521
18607
  continue;
18522
18608
  }
18609
+ if (sessionEntry && isInUploadBackoff(sessionEntry, filePath, now)) {
18610
+ continue;
18611
+ }
18523
18612
  allFiles.push(fileEntry);
18524
18613
  }
18525
18614
  } catch {
@@ -18535,6 +18624,9 @@ async function scanContextFiles(workspaceRoot, platform2, sessionId) {
18535
18624
  if (prevMtime !== void 0 && group.maxMtimeMs <= prevMtime) {
18536
18625
  continue;
18537
18626
  }
18627
+ if (isInUploadBackoff(sessionEntry, group.sessionKey, now)) {
18628
+ continue;
18629
+ }
18538
18630
  }
18539
18631
  allSkillGroups.push(group);
18540
18632
  }
@@ -19123,6 +19215,7 @@ var lastRunAt = /* @__PURE__ */ new Map();
19123
19215
  var lastDirsMtimeMs = /* @__PURE__ */ new Map();
19124
19216
  var seenSkillMd5s = /* @__PURE__ */ new Set();
19125
19217
  var killSwitchLogged = false;
19218
+ var optInDisabledLogged = false;
19126
19219
  async function runQuarantineCheckIfNeeded(opts) {
19127
19220
  const { sessionId, cwd, gqlClient, log: log2 } = opts;
19128
19221
  if (process.env[KILL_SWITCH_ENV] === "1") {
@@ -19149,14 +19242,14 @@ async function runQuarantineCheckIfNeeded(opts) {
19149
19242
  return;
19150
19243
  }
19151
19244
  lastDirsMtimeMs.set(sessionId, dirsMtime);
19152
- log2.info(
19245
+ log2.debug(
19153
19246
  { sessionId, metric: Metric.CHECK_TRIGGERED, hasNewSkills },
19154
19247
  "skill_quarantine: check start"
19155
19248
  );
19156
19249
  const t0 = Date.now();
19157
19250
  try {
19158
19251
  await reconcileAndSweep(log2);
19159
- log2.info(
19252
+ log2.debug(
19160
19253
  { sessionId, count: installed.length, metric: Metric.SKILLS_CHECKED },
19161
19254
  "skill_quarantine: skills enumerated"
19162
19255
  );
@@ -19176,10 +19269,13 @@ async function runQuarantineCheckIfNeeded(opts) {
19176
19269
  log2
19177
19270
  );
19178
19271
  if (!quarantineEnabled) {
19179
- log2.info(
19180
- { sessionId, metric: Metric.CHECK_DISABLED_ORG },
19181
- "skill_quarantine: opt-in not enabled for any org of caller; skipping enforcement"
19182
- );
19272
+ if (!optInDisabledLogged) {
19273
+ log2.info(
19274
+ { sessionId, metric: Metric.CHECK_DISABLED_ORG },
19275
+ "skill_quarantine: opt-in not enabled for any org of caller; skipping enforcement"
19276
+ );
19277
+ optInDisabledLogged = true;
19278
+ }
19183
19279
  return;
19184
19280
  }
19185
19281
  for (const skill of installed) {
@@ -19204,7 +19300,7 @@ async function runQuarantineCheckIfNeeded(opts) {
19204
19300
  }
19205
19301
  }
19206
19302
  } finally {
19207
- log2.info(
19303
+ log2.debug(
19208
19304
  {
19209
19305
  sessionId,
19210
19306
  duration_ms: Date.now() - t0,
@@ -19316,9 +19412,27 @@ import path21 from "path";
19316
19412
  import { promisify } from "util";
19317
19413
 
19318
19414
  // src/features/analysis/context_file_uploader.ts
19415
+ import { setTimeout as sleep3 } from "timers/promises";
19319
19416
  import pLimit7 from "p-limit";
19320
19417
  init_client_generates();
19321
19418
  var UPLOAD_CONCURRENCY = 5;
19419
+ var UPLOAD_MAX_ATTEMPTS = 2;
19420
+ var UPLOAD_RETRY_BASE_MS = 250;
19421
+ async function uploadWithRetry(args) {
19422
+ for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt++) {
19423
+ try {
19424
+ await uploadFile(args);
19425
+ return;
19426
+ } catch (err) {
19427
+ const isLastAttempt = attempt === UPLOAD_MAX_ATTEMPTS;
19428
+ if (isLastAttempt || !isTransientUploadError(err)) {
19429
+ throw err;
19430
+ }
19431
+ const backoff = UPLOAD_RETRY_BASE_MS * 2 ** (attempt - 1) * (1 + Math.random());
19432
+ await sleep3(backoff);
19433
+ }
19434
+ }
19435
+ }
19322
19436
  async function uploadContextRecords(opts) {
19323
19437
  const {
19324
19438
  processedFiles,
@@ -19339,6 +19453,8 @@ async function uploadContextRecords(opts) {
19339
19453
  const records = [];
19340
19454
  const uploadedFiles = [];
19341
19455
  const uploadedSkillGroups = [];
19456
+ const failedFiles = [];
19457
+ const failedSkillGroups = [];
19342
19458
  const limit = pLimit7(UPLOAD_CONCURRENCY);
19343
19459
  const extraFields = {
19344
19460
  ...repositoryUrl !== void 0 && { repositoryUrl },
@@ -19351,13 +19467,14 @@ async function uploadContextRecords(opts) {
19351
19467
  (pf) => limit(async () => {
19352
19468
  const s3Key = `${keyPrefix}ctx-${pf.md5}.bin`;
19353
19469
  try {
19354
- await uploadFile({
19470
+ await uploadWithRetry({
19355
19471
  file: Buffer.from(pf.sanitizedContent, "utf-8"),
19356
19472
  url,
19357
19473
  uploadKey: s3Key,
19358
19474
  uploadFields
19359
19475
  });
19360
19476
  } catch (err) {
19477
+ failedFiles.push(pf.entry);
19361
19478
  onFileError?.(pf.entry.name, err);
19362
19479
  return;
19363
19480
  }
@@ -19385,13 +19502,14 @@ async function uploadContextRecords(opts) {
19385
19502
  (ps) => limit(async () => {
19386
19503
  const s3Key = `${keyPrefix}skill-${ps.md5}.zip`;
19387
19504
  try {
19388
- await uploadFile({
19505
+ await uploadWithRetry({
19389
19506
  file: ps.zipBuffer,
19390
19507
  url,
19391
19508
  uploadKey: s3Key,
19392
19509
  uploadFields
19393
19510
  });
19394
19511
  } catch (err) {
19512
+ failedSkillGroups.push(ps.group);
19395
19513
  onSkillError?.(ps.group.name, err);
19396
19514
  return;
19397
19515
  }
@@ -19417,7 +19535,13 @@ async function uploadContextRecords(opts) {
19417
19535
  )
19418
19536
  ];
19419
19537
  await Promise.allSettled(tasks);
19420
- return { records, uploadedFiles, uploadedSkillGroups };
19538
+ return {
19539
+ records,
19540
+ uploadedFiles,
19541
+ uploadedSkillGroups,
19542
+ failedFiles,
19543
+ failedSkillGroups
19544
+ };
19421
19545
  }
19422
19546
  async function runContextFileUploadPipeline(opts) {
19423
19547
  const {
@@ -19443,7 +19567,13 @@ async function runContextFileUploadPipeline(opts) {
19443
19567
  return null;
19444
19568
  }
19445
19569
  const now = (/* @__PURE__ */ new Date()).toISOString();
19446
- const { records, uploadedFiles, uploadedSkillGroups } = await uploadContextRecords({
19570
+ const {
19571
+ records,
19572
+ uploadedFiles,
19573
+ uploadedSkillGroups,
19574
+ failedFiles,
19575
+ failedSkillGroups
19576
+ } = await uploadContextRecords({
19447
19577
  processedFiles,
19448
19578
  processedSkills,
19449
19579
  keyPrefix,
@@ -19459,6 +19589,9 @@ async function runContextFileUploadPipeline(opts) {
19459
19589
  onFileError,
19460
19590
  onSkillError
19461
19591
  });
19592
+ if (failedFiles.length > 0 || failedSkillGroups.length > 0) {
19593
+ markContextUploadFailed(sessionId, failedFiles, failedSkillGroups);
19594
+ }
19462
19595
  if (records.length === 0) {
19463
19596
  return { fileCount: 0, skillCount: 0 };
19464
19597
  }
@@ -19810,7 +19943,7 @@ function createLogger(config2) {
19810
19943
 
19811
19944
  // src/features/claude_code/hook_logger.ts
19812
19945
  var DD_RUM_TOKEN = true ? "pubf59c0182545bfb4c299175119f1abf9b" : "";
19813
- var CLI_VERSION = true ? "1.4.27" : "unknown";
19946
+ var CLI_VERSION = true ? "1.4.29" : "unknown";
19814
19947
  var NAMESPACE = "mobbdev-claude-code-hook-logs";
19815
19948
  var claudeCodeVersion;
19816
19949
  function buildDdTags() {
@@ -19989,7 +20122,7 @@ async function readNewTranscriptEntries(transcriptPath, sessionId, sessionStore,
19989
20122
  const stat5 = await fh.stat();
19990
20123
  fileSize = stat5.size;
19991
20124
  if (cursor.byteOffset >= stat5.size) {
19992
- hookLog.info({ data: { sessionId } }, "No new data in transcript file");
20125
+ hookLog.debug({ data: { sessionId } }, "No new data in transcript file");
19993
20126
  return {
19994
20127
  entries: [],
19995
20128
  endByteOffset: fileSize,
@@ -20226,12 +20359,12 @@ async function processTranscript(input, sessionStore, log2, maxEntries = DAEMON_
20226
20359
  );
20227
20360
  const cursorKey = getCursorKey(resolvedTranscriptPath);
20228
20361
  if (rawEntries.length === 0) {
20229
- log2.info("No new entries to upload");
20362
+ log2.debug("No new entries to upload");
20230
20363
  return { entriesUploaded: 0, entriesSkipped: 0, errors: 0 };
20231
20364
  }
20232
20365
  const { filtered: entries, filteredOut } = filterEntries(rawEntries);
20233
20366
  if (filteredOut > 0) {
20234
- log2.info(
20367
+ log2.debug(
20235
20368
  { data: { filteredOut, remaining: entries.length } },
20236
20369
  "Filtered out noise entries"
20237
20370
  );
@@ -20914,7 +21047,7 @@ async function startDaemon() {
20914
21047
  },
20915
21048
  "daemon poll cycle"
20916
21049
  );
20917
- await sleep3(DAEMON_POLL_INTERVAL_MS);
21050
+ await sleep4(DAEMON_POLL_INTERVAL_MS);
20918
21051
  }
20919
21052
  }
20920
21053
  async function acquirePidFile() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "1.4.27",
3
+ "version": "1.4.29",
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",