@detiq/crawler 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -33,3 +33,5 @@ export type { ProxyTier, ProxyTierConfig } from './proxy.js';
33
33
  export { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';
34
34
  export { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';
35
35
  export type { Span, FinishedSpan } from './telemetry.js';
36
+ export { analyzeFailure } from './failure-analysis-agent.js';
37
+ export type { FailureContext } from './failure-analysis-agent.js';
package/dist/index.js CHANGED
@@ -24,11 +24,13 @@ import { extractMarkdown } from "./markdown-extract.js";
24
24
  import { TieredProxyManager } from "./proxy.js";
25
25
  import { RedisCrawlFrontier, isRedisFrontierEnabled } from "./redis-frontier.js";
26
26
  import { startSpan, endSpan, withSpan, increment, flushMetrics } from "./telemetry.js";
27
+ import { analyzeFailure } from "./failure-analysis-agent.js";
27
28
  export {
28
29
  CHROME_TLS_HEADERS,
29
30
  RedisCrawlFrontier,
30
31
  TieredProxyManager,
31
32
  adBlockerProfile,
33
+ analyzeFailure,
32
34
  chromeTlsAgent,
33
35
  closeLiveView,
34
36
  completeLiveView,
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["export * from './crawler.js';\nexport * from './visual-diff-verdict.js';\nexport * from './auth.js';\nexport * from './screen-reader.js';\nexport * from './job-queue.js';\nexport { startSqsWorker } from './sqs-queue.js';\nexport * from './session-capture.js';\nexport * from './session-export.js';\nexport * from './test-executor.js';\nexport { startLiveView, getFrame, liveViewNavigate, completeLiveView, closeLiveView, sendInput } from './live-view.js';\nexport { waitForEmailOTP, getInboxEmail } from './email-otp.js';\nexport { getSsoToken, getSsoClientCredentialsToken } from './sso-token.js';\nexport type { SsoConfig, SsoProvider } from './sso-token.js';\nexport * from './engine-router.js';\nexport * from './profiles/domain-profile.js';\nexport * from './policies/crawl-safety.js';\nexport * from './extractors/artifact-envelope.js';\nexport * from './network-observer.js';\nexport { createChromeTlsAgent, probeTlsFingerprint, chromeTlsAgent, CHROME_TLS_HEADERS } from './tls-fingerprint.js';\nexport type { TlsFingerprintOptions } from './tls-fingerprint.js';\nexport { prepareExtensionProfile, adBlockerProfile } from './browser-extension.js';\nexport type { ExtensionSource, ExtensionProfile } from './browser-extension.js';\nexport { createRedisQueue, maybeCreateRedisQueue, isRedisQueueEnabled } from './redis-queue.js';\nexport type { RedisQueueJob, RedisQueueClient } from './redis-queue.js';\nexport { discoverProjectUrls, discoverSitemapUrls } from './sitemap.js';\nexport type { UrlCandidate } from './sitemap.js';\nexport { httpCrawl } from './http-engine.js';\nexport { extractMarkdown } from './markdown-extract.js';\nexport type { ExtractedMarkdown } from './markdown-extract.js';\nexport type { HttpScreen, HttpCrawlOpts, HttpCrawlResult, HttpOutputFormat } from './http-engine.js';\nexport { TieredProxyManager } from './proxy.js';\nexport type { ProxyTier, ProxyTierConfig } from './proxy.js';\nexport { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';\nexport { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';\nexport type { Span, FinishedSpan } from './telemetry.js';\n"],
5
- "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB;AAC/B,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,eAAe,UAAU,kBAAkB,kBAAkB,eAAe,iBAAiB;AACtG,SAAS,iBAAiB,qBAAqB;AAC/C,SAAS,aAAa,oCAAoC;AAE1D,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB,qBAAqB,gBAAgB,0BAA0B;AAE9F,SAAS,yBAAyB,wBAAwB;AAE1D,SAAS,kBAAkB,uBAAuB,2BAA2B;AAE7E,SAAS,qBAAqB,2BAA2B;AAEzD,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAGhC,SAAS,0BAA0B;AAEnC,SAAS,oBAAoB,8BAA8B;AAC3D,SAAS,WAAW,SAAS,UAAU,WAAW,oBAAoB;",
4
+ "sourcesContent": ["export * from './crawler.js';\nexport * from './visual-diff-verdict.js';\nexport * from './auth.js';\nexport * from './screen-reader.js';\nexport * from './job-queue.js';\nexport { startSqsWorker } from './sqs-queue.js';\nexport * from './session-capture.js';\nexport * from './session-export.js';\nexport * from './test-executor.js';\nexport { startLiveView, getFrame, liveViewNavigate, completeLiveView, closeLiveView, sendInput } from './live-view.js';\nexport { waitForEmailOTP, getInboxEmail } from './email-otp.js';\nexport { getSsoToken, getSsoClientCredentialsToken } from './sso-token.js';\nexport type { SsoConfig, SsoProvider } from './sso-token.js';\nexport * from './engine-router.js';\nexport * from './profiles/domain-profile.js';\nexport * from './policies/crawl-safety.js';\nexport * from './extractors/artifact-envelope.js';\nexport * from './network-observer.js';\nexport { createChromeTlsAgent, probeTlsFingerprint, chromeTlsAgent, CHROME_TLS_HEADERS } from './tls-fingerprint.js';\nexport type { TlsFingerprintOptions } from './tls-fingerprint.js';\nexport { prepareExtensionProfile, adBlockerProfile } from './browser-extension.js';\nexport type { ExtensionSource, ExtensionProfile } from './browser-extension.js';\nexport { createRedisQueue, maybeCreateRedisQueue, isRedisQueueEnabled } from './redis-queue.js';\nexport type { RedisQueueJob, RedisQueueClient } from './redis-queue.js';\nexport { discoverProjectUrls, discoverSitemapUrls } from './sitemap.js';\nexport type { UrlCandidate } from './sitemap.js';\nexport { httpCrawl } from './http-engine.js';\nexport { extractMarkdown } from './markdown-extract.js';\nexport type { ExtractedMarkdown } from './markdown-extract.js';\nexport type { HttpScreen, HttpCrawlOpts, HttpCrawlResult, HttpOutputFormat } from './http-engine.js';\nexport { TieredProxyManager } from './proxy.js';\nexport type { ProxyTier, ProxyTierConfig } from './proxy.js';\nexport { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';\nexport { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';\nexport type { Span, FinishedSpan } from './telemetry.js';\nexport { analyzeFailure } from './failure-analysis-agent.js';\nexport type { FailureContext } from './failure-analysis-agent.js';\n"],
5
+ "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB;AAC/B,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,eAAe,UAAU,kBAAkB,kBAAkB,eAAe,iBAAiB;AACtG,SAAS,iBAAiB,qBAAqB;AAC/C,SAAS,aAAa,oCAAoC;AAE1D,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB,qBAAqB,gBAAgB,0BAA0B;AAE9F,SAAS,yBAAyB,wBAAwB;AAE1D,SAAS,kBAAkB,uBAAuB,2BAA2B;AAE7E,SAAS,qBAAqB,2BAA2B;AAEzD,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAGhC,SAAS,0BAA0B;AAEnC,SAAS,oBAAoB,8BAA8B;AAC3D,SAAS,WAAW,SAAS,UAAU,WAAW,oBAAoB;AAEtE,SAAS,sBAAsB;",
6
6
  "names": []
7
7
  }
@@ -84,6 +84,13 @@ export declare function claimNextQueuedJob(): Promise<ClaimedJob | null>;
84
84
  * died (crash, restart, OOM) without ever marking them DONE/FAILED. Idempotent and
85
85
  * safe to run from every worker process; only affects rows past the timeout. */
86
86
  export declare function requeueStaleRunningJobs(): Promise<void>;
87
+ /**
88
+ * Requeues any RUNNING jobs claimed by a different worker (i.e., a previous
89
+ * process instance). Call once at startup before the poll loop begins — this
90
+ * provides immediate recovery instead of waiting the full STALE_JOB_TIMEOUT_MINUTES.
91
+ * Safe to call from multiple replicas simultaneously (each uses its own WORKER_ID).
92
+ */
93
+ export declare function requeueOrphanedJobs(): Promise<void>;
87
94
  /**
88
95
  * Starts the durable job worker poll loop. This is what makes job processing
89
96
  * survive a restart — enqueueJob()'s immediate same-process claim attempt is
package/dist/job-queue.js CHANGED
@@ -130,6 +130,26 @@ async function requeueStaleRunningJobs() {
130
130
  logger.warn({ jobIds: rows.map((r) => r.id), staleTimeoutMinutes: STALE_JOB_TIMEOUT_MINUTES }, "[job-worker] requeued stale RUNNING job(s)");
131
131
  }
132
132
  }
133
+ async function requeueOrphanedJobs() {
134
+ const rows = await prismaAdmin.$queryRaw`
135
+ UPDATE "PlatformJob"
136
+ SET status = 'QUEUED',
137
+ "workerId" = NULL,
138
+ result = jsonb_set(
139
+ COALESCE(result::jsonb, '{}'::jsonb),
140
+ '{step}',
141
+ to_jsonb('Worker restarted — queued for recovery'::text),
142
+ true
143
+ )
144
+ WHERE status = 'RUNNING'
145
+ AND "workerId" IS NOT NULL
146
+ AND "workerId" != ${WORKER_ID}
147
+ RETURNING id
148
+ `;
149
+ if (rows.length > 0) {
150
+ logger.warn({ jobIds: rows.map((r) => r.id), workerId: WORKER_ID }, "[job-worker] requeued orphaned RUNNING job(s) from dead worker");
151
+ }
152
+ }
133
153
  async function dispatchClaimedJob(job) {
134
154
  const dispatcher = jobDispatchTable?.[job.type];
135
155
  if (!dispatcher) {
@@ -181,6 +201,9 @@ function startJobWorkerLoop(opts) {
181
201
  }
182
202
  };
183
203
  workerLoopTimer = setInterval(tick, pollIntervalMs);
204
+ requeueOrphanedJobs().catch(
205
+ (err) => logger.error({ err: String(err) }, "[job-worker] startup orphan sweep failed")
206
+ );
184
207
  void tick();
185
208
  }
186
209
  function stopJobWorkerLoop() {
@@ -238,14 +261,24 @@ async function getJob(tenantId, jobId) {
238
261
  );
239
262
  }
240
263
  async function listJobs(tenantId, projectId) {
241
- return withTenant(
242
- tenantId,
243
- (tx) => tx.platformJob.findMany({
264
+ return withTenant(tenantId, async (tx) => {
265
+ const jobs = await tx.platformJob.findMany({
244
266
  where: { tenantId, projectId },
245
267
  orderBy: { queuedAt: "desc" },
246
268
  take: 50
247
- })
248
- );
269
+ });
270
+ const jobIds = jobs.map((j) => j.id);
271
+ if (jobIds.length === 0) return jobs;
272
+ const modelRows = await tx.lLMCallLog.groupBy({
273
+ by: ["jobId"],
274
+ where: { tenantId, jobId: { in: jobIds } },
275
+ _max: { model: true, calledAt: true }
276
+ });
277
+ const modelByJobId = new Map(
278
+ modelRows.filter((r) => r.jobId && r._max?.model).map((r) => [r.jobId, r._max.model])
279
+ );
280
+ return jobs.map((j) => ({ ...j, llmModel: modelByJobId.get(j.id) ?? null }));
281
+ });
249
282
  }
250
283
  async function cancelJob(tenantId, jobId) {
251
284
  const controller = runningControllers.get(jobId);
@@ -374,13 +407,14 @@ async function investigateScreenChanges(tenantId, projectId, changedScreens) {
374
407
  async function processJob(jobId, config, opts) {
375
408
  const { tenantId, projectId } = config;
376
409
  const jobStartedAt = /* @__PURE__ */ new Date();
377
- await withTenant(
410
+ const claimed = await withTenant(
378
411
  tenantId,
379
- (tx) => tx.platformJob.update({
380
- where: { id: jobId },
412
+ (tx) => tx.platformJob.updateMany({
413
+ where: { id: jobId, status: { not: "CANCELLED" } },
381
414
  data: { status: "RUNNING", startedAt: jobStartedAt, heartbeatAt: jobStartedAt, workerId: WORKER_ID }
382
415
  })
383
416
  );
417
+ if (claimed.count === 0) return;
384
418
  const controller = new AbortController();
385
419
  runningControllers.set(jobId, controller);
386
420
  const heartbeatTimer = setInterval(() => {
@@ -2265,6 +2299,7 @@ export {
2265
2299
  listJobs,
2266
2300
  processDataRetentionPurge,
2267
2301
  processJob,
2302
+ requeueOrphanedJobs,
2268
2303
  requeueStaleRunningJobs,
2269
2304
  rerunJob,
2270
2305
  resumeJob,