@dudousxd/nestjs-durable-telescope 0.8.0 → 0.9.0

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.cjs CHANGED
@@ -178,6 +178,7 @@ var DurableTelescopeWatcher = class {
178
178
  event: event.type,
179
179
  workflow: event.workflow,
180
180
  runId: event.runId,
181
+ parentRunId: (0, import_nestjs_durable_core.parentRunIdOf)(event.runId),
181
182
  seq: event.seq,
182
183
  name: event.name,
183
184
  kind: event.kind,
@@ -228,6 +229,8 @@ var DurableTelescopeWatcher = class {
228
229
  event.type,
229
230
  `run:${event.runId}`
230
231
  ];
232
+ const parent = (0, import_nestjs_durable_core.parentRunIdOf)(event.runId);
233
+ if (parent) tags.push(`parent:${parent}`);
231
234
  if (event.workflow) tags.push(`workflow:${event.workflow}`);
232
235
  if (event.kind) tags.push(`kind:${event.kind}`);
233
236
  if (event.type === "run.failed") tags.push("failed");
@@ -568,6 +571,16 @@ function dedupeRunEvents(entries) {
568
571
  }
569
572
  __name(dedupeRunEvents, "dedupeRunEvents");
570
573
  var STATE_CAP = 1e4;
574
+ function countByStatus(store, status) {
575
+ if (store.countRuns) return store.countRuns({
576
+ status
577
+ });
578
+ return store.listRuns({
579
+ status,
580
+ limit: STATE_CAP
581
+ }).then((runs) => runs.length);
582
+ }
583
+ __name(countByStatus, "countByStatus");
571
584
  function durableStateProvider() {
572
585
  return {
573
586
  name: "durable.state",
@@ -576,12 +589,8 @@ function durableStateProvider() {
576
589
  strict: false
577
590
  });
578
591
  const status = query?.status ?? "dead";
579
- const runs = await store.listRuns({
580
- status,
581
- limit: STATE_CAP
582
- });
583
592
  return {
584
- value: runs.length
593
+ value: await countByStatus(store, status)
585
594
  };
586
595
  }
587
596
  };
@@ -977,10 +986,7 @@ function durableStateBreakdownProvider() {
977
986
  const store = ctx.moduleRef.get(import_nestjs_durable_core2.STATE_STORE_CANONICAL, {
978
987
  strict: false
979
988
  });
980
- const counts = await Promise.all(STATE_BREAKDOWN_STATUSES.map((status) => store.listRuns({
981
- status,
982
- limit: STATE_CAP
983
- }).then((runs) => runs.length)));
989
+ const counts = await Promise.all(STATE_BREAKDOWN_STATUSES.map((status) => countByStatus(store, status)));
984
990
  const segments = STATE_BREAKDOWN_STATUSES.map((label, i) => ({
985
991
  label,
986
992
  value: counts[i],
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/durable-telescope.watcher.ts","../src/durable-execution-scope.ts","../src/durable-run-trace.ts","../src/durable-telescope.extension.ts","../src/durable-dashboard.spec-data.ts","../src/durable-data-providers.ts"],"sourcesContent":["export * from './durable-telescope.watcher';\nexport * from './durable-execution-scope';\nexport * from './durable-run-trace';\nexport * from './durable-telescope.extension';\nexport * from './durable-data-providers';\nexport * from './durable-dashboard.spec-data';\n","import {\n type EngineEvent,\n WorkflowEngine,\n useDurableExecution,\n} from '@dudousxd/nestjs-durable-core';\nimport type { Watcher, WatcherContext } from '@dudousxd/nestjs-telescope';\nimport { SpanStatusCode, context, trace } from '@opentelemetry/api';\nimport { durableExecutionScope } from './durable-execution-scope';\nimport { runTraceContext, runTraceId } from './durable-run-trace';\n\n/**\n * A Telescope watcher that turns durable-workflow lifecycle events into Telescope entries, so\n * runs and steps (including remote/Python steps) show up alongside the app's requests, queries\n * and jobs in the Telescope UI — under the **Workflows** tab.\n *\n * It does two things, and the second is the one that makes a failing workflow debuggable:\n *\n * 1. **Records an entry per engine event**, stamped with the run's trace (see\n * {@link runTraceContext}), so a whole run — across all its turns, all its steps and every\n * process that executed them — reads as a single trace at `/telescope#/traces/<id>`.\n *\n * 2. **Opens a Telescope batch around the execution itself** (see {@link durableExecutionScope}),\n * through the engine's `useDurableExecution` seam. Without this, everything a workflow turn or a\n * step handler *did* — its queries, its logs, the exception it threw — was recorded outside any\n * batch and outside any span: traceless, and so impossible to correlate back to the step that\n * caused it. Recording an event after the fact cannot fix that; the scope has to be open while\n * the body runs.\n *\n * Add it to `TelescopeModule.forRoot({ watchers: [new DurableTelescopeWatcher()] })`. It resolves\n * the engine from the (global) durable providers and subscribes to its events.\n *\n * ## What is NOT covered\n *\n * A **remote step** — one whose handler lives in another runtime, e.g. the Python worker — executes\n * out of process, so nothing here can wrap it. Its lifecycle still shows up (the engine emits\n * `step.started`/`step.completed`/`step.failed` when it dispatches and when the result lands, and\n * those entries carry the run's trace id), but the queries and exceptions *inside* that handler are\n * that runtime's to record. Because the trace id is derived from the run id rather than held in this\n * process, a remote runtime that derives it the same way joins the same trace — but that is a change\n * on its side, not something this watcher can do on its behalf.\n *\n * A **remote workflow** (a run whose body itself lives in another runtime) is likewise unwrapped:\n * the engine dispatches a workflow task and awaits a decision rather than executing a body, and a\n * scope around a dispatch would describe the wrong thing.\n */\nexport class DurableTelescopeWatcher implements Watcher {\n readonly type = 'durable';\n /** Removes the execution wrapper from the engine's process-level registry. */\n private disposeScope: (() => void) | undefined;\n\n register(ctx: WatcherContext): void {\n // Registered FIRST, and unconditionally: this is the half that works without a local engine. A\n // thin worker binds `WorkflowEngine` to a start-only facade with no event stream, but it is\n // exactly where step handlers run — so the execution scope belongs there even though there is\n // nothing local to subscribe to.\n this.disposeScope?.();\n this.disposeScope = useDurableExecution(durableExecutionScope(ctx));\n\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false });\n // A store-less thin-worker / tenant deployment (e.g. a local `DURABLE_TENANT=…` dev stack) binds\n // the `WorkflowEngine` token to a start-only `DurableStartClient` facade — it proxies run starts\n // over the transport and has NO local lifecycle event stream (those events live on the operator\n // that holds the store). There's nothing local to observe, so skip the subscription gracefully\n // instead of throwing \"engine.subscribe is not a function\".\n if (typeof (engine as Partial<WorkflowEngine>)?.subscribe !== 'function') return;\n const tracer = trace.getTracer('@dudousxd/nestjs-durable-telescope');\n\n engine.subscribe((event) => {\n const record = () =>\n ctx.record({\n type: this.type,\n // Stated explicitly rather than left to the ambient span, and the Recorder honours an\n // explicit `traceId` above everything else. A lifecycle event is very often emitted\n // OUTSIDE any execution scope — a remote step's result lands in a transport callback, a\n // recovery sweep emits for a run this process never executed — and those entries are\n // exactly the ones an operator follows into the trace. We know the run, so we know the\n // trace; there is no reason to let it depend on where the emit happened to land.\n traceId: runTraceId(event.runId),\n content: {\n event: event.type,\n workflow: event.workflow,\n runId: event.runId,\n seq: event.seq,\n name: event.name,\n kind: event.kind,\n output: event.output,\n error: event.error,\n durationMs: event.durationMs,\n },\n tags: this.tags(event),\n });\n\n // Anchor on the run's trace. Derived from the run id, so this event groups with the entries\n // recorded *inside* the turn/step scopes without either side having to hand the other a span —\n // which is the only thing that works once a step executes on a later turn, or in another pod.\n const base = runTraceContext(event.runId);\n\n // A step/signal/sleep event → give it its own child span so it's a distinct node in the\n // trace; run-level events record against the run's own span context. Either way the active\n // span at record time carries the run's trace id, so all entries group into one trace.\n if (event.name != null && event.seq != null) {\n const step = tracer.startSpan(\n `step ${event.name}`,\n {\n attributes: {\n 'durable.run_id': event.runId,\n 'durable.step.seq': event.seq,\n 'durable.step.kind': event.kind,\n },\n },\n base,\n );\n if (event.type === 'step.failed') {\n const message = event.error?.message;\n step.setStatus(\n message !== undefined\n ? { code: SpanStatusCode.ERROR, message }\n : { code: SpanStatusCode.ERROR },\n );\n }\n context.with(trace.setSpan(base, step), record);\n step.end();\n } else {\n context.with(base, record);\n }\n });\n }\n\n /**\n * Detach the execution wrapper. The `Watcher` SPI has no teardown hook, so nothing calls this in a\n * running app — where the wrapper should live exactly as long as the process does. It exists so a\n * test that registers a watcher does not leave a wrapper behind in the process-level registry for\n * the next test to trip over.\n */\n unregister(): void {\n this.disposeScope?.();\n this.disposeScope = undefined;\n }\n\n private tags(event: EngineEvent): string[] {\n const tags = ['durable', event.type, `run:${event.runId}`];\n if (event.workflow) tags.push(`workflow:${event.workflow}`);\n if (event.kind) tags.push(`kind:${event.kind}`);\n if (event.type === 'run.failed') tags.push('failed');\n return tags;\n }\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport { randomBytes } from 'node:crypto';\nimport type { DurableExecution, DurableExecutionWrapper } from '@dudousxd/nestjs-durable-core';\nimport type { WatcherContext } from '@dudousxd/nestjs-telescope';\nimport { SpanStatusCode, context, trace } from '@opentelemetry/api';\nimport { runTraceContext, runTraceId } from './durable-run-trace';\n\n/** What the scope publishes to anything recording on this async path. */\nexport interface DurableScope {\n /** The run whose unit of work is executing. */\n readonly runId: string;\n /** The run's trace — see {@link runTraceId}. */\n readonly traceId: string;\n /** This scope's span within that trace. Stable for every entry recorded inside it. */\n readonly spanId: string;\n /**\n * Identity of the wrapper that opened this scope. Reuse is only ever offered to the wrapper that\n * owns the scope: two Telescope instances in one process (a torn-down test module whose wrapper\n * outlived it, an app that mounts two consoles) would otherwise have the first one's scope\n * silently suppress the second one's batch, and the second console would record everything\n * batchless while looking correctly wired.\n */\n readonly owner: object;\n}\n\n/**\n * The durable scope THIS package has open on the current async path, if any.\n *\n * Telescope's own batch context is not readable from a watcher (`WatcherContext` exposes\n * `runInBatch`/`beginBatch` but no `currentBatch()`), and we would not want to read it even if it\n * were: the question is not \"is any batch open\" — an HTTP request that happened to start a run\n * inline has one — but \"is the scope on this path the one we opened for this same run\".\n */\nconst activeScope = new AsyncLocalStorage<DurableScope>();\n\n/** The durable execution scope active on this async path, or undefined outside one. */\nexport function currentDurableScope(): DurableScope | undefined {\n return activeScope.getStore();\n}\n\n/** `BatchOrigin` has no durable/workflow member — see {@link durableExecutionScope} for the choice. */\nconst DURABLE_BATCH_ORIGIN = 'queue';\n\nfunction spanName(execution: DurableExecution): string {\n if (execution.unit === 'step') return `step ${execution.name ?? '?'}`;\n return `workflow ${execution.workflow ?? '?'}`;\n}\n\nfunction spanAttributes(execution: DurableExecution): Record<string, string | number> {\n const attributes: Record<string, string | number> = { 'durable.run_id': execution.runId };\n if (execution.workflow !== undefined) attributes['durable.workflow'] = execution.workflow;\n if (execution.name !== undefined) attributes['durable.step.name'] = execution.name;\n if (execution.seq !== undefined) attributes['durable.step.seq'] = execution.seq;\n if (execution.attempt !== undefined) attributes['durable.step.attempt'] = execution.attempt;\n return attributes;\n}\n\n/**\n * Builds the {@link DurableExecutionWrapper} that puts every workflow turn and every step handler\n * body inside a Telescope batch and an OTel span, so the queries, logs and exceptions those bodies\n * produce are correlated to the unit of durable work that caused them instead of landing traceless.\n *\n * ## Why a batch around execution and not around the event\n *\n * The watcher's engine-event subscription records what *happened*, after the fact. A batch opened\n * there would contain exactly one entry — the durable entry itself — and nothing the step actually\n * did, because by then the handler has returned and the exception it threw has long since been\n * recorded with no batch at all. Wrapping the execution is the only shape that puts the failing code\n * inside the scope.\n *\n * ## Nesting: reuse the batch when the same run already has one open on this path\n *\n * With an in-process transport (event-emitter, in-memory, the DB transport's inline path) a\n * dispatched step's handler starts synchronously inside the turn that dispatched it, and the result\n * it produces resumes the next turn on that same async path — so the whole run is one unbroken\n * causal chain. Opening a fresh batch at each hop would split that chain across many batch ids for\n * no gain: Telescope's entry model has no parent-batch pointer, so the split is not recoverable in\n * the UI — the step's queries would appear in a batch with nothing visible causing them.\n *\n * So the rule is the causal one, not the syntactic one: a unit reuses the batch when THIS wrapper\n * already has one open for THIS run on this async path. The consequence is worth stating plainly,\n * because it differs by transport and looks like an inconsistency if you meet it unprepared:\n *\n * - **in-process transport** — one batch for the whole run (every turn, every step), because it is\n * genuinely one chain;\n * - **queue-backed transport** (BullMQ, SQS, a thin worker) — one batch per turn and one per step,\n * because each really is a separate entry point, in a separate process, with no ambient scope to\n * inherit.\n *\n * What holds either way is the trace: every entry of a run carries {@link runTraceId}, so the run is\n * one trace whether it executed in one process or twenty. That invariant is the thing to rely on;\n * batch granularity follows the execution shape.\n *\n * A different run is a different chain and always gets its own batch. That includes a child\n * workflow: it has its own run id, its own timeline and its own dashboard row, and its trace is\n * anchored on ITS run id (see {@link runTraceContext}), so it reads as a related-but-distinct trace\n * instead of being folded into the parent's. The parent/child relationship is already carried by the\n * run graph; flattening the batches to restate it would erase the child's own boundaries and gain\n * nothing.\n *\n * ## Origin\n *\n * `BatchOrigin` in `@dudousxd/nestjs-telescope` is the closed union\n * `'http' | 'queue' | 'schedule' | 'cli' | 'manual'` — it has no durable/workflow member. Widening\n * it belongs to that repo and a coordinated release, so this uses `'queue'`, which is also the\n * honest description: durable work reaches an executor by being dispatched over a transport\n * (BullMQ, SQS, an event emitter), exactly like the jobs the BullMQ watcher marks `'queue'`.\n */\nexport function durableExecutionScope(ctx: WatcherContext): DurableExecutionWrapper {\n const tracer = trace.getTracer('@dudousxd/nestjs-durable-telescope');\n const owner = {};\n\n return (execution, next) => {\n // Already inside our own scope for this run — reuse it (see \"Nesting\" above).\n const open = activeScope.getStore();\n if (open?.owner === owner && open.runId === execution.runId) return next();\n // Telescope switched off: `runInBatch` would already short-circuit, but the span and the ALS\n // frame would not, so check here and hand the body straight back.\n if (ctx.config.enabled === false) return next();\n\n try {\n const parent = runTraceContext(execution.runId);\n const span = tracer.startSpan(\n spanName(execution),\n { attributes: spanAttributes(execution) },\n parent,\n );\n // Prefer the real span's id when an OTel SDK produced one; a host with no SDK gets a\n // non-recording span whose id is the run's synthetic parent, which would make every scope in\n // a run look like one span. Mint an id in that case so the scopes stay distinguishable.\n const spanContext = span.spanContext();\n const scope: DurableScope = {\n runId: execution.runId,\n traceId: runTraceId(execution.runId),\n spanId: span.isRecording() ? spanContext.spanId : randomBytes(8).toString('hex'),\n owner,\n };\n\n return context.with(trace.setSpan(parent, span), () =>\n activeScope.run(scope, () =>\n ctx.runInBatch(DURABLE_BATCH_ORIGIN, next).then(\n (value) => {\n span.end();\n return value;\n },\n (error: unknown) => {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n ...(error instanceof Error ? { message: error.message } : {}),\n });\n span.end();\n throw error; // never swallow the workflow's error\n },\n ),\n ),\n );\n } catch {\n // Opening the scope failed (a broken tracer, a Telescope in a bad state). Run the work anyway,\n // untraced. The BullMQ and schedule watchers hold the same line from the other direction —\n // they never let a recording failure fail a healthy job — and it matters more here, because\n // this wrapper sits around a workflow body whose failure is durably persisted.\n return next();\n }\n };\n}\n\n/** Minimal structural view of Telescope's `TraceContextProvider` (it is one method, and typing\n * against the interface would drag a value import in for nothing). */\ninterface TraceContextLike {\n current(): { traceId: string; spanId: string } | null;\n}\n\n/**\n * A Telescope `traceContext` provider that reports the active durable scope, so entries recorded by\n * OTHER watchers (queries, cache, outbound HTTP) during a workflow turn or a step body carry the\n * run's trace id.\n *\n * Wire it as `TelescopeModule.forRoot({ traceContext: durableTraceContext() })`.\n *\n * This exists because Telescope resolves an entry's trace id from an ambient OTel span, and\n * `@opentelemetry/api` on its own propagates nothing: without a registered context manager\n * `context.with(...)` is a no-op and `getActiveSpan()` is always undefined. An app that runs a full\n * OTel SDK needs none of this — it should pass `OtelTraceContextProvider` (optionally as the\n * `delegate` below, which is then consulted first so the app's real spans win), and the scope's\n * spans hang off the run's trace anyway. An app that runs no OTel at all — the common case — would\n * otherwise get correlated batches and no trace ids, which is half the feature.\n */\nexport function durableTraceContext(delegate?: TraceContextLike): TraceContextLike {\n return {\n current() {\n // The host's own tracing wins where it has an answer: inside a durable scope it will report a\n // span from this run's trace anyway, and outside one it is the only source there is.\n const fromDelegate = delegate?.current() ?? null;\n if (fromDelegate) return fromDelegate;\n const scope = activeScope.getStore();\n return scope ? { traceId: scope.traceId, spanId: scope.spanId } : null;\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport { type Context, TraceFlags, context, trace } from '@opentelemetry/api';\n\n/**\n * The trace a run's spans hang under, DERIVED FROM THE RUN ID rather than held as a live root span.\n *\n * The obvious implementation — open a root span on `run.started`, keep it in a map, end it when the\n * run settles — cannot work for a durable workflow. A run suspends: `ctx.step` dispatches and the\n * turn unwinds, and the step body then executes minutes (or days) later, on a different turn, very\n * often in a different process. A live root span would either have to be ended at every suspension —\n * fragmenting one run into a trace per turn, so the step that failed is in a different trace from\n * the turn that dispatched it — or held open across the suspension, which pins a span object for the\n * lifetime of a workflow that is explicitly designed to wait for days.\n *\n * Hashing the run id sidesteps both. The trace id is a pure function of the run, so every turn and\n * every step of a run lands in ONE trace with no state held anywhere, and a worker in another\n * process computes the same id from the same run id — which is what makes a remotely-executed step\n * correlate with the run that dispatched it at all.\n *\n * The parent span context is marked `isRemote` because that is what it is: a reference to a trace\n * whose \"root\" is the run itself, not a span this process ever recorded.\n */\nexport function runTraceId(runId: string): string {\n // 16 bytes of hex, as the W3C trace-context spec requires. sha256 is not used as a security\n // primitive here — only as a well-distributed map from an arbitrary run id into that space.\n return createHash('sha256').update(`durable:run:${runId}`).digest('hex').slice(0, 32);\n}\n\n/** The synthetic parent span id for a run's trace — derived the same way, from a different prefix. */\nfunction runSpanId(runId: string): string {\n return createHash('sha256').update(`durable:run-span:${runId}`).digest('hex').slice(0, 16);\n}\n\n/**\n * An OTel {@link Context} whose active span context is the run's synthetic root. Spans started\n * against it are children of the run, and Telescope's trace-context provider stamps every entry\n * recorded inside it with {@link runTraceId}.\n *\n * `TraceFlags.SAMPLED` is set deliberately: a non-sampled parent leaves the derived span\n * non-recording, and its span context would then never reach an exporter — the run would be\n * traceable in Telescope but invisible in any OTel backend, which is the confusing half-state.\n */\nexport function runTraceContext(runId: string, parent: Context = context.active()): Context {\n return trace.setSpanContext(parent, {\n traceId: runTraceId(runId),\n spanId: runSpanId(runId),\n traceFlags: TraceFlags.SAMPLED,\n isRemote: true,\n });\n}\n","import { defineTelescopeExtension } from '@dudousxd/nestjs-telescope';\nimport { durableDashboard } from './durable-dashboard.spec-data';\nimport {\n durableDurationProvider,\n durableRecentFailuresProvider,\n durableRunsOverTimeProvider,\n durableStateBreakdownProvider,\n durableStateProvider,\n durableSuccessRateProvider,\n durableThroughputProvider,\n durableTimeseriesProvider,\n durableWorkerHealthProvider,\n durableWorkerStatusProvider,\n} from './durable-data-providers';\nimport { DurableTelescopeWatcher } from './durable-telescope.watcher';\n\n/** The first-class Telescope extension for nestjs-durable: watcher + Workflows dashboard. */\nexport function durableTelescopeExtension(\n opts: { runHref?: string; recentFailuresWindowMs?: number } = {},\n) {\n return defineTelescopeExtension({\n name: 'durable',\n watchers: () => [new DurableTelescopeWatcher()],\n entryTypes: () => [{ id: 'durable', label: 'Workflows', dot: 'bg-amber-400' }],\n dashboards: () => [durableDashboard(opts)],\n dataProviders: () => [\n durableStateProvider(),\n durableTimeseriesProvider(),\n durableRecentFailuresProvider(),\n durableWorkerHealthProvider(),\n durableWorkerStatusProvider(),\n durableDurationProvider(),\n durableRunsOverTimeProvider(),\n durableSuccessRateProvider(),\n durableThroughputProvider(),\n durableStateBreakdownProvider(),\n ],\n });\n}\n","import type { DashboardSpec } from '@dudousxd/nestjs-telescope';\n\n/**\n * The \"Workflows\" health dashboard — golden-signals sections layout.\n *\n * `runHref` is a URL template for deep-linking a run out to the durable dashboard\n * (e.g. '/durable/runs/{runId}'). `recentFailuresWindowMs` bounds the \"Stuck runs\"\n * table (default 24h) so a healthy system shows an empty table instead of\n * days-old failures; pass `0` to show all.\n */\nexport function durableDashboard(\n opts: { runHref?: string; recentFailuresWindowMs?: number } = {},\n): DashboardSpec {\n const runHref = opts.runHref ?? '/durable/runs/{runId}';\n const windowMs = opts.recentFailuresWindowMs ?? 24 * 60 * 60 * 1000;\n return {\n id: 'durable.workflows',\n label: 'Workflows',\n panels: [],\n sections: [\n {\n title: 'Health',\n cols: 4,\n panels: [\n {\n kind: 'gauge',\n title: 'Success rate',\n data: { provider: 'durable.successRate' },\n max: 1,\n format: 'percent',\n thresholds: { warn: 0.98, bad: 0.95, direction: 'down-bad' },\n },\n {\n kind: 'stat',\n title: 'Duration p95',\n data: { provider: 'durable.duration', query: { metric: 'p95' } },\n format: 'duration',\n spark: false,\n thresholds: { warn: 2000, bad: 5000, direction: 'up-bad' },\n },\n {\n kind: 'stat',\n title: 'Backlog',\n data: { provider: 'durable.state', query: { status: 'pending' } },\n spark: false,\n thresholds: { warn: 50, bad: 200, direction: 'up-bad' },\n },\n {\n kind: 'stat',\n title: 'Throughput',\n data: { provider: 'durable.throughput' },\n format: 'rate',\n spark: true,\n },\n ],\n },\n {\n title: 'Needs attention',\n cols: 3,\n panels: [\n {\n kind: 'topN',\n title: 'Top failing workflows',\n data: { provider: 'durable.timeseries', query: { metric: 'topFailures' } },\n limit: 8,\n },\n {\n kind: 'table',\n title: 'Stuck runs',\n data: { provider: 'durable.recentFailures', query: { windowMs } },\n columns: [\n { key: 'updatedAt', label: 'Updated' },\n { key: 'workflow', label: 'Workflow' },\n { key: 'runId', label: 'Run', link: { href: runHref } },\n { key: 'error', label: 'Error' },\n ],\n },\n {\n kind: 'table',\n // Lists ALL worker groups (starved ones sorted first); the Status column\n // flags STARVED (queued > 0 with zero live workers) vs ok. Titling it\n // \"Starved worker groups\" read as if every listed group were starved.\n title: 'Worker health',\n data: { provider: 'durable.workerHealth' },\n columns: [\n { key: 'group', label: 'Group' },\n { key: 'queued', label: 'Queued' },\n { key: 'liveWorkers', label: 'Workers' },\n { key: 'status', label: 'Status' },\n ],\n },\n ],\n },\n {\n title: 'Workers',\n // One column, because this section holds exactly one panel and that panel is the widest\n // table on the dashboard. A section renders as a fixed `grid-cols-N` with no `colSpan`, so\n // `cols: 2` gave the widest table on the dashboard half the viewport and left the cell\n // beside it empty — it scrolled sideways inside its own card while a hole sat next to it.\n cols: 1,\n panels: [\n {\n kind: 'table',\n // One row per LIVE worker (flattened from each group's heartbeats), exposing the live\n // WorkerStatus the heartbeat carries: fixed vs. adaptive limit, in-flight saturation,\n // RAM%/CPU%, throughput, p95, and why the adaptive controller last moved. A worker from\n // an older SDK with no status still lists, reading 'n/a' — distinct from the '—' of a\n // worker that reports fine but has nothing to measure yet, which is what an idle\n // deployment looks like (throughput and p95 come off a rolling window of completions,\n // so they are empty until a step finishes inside it). The\n // adaptive min–max range rides the In-flight column's row value (`minMax` key) for those\n // that want it; the visible columns stay readable.\n title: 'Workers',\n data: { provider: 'durable.workerStatus' },\n columns: [\n { key: 'group', label: 'Group' },\n { key: 'instanceId', label: 'Worker' },\n { key: 'mode', label: 'Mode' },\n // No separate `limit` column: In-flight renders `<inFlight>/<limit>`, so a Limit\n // column beside it repeats its own denominator in every row.\n { key: 'saturation', label: 'In-flight' },\n { key: 'queued', label: 'Queued' },\n { key: 'rssPct', label: 'RAM %' },\n { key: 'cpuPct', label: 'CPU %' },\n { key: 'throughputPerMin', label: 'Thrpt/min' },\n { key: 'p95Ms', label: 'p95 ms' },\n { key: 'lastAdjust', label: 'Last adjust' },\n ],\n },\n ],\n },\n {\n title: 'Trends',\n cols: 3,\n panels: [\n {\n kind: 'timeseries',\n title: 'Runs over time',\n data: { provider: 'durable.runsOverTime' },\n series: ['done', 'failed'],\n style: 'stacked',\n },\n {\n kind: 'distribution',\n title: 'Duration distribution',\n data: { provider: 'durable.duration' },\n markers: ['p50', 'p95', 'p99'],\n format: 'duration',\n },\n {\n kind: 'breakdown',\n title: 'Runs by state',\n data: { provider: 'durable.stateBreakdown' },\n style: 'donut',\n },\n ],\n },\n ],\n };\n}\n","import { STATE_STORE_CANONICAL, WorkflowEngine } from '@dudousxd/nestjs-durable-core';\nimport type { RunStatus, StateStore, WorkerStatus } from '@dudousxd/nestjs-durable-core';\nimport type { DataProvider, ExtensionContext } from '@dudousxd/nestjs-telescope';\nimport { TELESCOPE_STORAGE } from '@dudousxd/nestjs-telescope';\n\n// ─── Shared helpers ───────────────────────────────────────────────────────────\n\n/** Compute the p-th percentile of a SORTED array. */\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[Math.max(0, idx)] ?? 0;\n}\n\ntype StorageEntry = { content?: unknown; createdAt?: Date };\n\n/** Fetch the durable entry page from TELESCOPE_STORAGE. */\nasync function fetchEntries(ctx: ExtensionContext): Promise<StorageEntry[]> {\n const storage = ctx.moduleRef.get(TELESCOPE_STORAGE, { strict: false }) as {\n get(q: { type?: string; limit?: number }): Promise<{ data: StorageEntry[] }>;\n };\n const page = await storage.get({ type: 'durable', limit: 5_000 });\n return page.data;\n}\n\n/**\n * Split entries into current/previous equal-length windows.\n * current: (now - windowMs, now]\n * previous: (now - 2*windowMs, now - windowMs]\n */\nfunction splitWindows(\n entries: StorageEntry[],\n windowMs: number,\n now: number,\n): { current: StorageEntry[]; previous: StorageEntry[] } {\n const start = now - windowMs;\n const prevStart = start - windowMs;\n return {\n current: entries.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > start && t <= now;\n }),\n previous: entries.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > prevStart && t <= start;\n }),\n };\n}\n\n/** Compute success rate (completed / total) for a list of entries, returns 1 when no data. */\nfunction successRateOf(entries: StorageEntry[]): number {\n let completed = 0;\n let failed = 0;\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string };\n if (c.event === 'run.completed') completed += 1;\n else if (c.event === 'run.failed') failed += 1;\n }\n const total = completed + failed;\n return total === 0 ? 1 : completed / total;\n}\n\n/** Run-level lifecycle events — each can legitimately occur at most once per runId. */\nconst RUN_LIFECYCLE_EVENTS = new Set([\n 'run.started',\n 'run.completed',\n 'run.failed',\n 'run.suspended',\n]);\n\n/**\n * Deduplicate run-level lifecycle entries by `${event}:${runId}`, keeping the FIRST occurrence.\n *\n * The durable engine emits each run lifecycle event on every pod (1 worker + N api pods) and the\n * watcher records on each, so a single `run.completed` for a given runId is captured multiple times\n * with identical content but distinct entry ids / createdAt (±ms) — inflating completed/failed\n * counts. A given runId can legitimately have at most ONE `run.started` and ONE terminal\n * `run.completed`/`run.failed`/`run.suspended`, so collapsing by `${event}:${runId}` is provably\n * safe. Step events and any entry without a run-level event + runId pass through untouched — a step\n * can legitimately repeat across attempts, and the watcher's step content carries no stepId/attempt\n * to key on.\n */\nfunction dedupeRunEvents(entries: StorageEntry[]): StorageEntry[] {\n const seen = new Set<string>();\n const out: StorageEntry[] = [];\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string };\n if (c.event && c.runId && RUN_LIFECYCLE_EVENTS.has(c.event)) {\n const key = `${c.event}:${c.runId}`;\n if (seen.has(key)) continue;\n seen.add(key);\n }\n out.push(e);\n }\n return out;\n}\n\nconst STATE_CAP = 10_000;\n\n// ─── Existing providers ───────────────────────────────────────────────────────\n\n/** Source C: current-state gauge from the durable store. query.status selects which (default 'dead'). */\nexport function durableStateProvider(): DataProvider {\n return {\n name: 'durable.state',\n async resolve(query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const status = (query?.status as RunStatus) ?? 'dead';\n const runs = await store.listRuns({ status, limit: STATE_CAP });\n return { value: runs.length };\n },\n };\n}\n\n/**\n * Source A: rollups from captured `durable` entries in Telescope's own storage.\n * Reads recent run.* lifecycle entries and aggregates. Bounded by Telescope's prune\n * window (by design — this is the \"history\" series, not the source of truth).\n */\nexport function durableTimeseriesProvider(): DataProvider {\n return {\n name: 'durable.timeseries',\n async resolve(query, ctx: ExtensionContext) {\n // TELESCOPE_STORAGE.get(query) → { data: Entry[]; nextCursor }. Entry = { content, createdAt, ... }.\n const storage = ctx.moduleRef.get(TELESCOPE_STORAGE, { strict: false }) as {\n get(q: { type?: string; limit?: number }): Promise<{\n data: Array<{ content?: unknown; createdAt?: Date }>;\n }>;\n };\n const limit = Math.min(5_000, Math.max(100, Number(query?.limit ?? 2_000)));\n const page = await storage.get({ type: 'durable', limit });\n\n let completed = 0;\n let failed = 0;\n const failByWorkflow = new Map<string, number>();\n // Dedup triplicated lifecycle events (one event recorded on every pod) before counting.\n for (const e of dedupeRunEvents(page.data)) {\n const c = (e.content ?? {}) as { event?: string; workflow?: string };\n if (c.event === 'run.completed') completed += 1;\n else if (c.event === 'run.failed') {\n failed += 1;\n const wf = c.workflow ?? 'unknown';\n failByWorkflow.set(wf, (failByWorkflow.get(wf) ?? 0) + 1);\n }\n }\n const total = completed + failed;\n const metric = (query?.metric as string) ?? 'successRate';\n if (metric === 'successRate') return { value: total === 0 ? 1 : completed / total };\n if (metric === 'failed') return { value: failed };\n if (metric === 'total') return { value: total };\n if (metric === 'topFailures') {\n const items = [...failByWorkflow.entries()]\n .map(([label, value]) => ({ label, value }))\n .sort((a, b) => b.value - a.value);\n return { items };\n }\n return { value: total };\n },\n };\n}\n\n/**\n * Source C: recent failed + dead runs as table rows (newest first), time-bounded.\n * Only failures updated within `query.windowMs` (default 24h) are returned, so a\n * healthy system shows an EMPTY table instead of surfacing days-old failures as if\n * they were a live incident. `windowMs: 0` disables the window (return all). Each\n * row carries a compact `updatedAt` stamp so recency is visible in the table.\n */\nexport function durableRecentFailuresProvider(): DataProvider {\n return {\n name: 'durable.recentFailures',\n async resolve(query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50)));\n const windowMs = query?.windowMs === undefined ? 24 * 60 * 60 * 1000 : Number(query.windowMs);\n const cutoff = windowMs > 0 ? Date.now() - windowMs : 0;\n const [failed, dead] = await Promise.all([\n store.listRuns({ status: 'failed', limit }),\n store.listRuns({ status: 'dead', limit }),\n ]);\n const rows = [...failed, ...dead]\n .filter((r) => +new Date(r.updatedAt) >= cutoff)\n .sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt))\n .slice(0, limit)\n .map((r) => ({\n // Compact UTC stamp (\"YYYY-MM-DD HH:mm Z\"); the generic table renderer\n // prints column values as-is, so we format here.\n updatedAt: `${new Date(r.updatedAt).toISOString().replace('T', ' ').slice(0, 16)}Z`,\n workflow: r.workflow,\n runId: r.id,\n error: r.error?.message ?? '',\n }));\n return { rows };\n },\n };\n}\n\n/**\n * Source D: per-group worker health from the engine (`WorkflowEngine.workerHealth()` — queue depth\n * vs. live worker heartbeats). `query.metric: 'starvedCount'` returns the number of groups with work\n * queued and zero live workers (the \"alive but not consuming\" alert state) as a stat; otherwise a\n * table, starved groups sorted first. Empty when the transport can't report health (in-process).\n */\nexport function durableWorkerHealthProvider(): DataProvider {\n return {\n name: 'durable.workerHealth',\n async resolve(query, ctx: ExtensionContext) {\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false }) as WorkflowEngine;\n const health = await engine.workerHealth();\n const isStarved = (g: { depth: number; liveWorkers: unknown[] }) =>\n g.depth > 0 && g.liveWorkers.length === 0;\n if ((query?.metric as string) === 'starvedCount') {\n return { value: health.filter(isStarved).length };\n }\n const rows = health\n .slice()\n // Starved groups first, then deepest backlog — the rows that need attention rise to the top.\n .sort((a, b) => Number(isStarved(b)) - Number(isStarved(a)) || b.depth - a.depth)\n .map((g) => ({\n group: g.group,\n queued: g.depth,\n liveWorkers: g.liveWorkers.length,\n status: isStarved(g) ? 'STARVED' : 'ok',\n }));\n return { rows };\n },\n };\n}\n\n/** Compact \"Nm ago\" relative stamp for an epoch-ms instant (worker last-adjust recency). */\nfunction relAgo(atMs: number): string {\n const s = Math.max(0, Math.round((Date.now() - atMs) / 1000));\n if (s < 60) return `${s}s ago`;\n const m = Math.round(s / 60);\n if (m < 60) return `${m}m ago`;\n const h = Math.round(m / 60);\n if (h < 24) return `${h}h ago`;\n return `${Math.round(h / 24)}d ago`;\n}\n\n/**\n * What a cell says when it has no number, and the two cases it has to keep apart.\n *\n * A blank cell used to mean both \"this worker's SDK is too old to report anything\" and \"this worker\n * reports fine, it just has nothing to measure yet\" — and the second is the common one on an idle\n * deployment, where the adaptive controller's rolling window is empty because no step has completed\n * inside it. Reading one as the other sends someone hunting for a broken worker that is merely idle,\n * or ignoring a fleet that has genuinely stopped reporting.\n *\n * `n/a` is the heartbeat carrying no {@link WorkerStatus} at all; `—` is a status that carries no\n * value for this field yet.\n */\nconst NOT_REPORTED = 'n/a';\nconst NOT_YET_MEASURED = '—';\n\n/**\n * Render a worker's last limit change as \"shrink 8→5 2m ago\".\n *\n * `—` here means the adaptive controller has not moved the limit yet — the normal state of a worker\n * that has been sitting at its floor. A fixed-mode worker never adjusts at all, which is what its\n * `Mode` column already says.\n */\nfunction fmtLastAdjust(status: WorkerStatus | undefined): string {\n if (!status) return NOT_REPORTED;\n const a = status.lastAdjust;\n if (!a) return NOT_YET_MEASURED;\n return `${a.reason} ${a.from}→${a.to} ${relAgo(a.at)}`;\n}\n\n/** Round a 0..100(+) percent to a whole number for table display. */\nfunction fmtPct(n: number | undefined, status: WorkerStatus | undefined): number | string {\n if (typeof n === 'number') return Math.round(n);\n return status ? NOT_YET_MEASURED : NOT_REPORTED;\n}\n\n/** Round a rate/latency to a whole number for table display. */\nfunction fmtNum(n: number | undefined, status: WorkerStatus | undefined): number | string {\n if (typeof n === 'number') return Math.round(n);\n return status ? NOT_YET_MEASURED : NOT_REPORTED;\n}\n\n/**\n * Source D (per-worker): flatten `WorkflowEngine.workerHealth()` to ONE row per live worker, exposing\n * the live {@link WorkerStatus} each heartbeat now carries (mode/limit/min-max, in-flight saturation,\n * RAM%/CPU%, throughput, p95, last adjust). A worker from an older SDK with no `status` still renders\n * — its measured fields fall back to '—', so a mixed fleet stays visible. Rows sort backlog-bearing\n * groups first, then by group, then instanceId. Empty when the transport can't report health.\n */\nexport function durableWorkerStatusProvider(): DataProvider {\n return {\n name: 'durable.workerStatus',\n async resolve(_query, ctx: ExtensionContext) {\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false }) as WorkflowEngine;\n const health = await engine.workerHealth();\n const rows = health\n .slice()\n // Groups with a backlog first (the workers worth scrutinising), then by group name.\n .sort((a, b) => Number(b.depth > 0) - Number(a.depth > 0) || a.group.localeCompare(b.group))\n .flatMap((group) =>\n group.liveWorkers\n .slice()\n .sort((a, b) => a.instanceId.localeCompare(b.instanceId))\n .map((worker) => {\n const status = worker.status;\n const concurrency = status?.concurrency;\n // Mode/limit/in-flight are not measurements — a status either declares them or the\n // heartbeat carried no status at all, so their absence is always `n/a`.\n const mode = concurrency?.mode ?? NOT_REPORTED;\n const limit = concurrency?.limit ?? NOT_REPORTED;\n const inFlight = status?.inFlight ?? NOT_REPORTED;\n const minMax =\n concurrency?.mode === 'adaptive'\n ? `${concurrency.min ?? 1}–${concurrency.max ?? 32}`\n : NOT_REPORTED;\n const saturation =\n concurrency && status ? `${status.inFlight}/${concurrency.limit}` : NOT_REPORTED;\n return {\n group: group.group,\n instanceId: worker.instanceId,\n mode,\n limit,\n minMax,\n inFlight,\n saturation,\n queued: group.depth,\n rssPct: fmtPct(status?.rssPct, status),\n cpuPct: fmtPct(status?.cpuPct, status),\n throughputPerMin: fmtNum(status?.throughputPerMin, status),\n p95Ms: fmtNum(status?.p95Ms, status),\n lastAdjust: fmtLastAdjust(status),\n };\n }),\n );\n return { rows };\n },\n };\n}\n\n// ─── New golden-signals providers ────────────────────────────────────────────\n\n/**\n * Duration percentiles (p50/p95/p99) + a histogram of ~8 buckets.\n * Durations are read from `content.durationMs` on `run.completed`/`run.failed`; when absent,\n * computed by pairing a `run.started` and its corresponding `run.completed`/`run.failed` entry\n * by `runId` using their `createdAt` timestamps.\n *\n * With `query.metric === 'p50'|'p95'|'p99'` returns `{ value }` for use as a stat panel.\n */\nexport function durableDurationProvider(): DataProvider {\n return {\n name: 'durable.duration',\n async resolve(query, ctx: ExtensionContext) {\n // Dedup triplicated lifecycle events so paired durations aren't double-counted.\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n\n // Index run.started entries by runId for the pairing fallback.\n const startedAt = new Map<string, number>();\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string };\n if (c.event === 'run.started' && c.runId && e.createdAt) {\n startedAt.set(c.runId, +new Date(e.createdAt));\n }\n }\n\n const durs: number[] = [];\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string; durationMs?: number };\n if (c.event === 'run.completed' || c.event === 'run.failed') {\n if (typeof c.durationMs === 'number') {\n durs.push(c.durationMs);\n } else if (c.runId) {\n const start = startedAt.get(c.runId);\n const end = e.createdAt ? +new Date(e.createdAt) : undefined;\n if (start !== undefined && end !== undefined && end >= start) {\n durs.push(end - start);\n }\n }\n }\n }\n durs.sort((a, b) => a - b);\n\n const p50 = percentile(durs, 50);\n const p95 = percentile(durs, 95);\n const p99 = percentile(durs, 99);\n\n // Stat shortcut: return a single value for use in stat panels.\n const metric = (query as Record<string, unknown>)?.metric as string | undefined;\n if (metric === 'p50') return { value: p50 };\n if (metric === 'p95') return { value: p95 };\n if (metric === 'p99') return { value: p99 };\n\n // Build ~8 equal-width buckets between 0 and max.\n const max = durs.at(-1) ?? 0;\n const bucketCount = 8;\n const size = Math.max(1, Math.ceil((max + 1) / bucketCount));\n const buckets = Array.from({ length: bucketCount }, (_, i) => ({\n label: `${Math.round((i * size) / 100) / 10}s`,\n count: 0,\n }));\n for (const d of durs) {\n const bi = Math.min(bucketCount - 1, Math.floor(d / size));\n const bucket = buckets[bi];\n if (bucket) bucket.count += 1;\n }\n\n return { buckets, p50, p95, p99 };\n },\n };\n}\n\n/**\n * Buckets `run.completed` (-> done) and `run.failed` (-> failed) entries by `createdAt` into\n * N=`query.buckets ?? 24` equal-width time buckets. Returns `{ rows: Array<{ label; done; failed }> }`.\n */\nexport function durableRunsOverTimeProvider(): DataProvider {\n return {\n name: 'durable.runsOverTime',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const n = Number((query as Record<string, unknown>)?.buckets ?? 24);\n const now = Date.now();\n\n // Determine the time window from the oldest entry or default to 24h.\n let minT = now;\n for (const e of entries) {\n if (e.createdAt) {\n const t = +new Date(e.createdAt);\n if (t < minT) minT = t;\n }\n }\n const span = Math.max(now - minT, 1);\n const bucketSize = span / n;\n\n const rows: Array<{ label: string; done: number; failed: number }> = Array.from(\n { length: n },\n (_, i) => {\n const bucketStart = new Date(minT + i * bucketSize);\n const label = bucketStart.toISOString().slice(11, 16); // \"HH:mm\"\n return { label, done: 0, failed: 0 };\n },\n );\n\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string };\n if (c.event !== 'run.completed' && c.event !== 'run.failed') continue;\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n const idx = Math.min(n - 1, Math.floor((t - minT) / bucketSize));\n const row = rows[idx];\n if (row) {\n if (c.event === 'run.completed') row.done += 1;\n else row.failed += 1;\n }\n }\n\n return { rows };\n },\n };\n}\n\n/**\n * Success rate over the last `query.windowMs` (default 24h), with a `delta` vs. the previous\n * equal-length window and a `spark` array of per-sub-bucket success rates.\n */\nexport function durableSuccessRateProvider(): DataProvider {\n return {\n name: 'durable.successRate',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const windowMs = Number((query as Record<string, unknown>)?.windowMs ?? 24 * 60 * 60 * 1000);\n const now = Date.now();\n const { current, previous } = splitWindows(entries, windowMs, now);\n\n const value = successRateOf(current);\n const prevRate = successRateOf(previous);\n const delta = previous.length > 0 ? value - prevRate : undefined;\n\n // Spark: split the current window into 8 sub-buckets.\n const sparkBuckets = 8;\n const bucketSize = windowMs / sparkBuckets;\n const sparkStart = now - windowMs;\n const spark = Array.from({ length: sparkBuckets }, (_, i) => {\n const bStart = sparkStart + i * bucketSize;\n const bEnd = bStart + bucketSize;\n const bEntries = current.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > bStart && t <= bEnd;\n });\n return successRateOf(bEntries);\n });\n\n return { value, delta, spark };\n },\n };\n}\n\n/**\n * Completed runs per hour over the last `query.windowMs` (default 24h), with a `delta` vs. the\n * previous equal-length window and a `spark` array of per-sub-bucket throughput values.\n */\nexport function durableThroughputProvider(): DataProvider {\n return {\n name: 'durable.throughput',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const windowMs = Number((query as Record<string, unknown>)?.windowMs ?? 24 * 60 * 60 * 1000);\n const now = Date.now();\n const { current, previous } = splitWindows(entries, windowMs, now);\n\n const countCompleted = (es: StorageEntry[]) =>\n es.filter((e) => ((e.content ?? {}) as { event?: string }).event === 'run.completed')\n .length;\n\n const windowHours = windowMs / (60 * 60 * 1000);\n const value = countCompleted(current) / windowHours;\n const prevValue = countCompleted(previous) / windowHours;\n const delta = previous.length > 0 ? value - prevValue : undefined;\n\n // Spark: 8 sub-buckets of current window.\n const sparkBuckets = 8;\n const bucketSize = windowMs / sparkBuckets;\n const bucketHours = bucketSize / (60 * 60 * 1000);\n const sparkStart = now - windowMs;\n const spark = Array.from({ length: sparkBuckets }, (_, i) => {\n const bStart = sparkStart + i * bucketSize;\n const bEnd = bStart + bucketSize;\n const bEntries = current.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > bStart && t <= bEnd;\n });\n return countCompleted(bEntries) / bucketHours;\n });\n\n return { value, delta, spark };\n },\n };\n}\n\n// Palette aligned index-for-index with STATE_BREAKDOWN_STATUSES so each status reads with the\n// semantically-correct color: running=cyan, pending=amber, cancelling=orange, completed=green,\n// failed=red, dead=purple.\nconst STATE_BREAKDOWN_STATUSES: RunStatus[] = [\n 'running',\n 'pending',\n 'cancelling',\n 'completed',\n 'failed',\n 'dead',\n];\n/** Color palette for the state breakdown pie segments (aligned to STATE_BREAKDOWN_STATUSES). */\nconst STATE_BREAKDOWN_PALETTE = [\n '#38bdf8', // running → cyan/blue\n '#fbbf24', // pending → amber\n '#f59e0b', // cancelling → orange\n '#34d399', // completed → green\n '#f87171', // failed → red\n '#a78bfa', // dead → purple\n];\n\n/**\n * Counts from `STATE_STORE.listRuns({ status })` for each of the statuses and returns pie\n * segments with the standard palette: `{ segments: Array<{ label, value, color }> }`.\n */\nexport function durableStateBreakdownProvider(): DataProvider {\n return {\n name: 'durable.stateBreakdown',\n async resolve(_query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const counts = await Promise.all(\n STATE_BREAKDOWN_STATUSES.map((status) =>\n store.listRuns({ status, limit: STATE_CAP }).then((runs) => runs.length),\n ),\n );\n const segments = STATE_BREAKDOWN_STATUSES.map((label, i) => ({\n label,\n value: counts[i],\n color: STATE_BREAKDOWN_PALETTE[i],\n }));\n return { segments };\n },\n };\n}\n\n// NOTE: durableRetryHotspotsProvider is NOT implemented.\n// StateStore only exposes `listCheckpoints(runId: string)` (per-run lookup), with no cross-run\n// query by step `attempts`. There is no API to enumerate all step checkpoints across runs without\n// first listing every run — which would be O(N*M) and impractical. Task 13 should omit that panel.\n"],"mappings":"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","names":["import_api","import_node_crypto","import_api","runTraceId","runId","createHash","update","digest","slice","runSpanId","runTraceContext","parent","context","active","trace","setSpanContext","traceId","spanId","traceFlags","TraceFlags","SAMPLED","isRemote","activeScope","AsyncLocalStorage","currentDurableScope","getStore","DURABLE_BATCH_ORIGIN","spanName","execution","unit","name","workflow","spanAttributes","attributes","runId","undefined","seq","attempt","durableExecutionScope","ctx","tracer","trace","getTracer","owner","next","open","config","enabled","parent","runTraceContext","span","startSpan","spanContext","scope","traceId","runTraceId","spanId","isRecording","randomBytes","toString","context","with","setSpan","run","runInBatch","then","value","end","error","setStatus","code","SpanStatusCode","ERROR","Error","message","durableTraceContext","delegate","current","fromDelegate","DurableTelescopeWatcher","type","disposeScope","register","ctx","useDurableExecution","durableExecutionScope","engine","moduleRef","get","WorkflowEngine","strict","subscribe","tracer","trace","getTracer","event","record","traceId","runTraceId","runId","content","workflow","seq","name","kind","output","error","durationMs","tags","base","runTraceContext","step","startSpan","attributes","message","setStatus","undefined","code","SpanStatusCode","ERROR","context","with","setSpan","end","unregister","push","import_nestjs_telescope","durableDashboard","opts","runHref","windowMs","recentFailuresWindowMs","id","label","panels","sections","title","cols","kind","data","provider","max","format","thresholds","warn","bad","direction","query","metric","spark","status","limit","columns","key","link","href","series","style","markers","import_nestjs_durable_core","percentile","sorted","p","length","idx","Math","min","ceil","max","fetchEntries","ctx","storage","moduleRef","get","TELESCOPE_STORAGE","strict","page","type","limit","data","splitWindows","entries","windowMs","now","start","prevStart","current","filter","e","t","createdAt","Date","previous","successRateOf","completed","failed","c","content","event","total","RUN_LIFECYCLE_EVENTS","Set","dedupeRunEvents","seen","out","runId","has","key","add","push","STATE_CAP","durableStateProvider","name","resolve","query","store","STATE_STORE_CANONICAL","status","runs","listRuns","value","durableTimeseriesProvider","Number","failByWorkflow","Map","wf","workflow","set","metric","items","map","label","sort","a","b","durableRecentFailuresProvider","undefined","cutoff","dead","Promise","all","rows","r","updatedAt","slice","toISOString","replace","id","error","message","durableWorkerHealthProvider","engine","WorkflowEngine","health","workerHealth","isStarved","g","depth","liveWorkers","group","queued","relAgo","atMs","s","round","m","h","NOT_REPORTED","NOT_YET_MEASURED","fmtLastAdjust","lastAdjust","reason","from","to","at","fmtPct","n","fmtNum","durableWorkerStatusProvider","_query","localeCompare","flatMap","instanceId","worker","concurrency","mode","inFlight","minMax","saturation","rssPct","cpuPct","throughputPerMin","p95Ms","durableDurationProvider","startedAt","durs","durationMs","end","p50","p95","p99","bucketCount","size","buckets","Array","_","i","count","d","bi","floor","bucket","durableRunsOverTimeProvider","minT","span","bucketSize","bucketStart","done","row","durableSuccessRateProvider","prevRate","delta","sparkBuckets","sparkStart","spark","bStart","bEnd","bEntries","durableThroughputProvider","countCompleted","es","windowHours","prevValue","bucketHours","STATE_BREAKDOWN_STATUSES","STATE_BREAKDOWN_PALETTE","durableStateBreakdownProvider","counts","then","segments","color","durableTelescopeExtension","opts","defineTelescopeExtension","name","watchers","DurableTelescopeWatcher","entryTypes","id","label","dot","dashboards","durableDashboard","dataProviders","durableStateProvider","durableTimeseriesProvider","durableRecentFailuresProvider","durableWorkerHealthProvider","durableWorkerStatusProvider","durableDurationProvider","durableRunsOverTimeProvider","durableSuccessRateProvider","durableThroughputProvider","durableStateBreakdownProvider"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/durable-telescope.watcher.ts","../src/durable-execution-scope.ts","../src/durable-run-trace.ts","../src/durable-telescope.extension.ts","../src/durable-dashboard.spec-data.ts","../src/durable-data-providers.ts"],"sourcesContent":["export * from './durable-telescope.watcher';\nexport * from './durable-execution-scope';\nexport * from './durable-run-trace';\nexport * from './durable-telescope.extension';\nexport * from './durable-data-providers';\nexport * from './durable-dashboard.spec-data';\n","import {\n type EngineEvent,\n WorkflowEngine,\n parentRunIdOf,\n useDurableExecution,\n} from '@dudousxd/nestjs-durable-core';\nimport type { Watcher, WatcherContext } from '@dudousxd/nestjs-telescope';\nimport { SpanStatusCode, context, trace } from '@opentelemetry/api';\nimport { durableExecutionScope } from './durable-execution-scope';\nimport { runTraceContext, runTraceId } from './durable-run-trace';\n\n/**\n * A Telescope watcher that turns durable-workflow lifecycle events into Telescope entries, so\n * runs and steps (including remote/Python steps) show up alongside the app's requests, queries\n * and jobs in the Telescope UI — under the **Workflows** tab.\n *\n * It does two things, and the second is the one that makes a failing workflow debuggable:\n *\n * 1. **Records an entry per engine event**, stamped with the run's trace (see\n * {@link runTraceContext}), so a whole run — across all its turns, all its steps and every\n * process that executed them — reads as a single trace at `/telescope#/traces/<id>`.\n *\n * 2. **Opens a Telescope batch around the execution itself** (see {@link durableExecutionScope}),\n * through the engine's `useDurableExecution` seam. Without this, everything a workflow turn or a\n * step handler *did* — its queries, its logs, the exception it threw — was recorded outside any\n * batch and outside any span: traceless, and so impossible to correlate back to the step that\n * caused it. Recording an event after the fact cannot fix that; the scope has to be open while\n * the body runs.\n *\n * Add it to `TelescopeModule.forRoot({ watchers: [new DurableTelescopeWatcher()] })`. It resolves\n * the engine from the (global) durable providers and subscribes to its events.\n *\n * ## What is NOT covered\n *\n * A **remote step** — one whose handler lives in another runtime, e.g. the Python worker — executes\n * out of process, so nothing here can wrap it. Its lifecycle still shows up (the engine emits\n * `step.started`/`step.completed`/`step.failed` when it dispatches and when the result lands, and\n * those entries carry the run's trace id), but the queries and exceptions *inside* that handler are\n * that runtime's to record. Because the trace id is derived from the run id rather than held in this\n * process, a remote runtime that derives it the same way joins the same trace — but that is a change\n * on its side, not something this watcher can do on its behalf.\n *\n * A **remote workflow** (a run whose body itself lives in another runtime) is likewise unwrapped:\n * the engine dispatches a workflow task and awaits a decision rather than executing a body, and a\n * scope around a dispatch would describe the wrong thing.\n */\nexport class DurableTelescopeWatcher implements Watcher {\n readonly type = 'durable';\n /** Removes the execution wrapper from the engine's process-level registry. */\n private disposeScope: (() => void) | undefined;\n\n register(ctx: WatcherContext): void {\n // Registered FIRST, and unconditionally: this is the half that works without a local engine. A\n // thin worker binds `WorkflowEngine` to a start-only facade with no event stream, but it is\n // exactly where step handlers run — so the execution scope belongs there even though there is\n // nothing local to subscribe to.\n this.disposeScope?.();\n this.disposeScope = useDurableExecution(durableExecutionScope(ctx));\n\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false });\n // A store-less thin-worker / tenant deployment (e.g. a local `DURABLE_TENANT=…` dev stack) binds\n // the `WorkflowEngine` token to a start-only `DurableStartClient` facade — it proxies run starts\n // over the transport and has NO local lifecycle event stream (those events live on the operator\n // that holds the store). There's nothing local to observe, so skip the subscription gracefully\n // instead of throwing \"engine.subscribe is not a function\".\n if (typeof (engine as Partial<WorkflowEngine>)?.subscribe !== 'function') return;\n const tracer = trace.getTracer('@dudousxd/nestjs-durable-telescope');\n\n engine.subscribe((event) => {\n const record = () =>\n ctx.record({\n type: this.type,\n // Stated explicitly rather than left to the ambient span, and the Recorder honours an\n // explicit `traceId` above everything else. A lifecycle event is very often emitted\n // OUTSIDE any execution scope — a remote step's result lands in a transport callback, a\n // recovery sweep emits for a run this process never executed — and those entries are\n // exactly the ones an operator follows into the trace. We know the run, so we know the\n // trace; there is no reason to let it depend on where the emit happened to land.\n traceId: runTraceId(event.runId),\n content: {\n event: event.type,\n workflow: event.workflow,\n runId: event.runId,\n parentRunId: parentRunIdOf(event.runId),\n seq: event.seq,\n name: event.name,\n kind: event.kind,\n output: event.output,\n error: event.error,\n durationMs: event.durationMs,\n },\n tags: this.tags(event),\n });\n\n // Anchor on the run's trace. Derived from the run id, so this event groups with the entries\n // recorded *inside* the turn/step scopes without either side having to hand the other a span —\n // which is the only thing that works once a step executes on a later turn, or in another pod.\n const base = runTraceContext(event.runId);\n\n // A step/signal/sleep event → give it its own child span so it's a distinct node in the\n // trace; run-level events record against the run's own span context. Either way the active\n // span at record time carries the run's trace id, so all entries group into one trace.\n if (event.name != null && event.seq != null) {\n const step = tracer.startSpan(\n `step ${event.name}`,\n {\n attributes: {\n 'durable.run_id': event.runId,\n 'durable.step.seq': event.seq,\n 'durable.step.kind': event.kind,\n },\n },\n base,\n );\n if (event.type === 'step.failed') {\n const message = event.error?.message;\n step.setStatus(\n message !== undefined\n ? { code: SpanStatusCode.ERROR, message }\n : { code: SpanStatusCode.ERROR },\n );\n }\n context.with(trace.setSpan(base, step), record);\n step.end();\n } else {\n context.with(base, record);\n }\n });\n }\n\n /**\n * Detach the execution wrapper. The `Watcher` SPI has no teardown hook, so nothing calls this in a\n * running app — where the wrapper should live exactly as long as the process does. It exists so a\n * test that registers a watcher does not leave a wrapper behind in the process-level registry for\n * the next test to trip over.\n */\n unregister(): void {\n this.disposeScope?.();\n this.disposeScope = undefined;\n }\n\n private tags(event: EngineEvent): string[] {\n const tags = ['durable', event.type, `run:${event.runId}`];\n // A child run's own trace is separate from its parent's; this tag is how the entries of every\n // child of one run (a whole `ctx.all` fan) are found from the parent.\n const parent = parentRunIdOf(event.runId);\n if (parent) tags.push(`parent:${parent}`);\n if (event.workflow) tags.push(`workflow:${event.workflow}`);\n if (event.kind) tags.push(`kind:${event.kind}`);\n if (event.type === 'run.failed') tags.push('failed');\n return tags;\n }\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport { randomBytes } from 'node:crypto';\nimport type { DurableExecution, DurableExecutionWrapper } from '@dudousxd/nestjs-durable-core';\nimport type { WatcherContext } from '@dudousxd/nestjs-telescope';\nimport { SpanStatusCode, context, trace } from '@opentelemetry/api';\nimport { runTraceContext, runTraceId } from './durable-run-trace';\n\n/** What the scope publishes to anything recording on this async path. */\nexport interface DurableScope {\n /** The run whose unit of work is executing. */\n readonly runId: string;\n /** The run's trace — see {@link runTraceId}. */\n readonly traceId: string;\n /** This scope's span within that trace. Stable for every entry recorded inside it. */\n readonly spanId: string;\n /**\n * Identity of the wrapper that opened this scope. Reuse is only ever offered to the wrapper that\n * owns the scope: two Telescope instances in one process (a torn-down test module whose wrapper\n * outlived it, an app that mounts two consoles) would otherwise have the first one's scope\n * silently suppress the second one's batch, and the second console would record everything\n * batchless while looking correctly wired.\n */\n readonly owner: object;\n}\n\n/**\n * The durable scope THIS package has open on the current async path, if any.\n *\n * Telescope's own batch context is not readable from a watcher (`WatcherContext` exposes\n * `runInBatch`/`beginBatch` but no `currentBatch()`), and we would not want to read it even if it\n * were: the question is not \"is any batch open\" — an HTTP request that happened to start a run\n * inline has one — but \"is the scope on this path the one we opened for this same run\".\n */\nconst activeScope = new AsyncLocalStorage<DurableScope>();\n\n/** The durable execution scope active on this async path, or undefined outside one. */\nexport function currentDurableScope(): DurableScope | undefined {\n return activeScope.getStore();\n}\n\n/** `BatchOrigin` has no durable/workflow member — see {@link durableExecutionScope} for the choice. */\nconst DURABLE_BATCH_ORIGIN = 'queue';\n\nfunction spanName(execution: DurableExecution): string {\n if (execution.unit === 'step') return `step ${execution.name ?? '?'}`;\n return `workflow ${execution.workflow ?? '?'}`;\n}\n\nfunction spanAttributes(execution: DurableExecution): Record<string, string | number> {\n const attributes: Record<string, string | number> = { 'durable.run_id': execution.runId };\n if (execution.workflow !== undefined) attributes['durable.workflow'] = execution.workflow;\n if (execution.name !== undefined) attributes['durable.step.name'] = execution.name;\n if (execution.seq !== undefined) attributes['durable.step.seq'] = execution.seq;\n if (execution.attempt !== undefined) attributes['durable.step.attempt'] = execution.attempt;\n return attributes;\n}\n\n/**\n * Builds the {@link DurableExecutionWrapper} that puts every workflow turn and every step handler\n * body inside a Telescope batch and an OTel span, so the queries, logs and exceptions those bodies\n * produce are correlated to the unit of durable work that caused them instead of landing traceless.\n *\n * ## Why a batch around execution and not around the event\n *\n * The watcher's engine-event subscription records what *happened*, after the fact. A batch opened\n * there would contain exactly one entry — the durable entry itself — and nothing the step actually\n * did, because by then the handler has returned and the exception it threw has long since been\n * recorded with no batch at all. Wrapping the execution is the only shape that puts the failing code\n * inside the scope.\n *\n * ## Nesting: reuse the batch when the same run already has one open on this path\n *\n * With an in-process transport (event-emitter, in-memory, the DB transport's inline path) a\n * dispatched step's handler starts synchronously inside the turn that dispatched it, and the result\n * it produces resumes the next turn on that same async path — so the whole run is one unbroken\n * causal chain. Opening a fresh batch at each hop would split that chain across many batch ids for\n * no gain: Telescope's entry model has no parent-batch pointer, so the split is not recoverable in\n * the UI — the step's queries would appear in a batch with nothing visible causing them.\n *\n * So the rule is the causal one, not the syntactic one: a unit reuses the batch when THIS wrapper\n * already has one open for THIS run on this async path. The consequence is worth stating plainly,\n * because it differs by transport and looks like an inconsistency if you meet it unprepared:\n *\n * - **in-process transport** — one batch for the whole run (every turn, every step), because it is\n * genuinely one chain;\n * - **queue-backed transport** (BullMQ, SQS, a thin worker) — one batch per turn and one per step,\n * because each really is a separate entry point, in a separate process, with no ambient scope to\n * inherit.\n *\n * What holds either way is the trace: every entry of a run carries {@link runTraceId}, so the run is\n * one trace whether it executed in one process or twenty. That invariant is the thing to rely on;\n * batch granularity follows the execution shape.\n *\n * A different run is a different chain and always gets its own batch. That includes a child\n * workflow: it has its own run id, its own timeline and its own dashboard row, and its trace is\n * anchored on ITS run id (see {@link runTraceContext}), so it reads as a related-but-distinct trace\n * instead of being folded into the parent's. The parent/child relationship is already carried by the\n * run graph; flattening the batches to restate it would erase the child's own boundaries and gain\n * nothing.\n *\n * ## Origin\n *\n * `BatchOrigin` in `@dudousxd/nestjs-telescope` is the closed union\n * `'http' | 'queue' | 'schedule' | 'cli' | 'manual'` — it has no durable/workflow member. Widening\n * it belongs to that repo and a coordinated release, so this uses `'queue'`, which is also the\n * honest description: durable work reaches an executor by being dispatched over a transport\n * (BullMQ, SQS, an event emitter), exactly like the jobs the BullMQ watcher marks `'queue'`.\n */\nexport function durableExecutionScope(ctx: WatcherContext): DurableExecutionWrapper {\n const tracer = trace.getTracer('@dudousxd/nestjs-durable-telescope');\n const owner = {};\n\n return (execution, next) => {\n // Already inside our own scope for this run — reuse it (see \"Nesting\" above).\n const open = activeScope.getStore();\n if (open?.owner === owner && open.runId === execution.runId) return next();\n // Telescope switched off: `runInBatch` would already short-circuit, but the span and the ALS\n // frame would not, so check here and hand the body straight back.\n if (ctx.config.enabled === false) return next();\n\n try {\n const parent = runTraceContext(execution.runId);\n const span = tracer.startSpan(\n spanName(execution),\n { attributes: spanAttributes(execution) },\n parent,\n );\n // Prefer the real span's id when an OTel SDK produced one; a host with no SDK gets a\n // non-recording span whose id is the run's synthetic parent, which would make every scope in\n // a run look like one span. Mint an id in that case so the scopes stay distinguishable.\n const spanContext = span.spanContext();\n const scope: DurableScope = {\n runId: execution.runId,\n traceId: runTraceId(execution.runId),\n spanId: span.isRecording() ? spanContext.spanId : randomBytes(8).toString('hex'),\n owner,\n };\n\n return context.with(trace.setSpan(parent, span), () =>\n activeScope.run(scope, () =>\n ctx.runInBatch(DURABLE_BATCH_ORIGIN, next).then(\n (value) => {\n span.end();\n return value;\n },\n (error: unknown) => {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n ...(error instanceof Error ? { message: error.message } : {}),\n });\n span.end();\n throw error; // never swallow the workflow's error\n },\n ),\n ),\n );\n } catch {\n // Opening the scope failed (a broken tracer, a Telescope in a bad state). Run the work anyway,\n // untraced. The BullMQ and schedule watchers hold the same line from the other direction —\n // they never let a recording failure fail a healthy job — and it matters more here, because\n // this wrapper sits around a workflow body whose failure is durably persisted.\n return next();\n }\n };\n}\n\n/** Minimal structural view of Telescope's `TraceContextProvider` (it is one method, and typing\n * against the interface would drag a value import in for nothing). */\ninterface TraceContextLike {\n current(): { traceId: string; spanId: string } | null;\n}\n\n/**\n * A Telescope `traceContext` provider that reports the active durable scope, so entries recorded by\n * OTHER watchers (queries, cache, outbound HTTP) during a workflow turn or a step body carry the\n * run's trace id.\n *\n * Wire it as `TelescopeModule.forRoot({ traceContext: durableTraceContext() })`.\n *\n * This exists because Telescope resolves an entry's trace id from an ambient OTel span, and\n * `@opentelemetry/api` on its own propagates nothing: without a registered context manager\n * `context.with(...)` is a no-op and `getActiveSpan()` is always undefined. An app that runs a full\n * OTel SDK needs none of this — it should pass `OtelTraceContextProvider` (optionally as the\n * `delegate` below, which is then consulted first so the app's real spans win), and the scope's\n * spans hang off the run's trace anyway. An app that runs no OTel at all — the common case — would\n * otherwise get correlated batches and no trace ids, which is half the feature.\n */\nexport function durableTraceContext(delegate?: TraceContextLike): TraceContextLike {\n return {\n current() {\n // The host's own tracing wins where it has an answer: inside a durable scope it will report a\n // span from this run's trace anyway, and outside one it is the only source there is.\n const fromDelegate = delegate?.current() ?? null;\n if (fromDelegate) return fromDelegate;\n const scope = activeScope.getStore();\n return scope ? { traceId: scope.traceId, spanId: scope.spanId } : null;\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport { type Context, TraceFlags, context, trace } from '@opentelemetry/api';\n\n/**\n * The trace a run's spans hang under, DERIVED FROM THE RUN ID rather than held as a live root span.\n *\n * The obvious implementation — open a root span on `run.started`, keep it in a map, end it when the\n * run settles — cannot work for a durable workflow. A run suspends: `ctx.step` dispatches and the\n * turn unwinds, and the step body then executes minutes (or days) later, on a different turn, very\n * often in a different process. A live root span would either have to be ended at every suspension —\n * fragmenting one run into a trace per turn, so the step that failed is in a different trace from\n * the turn that dispatched it — or held open across the suspension, which pins a span object for the\n * lifetime of a workflow that is explicitly designed to wait for days.\n *\n * Hashing the run id sidesteps both. The trace id is a pure function of the run, so every turn and\n * every step of a run lands in ONE trace with no state held anywhere, and a worker in another\n * process computes the same id from the same run id — which is what makes a remotely-executed step\n * correlate with the run that dispatched it at all.\n *\n * The parent span context is marked `isRemote` because that is what it is: a reference to a trace\n * whose \"root\" is the run itself, not a span this process ever recorded.\n */\nexport function runTraceId(runId: string): string {\n // 16 bytes of hex, as the W3C trace-context spec requires. sha256 is not used as a security\n // primitive here — only as a well-distributed map from an arbitrary run id into that space.\n return createHash('sha256').update(`durable:run:${runId}`).digest('hex').slice(0, 32);\n}\n\n/** The synthetic parent span id for a run's trace — derived the same way, from a different prefix. */\nfunction runSpanId(runId: string): string {\n return createHash('sha256').update(`durable:run-span:${runId}`).digest('hex').slice(0, 16);\n}\n\n/**\n * An OTel {@link Context} whose active span context is the run's synthetic root. Spans started\n * against it are children of the run, and Telescope's trace-context provider stamps every entry\n * recorded inside it with {@link runTraceId}.\n *\n * `TraceFlags.SAMPLED` is set deliberately: a non-sampled parent leaves the derived span\n * non-recording, and its span context would then never reach an exporter — the run would be\n * traceable in Telescope but invisible in any OTel backend, which is the confusing half-state.\n */\nexport function runTraceContext(runId: string, parent: Context = context.active()): Context {\n return trace.setSpanContext(parent, {\n traceId: runTraceId(runId),\n spanId: runSpanId(runId),\n traceFlags: TraceFlags.SAMPLED,\n isRemote: true,\n });\n}\n","import { defineTelescopeExtension } from '@dudousxd/nestjs-telescope';\nimport { durableDashboard } from './durable-dashboard.spec-data';\nimport {\n durableDurationProvider,\n durableRecentFailuresProvider,\n durableRunsOverTimeProvider,\n durableStateBreakdownProvider,\n durableStateProvider,\n durableSuccessRateProvider,\n durableThroughputProvider,\n durableTimeseriesProvider,\n durableWorkerHealthProvider,\n durableWorkerStatusProvider,\n} from './durable-data-providers';\nimport { DurableTelescopeWatcher } from './durable-telescope.watcher';\n\n/** The first-class Telescope extension for nestjs-durable: watcher + Workflows dashboard. */\nexport function durableTelescopeExtension(\n opts: { runHref?: string; recentFailuresWindowMs?: number } = {},\n) {\n return defineTelescopeExtension({\n name: 'durable',\n watchers: () => [new DurableTelescopeWatcher()],\n entryTypes: () => [{ id: 'durable', label: 'Workflows', dot: 'bg-amber-400' }],\n dashboards: () => [durableDashboard(opts)],\n dataProviders: () => [\n durableStateProvider(),\n durableTimeseriesProvider(),\n durableRecentFailuresProvider(),\n durableWorkerHealthProvider(),\n durableWorkerStatusProvider(),\n durableDurationProvider(),\n durableRunsOverTimeProvider(),\n durableSuccessRateProvider(),\n durableThroughputProvider(),\n durableStateBreakdownProvider(),\n ],\n });\n}\n","import type { DashboardSpec } from '@dudousxd/nestjs-telescope';\n\n/**\n * The \"Workflows\" health dashboard — golden-signals sections layout.\n *\n * `runHref` is a URL template for deep-linking a run out to the durable dashboard\n * (e.g. '/durable/runs/{runId}'). `recentFailuresWindowMs` bounds the \"Stuck runs\"\n * table (default 24h) so a healthy system shows an empty table instead of\n * days-old failures; pass `0` to show all.\n */\nexport function durableDashboard(\n opts: { runHref?: string; recentFailuresWindowMs?: number } = {},\n): DashboardSpec {\n const runHref = opts.runHref ?? '/durable/runs/{runId}';\n const windowMs = opts.recentFailuresWindowMs ?? 24 * 60 * 60 * 1000;\n return {\n id: 'durable.workflows',\n label: 'Workflows',\n panels: [],\n sections: [\n {\n title: 'Health',\n cols: 4,\n panels: [\n {\n kind: 'gauge',\n title: 'Success rate',\n data: { provider: 'durable.successRate' },\n max: 1,\n format: 'percent',\n thresholds: { warn: 0.98, bad: 0.95, direction: 'down-bad' },\n },\n {\n kind: 'stat',\n title: 'Duration p95',\n data: { provider: 'durable.duration', query: { metric: 'p95' } },\n format: 'duration',\n spark: false,\n thresholds: { warn: 2000, bad: 5000, direction: 'up-bad' },\n },\n {\n kind: 'stat',\n title: 'Backlog',\n data: { provider: 'durable.state', query: { status: 'pending' } },\n spark: false,\n thresholds: { warn: 50, bad: 200, direction: 'up-bad' },\n },\n {\n kind: 'stat',\n title: 'Throughput',\n data: { provider: 'durable.throughput' },\n format: 'rate',\n spark: true,\n },\n ],\n },\n {\n title: 'Needs attention',\n cols: 3,\n panels: [\n {\n kind: 'topN',\n title: 'Top failing workflows',\n data: { provider: 'durable.timeseries', query: { metric: 'topFailures' } },\n limit: 8,\n },\n {\n kind: 'table',\n title: 'Stuck runs',\n data: { provider: 'durable.recentFailures', query: { windowMs } },\n columns: [\n { key: 'updatedAt', label: 'Updated' },\n { key: 'workflow', label: 'Workflow' },\n { key: 'runId', label: 'Run', link: { href: runHref } },\n { key: 'error', label: 'Error' },\n ],\n },\n {\n kind: 'table',\n // Lists ALL worker groups (starved ones sorted first); the Status column\n // flags STARVED (queued > 0 with zero live workers) vs ok. Titling it\n // \"Starved worker groups\" read as if every listed group were starved.\n title: 'Worker health',\n data: { provider: 'durable.workerHealth' },\n columns: [\n { key: 'group', label: 'Group' },\n { key: 'queued', label: 'Queued' },\n { key: 'liveWorkers', label: 'Workers' },\n { key: 'status', label: 'Status' },\n ],\n },\n ],\n },\n {\n title: 'Workers',\n // One column, because this section holds exactly one panel and that panel is the widest\n // table on the dashboard. A section renders as a fixed `grid-cols-N` with no `colSpan`, so\n // `cols: 2` gave the widest table on the dashboard half the viewport and left the cell\n // beside it empty — it scrolled sideways inside its own card while a hole sat next to it.\n cols: 1,\n panels: [\n {\n kind: 'table',\n // One row per LIVE worker (flattened from each group's heartbeats), exposing the live\n // WorkerStatus the heartbeat carries: fixed vs. adaptive limit, in-flight saturation,\n // RAM%/CPU%, throughput, p95, and why the adaptive controller last moved. A worker from\n // an older SDK with no status still lists, reading 'n/a' — distinct from the '—' of a\n // worker that reports fine but has nothing to measure yet, which is what an idle\n // deployment looks like (throughput and p95 come off a rolling window of completions,\n // so they are empty until a step finishes inside it). The\n // adaptive min–max range rides the In-flight column's row value (`minMax` key) for those\n // that want it; the visible columns stay readable.\n title: 'Workers',\n data: { provider: 'durable.workerStatus' },\n columns: [\n { key: 'group', label: 'Group' },\n { key: 'instanceId', label: 'Worker' },\n { key: 'mode', label: 'Mode' },\n // No separate `limit` column: In-flight renders `<inFlight>/<limit>`, so a Limit\n // column beside it repeats its own denominator in every row.\n { key: 'saturation', label: 'In-flight' },\n { key: 'queued', label: 'Queued' },\n { key: 'rssPct', label: 'RAM %' },\n { key: 'cpuPct', label: 'CPU %' },\n { key: 'throughputPerMin', label: 'Thrpt/min' },\n { key: 'p95Ms', label: 'p95 ms' },\n { key: 'lastAdjust', label: 'Last adjust' },\n ],\n },\n ],\n },\n {\n title: 'Trends',\n cols: 3,\n panels: [\n {\n kind: 'timeseries',\n title: 'Runs over time',\n data: { provider: 'durable.runsOverTime' },\n series: ['done', 'failed'],\n style: 'stacked',\n },\n {\n kind: 'distribution',\n title: 'Duration distribution',\n data: { provider: 'durable.duration' },\n markers: ['p50', 'p95', 'p99'],\n format: 'duration',\n },\n {\n kind: 'breakdown',\n title: 'Runs by state',\n data: { provider: 'durable.stateBreakdown' },\n style: 'donut',\n },\n ],\n },\n ],\n };\n}\n","import { STATE_STORE_CANONICAL, WorkflowEngine } from '@dudousxd/nestjs-durable-core';\nimport type { RunStatus, StateStore, WorkerStatus } from '@dudousxd/nestjs-durable-core';\nimport type { DataProvider, ExtensionContext } from '@dudousxd/nestjs-telescope';\nimport { TELESCOPE_STORAGE } from '@dudousxd/nestjs-telescope';\n\n// ─── Shared helpers ───────────────────────────────────────────────────────────\n\n/** Compute the p-th percentile of a SORTED array. */\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[Math.max(0, idx)] ?? 0;\n}\n\ntype StorageEntry = { content?: unknown; createdAt?: Date };\n\n/** Fetch the durable entry page from TELESCOPE_STORAGE. */\nasync function fetchEntries(ctx: ExtensionContext): Promise<StorageEntry[]> {\n const storage = ctx.moduleRef.get(TELESCOPE_STORAGE, { strict: false }) as {\n get(q: { type?: string; limit?: number }): Promise<{ data: StorageEntry[] }>;\n };\n const page = await storage.get({ type: 'durable', limit: 5_000 });\n return page.data;\n}\n\n/**\n * Split entries into current/previous equal-length windows.\n * current: (now - windowMs, now]\n * previous: (now - 2*windowMs, now - windowMs]\n */\nfunction splitWindows(\n entries: StorageEntry[],\n windowMs: number,\n now: number,\n): { current: StorageEntry[]; previous: StorageEntry[] } {\n const start = now - windowMs;\n const prevStart = start - windowMs;\n return {\n current: entries.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > start && t <= now;\n }),\n previous: entries.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > prevStart && t <= start;\n }),\n };\n}\n\n/** Compute success rate (completed / total) for a list of entries, returns 1 when no data. */\nfunction successRateOf(entries: StorageEntry[]): number {\n let completed = 0;\n let failed = 0;\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string };\n if (c.event === 'run.completed') completed += 1;\n else if (c.event === 'run.failed') failed += 1;\n }\n const total = completed + failed;\n return total === 0 ? 1 : completed / total;\n}\n\n/** Run-level lifecycle events — each can legitimately occur at most once per runId. */\nconst RUN_LIFECYCLE_EVENTS = new Set([\n 'run.started',\n 'run.completed',\n 'run.failed',\n 'run.suspended',\n]);\n\n/**\n * Deduplicate run-level lifecycle entries by `${event}:${runId}`, keeping the FIRST occurrence.\n *\n * The durable engine emits each run lifecycle event on every pod (1 worker + N api pods) and the\n * watcher records on each, so a single `run.completed` for a given runId is captured multiple times\n * with identical content but distinct entry ids / createdAt (±ms) — inflating completed/failed\n * counts. A given runId can legitimately have at most ONE `run.started` and ONE terminal\n * `run.completed`/`run.failed`/`run.suspended`, so collapsing by `${event}:${runId}` is provably\n * safe. Step events and any entry without a run-level event + runId pass through untouched — a step\n * can legitimately repeat across attempts, and the watcher's step content carries no stepId/attempt\n * to key on.\n */\nfunction dedupeRunEvents(entries: StorageEntry[]): StorageEntry[] {\n const seen = new Set<string>();\n const out: StorageEntry[] = [];\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string };\n if (c.event && c.runId && RUN_LIFECYCLE_EVENTS.has(c.event)) {\n const key = `${c.event}:${c.runId}`;\n if (seen.has(key)) continue;\n seen.add(key);\n }\n out.push(e);\n }\n return out;\n}\n\nconst STATE_CAP = 10_000;\n\n/**\n * How many runs are in `status`: an exact `COUNT` through the store's `countRuns`, or, for a store\n * without one, the length of a listing capped at {@link STATE_CAP}. A wide `ctx.all` fan can put\n * tens of thousands of runs in one status, which the capped listing would both under-count and pay\n * for by loading every row.\n */\nfunction countByStatus(store: StateStore, status: RunStatus): Promise<number> {\n if (store.countRuns) return store.countRuns({ status });\n return store.listRuns({ status, limit: STATE_CAP }).then((runs) => runs.length);\n}\n\n// ─── Existing providers ───────────────────────────────────────────────────────\n\n/** Source C: current-state gauge from the durable store. query.status selects which (default 'dead'). */\nexport function durableStateProvider(): DataProvider {\n return {\n name: 'durable.state',\n async resolve(query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const status = (query?.status as RunStatus) ?? 'dead';\n return { value: await countByStatus(store, status) };\n },\n };\n}\n\n/**\n * Source A: rollups from captured `durable` entries in Telescope's own storage.\n * Reads recent run.* lifecycle entries and aggregates. Bounded by Telescope's prune\n * window (by design — this is the \"history\" series, not the source of truth).\n */\nexport function durableTimeseriesProvider(): DataProvider {\n return {\n name: 'durable.timeseries',\n async resolve(query, ctx: ExtensionContext) {\n // TELESCOPE_STORAGE.get(query) → { data: Entry[]; nextCursor }. Entry = { content, createdAt, ... }.\n const storage = ctx.moduleRef.get(TELESCOPE_STORAGE, { strict: false }) as {\n get(q: { type?: string; limit?: number }): Promise<{\n data: Array<{ content?: unknown; createdAt?: Date }>;\n }>;\n };\n const limit = Math.min(5_000, Math.max(100, Number(query?.limit ?? 2_000)));\n const page = await storage.get({ type: 'durable', limit });\n\n let completed = 0;\n let failed = 0;\n const failByWorkflow = new Map<string, number>();\n // Dedup triplicated lifecycle events (one event recorded on every pod) before counting.\n for (const e of dedupeRunEvents(page.data)) {\n const c = (e.content ?? {}) as { event?: string; workflow?: string };\n if (c.event === 'run.completed') completed += 1;\n else if (c.event === 'run.failed') {\n failed += 1;\n const wf = c.workflow ?? 'unknown';\n failByWorkflow.set(wf, (failByWorkflow.get(wf) ?? 0) + 1);\n }\n }\n const total = completed + failed;\n const metric = (query?.metric as string) ?? 'successRate';\n if (metric === 'successRate') return { value: total === 0 ? 1 : completed / total };\n if (metric === 'failed') return { value: failed };\n if (metric === 'total') return { value: total };\n if (metric === 'topFailures') {\n const items = [...failByWorkflow.entries()]\n .map(([label, value]) => ({ label, value }))\n .sort((a, b) => b.value - a.value);\n return { items };\n }\n return { value: total };\n },\n };\n}\n\n/**\n * Source C: recent failed + dead runs as table rows (newest first), time-bounded.\n * Only failures updated within `query.windowMs` (default 24h) are returned, so a\n * healthy system shows an EMPTY table instead of surfacing days-old failures as if\n * they were a live incident. `windowMs: 0` disables the window (return all). Each\n * row carries a compact `updatedAt` stamp so recency is visible in the table.\n */\nexport function durableRecentFailuresProvider(): DataProvider {\n return {\n name: 'durable.recentFailures',\n async resolve(query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50)));\n const windowMs = query?.windowMs === undefined ? 24 * 60 * 60 * 1000 : Number(query.windowMs);\n const cutoff = windowMs > 0 ? Date.now() - windowMs : 0;\n const [failed, dead] = await Promise.all([\n store.listRuns({ status: 'failed', limit }),\n store.listRuns({ status: 'dead', limit }),\n ]);\n const rows = [...failed, ...dead]\n .filter((r) => +new Date(r.updatedAt) >= cutoff)\n .sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt))\n .slice(0, limit)\n .map((r) => ({\n // Compact UTC stamp (\"YYYY-MM-DD HH:mm Z\"); the generic table renderer\n // prints column values as-is, so we format here.\n updatedAt: `${new Date(r.updatedAt).toISOString().replace('T', ' ').slice(0, 16)}Z`,\n workflow: r.workflow,\n runId: r.id,\n error: r.error?.message ?? '',\n }));\n return { rows };\n },\n };\n}\n\n/**\n * Source D: per-group worker health from the engine (`WorkflowEngine.workerHealth()` — queue depth\n * vs. live worker heartbeats). `query.metric: 'starvedCount'` returns the number of groups with work\n * queued and zero live workers (the \"alive but not consuming\" alert state) as a stat; otherwise a\n * table, starved groups sorted first. Empty when the transport can't report health (in-process).\n */\nexport function durableWorkerHealthProvider(): DataProvider {\n return {\n name: 'durable.workerHealth',\n async resolve(query, ctx: ExtensionContext) {\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false }) as WorkflowEngine;\n const health = await engine.workerHealth();\n const isStarved = (g: { depth: number; liveWorkers: unknown[] }) =>\n g.depth > 0 && g.liveWorkers.length === 0;\n if ((query?.metric as string) === 'starvedCount') {\n return { value: health.filter(isStarved).length };\n }\n const rows = health\n .slice()\n // Starved groups first, then deepest backlog — the rows that need attention rise to the top.\n .sort((a, b) => Number(isStarved(b)) - Number(isStarved(a)) || b.depth - a.depth)\n .map((g) => ({\n group: g.group,\n queued: g.depth,\n liveWorkers: g.liveWorkers.length,\n status: isStarved(g) ? 'STARVED' : 'ok',\n }));\n return { rows };\n },\n };\n}\n\n/** Compact \"Nm ago\" relative stamp for an epoch-ms instant (worker last-adjust recency). */\nfunction relAgo(atMs: number): string {\n const s = Math.max(0, Math.round((Date.now() - atMs) / 1000));\n if (s < 60) return `${s}s ago`;\n const m = Math.round(s / 60);\n if (m < 60) return `${m}m ago`;\n const h = Math.round(m / 60);\n if (h < 24) return `${h}h ago`;\n return `${Math.round(h / 24)}d ago`;\n}\n\n/**\n * What a cell says when it has no number, and the two cases it has to keep apart.\n *\n * A blank cell used to mean both \"this worker's SDK is too old to report anything\" and \"this worker\n * reports fine, it just has nothing to measure yet\" — and the second is the common one on an idle\n * deployment, where the adaptive controller's rolling window is empty because no step has completed\n * inside it. Reading one as the other sends someone hunting for a broken worker that is merely idle,\n * or ignoring a fleet that has genuinely stopped reporting.\n *\n * `n/a` is the heartbeat carrying no {@link WorkerStatus} at all; `—` is a status that carries no\n * value for this field yet.\n */\nconst NOT_REPORTED = 'n/a';\nconst NOT_YET_MEASURED = '—';\n\n/**\n * Render a worker's last limit change as \"shrink 8→5 2m ago\".\n *\n * `—` here means the adaptive controller has not moved the limit yet — the normal state of a worker\n * that has been sitting at its floor. A fixed-mode worker never adjusts at all, which is what its\n * `Mode` column already says.\n */\nfunction fmtLastAdjust(status: WorkerStatus | undefined): string {\n if (!status) return NOT_REPORTED;\n const a = status.lastAdjust;\n if (!a) return NOT_YET_MEASURED;\n return `${a.reason} ${a.from}→${a.to} ${relAgo(a.at)}`;\n}\n\n/** Round a 0..100(+) percent to a whole number for table display. */\nfunction fmtPct(n: number | undefined, status: WorkerStatus | undefined): number | string {\n if (typeof n === 'number') return Math.round(n);\n return status ? NOT_YET_MEASURED : NOT_REPORTED;\n}\n\n/** Round a rate/latency to a whole number for table display. */\nfunction fmtNum(n: number | undefined, status: WorkerStatus | undefined): number | string {\n if (typeof n === 'number') return Math.round(n);\n return status ? NOT_YET_MEASURED : NOT_REPORTED;\n}\n\n/**\n * Source D (per-worker): flatten `WorkflowEngine.workerHealth()` to ONE row per live worker, exposing\n * the live {@link WorkerStatus} each heartbeat now carries (mode/limit/min-max, in-flight saturation,\n * RAM%/CPU%, throughput, p95, last adjust). A worker from an older SDK with no `status` still renders\n * — its measured fields fall back to '—', so a mixed fleet stays visible. Rows sort backlog-bearing\n * groups first, then by group, then instanceId. Empty when the transport can't report health.\n */\nexport function durableWorkerStatusProvider(): DataProvider {\n return {\n name: 'durable.workerStatus',\n async resolve(_query, ctx: ExtensionContext) {\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false }) as WorkflowEngine;\n const health = await engine.workerHealth();\n const rows = health\n .slice()\n // Groups with a backlog first (the workers worth scrutinising), then by group name.\n .sort((a, b) => Number(b.depth > 0) - Number(a.depth > 0) || a.group.localeCompare(b.group))\n .flatMap((group) =>\n group.liveWorkers\n .slice()\n .sort((a, b) => a.instanceId.localeCompare(b.instanceId))\n .map((worker) => {\n const status = worker.status;\n const concurrency = status?.concurrency;\n // Mode/limit/in-flight are not measurements — a status either declares them or the\n // heartbeat carried no status at all, so their absence is always `n/a`.\n const mode = concurrency?.mode ?? NOT_REPORTED;\n const limit = concurrency?.limit ?? NOT_REPORTED;\n const inFlight = status?.inFlight ?? NOT_REPORTED;\n const minMax =\n concurrency?.mode === 'adaptive'\n ? `${concurrency.min ?? 1}–${concurrency.max ?? 32}`\n : NOT_REPORTED;\n const saturation =\n concurrency && status ? `${status.inFlight}/${concurrency.limit}` : NOT_REPORTED;\n return {\n group: group.group,\n instanceId: worker.instanceId,\n mode,\n limit,\n minMax,\n inFlight,\n saturation,\n queued: group.depth,\n rssPct: fmtPct(status?.rssPct, status),\n cpuPct: fmtPct(status?.cpuPct, status),\n throughputPerMin: fmtNum(status?.throughputPerMin, status),\n p95Ms: fmtNum(status?.p95Ms, status),\n lastAdjust: fmtLastAdjust(status),\n };\n }),\n );\n return { rows };\n },\n };\n}\n\n// ─── New golden-signals providers ────────────────────────────────────────────\n\n/**\n * Duration percentiles (p50/p95/p99) + a histogram of ~8 buckets.\n * Durations are read from `content.durationMs` on `run.completed`/`run.failed`; when absent,\n * computed by pairing a `run.started` and its corresponding `run.completed`/`run.failed` entry\n * by `runId` using their `createdAt` timestamps.\n *\n * With `query.metric === 'p50'|'p95'|'p99'` returns `{ value }` for use as a stat panel.\n */\nexport function durableDurationProvider(): DataProvider {\n return {\n name: 'durable.duration',\n async resolve(query, ctx: ExtensionContext) {\n // Dedup triplicated lifecycle events so paired durations aren't double-counted.\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n\n // Index run.started entries by runId for the pairing fallback.\n const startedAt = new Map<string, number>();\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string };\n if (c.event === 'run.started' && c.runId && e.createdAt) {\n startedAt.set(c.runId, +new Date(e.createdAt));\n }\n }\n\n const durs: number[] = [];\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string; durationMs?: number };\n if (c.event === 'run.completed' || c.event === 'run.failed') {\n if (typeof c.durationMs === 'number') {\n durs.push(c.durationMs);\n } else if (c.runId) {\n const start = startedAt.get(c.runId);\n const end = e.createdAt ? +new Date(e.createdAt) : undefined;\n if (start !== undefined && end !== undefined && end >= start) {\n durs.push(end - start);\n }\n }\n }\n }\n durs.sort((a, b) => a - b);\n\n const p50 = percentile(durs, 50);\n const p95 = percentile(durs, 95);\n const p99 = percentile(durs, 99);\n\n // Stat shortcut: return a single value for use in stat panels.\n const metric = (query as Record<string, unknown>)?.metric as string | undefined;\n if (metric === 'p50') return { value: p50 };\n if (metric === 'p95') return { value: p95 };\n if (metric === 'p99') return { value: p99 };\n\n // Build ~8 equal-width buckets between 0 and max.\n const max = durs.at(-1) ?? 0;\n const bucketCount = 8;\n const size = Math.max(1, Math.ceil((max + 1) / bucketCount));\n const buckets = Array.from({ length: bucketCount }, (_, i) => ({\n label: `${Math.round((i * size) / 100) / 10}s`,\n count: 0,\n }));\n for (const d of durs) {\n const bi = Math.min(bucketCount - 1, Math.floor(d / size));\n const bucket = buckets[bi];\n if (bucket) bucket.count += 1;\n }\n\n return { buckets, p50, p95, p99 };\n },\n };\n}\n\n/**\n * Buckets `run.completed` (-> done) and `run.failed` (-> failed) entries by `createdAt` into\n * N=`query.buckets ?? 24` equal-width time buckets. Returns `{ rows: Array<{ label; done; failed }> }`.\n */\nexport function durableRunsOverTimeProvider(): DataProvider {\n return {\n name: 'durable.runsOverTime',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const n = Number((query as Record<string, unknown>)?.buckets ?? 24);\n const now = Date.now();\n\n // Determine the time window from the oldest entry or default to 24h.\n let minT = now;\n for (const e of entries) {\n if (e.createdAt) {\n const t = +new Date(e.createdAt);\n if (t < minT) minT = t;\n }\n }\n const span = Math.max(now - minT, 1);\n const bucketSize = span / n;\n\n const rows: Array<{ label: string; done: number; failed: number }> = Array.from(\n { length: n },\n (_, i) => {\n const bucketStart = new Date(minT + i * bucketSize);\n const label = bucketStart.toISOString().slice(11, 16); // \"HH:mm\"\n return { label, done: 0, failed: 0 };\n },\n );\n\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string };\n if (c.event !== 'run.completed' && c.event !== 'run.failed') continue;\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n const idx = Math.min(n - 1, Math.floor((t - minT) / bucketSize));\n const row = rows[idx];\n if (row) {\n if (c.event === 'run.completed') row.done += 1;\n else row.failed += 1;\n }\n }\n\n return { rows };\n },\n };\n}\n\n/**\n * Success rate over the last `query.windowMs` (default 24h), with a `delta` vs. the previous\n * equal-length window and a `spark` array of per-sub-bucket success rates.\n */\nexport function durableSuccessRateProvider(): DataProvider {\n return {\n name: 'durable.successRate',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const windowMs = Number((query as Record<string, unknown>)?.windowMs ?? 24 * 60 * 60 * 1000);\n const now = Date.now();\n const { current, previous } = splitWindows(entries, windowMs, now);\n\n const value = successRateOf(current);\n const prevRate = successRateOf(previous);\n const delta = previous.length > 0 ? value - prevRate : undefined;\n\n // Spark: split the current window into 8 sub-buckets.\n const sparkBuckets = 8;\n const bucketSize = windowMs / sparkBuckets;\n const sparkStart = now - windowMs;\n const spark = Array.from({ length: sparkBuckets }, (_, i) => {\n const bStart = sparkStart + i * bucketSize;\n const bEnd = bStart + bucketSize;\n const bEntries = current.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > bStart && t <= bEnd;\n });\n return successRateOf(bEntries);\n });\n\n return { value, delta, spark };\n },\n };\n}\n\n/**\n * Completed runs per hour over the last `query.windowMs` (default 24h), with a `delta` vs. the\n * previous equal-length window and a `spark` array of per-sub-bucket throughput values.\n */\nexport function durableThroughputProvider(): DataProvider {\n return {\n name: 'durable.throughput',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const windowMs = Number((query as Record<string, unknown>)?.windowMs ?? 24 * 60 * 60 * 1000);\n const now = Date.now();\n const { current, previous } = splitWindows(entries, windowMs, now);\n\n const countCompleted = (es: StorageEntry[]) =>\n es.filter((e) => ((e.content ?? {}) as { event?: string }).event === 'run.completed')\n .length;\n\n const windowHours = windowMs / (60 * 60 * 1000);\n const value = countCompleted(current) / windowHours;\n const prevValue = countCompleted(previous) / windowHours;\n const delta = previous.length > 0 ? value - prevValue : undefined;\n\n // Spark: 8 sub-buckets of current window.\n const sparkBuckets = 8;\n const bucketSize = windowMs / sparkBuckets;\n const bucketHours = bucketSize / (60 * 60 * 1000);\n const sparkStart = now - windowMs;\n const spark = Array.from({ length: sparkBuckets }, (_, i) => {\n const bStart = sparkStart + i * bucketSize;\n const bEnd = bStart + bucketSize;\n const bEntries = current.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > bStart && t <= bEnd;\n });\n return countCompleted(bEntries) / bucketHours;\n });\n\n return { value, delta, spark };\n },\n };\n}\n\n// Palette aligned index-for-index with STATE_BREAKDOWN_STATUSES so each status reads with the\n// semantically-correct color: running=cyan, pending=amber, cancelling=orange, completed=green,\n// failed=red, dead=purple.\nconst STATE_BREAKDOWN_STATUSES: RunStatus[] = [\n 'running',\n 'pending',\n 'cancelling',\n 'completed',\n 'failed',\n 'dead',\n];\n/** Color palette for the state breakdown pie segments (aligned to STATE_BREAKDOWN_STATUSES). */\nconst STATE_BREAKDOWN_PALETTE = [\n '#38bdf8', // running → cyan/blue\n '#fbbf24', // pending → amber\n '#f59e0b', // cancelling → orange\n '#34d399', // completed → green\n '#f87171', // failed → red\n '#a78bfa', // dead → purple\n];\n\n/**\n * Counts per status (see {@link countByStatus}) for each of the statuses and returns pie\n * segments with the standard palette: `{ segments: Array<{ label, value, color }> }`.\n */\nexport function durableStateBreakdownProvider(): DataProvider {\n return {\n name: 'durable.stateBreakdown',\n async resolve(_query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const counts = await Promise.all(\n STATE_BREAKDOWN_STATUSES.map((status) => countByStatus(store, status)),\n );\n const segments = STATE_BREAKDOWN_STATUSES.map((label, i) => ({\n label,\n value: counts[i],\n color: STATE_BREAKDOWN_PALETTE[i],\n }));\n return { segments };\n },\n };\n}\n\n// NOTE: durableRetryHotspotsProvider is NOT implemented.\n// StateStore only exposes `listCheckpoints(runId: string)` (per-run lookup), with no cross-run\n// query by step `attempts`. There is no API to enumerate all step checkpoints across runs without\n// first listing every run — which would be O(N*M) and impractical. Task 13 should omit that panel.\n"],"mappings":"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","names":["import_api","import_node_crypto","import_api","runTraceId","runId","createHash","update","digest","slice","runSpanId","runTraceContext","parent","context","active","trace","setSpanContext","traceId","spanId","traceFlags","TraceFlags","SAMPLED","isRemote","activeScope","AsyncLocalStorage","currentDurableScope","getStore","DURABLE_BATCH_ORIGIN","spanName","execution","unit","name","workflow","spanAttributes","attributes","runId","undefined","seq","attempt","durableExecutionScope","ctx","tracer","trace","getTracer","owner","next","open","config","enabled","parent","runTraceContext","span","startSpan","spanContext","scope","traceId","runTraceId","spanId","isRecording","randomBytes","toString","context","with","setSpan","run","runInBatch","then","value","end","error","setStatus","code","SpanStatusCode","ERROR","Error","message","durableTraceContext","delegate","current","fromDelegate","DurableTelescopeWatcher","type","disposeScope","register","ctx","useDurableExecution","durableExecutionScope","engine","moduleRef","get","WorkflowEngine","strict","subscribe","tracer","trace","getTracer","event","record","traceId","runTraceId","runId","content","workflow","parentRunId","parentRunIdOf","seq","name","kind","output","error","durationMs","tags","base","runTraceContext","step","startSpan","attributes","message","setStatus","undefined","code","SpanStatusCode","ERROR","context","with","setSpan","end","unregister","parent","push","import_nestjs_telescope","durableDashboard","opts","runHref","windowMs","recentFailuresWindowMs","id","label","panels","sections","title","cols","kind","data","provider","max","format","thresholds","warn","bad","direction","query","metric","spark","status","limit","columns","key","link","href","series","style","markers","import_nestjs_durable_core","percentile","sorted","p","length","idx","Math","min","ceil","max","fetchEntries","ctx","storage","moduleRef","get","TELESCOPE_STORAGE","strict","page","type","limit","data","splitWindows","entries","windowMs","now","start","prevStart","current","filter","e","t","createdAt","Date","previous","successRateOf","completed","failed","c","content","event","total","RUN_LIFECYCLE_EVENTS","Set","dedupeRunEvents","seen","out","runId","has","key","add","push","STATE_CAP","countByStatus","store","status","countRuns","listRuns","then","runs","durableStateProvider","name","resolve","query","STATE_STORE_CANONICAL","value","durableTimeseriesProvider","Number","failByWorkflow","Map","wf","workflow","set","metric","items","map","label","sort","a","b","durableRecentFailuresProvider","undefined","cutoff","dead","Promise","all","rows","r","updatedAt","slice","toISOString","replace","id","error","message","durableWorkerHealthProvider","engine","WorkflowEngine","health","workerHealth","isStarved","g","depth","liveWorkers","group","queued","relAgo","atMs","s","round","m","h","NOT_REPORTED","NOT_YET_MEASURED","fmtLastAdjust","lastAdjust","reason","from","to","at","fmtPct","n","fmtNum","durableWorkerStatusProvider","_query","localeCompare","flatMap","instanceId","worker","concurrency","mode","inFlight","minMax","saturation","rssPct","cpuPct","throughputPerMin","p95Ms","durableDurationProvider","startedAt","durs","durationMs","end","p50","p95","p99","bucketCount","size","buckets","Array","_","i","count","d","bi","floor","bucket","durableRunsOverTimeProvider","minT","span","bucketSize","bucketStart","done","row","durableSuccessRateProvider","prevRate","delta","sparkBuckets","sparkStart","spark","bStart","bEnd","bEntries","durableThroughputProvider","countCompleted","es","windowHours","prevValue","bucketHours","STATE_BREAKDOWN_STATUSES","STATE_BREAKDOWN_PALETTE","durableStateBreakdownProvider","counts","segments","color","durableTelescopeExtension","opts","defineTelescopeExtension","name","watchers","DurableTelescopeWatcher","entryTypes","id","label","dot","dashboards","durableDashboard","dataProviders","durableStateProvider","durableTimeseriesProvider","durableRecentFailuresProvider","durableWorkerHealthProvider","durableWorkerStatusProvider","durableDurationProvider","durableRunsOverTimeProvider","durableSuccessRateProvider","durableThroughputProvider","durableStateBreakdownProvider"]}
package/dist/index.d.cts CHANGED
@@ -242,7 +242,7 @@ declare function durableSuccessRateProvider(): DataProvider;
242
242
  */
243
243
  declare function durableThroughputProvider(): DataProvider;
244
244
  /**
245
- * Counts from `STATE_STORE.listRuns({ status })` for each of the statuses and returns pie
245
+ * Counts per status (see {@link countByStatus}) for each of the statuses and returns pie
246
246
  * segments with the standard palette: `{ segments: Array<{ label, value, color }> }`.
247
247
  */
248
248
  declare function durableStateBreakdownProvider(): DataProvider;
package/dist/index.d.ts CHANGED
@@ -242,7 +242,7 @@ declare function durableSuccessRateProvider(): DataProvider;
242
242
  */
243
243
  declare function durableThroughputProvider(): DataProvider;
244
244
  /**
245
- * Counts from `STATE_STORE.listRuns({ status })` for each of the statuses and returns pie
245
+ * Counts per status (see {@link countByStatus}) for each of the statuses and returns pie
246
246
  * segments with the standard palette: `{ segments: Array<{ label, value, color }> }`.
247
247
  */
248
248
  declare function durableStateBreakdownProvider(): DataProvider;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/durable-telescope.watcher.ts
5
- import { WorkflowEngine, useDurableExecution } from "@dudousxd/nestjs-durable-core";
5
+ import { WorkflowEngine, parentRunIdOf, useDurableExecution } from "@dudousxd/nestjs-durable-core";
6
6
  import { SpanStatusCode as SpanStatusCode2, context as context3, trace as trace3 } from "@opentelemetry/api";
7
7
 
8
8
  // src/durable-execution-scope.ts
@@ -137,6 +137,7 @@ var DurableTelescopeWatcher = class {
137
137
  event: event.type,
138
138
  workflow: event.workflow,
139
139
  runId: event.runId,
140
+ parentRunId: parentRunIdOf(event.runId),
140
141
  seq: event.seq,
141
142
  name: event.name,
142
143
  kind: event.kind,
@@ -187,6 +188,8 @@ var DurableTelescopeWatcher = class {
187
188
  event.type,
188
189
  `run:${event.runId}`
189
190
  ];
191
+ const parent = parentRunIdOf(event.runId);
192
+ if (parent) tags.push(`parent:${parent}`);
190
193
  if (event.workflow) tags.push(`workflow:${event.workflow}`);
191
194
  if (event.kind) tags.push(`kind:${event.kind}`);
192
195
  if (event.type === "run.failed") tags.push("failed");
@@ -527,6 +530,16 @@ function dedupeRunEvents(entries) {
527
530
  }
528
531
  __name(dedupeRunEvents, "dedupeRunEvents");
529
532
  var STATE_CAP = 1e4;
533
+ function countByStatus(store, status) {
534
+ if (store.countRuns) return store.countRuns({
535
+ status
536
+ });
537
+ return store.listRuns({
538
+ status,
539
+ limit: STATE_CAP
540
+ }).then((runs) => runs.length);
541
+ }
542
+ __name(countByStatus, "countByStatus");
530
543
  function durableStateProvider() {
531
544
  return {
532
545
  name: "durable.state",
@@ -535,12 +548,8 @@ function durableStateProvider() {
535
548
  strict: false
536
549
  });
537
550
  const status = query?.status ?? "dead";
538
- const runs = await store.listRuns({
539
- status,
540
- limit: STATE_CAP
541
- });
542
551
  return {
543
- value: runs.length
552
+ value: await countByStatus(store, status)
544
553
  };
545
554
  }
546
555
  };
@@ -936,10 +945,7 @@ function durableStateBreakdownProvider() {
936
945
  const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, {
937
946
  strict: false
938
947
  });
939
- const counts = await Promise.all(STATE_BREAKDOWN_STATUSES.map((status) => store.listRuns({
940
- status,
941
- limit: STATE_CAP
942
- }).then((runs) => runs.length)));
948
+ const counts = await Promise.all(STATE_BREAKDOWN_STATUSES.map((status) => countByStatus(store, status)));
943
949
  const segments = STATE_BREAKDOWN_STATUSES.map((label, i) => ({
944
950
  label,
945
951
  value: counts[i],
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/durable-telescope.watcher.ts","../src/durable-execution-scope.ts","../src/durable-run-trace.ts","../src/durable-telescope.extension.ts","../src/durable-dashboard.spec-data.ts","../src/durable-data-providers.ts"],"sourcesContent":["import {\n type EngineEvent,\n WorkflowEngine,\n useDurableExecution,\n} from '@dudousxd/nestjs-durable-core';\nimport type { Watcher, WatcherContext } from '@dudousxd/nestjs-telescope';\nimport { SpanStatusCode, context, trace } from '@opentelemetry/api';\nimport { durableExecutionScope } from './durable-execution-scope';\nimport { runTraceContext, runTraceId } from './durable-run-trace';\n\n/**\n * A Telescope watcher that turns durable-workflow lifecycle events into Telescope entries, so\n * runs and steps (including remote/Python steps) show up alongside the app's requests, queries\n * and jobs in the Telescope UI — under the **Workflows** tab.\n *\n * It does two things, and the second is the one that makes a failing workflow debuggable:\n *\n * 1. **Records an entry per engine event**, stamped with the run's trace (see\n * {@link runTraceContext}), so a whole run — across all its turns, all its steps and every\n * process that executed them — reads as a single trace at `/telescope#/traces/<id>`.\n *\n * 2. **Opens a Telescope batch around the execution itself** (see {@link durableExecutionScope}),\n * through the engine's `useDurableExecution` seam. Without this, everything a workflow turn or a\n * step handler *did* — its queries, its logs, the exception it threw — was recorded outside any\n * batch and outside any span: traceless, and so impossible to correlate back to the step that\n * caused it. Recording an event after the fact cannot fix that; the scope has to be open while\n * the body runs.\n *\n * Add it to `TelescopeModule.forRoot({ watchers: [new DurableTelescopeWatcher()] })`. It resolves\n * the engine from the (global) durable providers and subscribes to its events.\n *\n * ## What is NOT covered\n *\n * A **remote step** — one whose handler lives in another runtime, e.g. the Python worker — executes\n * out of process, so nothing here can wrap it. Its lifecycle still shows up (the engine emits\n * `step.started`/`step.completed`/`step.failed` when it dispatches and when the result lands, and\n * those entries carry the run's trace id), but the queries and exceptions *inside* that handler are\n * that runtime's to record. Because the trace id is derived from the run id rather than held in this\n * process, a remote runtime that derives it the same way joins the same trace — but that is a change\n * on its side, not something this watcher can do on its behalf.\n *\n * A **remote workflow** (a run whose body itself lives in another runtime) is likewise unwrapped:\n * the engine dispatches a workflow task and awaits a decision rather than executing a body, and a\n * scope around a dispatch would describe the wrong thing.\n */\nexport class DurableTelescopeWatcher implements Watcher {\n readonly type = 'durable';\n /** Removes the execution wrapper from the engine's process-level registry. */\n private disposeScope: (() => void) | undefined;\n\n register(ctx: WatcherContext): void {\n // Registered FIRST, and unconditionally: this is the half that works without a local engine. A\n // thin worker binds `WorkflowEngine` to a start-only facade with no event stream, but it is\n // exactly where step handlers run — so the execution scope belongs there even though there is\n // nothing local to subscribe to.\n this.disposeScope?.();\n this.disposeScope = useDurableExecution(durableExecutionScope(ctx));\n\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false });\n // A store-less thin-worker / tenant deployment (e.g. a local `DURABLE_TENANT=…` dev stack) binds\n // the `WorkflowEngine` token to a start-only `DurableStartClient` facade — it proxies run starts\n // over the transport and has NO local lifecycle event stream (those events live on the operator\n // that holds the store). There's nothing local to observe, so skip the subscription gracefully\n // instead of throwing \"engine.subscribe is not a function\".\n if (typeof (engine as Partial<WorkflowEngine>)?.subscribe !== 'function') return;\n const tracer = trace.getTracer('@dudousxd/nestjs-durable-telescope');\n\n engine.subscribe((event) => {\n const record = () =>\n ctx.record({\n type: this.type,\n // Stated explicitly rather than left to the ambient span, and the Recorder honours an\n // explicit `traceId` above everything else. A lifecycle event is very often emitted\n // OUTSIDE any execution scope — a remote step's result lands in a transport callback, a\n // recovery sweep emits for a run this process never executed — and those entries are\n // exactly the ones an operator follows into the trace. We know the run, so we know the\n // trace; there is no reason to let it depend on where the emit happened to land.\n traceId: runTraceId(event.runId),\n content: {\n event: event.type,\n workflow: event.workflow,\n runId: event.runId,\n seq: event.seq,\n name: event.name,\n kind: event.kind,\n output: event.output,\n error: event.error,\n durationMs: event.durationMs,\n },\n tags: this.tags(event),\n });\n\n // Anchor on the run's trace. Derived from the run id, so this event groups with the entries\n // recorded *inside* the turn/step scopes without either side having to hand the other a span —\n // which is the only thing that works once a step executes on a later turn, or in another pod.\n const base = runTraceContext(event.runId);\n\n // A step/signal/sleep event → give it its own child span so it's a distinct node in the\n // trace; run-level events record against the run's own span context. Either way the active\n // span at record time carries the run's trace id, so all entries group into one trace.\n if (event.name != null && event.seq != null) {\n const step = tracer.startSpan(\n `step ${event.name}`,\n {\n attributes: {\n 'durable.run_id': event.runId,\n 'durable.step.seq': event.seq,\n 'durable.step.kind': event.kind,\n },\n },\n base,\n );\n if (event.type === 'step.failed') {\n const message = event.error?.message;\n step.setStatus(\n message !== undefined\n ? { code: SpanStatusCode.ERROR, message }\n : { code: SpanStatusCode.ERROR },\n );\n }\n context.with(trace.setSpan(base, step), record);\n step.end();\n } else {\n context.with(base, record);\n }\n });\n }\n\n /**\n * Detach the execution wrapper. The `Watcher` SPI has no teardown hook, so nothing calls this in a\n * running app — where the wrapper should live exactly as long as the process does. It exists so a\n * test that registers a watcher does not leave a wrapper behind in the process-level registry for\n * the next test to trip over.\n */\n unregister(): void {\n this.disposeScope?.();\n this.disposeScope = undefined;\n }\n\n private tags(event: EngineEvent): string[] {\n const tags = ['durable', event.type, `run:${event.runId}`];\n if (event.workflow) tags.push(`workflow:${event.workflow}`);\n if (event.kind) tags.push(`kind:${event.kind}`);\n if (event.type === 'run.failed') tags.push('failed');\n return tags;\n }\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport { randomBytes } from 'node:crypto';\nimport type { DurableExecution, DurableExecutionWrapper } from '@dudousxd/nestjs-durable-core';\nimport type { WatcherContext } from '@dudousxd/nestjs-telescope';\nimport { SpanStatusCode, context, trace } from '@opentelemetry/api';\nimport { runTraceContext, runTraceId } from './durable-run-trace';\n\n/** What the scope publishes to anything recording on this async path. */\nexport interface DurableScope {\n /** The run whose unit of work is executing. */\n readonly runId: string;\n /** The run's trace — see {@link runTraceId}. */\n readonly traceId: string;\n /** This scope's span within that trace. Stable for every entry recorded inside it. */\n readonly spanId: string;\n /**\n * Identity of the wrapper that opened this scope. Reuse is only ever offered to the wrapper that\n * owns the scope: two Telescope instances in one process (a torn-down test module whose wrapper\n * outlived it, an app that mounts two consoles) would otherwise have the first one's scope\n * silently suppress the second one's batch, and the second console would record everything\n * batchless while looking correctly wired.\n */\n readonly owner: object;\n}\n\n/**\n * The durable scope THIS package has open on the current async path, if any.\n *\n * Telescope's own batch context is not readable from a watcher (`WatcherContext` exposes\n * `runInBatch`/`beginBatch` but no `currentBatch()`), and we would not want to read it even if it\n * were: the question is not \"is any batch open\" — an HTTP request that happened to start a run\n * inline has one — but \"is the scope on this path the one we opened for this same run\".\n */\nconst activeScope = new AsyncLocalStorage<DurableScope>();\n\n/** The durable execution scope active on this async path, or undefined outside one. */\nexport function currentDurableScope(): DurableScope | undefined {\n return activeScope.getStore();\n}\n\n/** `BatchOrigin` has no durable/workflow member — see {@link durableExecutionScope} for the choice. */\nconst DURABLE_BATCH_ORIGIN = 'queue';\n\nfunction spanName(execution: DurableExecution): string {\n if (execution.unit === 'step') return `step ${execution.name ?? '?'}`;\n return `workflow ${execution.workflow ?? '?'}`;\n}\n\nfunction spanAttributes(execution: DurableExecution): Record<string, string | number> {\n const attributes: Record<string, string | number> = { 'durable.run_id': execution.runId };\n if (execution.workflow !== undefined) attributes['durable.workflow'] = execution.workflow;\n if (execution.name !== undefined) attributes['durable.step.name'] = execution.name;\n if (execution.seq !== undefined) attributes['durable.step.seq'] = execution.seq;\n if (execution.attempt !== undefined) attributes['durable.step.attempt'] = execution.attempt;\n return attributes;\n}\n\n/**\n * Builds the {@link DurableExecutionWrapper} that puts every workflow turn and every step handler\n * body inside a Telescope batch and an OTel span, so the queries, logs and exceptions those bodies\n * produce are correlated to the unit of durable work that caused them instead of landing traceless.\n *\n * ## Why a batch around execution and not around the event\n *\n * The watcher's engine-event subscription records what *happened*, after the fact. A batch opened\n * there would contain exactly one entry — the durable entry itself — and nothing the step actually\n * did, because by then the handler has returned and the exception it threw has long since been\n * recorded with no batch at all. Wrapping the execution is the only shape that puts the failing code\n * inside the scope.\n *\n * ## Nesting: reuse the batch when the same run already has one open on this path\n *\n * With an in-process transport (event-emitter, in-memory, the DB transport's inline path) a\n * dispatched step's handler starts synchronously inside the turn that dispatched it, and the result\n * it produces resumes the next turn on that same async path — so the whole run is one unbroken\n * causal chain. Opening a fresh batch at each hop would split that chain across many batch ids for\n * no gain: Telescope's entry model has no parent-batch pointer, so the split is not recoverable in\n * the UI — the step's queries would appear in a batch with nothing visible causing them.\n *\n * So the rule is the causal one, not the syntactic one: a unit reuses the batch when THIS wrapper\n * already has one open for THIS run on this async path. The consequence is worth stating plainly,\n * because it differs by transport and looks like an inconsistency if you meet it unprepared:\n *\n * - **in-process transport** — one batch for the whole run (every turn, every step), because it is\n * genuinely one chain;\n * - **queue-backed transport** (BullMQ, SQS, a thin worker) — one batch per turn and one per step,\n * because each really is a separate entry point, in a separate process, with no ambient scope to\n * inherit.\n *\n * What holds either way is the trace: every entry of a run carries {@link runTraceId}, so the run is\n * one trace whether it executed in one process or twenty. That invariant is the thing to rely on;\n * batch granularity follows the execution shape.\n *\n * A different run is a different chain and always gets its own batch. That includes a child\n * workflow: it has its own run id, its own timeline and its own dashboard row, and its trace is\n * anchored on ITS run id (see {@link runTraceContext}), so it reads as a related-but-distinct trace\n * instead of being folded into the parent's. The parent/child relationship is already carried by the\n * run graph; flattening the batches to restate it would erase the child's own boundaries and gain\n * nothing.\n *\n * ## Origin\n *\n * `BatchOrigin` in `@dudousxd/nestjs-telescope` is the closed union\n * `'http' | 'queue' | 'schedule' | 'cli' | 'manual'` — it has no durable/workflow member. Widening\n * it belongs to that repo and a coordinated release, so this uses `'queue'`, which is also the\n * honest description: durable work reaches an executor by being dispatched over a transport\n * (BullMQ, SQS, an event emitter), exactly like the jobs the BullMQ watcher marks `'queue'`.\n */\nexport function durableExecutionScope(ctx: WatcherContext): DurableExecutionWrapper {\n const tracer = trace.getTracer('@dudousxd/nestjs-durable-telescope');\n const owner = {};\n\n return (execution, next) => {\n // Already inside our own scope for this run — reuse it (see \"Nesting\" above).\n const open = activeScope.getStore();\n if (open?.owner === owner && open.runId === execution.runId) return next();\n // Telescope switched off: `runInBatch` would already short-circuit, but the span and the ALS\n // frame would not, so check here and hand the body straight back.\n if (ctx.config.enabled === false) return next();\n\n try {\n const parent = runTraceContext(execution.runId);\n const span = tracer.startSpan(\n spanName(execution),\n { attributes: spanAttributes(execution) },\n parent,\n );\n // Prefer the real span's id when an OTel SDK produced one; a host with no SDK gets a\n // non-recording span whose id is the run's synthetic parent, which would make every scope in\n // a run look like one span. Mint an id in that case so the scopes stay distinguishable.\n const spanContext = span.spanContext();\n const scope: DurableScope = {\n runId: execution.runId,\n traceId: runTraceId(execution.runId),\n spanId: span.isRecording() ? spanContext.spanId : randomBytes(8).toString('hex'),\n owner,\n };\n\n return context.with(trace.setSpan(parent, span), () =>\n activeScope.run(scope, () =>\n ctx.runInBatch(DURABLE_BATCH_ORIGIN, next).then(\n (value) => {\n span.end();\n return value;\n },\n (error: unknown) => {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n ...(error instanceof Error ? { message: error.message } : {}),\n });\n span.end();\n throw error; // never swallow the workflow's error\n },\n ),\n ),\n );\n } catch {\n // Opening the scope failed (a broken tracer, a Telescope in a bad state). Run the work anyway,\n // untraced. The BullMQ and schedule watchers hold the same line from the other direction —\n // they never let a recording failure fail a healthy job — and it matters more here, because\n // this wrapper sits around a workflow body whose failure is durably persisted.\n return next();\n }\n };\n}\n\n/** Minimal structural view of Telescope's `TraceContextProvider` (it is one method, and typing\n * against the interface would drag a value import in for nothing). */\ninterface TraceContextLike {\n current(): { traceId: string; spanId: string } | null;\n}\n\n/**\n * A Telescope `traceContext` provider that reports the active durable scope, so entries recorded by\n * OTHER watchers (queries, cache, outbound HTTP) during a workflow turn or a step body carry the\n * run's trace id.\n *\n * Wire it as `TelescopeModule.forRoot({ traceContext: durableTraceContext() })`.\n *\n * This exists because Telescope resolves an entry's trace id from an ambient OTel span, and\n * `@opentelemetry/api` on its own propagates nothing: without a registered context manager\n * `context.with(...)` is a no-op and `getActiveSpan()` is always undefined. An app that runs a full\n * OTel SDK needs none of this — it should pass `OtelTraceContextProvider` (optionally as the\n * `delegate` below, which is then consulted first so the app's real spans win), and the scope's\n * spans hang off the run's trace anyway. An app that runs no OTel at all — the common case — would\n * otherwise get correlated batches and no trace ids, which is half the feature.\n */\nexport function durableTraceContext(delegate?: TraceContextLike): TraceContextLike {\n return {\n current() {\n // The host's own tracing wins where it has an answer: inside a durable scope it will report a\n // span from this run's trace anyway, and outside one it is the only source there is.\n const fromDelegate = delegate?.current() ?? null;\n if (fromDelegate) return fromDelegate;\n const scope = activeScope.getStore();\n return scope ? { traceId: scope.traceId, spanId: scope.spanId } : null;\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport { type Context, TraceFlags, context, trace } from '@opentelemetry/api';\n\n/**\n * The trace a run's spans hang under, DERIVED FROM THE RUN ID rather than held as a live root span.\n *\n * The obvious implementation — open a root span on `run.started`, keep it in a map, end it when the\n * run settles — cannot work for a durable workflow. A run suspends: `ctx.step` dispatches and the\n * turn unwinds, and the step body then executes minutes (or days) later, on a different turn, very\n * often in a different process. A live root span would either have to be ended at every suspension —\n * fragmenting one run into a trace per turn, so the step that failed is in a different trace from\n * the turn that dispatched it — or held open across the suspension, which pins a span object for the\n * lifetime of a workflow that is explicitly designed to wait for days.\n *\n * Hashing the run id sidesteps both. The trace id is a pure function of the run, so every turn and\n * every step of a run lands in ONE trace with no state held anywhere, and a worker in another\n * process computes the same id from the same run id — which is what makes a remotely-executed step\n * correlate with the run that dispatched it at all.\n *\n * The parent span context is marked `isRemote` because that is what it is: a reference to a trace\n * whose \"root\" is the run itself, not a span this process ever recorded.\n */\nexport function runTraceId(runId: string): string {\n // 16 bytes of hex, as the W3C trace-context spec requires. sha256 is not used as a security\n // primitive here — only as a well-distributed map from an arbitrary run id into that space.\n return createHash('sha256').update(`durable:run:${runId}`).digest('hex').slice(0, 32);\n}\n\n/** The synthetic parent span id for a run's trace — derived the same way, from a different prefix. */\nfunction runSpanId(runId: string): string {\n return createHash('sha256').update(`durable:run-span:${runId}`).digest('hex').slice(0, 16);\n}\n\n/**\n * An OTel {@link Context} whose active span context is the run's synthetic root. Spans started\n * against it are children of the run, and Telescope's trace-context provider stamps every entry\n * recorded inside it with {@link runTraceId}.\n *\n * `TraceFlags.SAMPLED` is set deliberately: a non-sampled parent leaves the derived span\n * non-recording, and its span context would then never reach an exporter — the run would be\n * traceable in Telescope but invisible in any OTel backend, which is the confusing half-state.\n */\nexport function runTraceContext(runId: string, parent: Context = context.active()): Context {\n return trace.setSpanContext(parent, {\n traceId: runTraceId(runId),\n spanId: runSpanId(runId),\n traceFlags: TraceFlags.SAMPLED,\n isRemote: true,\n });\n}\n","import { defineTelescopeExtension } from '@dudousxd/nestjs-telescope';\nimport { durableDashboard } from './durable-dashboard.spec-data';\nimport {\n durableDurationProvider,\n durableRecentFailuresProvider,\n durableRunsOverTimeProvider,\n durableStateBreakdownProvider,\n durableStateProvider,\n durableSuccessRateProvider,\n durableThroughputProvider,\n durableTimeseriesProvider,\n durableWorkerHealthProvider,\n durableWorkerStatusProvider,\n} from './durable-data-providers';\nimport { DurableTelescopeWatcher } from './durable-telescope.watcher';\n\n/** The first-class Telescope extension for nestjs-durable: watcher + Workflows dashboard. */\nexport function durableTelescopeExtension(\n opts: { runHref?: string; recentFailuresWindowMs?: number } = {},\n) {\n return defineTelescopeExtension({\n name: 'durable',\n watchers: () => [new DurableTelescopeWatcher()],\n entryTypes: () => [{ id: 'durable', label: 'Workflows', dot: 'bg-amber-400' }],\n dashboards: () => [durableDashboard(opts)],\n dataProviders: () => [\n durableStateProvider(),\n durableTimeseriesProvider(),\n durableRecentFailuresProvider(),\n durableWorkerHealthProvider(),\n durableWorkerStatusProvider(),\n durableDurationProvider(),\n durableRunsOverTimeProvider(),\n durableSuccessRateProvider(),\n durableThroughputProvider(),\n durableStateBreakdownProvider(),\n ],\n });\n}\n","import type { DashboardSpec } from '@dudousxd/nestjs-telescope';\n\n/**\n * The \"Workflows\" health dashboard — golden-signals sections layout.\n *\n * `runHref` is a URL template for deep-linking a run out to the durable dashboard\n * (e.g. '/durable/runs/{runId}'). `recentFailuresWindowMs` bounds the \"Stuck runs\"\n * table (default 24h) so a healthy system shows an empty table instead of\n * days-old failures; pass `0` to show all.\n */\nexport function durableDashboard(\n opts: { runHref?: string; recentFailuresWindowMs?: number } = {},\n): DashboardSpec {\n const runHref = opts.runHref ?? '/durable/runs/{runId}';\n const windowMs = opts.recentFailuresWindowMs ?? 24 * 60 * 60 * 1000;\n return {\n id: 'durable.workflows',\n label: 'Workflows',\n panels: [],\n sections: [\n {\n title: 'Health',\n cols: 4,\n panels: [\n {\n kind: 'gauge',\n title: 'Success rate',\n data: { provider: 'durable.successRate' },\n max: 1,\n format: 'percent',\n thresholds: { warn: 0.98, bad: 0.95, direction: 'down-bad' },\n },\n {\n kind: 'stat',\n title: 'Duration p95',\n data: { provider: 'durable.duration', query: { metric: 'p95' } },\n format: 'duration',\n spark: false,\n thresholds: { warn: 2000, bad: 5000, direction: 'up-bad' },\n },\n {\n kind: 'stat',\n title: 'Backlog',\n data: { provider: 'durable.state', query: { status: 'pending' } },\n spark: false,\n thresholds: { warn: 50, bad: 200, direction: 'up-bad' },\n },\n {\n kind: 'stat',\n title: 'Throughput',\n data: { provider: 'durable.throughput' },\n format: 'rate',\n spark: true,\n },\n ],\n },\n {\n title: 'Needs attention',\n cols: 3,\n panels: [\n {\n kind: 'topN',\n title: 'Top failing workflows',\n data: { provider: 'durable.timeseries', query: { metric: 'topFailures' } },\n limit: 8,\n },\n {\n kind: 'table',\n title: 'Stuck runs',\n data: { provider: 'durable.recentFailures', query: { windowMs } },\n columns: [\n { key: 'updatedAt', label: 'Updated' },\n { key: 'workflow', label: 'Workflow' },\n { key: 'runId', label: 'Run', link: { href: runHref } },\n { key: 'error', label: 'Error' },\n ],\n },\n {\n kind: 'table',\n // Lists ALL worker groups (starved ones sorted first); the Status column\n // flags STARVED (queued > 0 with zero live workers) vs ok. Titling it\n // \"Starved worker groups\" read as if every listed group were starved.\n title: 'Worker health',\n data: { provider: 'durable.workerHealth' },\n columns: [\n { key: 'group', label: 'Group' },\n { key: 'queued', label: 'Queued' },\n { key: 'liveWorkers', label: 'Workers' },\n { key: 'status', label: 'Status' },\n ],\n },\n ],\n },\n {\n title: 'Workers',\n // One column, because this section holds exactly one panel and that panel is the widest\n // table on the dashboard. A section renders as a fixed `grid-cols-N` with no `colSpan`, so\n // `cols: 2` gave the widest table on the dashboard half the viewport and left the cell\n // beside it empty — it scrolled sideways inside its own card while a hole sat next to it.\n cols: 1,\n panels: [\n {\n kind: 'table',\n // One row per LIVE worker (flattened from each group's heartbeats), exposing the live\n // WorkerStatus the heartbeat carries: fixed vs. adaptive limit, in-flight saturation,\n // RAM%/CPU%, throughput, p95, and why the adaptive controller last moved. A worker from\n // an older SDK with no status still lists, reading 'n/a' — distinct from the '—' of a\n // worker that reports fine but has nothing to measure yet, which is what an idle\n // deployment looks like (throughput and p95 come off a rolling window of completions,\n // so they are empty until a step finishes inside it). The\n // adaptive min–max range rides the In-flight column's row value (`minMax` key) for those\n // that want it; the visible columns stay readable.\n title: 'Workers',\n data: { provider: 'durable.workerStatus' },\n columns: [\n { key: 'group', label: 'Group' },\n { key: 'instanceId', label: 'Worker' },\n { key: 'mode', label: 'Mode' },\n // No separate `limit` column: In-flight renders `<inFlight>/<limit>`, so a Limit\n // column beside it repeats its own denominator in every row.\n { key: 'saturation', label: 'In-flight' },\n { key: 'queued', label: 'Queued' },\n { key: 'rssPct', label: 'RAM %' },\n { key: 'cpuPct', label: 'CPU %' },\n { key: 'throughputPerMin', label: 'Thrpt/min' },\n { key: 'p95Ms', label: 'p95 ms' },\n { key: 'lastAdjust', label: 'Last adjust' },\n ],\n },\n ],\n },\n {\n title: 'Trends',\n cols: 3,\n panels: [\n {\n kind: 'timeseries',\n title: 'Runs over time',\n data: { provider: 'durable.runsOverTime' },\n series: ['done', 'failed'],\n style: 'stacked',\n },\n {\n kind: 'distribution',\n title: 'Duration distribution',\n data: { provider: 'durable.duration' },\n markers: ['p50', 'p95', 'p99'],\n format: 'duration',\n },\n {\n kind: 'breakdown',\n title: 'Runs by state',\n data: { provider: 'durable.stateBreakdown' },\n style: 'donut',\n },\n ],\n },\n ],\n };\n}\n","import { STATE_STORE_CANONICAL, WorkflowEngine } from '@dudousxd/nestjs-durable-core';\nimport type { RunStatus, StateStore, WorkerStatus } from '@dudousxd/nestjs-durable-core';\nimport type { DataProvider, ExtensionContext } from '@dudousxd/nestjs-telescope';\nimport { TELESCOPE_STORAGE } from '@dudousxd/nestjs-telescope';\n\n// ─── Shared helpers ───────────────────────────────────────────────────────────\n\n/** Compute the p-th percentile of a SORTED array. */\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[Math.max(0, idx)] ?? 0;\n}\n\ntype StorageEntry = { content?: unknown; createdAt?: Date };\n\n/** Fetch the durable entry page from TELESCOPE_STORAGE. */\nasync function fetchEntries(ctx: ExtensionContext): Promise<StorageEntry[]> {\n const storage = ctx.moduleRef.get(TELESCOPE_STORAGE, { strict: false }) as {\n get(q: { type?: string; limit?: number }): Promise<{ data: StorageEntry[] }>;\n };\n const page = await storage.get({ type: 'durable', limit: 5_000 });\n return page.data;\n}\n\n/**\n * Split entries into current/previous equal-length windows.\n * current: (now - windowMs, now]\n * previous: (now - 2*windowMs, now - windowMs]\n */\nfunction splitWindows(\n entries: StorageEntry[],\n windowMs: number,\n now: number,\n): { current: StorageEntry[]; previous: StorageEntry[] } {\n const start = now - windowMs;\n const prevStart = start - windowMs;\n return {\n current: entries.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > start && t <= now;\n }),\n previous: entries.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > prevStart && t <= start;\n }),\n };\n}\n\n/** Compute success rate (completed / total) for a list of entries, returns 1 when no data. */\nfunction successRateOf(entries: StorageEntry[]): number {\n let completed = 0;\n let failed = 0;\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string };\n if (c.event === 'run.completed') completed += 1;\n else if (c.event === 'run.failed') failed += 1;\n }\n const total = completed + failed;\n return total === 0 ? 1 : completed / total;\n}\n\n/** Run-level lifecycle events — each can legitimately occur at most once per runId. */\nconst RUN_LIFECYCLE_EVENTS = new Set([\n 'run.started',\n 'run.completed',\n 'run.failed',\n 'run.suspended',\n]);\n\n/**\n * Deduplicate run-level lifecycle entries by `${event}:${runId}`, keeping the FIRST occurrence.\n *\n * The durable engine emits each run lifecycle event on every pod (1 worker + N api pods) and the\n * watcher records on each, so a single `run.completed` for a given runId is captured multiple times\n * with identical content but distinct entry ids / createdAt (±ms) — inflating completed/failed\n * counts. A given runId can legitimately have at most ONE `run.started` and ONE terminal\n * `run.completed`/`run.failed`/`run.suspended`, so collapsing by `${event}:${runId}` is provably\n * safe. Step events and any entry without a run-level event + runId pass through untouched — a step\n * can legitimately repeat across attempts, and the watcher's step content carries no stepId/attempt\n * to key on.\n */\nfunction dedupeRunEvents(entries: StorageEntry[]): StorageEntry[] {\n const seen = new Set<string>();\n const out: StorageEntry[] = [];\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string };\n if (c.event && c.runId && RUN_LIFECYCLE_EVENTS.has(c.event)) {\n const key = `${c.event}:${c.runId}`;\n if (seen.has(key)) continue;\n seen.add(key);\n }\n out.push(e);\n }\n return out;\n}\n\nconst STATE_CAP = 10_000;\n\n// ─── Existing providers ───────────────────────────────────────────────────────\n\n/** Source C: current-state gauge from the durable store. query.status selects which (default 'dead'). */\nexport function durableStateProvider(): DataProvider {\n return {\n name: 'durable.state',\n async resolve(query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const status = (query?.status as RunStatus) ?? 'dead';\n const runs = await store.listRuns({ status, limit: STATE_CAP });\n return { value: runs.length };\n },\n };\n}\n\n/**\n * Source A: rollups from captured `durable` entries in Telescope's own storage.\n * Reads recent run.* lifecycle entries and aggregates. Bounded by Telescope's prune\n * window (by design — this is the \"history\" series, not the source of truth).\n */\nexport function durableTimeseriesProvider(): DataProvider {\n return {\n name: 'durable.timeseries',\n async resolve(query, ctx: ExtensionContext) {\n // TELESCOPE_STORAGE.get(query) → { data: Entry[]; nextCursor }. Entry = { content, createdAt, ... }.\n const storage = ctx.moduleRef.get(TELESCOPE_STORAGE, { strict: false }) as {\n get(q: { type?: string; limit?: number }): Promise<{\n data: Array<{ content?: unknown; createdAt?: Date }>;\n }>;\n };\n const limit = Math.min(5_000, Math.max(100, Number(query?.limit ?? 2_000)));\n const page = await storage.get({ type: 'durable', limit });\n\n let completed = 0;\n let failed = 0;\n const failByWorkflow = new Map<string, number>();\n // Dedup triplicated lifecycle events (one event recorded on every pod) before counting.\n for (const e of dedupeRunEvents(page.data)) {\n const c = (e.content ?? {}) as { event?: string; workflow?: string };\n if (c.event === 'run.completed') completed += 1;\n else if (c.event === 'run.failed') {\n failed += 1;\n const wf = c.workflow ?? 'unknown';\n failByWorkflow.set(wf, (failByWorkflow.get(wf) ?? 0) + 1);\n }\n }\n const total = completed + failed;\n const metric = (query?.metric as string) ?? 'successRate';\n if (metric === 'successRate') return { value: total === 0 ? 1 : completed / total };\n if (metric === 'failed') return { value: failed };\n if (metric === 'total') return { value: total };\n if (metric === 'topFailures') {\n const items = [...failByWorkflow.entries()]\n .map(([label, value]) => ({ label, value }))\n .sort((a, b) => b.value - a.value);\n return { items };\n }\n return { value: total };\n },\n };\n}\n\n/**\n * Source C: recent failed + dead runs as table rows (newest first), time-bounded.\n * Only failures updated within `query.windowMs` (default 24h) are returned, so a\n * healthy system shows an EMPTY table instead of surfacing days-old failures as if\n * they were a live incident. `windowMs: 0` disables the window (return all). Each\n * row carries a compact `updatedAt` stamp so recency is visible in the table.\n */\nexport function durableRecentFailuresProvider(): DataProvider {\n return {\n name: 'durable.recentFailures',\n async resolve(query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50)));\n const windowMs = query?.windowMs === undefined ? 24 * 60 * 60 * 1000 : Number(query.windowMs);\n const cutoff = windowMs > 0 ? Date.now() - windowMs : 0;\n const [failed, dead] = await Promise.all([\n store.listRuns({ status: 'failed', limit }),\n store.listRuns({ status: 'dead', limit }),\n ]);\n const rows = [...failed, ...dead]\n .filter((r) => +new Date(r.updatedAt) >= cutoff)\n .sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt))\n .slice(0, limit)\n .map((r) => ({\n // Compact UTC stamp (\"YYYY-MM-DD HH:mm Z\"); the generic table renderer\n // prints column values as-is, so we format here.\n updatedAt: `${new Date(r.updatedAt).toISOString().replace('T', ' ').slice(0, 16)}Z`,\n workflow: r.workflow,\n runId: r.id,\n error: r.error?.message ?? '',\n }));\n return { rows };\n },\n };\n}\n\n/**\n * Source D: per-group worker health from the engine (`WorkflowEngine.workerHealth()` — queue depth\n * vs. live worker heartbeats). `query.metric: 'starvedCount'` returns the number of groups with work\n * queued and zero live workers (the \"alive but not consuming\" alert state) as a stat; otherwise a\n * table, starved groups sorted first. Empty when the transport can't report health (in-process).\n */\nexport function durableWorkerHealthProvider(): DataProvider {\n return {\n name: 'durable.workerHealth',\n async resolve(query, ctx: ExtensionContext) {\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false }) as WorkflowEngine;\n const health = await engine.workerHealth();\n const isStarved = (g: { depth: number; liveWorkers: unknown[] }) =>\n g.depth > 0 && g.liveWorkers.length === 0;\n if ((query?.metric as string) === 'starvedCount') {\n return { value: health.filter(isStarved).length };\n }\n const rows = health\n .slice()\n // Starved groups first, then deepest backlog — the rows that need attention rise to the top.\n .sort((a, b) => Number(isStarved(b)) - Number(isStarved(a)) || b.depth - a.depth)\n .map((g) => ({\n group: g.group,\n queued: g.depth,\n liveWorkers: g.liveWorkers.length,\n status: isStarved(g) ? 'STARVED' : 'ok',\n }));\n return { rows };\n },\n };\n}\n\n/** Compact \"Nm ago\" relative stamp for an epoch-ms instant (worker last-adjust recency). */\nfunction relAgo(atMs: number): string {\n const s = Math.max(0, Math.round((Date.now() - atMs) / 1000));\n if (s < 60) return `${s}s ago`;\n const m = Math.round(s / 60);\n if (m < 60) return `${m}m ago`;\n const h = Math.round(m / 60);\n if (h < 24) return `${h}h ago`;\n return `${Math.round(h / 24)}d ago`;\n}\n\n/**\n * What a cell says when it has no number, and the two cases it has to keep apart.\n *\n * A blank cell used to mean both \"this worker's SDK is too old to report anything\" and \"this worker\n * reports fine, it just has nothing to measure yet\" — and the second is the common one on an idle\n * deployment, where the adaptive controller's rolling window is empty because no step has completed\n * inside it. Reading one as the other sends someone hunting for a broken worker that is merely idle,\n * or ignoring a fleet that has genuinely stopped reporting.\n *\n * `n/a` is the heartbeat carrying no {@link WorkerStatus} at all; `—` is a status that carries no\n * value for this field yet.\n */\nconst NOT_REPORTED = 'n/a';\nconst NOT_YET_MEASURED = '—';\n\n/**\n * Render a worker's last limit change as \"shrink 8→5 2m ago\".\n *\n * `—` here means the adaptive controller has not moved the limit yet — the normal state of a worker\n * that has been sitting at its floor. A fixed-mode worker never adjusts at all, which is what its\n * `Mode` column already says.\n */\nfunction fmtLastAdjust(status: WorkerStatus | undefined): string {\n if (!status) return NOT_REPORTED;\n const a = status.lastAdjust;\n if (!a) return NOT_YET_MEASURED;\n return `${a.reason} ${a.from}→${a.to} ${relAgo(a.at)}`;\n}\n\n/** Round a 0..100(+) percent to a whole number for table display. */\nfunction fmtPct(n: number | undefined, status: WorkerStatus | undefined): number | string {\n if (typeof n === 'number') return Math.round(n);\n return status ? NOT_YET_MEASURED : NOT_REPORTED;\n}\n\n/** Round a rate/latency to a whole number for table display. */\nfunction fmtNum(n: number | undefined, status: WorkerStatus | undefined): number | string {\n if (typeof n === 'number') return Math.round(n);\n return status ? NOT_YET_MEASURED : NOT_REPORTED;\n}\n\n/**\n * Source D (per-worker): flatten `WorkflowEngine.workerHealth()` to ONE row per live worker, exposing\n * the live {@link WorkerStatus} each heartbeat now carries (mode/limit/min-max, in-flight saturation,\n * RAM%/CPU%, throughput, p95, last adjust). A worker from an older SDK with no `status` still renders\n * — its measured fields fall back to '—', so a mixed fleet stays visible. Rows sort backlog-bearing\n * groups first, then by group, then instanceId. Empty when the transport can't report health.\n */\nexport function durableWorkerStatusProvider(): DataProvider {\n return {\n name: 'durable.workerStatus',\n async resolve(_query, ctx: ExtensionContext) {\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false }) as WorkflowEngine;\n const health = await engine.workerHealth();\n const rows = health\n .slice()\n // Groups with a backlog first (the workers worth scrutinising), then by group name.\n .sort((a, b) => Number(b.depth > 0) - Number(a.depth > 0) || a.group.localeCompare(b.group))\n .flatMap((group) =>\n group.liveWorkers\n .slice()\n .sort((a, b) => a.instanceId.localeCompare(b.instanceId))\n .map((worker) => {\n const status = worker.status;\n const concurrency = status?.concurrency;\n // Mode/limit/in-flight are not measurements — a status either declares them or the\n // heartbeat carried no status at all, so their absence is always `n/a`.\n const mode = concurrency?.mode ?? NOT_REPORTED;\n const limit = concurrency?.limit ?? NOT_REPORTED;\n const inFlight = status?.inFlight ?? NOT_REPORTED;\n const minMax =\n concurrency?.mode === 'adaptive'\n ? `${concurrency.min ?? 1}–${concurrency.max ?? 32}`\n : NOT_REPORTED;\n const saturation =\n concurrency && status ? `${status.inFlight}/${concurrency.limit}` : NOT_REPORTED;\n return {\n group: group.group,\n instanceId: worker.instanceId,\n mode,\n limit,\n minMax,\n inFlight,\n saturation,\n queued: group.depth,\n rssPct: fmtPct(status?.rssPct, status),\n cpuPct: fmtPct(status?.cpuPct, status),\n throughputPerMin: fmtNum(status?.throughputPerMin, status),\n p95Ms: fmtNum(status?.p95Ms, status),\n lastAdjust: fmtLastAdjust(status),\n };\n }),\n );\n return { rows };\n },\n };\n}\n\n// ─── New golden-signals providers ────────────────────────────────────────────\n\n/**\n * Duration percentiles (p50/p95/p99) + a histogram of ~8 buckets.\n * Durations are read from `content.durationMs` on `run.completed`/`run.failed`; when absent,\n * computed by pairing a `run.started` and its corresponding `run.completed`/`run.failed` entry\n * by `runId` using their `createdAt` timestamps.\n *\n * With `query.metric === 'p50'|'p95'|'p99'` returns `{ value }` for use as a stat panel.\n */\nexport function durableDurationProvider(): DataProvider {\n return {\n name: 'durable.duration',\n async resolve(query, ctx: ExtensionContext) {\n // Dedup triplicated lifecycle events so paired durations aren't double-counted.\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n\n // Index run.started entries by runId for the pairing fallback.\n const startedAt = new Map<string, number>();\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string };\n if (c.event === 'run.started' && c.runId && e.createdAt) {\n startedAt.set(c.runId, +new Date(e.createdAt));\n }\n }\n\n const durs: number[] = [];\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string; durationMs?: number };\n if (c.event === 'run.completed' || c.event === 'run.failed') {\n if (typeof c.durationMs === 'number') {\n durs.push(c.durationMs);\n } else if (c.runId) {\n const start = startedAt.get(c.runId);\n const end = e.createdAt ? +new Date(e.createdAt) : undefined;\n if (start !== undefined && end !== undefined && end >= start) {\n durs.push(end - start);\n }\n }\n }\n }\n durs.sort((a, b) => a - b);\n\n const p50 = percentile(durs, 50);\n const p95 = percentile(durs, 95);\n const p99 = percentile(durs, 99);\n\n // Stat shortcut: return a single value for use in stat panels.\n const metric = (query as Record<string, unknown>)?.metric as string | undefined;\n if (metric === 'p50') return { value: p50 };\n if (metric === 'p95') return { value: p95 };\n if (metric === 'p99') return { value: p99 };\n\n // Build ~8 equal-width buckets between 0 and max.\n const max = durs.at(-1) ?? 0;\n const bucketCount = 8;\n const size = Math.max(1, Math.ceil((max + 1) / bucketCount));\n const buckets = Array.from({ length: bucketCount }, (_, i) => ({\n label: `${Math.round((i * size) / 100) / 10}s`,\n count: 0,\n }));\n for (const d of durs) {\n const bi = Math.min(bucketCount - 1, Math.floor(d / size));\n const bucket = buckets[bi];\n if (bucket) bucket.count += 1;\n }\n\n return { buckets, p50, p95, p99 };\n },\n };\n}\n\n/**\n * Buckets `run.completed` (-> done) and `run.failed` (-> failed) entries by `createdAt` into\n * N=`query.buckets ?? 24` equal-width time buckets. Returns `{ rows: Array<{ label; done; failed }> }`.\n */\nexport function durableRunsOverTimeProvider(): DataProvider {\n return {\n name: 'durable.runsOverTime',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const n = Number((query as Record<string, unknown>)?.buckets ?? 24);\n const now = Date.now();\n\n // Determine the time window from the oldest entry or default to 24h.\n let minT = now;\n for (const e of entries) {\n if (e.createdAt) {\n const t = +new Date(e.createdAt);\n if (t < minT) minT = t;\n }\n }\n const span = Math.max(now - minT, 1);\n const bucketSize = span / n;\n\n const rows: Array<{ label: string; done: number; failed: number }> = Array.from(\n { length: n },\n (_, i) => {\n const bucketStart = new Date(minT + i * bucketSize);\n const label = bucketStart.toISOString().slice(11, 16); // \"HH:mm\"\n return { label, done: 0, failed: 0 };\n },\n );\n\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string };\n if (c.event !== 'run.completed' && c.event !== 'run.failed') continue;\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n const idx = Math.min(n - 1, Math.floor((t - minT) / bucketSize));\n const row = rows[idx];\n if (row) {\n if (c.event === 'run.completed') row.done += 1;\n else row.failed += 1;\n }\n }\n\n return { rows };\n },\n };\n}\n\n/**\n * Success rate over the last `query.windowMs` (default 24h), with a `delta` vs. the previous\n * equal-length window and a `spark` array of per-sub-bucket success rates.\n */\nexport function durableSuccessRateProvider(): DataProvider {\n return {\n name: 'durable.successRate',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const windowMs = Number((query as Record<string, unknown>)?.windowMs ?? 24 * 60 * 60 * 1000);\n const now = Date.now();\n const { current, previous } = splitWindows(entries, windowMs, now);\n\n const value = successRateOf(current);\n const prevRate = successRateOf(previous);\n const delta = previous.length > 0 ? value - prevRate : undefined;\n\n // Spark: split the current window into 8 sub-buckets.\n const sparkBuckets = 8;\n const bucketSize = windowMs / sparkBuckets;\n const sparkStart = now - windowMs;\n const spark = Array.from({ length: sparkBuckets }, (_, i) => {\n const bStart = sparkStart + i * bucketSize;\n const bEnd = bStart + bucketSize;\n const bEntries = current.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > bStart && t <= bEnd;\n });\n return successRateOf(bEntries);\n });\n\n return { value, delta, spark };\n },\n };\n}\n\n/**\n * Completed runs per hour over the last `query.windowMs` (default 24h), with a `delta` vs. the\n * previous equal-length window and a `spark` array of per-sub-bucket throughput values.\n */\nexport function durableThroughputProvider(): DataProvider {\n return {\n name: 'durable.throughput',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const windowMs = Number((query as Record<string, unknown>)?.windowMs ?? 24 * 60 * 60 * 1000);\n const now = Date.now();\n const { current, previous } = splitWindows(entries, windowMs, now);\n\n const countCompleted = (es: StorageEntry[]) =>\n es.filter((e) => ((e.content ?? {}) as { event?: string }).event === 'run.completed')\n .length;\n\n const windowHours = windowMs / (60 * 60 * 1000);\n const value = countCompleted(current) / windowHours;\n const prevValue = countCompleted(previous) / windowHours;\n const delta = previous.length > 0 ? value - prevValue : undefined;\n\n // Spark: 8 sub-buckets of current window.\n const sparkBuckets = 8;\n const bucketSize = windowMs / sparkBuckets;\n const bucketHours = bucketSize / (60 * 60 * 1000);\n const sparkStart = now - windowMs;\n const spark = Array.from({ length: sparkBuckets }, (_, i) => {\n const bStart = sparkStart + i * bucketSize;\n const bEnd = bStart + bucketSize;\n const bEntries = current.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > bStart && t <= bEnd;\n });\n return countCompleted(bEntries) / bucketHours;\n });\n\n return { value, delta, spark };\n },\n };\n}\n\n// Palette aligned index-for-index with STATE_BREAKDOWN_STATUSES so each status reads with the\n// semantically-correct color: running=cyan, pending=amber, cancelling=orange, completed=green,\n// failed=red, dead=purple.\nconst STATE_BREAKDOWN_STATUSES: RunStatus[] = [\n 'running',\n 'pending',\n 'cancelling',\n 'completed',\n 'failed',\n 'dead',\n];\n/** Color palette for the state breakdown pie segments (aligned to STATE_BREAKDOWN_STATUSES). */\nconst STATE_BREAKDOWN_PALETTE = [\n '#38bdf8', // running → cyan/blue\n '#fbbf24', // pending → amber\n '#f59e0b', // cancelling → orange\n '#34d399', // completed → green\n '#f87171', // failed → red\n '#a78bfa', // dead → purple\n];\n\n/**\n * Counts from `STATE_STORE.listRuns({ status })` for each of the statuses and returns pie\n * segments with the standard palette: `{ segments: Array<{ label, value, color }> }`.\n */\nexport function durableStateBreakdownProvider(): DataProvider {\n return {\n name: 'durable.stateBreakdown',\n async resolve(_query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const counts = await Promise.all(\n STATE_BREAKDOWN_STATUSES.map((status) =>\n store.listRuns({ status, limit: STATE_CAP }).then((runs) => runs.length),\n ),\n );\n const segments = STATE_BREAKDOWN_STATUSES.map((label, i) => ({\n label,\n value: counts[i],\n color: STATE_BREAKDOWN_PALETTE[i],\n }));\n return { segments };\n },\n };\n}\n\n// NOTE: durableRetryHotspotsProvider is NOT implemented.\n// StateStore only exposes `listCheckpoints(runId: string)` (per-run lookup), with no cross-run\n// query by step `attempts`. There is no API to enumerate all step checkpoints across runs without\n// first listing every run — which would be O(N*M) and impractical. Task 13 should omit that panel.\n"],"mappings":"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","names":["WorkflowEngine","useDurableExecution","SpanStatusCode","context","trace","AsyncLocalStorage","randomBytes","SpanStatusCode","context","trace","createHash","TraceFlags","context","trace","runTraceId","runId","createHash","update","digest","slice","runSpanId","runTraceContext","parent","context","active","trace","setSpanContext","traceId","spanId","traceFlags","TraceFlags","SAMPLED","isRemote","activeScope","AsyncLocalStorage","currentDurableScope","getStore","DURABLE_BATCH_ORIGIN","spanName","execution","unit","name","workflow","spanAttributes","attributes","runId","undefined","seq","attempt","durableExecutionScope","ctx","tracer","trace","getTracer","owner","next","open","config","enabled","parent","runTraceContext","span","startSpan","spanContext","scope","traceId","runTraceId","spanId","isRecording","randomBytes","toString","context","with","setSpan","run","runInBatch","then","value","end","error","setStatus","code","SpanStatusCode","ERROR","Error","message","durableTraceContext","delegate","current","fromDelegate","DurableTelescopeWatcher","type","disposeScope","register","ctx","useDurableExecution","durableExecutionScope","engine","moduleRef","get","WorkflowEngine","strict","subscribe","tracer","trace","getTracer","event","record","traceId","runTraceId","runId","content","workflow","seq","name","kind","output","error","durationMs","tags","base","runTraceContext","step","startSpan","attributes","message","setStatus","undefined","code","SpanStatusCode","ERROR","context","with","setSpan","end","unregister","push","defineTelescopeExtension","durableDashboard","opts","runHref","windowMs","recentFailuresWindowMs","id","label","panels","sections","title","cols","kind","data","provider","max","format","thresholds","warn","bad","direction","query","metric","spark","status","limit","columns","key","link","href","series","style","markers","STATE_STORE_CANONICAL","WorkflowEngine","TELESCOPE_STORAGE","percentile","sorted","p","length","idx","Math","min","ceil","max","fetchEntries","ctx","storage","moduleRef","get","TELESCOPE_STORAGE","strict","page","type","limit","data","splitWindows","entries","windowMs","now","start","prevStart","current","filter","e","t","createdAt","Date","previous","successRateOf","completed","failed","c","content","event","total","RUN_LIFECYCLE_EVENTS","Set","dedupeRunEvents","seen","out","runId","has","key","add","push","STATE_CAP","durableStateProvider","name","resolve","query","store","STATE_STORE_CANONICAL","status","runs","listRuns","value","durableTimeseriesProvider","Number","failByWorkflow","Map","wf","workflow","set","metric","items","map","label","sort","a","b","durableRecentFailuresProvider","undefined","cutoff","dead","Promise","all","rows","r","updatedAt","slice","toISOString","replace","id","error","message","durableWorkerHealthProvider","engine","WorkflowEngine","health","workerHealth","isStarved","g","depth","liveWorkers","group","queued","relAgo","atMs","s","round","m","h","NOT_REPORTED","NOT_YET_MEASURED","fmtLastAdjust","lastAdjust","reason","from","to","at","fmtPct","n","fmtNum","durableWorkerStatusProvider","_query","localeCompare","flatMap","instanceId","worker","concurrency","mode","inFlight","minMax","saturation","rssPct","cpuPct","throughputPerMin","p95Ms","durableDurationProvider","startedAt","durs","durationMs","end","p50","p95","p99","bucketCount","size","buckets","Array","_","i","count","d","bi","floor","bucket","durableRunsOverTimeProvider","minT","span","bucketSize","bucketStart","done","row","durableSuccessRateProvider","prevRate","delta","sparkBuckets","sparkStart","spark","bStart","bEnd","bEntries","durableThroughputProvider","countCompleted","es","windowHours","prevValue","bucketHours","STATE_BREAKDOWN_STATUSES","STATE_BREAKDOWN_PALETTE","durableStateBreakdownProvider","counts","then","segments","color","durableTelescopeExtension","opts","defineTelescopeExtension","name","watchers","DurableTelescopeWatcher","entryTypes","id","label","dot","dashboards","durableDashboard","dataProviders","durableStateProvider","durableTimeseriesProvider","durableRecentFailuresProvider","durableWorkerHealthProvider","durableWorkerStatusProvider","durableDurationProvider","durableRunsOverTimeProvider","durableSuccessRateProvider","durableThroughputProvider","durableStateBreakdownProvider"]}
1
+ {"version":3,"sources":["../src/durable-telescope.watcher.ts","../src/durable-execution-scope.ts","../src/durable-run-trace.ts","../src/durable-telescope.extension.ts","../src/durable-dashboard.spec-data.ts","../src/durable-data-providers.ts"],"sourcesContent":["import {\n type EngineEvent,\n WorkflowEngine,\n parentRunIdOf,\n useDurableExecution,\n} from '@dudousxd/nestjs-durable-core';\nimport type { Watcher, WatcherContext } from '@dudousxd/nestjs-telescope';\nimport { SpanStatusCode, context, trace } from '@opentelemetry/api';\nimport { durableExecutionScope } from './durable-execution-scope';\nimport { runTraceContext, runTraceId } from './durable-run-trace';\n\n/**\n * A Telescope watcher that turns durable-workflow lifecycle events into Telescope entries, so\n * runs and steps (including remote/Python steps) show up alongside the app's requests, queries\n * and jobs in the Telescope UI — under the **Workflows** tab.\n *\n * It does two things, and the second is the one that makes a failing workflow debuggable:\n *\n * 1. **Records an entry per engine event**, stamped with the run's trace (see\n * {@link runTraceContext}), so a whole run — across all its turns, all its steps and every\n * process that executed them — reads as a single trace at `/telescope#/traces/<id>`.\n *\n * 2. **Opens a Telescope batch around the execution itself** (see {@link durableExecutionScope}),\n * through the engine's `useDurableExecution` seam. Without this, everything a workflow turn or a\n * step handler *did* — its queries, its logs, the exception it threw — was recorded outside any\n * batch and outside any span: traceless, and so impossible to correlate back to the step that\n * caused it. Recording an event after the fact cannot fix that; the scope has to be open while\n * the body runs.\n *\n * Add it to `TelescopeModule.forRoot({ watchers: [new DurableTelescopeWatcher()] })`. It resolves\n * the engine from the (global) durable providers and subscribes to its events.\n *\n * ## What is NOT covered\n *\n * A **remote step** — one whose handler lives in another runtime, e.g. the Python worker — executes\n * out of process, so nothing here can wrap it. Its lifecycle still shows up (the engine emits\n * `step.started`/`step.completed`/`step.failed` when it dispatches and when the result lands, and\n * those entries carry the run's trace id), but the queries and exceptions *inside* that handler are\n * that runtime's to record. Because the trace id is derived from the run id rather than held in this\n * process, a remote runtime that derives it the same way joins the same trace — but that is a change\n * on its side, not something this watcher can do on its behalf.\n *\n * A **remote workflow** (a run whose body itself lives in another runtime) is likewise unwrapped:\n * the engine dispatches a workflow task and awaits a decision rather than executing a body, and a\n * scope around a dispatch would describe the wrong thing.\n */\nexport class DurableTelescopeWatcher implements Watcher {\n readonly type = 'durable';\n /** Removes the execution wrapper from the engine's process-level registry. */\n private disposeScope: (() => void) | undefined;\n\n register(ctx: WatcherContext): void {\n // Registered FIRST, and unconditionally: this is the half that works without a local engine. A\n // thin worker binds `WorkflowEngine` to a start-only facade with no event stream, but it is\n // exactly where step handlers run — so the execution scope belongs there even though there is\n // nothing local to subscribe to.\n this.disposeScope?.();\n this.disposeScope = useDurableExecution(durableExecutionScope(ctx));\n\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false });\n // A store-less thin-worker / tenant deployment (e.g. a local `DURABLE_TENANT=…` dev stack) binds\n // the `WorkflowEngine` token to a start-only `DurableStartClient` facade — it proxies run starts\n // over the transport and has NO local lifecycle event stream (those events live on the operator\n // that holds the store). There's nothing local to observe, so skip the subscription gracefully\n // instead of throwing \"engine.subscribe is not a function\".\n if (typeof (engine as Partial<WorkflowEngine>)?.subscribe !== 'function') return;\n const tracer = trace.getTracer('@dudousxd/nestjs-durable-telescope');\n\n engine.subscribe((event) => {\n const record = () =>\n ctx.record({\n type: this.type,\n // Stated explicitly rather than left to the ambient span, and the Recorder honours an\n // explicit `traceId` above everything else. A lifecycle event is very often emitted\n // OUTSIDE any execution scope — a remote step's result lands in a transport callback, a\n // recovery sweep emits for a run this process never executed — and those entries are\n // exactly the ones an operator follows into the trace. We know the run, so we know the\n // trace; there is no reason to let it depend on where the emit happened to land.\n traceId: runTraceId(event.runId),\n content: {\n event: event.type,\n workflow: event.workflow,\n runId: event.runId,\n parentRunId: parentRunIdOf(event.runId),\n seq: event.seq,\n name: event.name,\n kind: event.kind,\n output: event.output,\n error: event.error,\n durationMs: event.durationMs,\n },\n tags: this.tags(event),\n });\n\n // Anchor on the run's trace. Derived from the run id, so this event groups with the entries\n // recorded *inside* the turn/step scopes without either side having to hand the other a span —\n // which is the only thing that works once a step executes on a later turn, or in another pod.\n const base = runTraceContext(event.runId);\n\n // A step/signal/sleep event → give it its own child span so it's a distinct node in the\n // trace; run-level events record against the run's own span context. Either way the active\n // span at record time carries the run's trace id, so all entries group into one trace.\n if (event.name != null && event.seq != null) {\n const step = tracer.startSpan(\n `step ${event.name}`,\n {\n attributes: {\n 'durable.run_id': event.runId,\n 'durable.step.seq': event.seq,\n 'durable.step.kind': event.kind,\n },\n },\n base,\n );\n if (event.type === 'step.failed') {\n const message = event.error?.message;\n step.setStatus(\n message !== undefined\n ? { code: SpanStatusCode.ERROR, message }\n : { code: SpanStatusCode.ERROR },\n );\n }\n context.with(trace.setSpan(base, step), record);\n step.end();\n } else {\n context.with(base, record);\n }\n });\n }\n\n /**\n * Detach the execution wrapper. The `Watcher` SPI has no teardown hook, so nothing calls this in a\n * running app — where the wrapper should live exactly as long as the process does. It exists so a\n * test that registers a watcher does not leave a wrapper behind in the process-level registry for\n * the next test to trip over.\n */\n unregister(): void {\n this.disposeScope?.();\n this.disposeScope = undefined;\n }\n\n private tags(event: EngineEvent): string[] {\n const tags = ['durable', event.type, `run:${event.runId}`];\n // A child run's own trace is separate from its parent's; this tag is how the entries of every\n // child of one run (a whole `ctx.all` fan) are found from the parent.\n const parent = parentRunIdOf(event.runId);\n if (parent) tags.push(`parent:${parent}`);\n if (event.workflow) tags.push(`workflow:${event.workflow}`);\n if (event.kind) tags.push(`kind:${event.kind}`);\n if (event.type === 'run.failed') tags.push('failed');\n return tags;\n }\n}\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport { randomBytes } from 'node:crypto';\nimport type { DurableExecution, DurableExecutionWrapper } from '@dudousxd/nestjs-durable-core';\nimport type { WatcherContext } from '@dudousxd/nestjs-telescope';\nimport { SpanStatusCode, context, trace } from '@opentelemetry/api';\nimport { runTraceContext, runTraceId } from './durable-run-trace';\n\n/** What the scope publishes to anything recording on this async path. */\nexport interface DurableScope {\n /** The run whose unit of work is executing. */\n readonly runId: string;\n /** The run's trace — see {@link runTraceId}. */\n readonly traceId: string;\n /** This scope's span within that trace. Stable for every entry recorded inside it. */\n readonly spanId: string;\n /**\n * Identity of the wrapper that opened this scope. Reuse is only ever offered to the wrapper that\n * owns the scope: two Telescope instances in one process (a torn-down test module whose wrapper\n * outlived it, an app that mounts two consoles) would otherwise have the first one's scope\n * silently suppress the second one's batch, and the second console would record everything\n * batchless while looking correctly wired.\n */\n readonly owner: object;\n}\n\n/**\n * The durable scope THIS package has open on the current async path, if any.\n *\n * Telescope's own batch context is not readable from a watcher (`WatcherContext` exposes\n * `runInBatch`/`beginBatch` but no `currentBatch()`), and we would not want to read it even if it\n * were: the question is not \"is any batch open\" — an HTTP request that happened to start a run\n * inline has one — but \"is the scope on this path the one we opened for this same run\".\n */\nconst activeScope = new AsyncLocalStorage<DurableScope>();\n\n/** The durable execution scope active on this async path, or undefined outside one. */\nexport function currentDurableScope(): DurableScope | undefined {\n return activeScope.getStore();\n}\n\n/** `BatchOrigin` has no durable/workflow member — see {@link durableExecutionScope} for the choice. */\nconst DURABLE_BATCH_ORIGIN = 'queue';\n\nfunction spanName(execution: DurableExecution): string {\n if (execution.unit === 'step') return `step ${execution.name ?? '?'}`;\n return `workflow ${execution.workflow ?? '?'}`;\n}\n\nfunction spanAttributes(execution: DurableExecution): Record<string, string | number> {\n const attributes: Record<string, string | number> = { 'durable.run_id': execution.runId };\n if (execution.workflow !== undefined) attributes['durable.workflow'] = execution.workflow;\n if (execution.name !== undefined) attributes['durable.step.name'] = execution.name;\n if (execution.seq !== undefined) attributes['durable.step.seq'] = execution.seq;\n if (execution.attempt !== undefined) attributes['durable.step.attempt'] = execution.attempt;\n return attributes;\n}\n\n/**\n * Builds the {@link DurableExecutionWrapper} that puts every workflow turn and every step handler\n * body inside a Telescope batch and an OTel span, so the queries, logs and exceptions those bodies\n * produce are correlated to the unit of durable work that caused them instead of landing traceless.\n *\n * ## Why a batch around execution and not around the event\n *\n * The watcher's engine-event subscription records what *happened*, after the fact. A batch opened\n * there would contain exactly one entry — the durable entry itself — and nothing the step actually\n * did, because by then the handler has returned and the exception it threw has long since been\n * recorded with no batch at all. Wrapping the execution is the only shape that puts the failing code\n * inside the scope.\n *\n * ## Nesting: reuse the batch when the same run already has one open on this path\n *\n * With an in-process transport (event-emitter, in-memory, the DB transport's inline path) a\n * dispatched step's handler starts synchronously inside the turn that dispatched it, and the result\n * it produces resumes the next turn on that same async path — so the whole run is one unbroken\n * causal chain. Opening a fresh batch at each hop would split that chain across many batch ids for\n * no gain: Telescope's entry model has no parent-batch pointer, so the split is not recoverable in\n * the UI — the step's queries would appear in a batch with nothing visible causing them.\n *\n * So the rule is the causal one, not the syntactic one: a unit reuses the batch when THIS wrapper\n * already has one open for THIS run on this async path. The consequence is worth stating plainly,\n * because it differs by transport and looks like an inconsistency if you meet it unprepared:\n *\n * - **in-process transport** — one batch for the whole run (every turn, every step), because it is\n * genuinely one chain;\n * - **queue-backed transport** (BullMQ, SQS, a thin worker) — one batch per turn and one per step,\n * because each really is a separate entry point, in a separate process, with no ambient scope to\n * inherit.\n *\n * What holds either way is the trace: every entry of a run carries {@link runTraceId}, so the run is\n * one trace whether it executed in one process or twenty. That invariant is the thing to rely on;\n * batch granularity follows the execution shape.\n *\n * A different run is a different chain and always gets its own batch. That includes a child\n * workflow: it has its own run id, its own timeline and its own dashboard row, and its trace is\n * anchored on ITS run id (see {@link runTraceContext}), so it reads as a related-but-distinct trace\n * instead of being folded into the parent's. The parent/child relationship is already carried by the\n * run graph; flattening the batches to restate it would erase the child's own boundaries and gain\n * nothing.\n *\n * ## Origin\n *\n * `BatchOrigin` in `@dudousxd/nestjs-telescope` is the closed union\n * `'http' | 'queue' | 'schedule' | 'cli' | 'manual'` — it has no durable/workflow member. Widening\n * it belongs to that repo and a coordinated release, so this uses `'queue'`, which is also the\n * honest description: durable work reaches an executor by being dispatched over a transport\n * (BullMQ, SQS, an event emitter), exactly like the jobs the BullMQ watcher marks `'queue'`.\n */\nexport function durableExecutionScope(ctx: WatcherContext): DurableExecutionWrapper {\n const tracer = trace.getTracer('@dudousxd/nestjs-durable-telescope');\n const owner = {};\n\n return (execution, next) => {\n // Already inside our own scope for this run — reuse it (see \"Nesting\" above).\n const open = activeScope.getStore();\n if (open?.owner === owner && open.runId === execution.runId) return next();\n // Telescope switched off: `runInBatch` would already short-circuit, but the span and the ALS\n // frame would not, so check here and hand the body straight back.\n if (ctx.config.enabled === false) return next();\n\n try {\n const parent = runTraceContext(execution.runId);\n const span = tracer.startSpan(\n spanName(execution),\n { attributes: spanAttributes(execution) },\n parent,\n );\n // Prefer the real span's id when an OTel SDK produced one; a host with no SDK gets a\n // non-recording span whose id is the run's synthetic parent, which would make every scope in\n // a run look like one span. Mint an id in that case so the scopes stay distinguishable.\n const spanContext = span.spanContext();\n const scope: DurableScope = {\n runId: execution.runId,\n traceId: runTraceId(execution.runId),\n spanId: span.isRecording() ? spanContext.spanId : randomBytes(8).toString('hex'),\n owner,\n };\n\n return context.with(trace.setSpan(parent, span), () =>\n activeScope.run(scope, () =>\n ctx.runInBatch(DURABLE_BATCH_ORIGIN, next).then(\n (value) => {\n span.end();\n return value;\n },\n (error: unknown) => {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n ...(error instanceof Error ? { message: error.message } : {}),\n });\n span.end();\n throw error; // never swallow the workflow's error\n },\n ),\n ),\n );\n } catch {\n // Opening the scope failed (a broken tracer, a Telescope in a bad state). Run the work anyway,\n // untraced. The BullMQ and schedule watchers hold the same line from the other direction —\n // they never let a recording failure fail a healthy job — and it matters more here, because\n // this wrapper sits around a workflow body whose failure is durably persisted.\n return next();\n }\n };\n}\n\n/** Minimal structural view of Telescope's `TraceContextProvider` (it is one method, and typing\n * against the interface would drag a value import in for nothing). */\ninterface TraceContextLike {\n current(): { traceId: string; spanId: string } | null;\n}\n\n/**\n * A Telescope `traceContext` provider that reports the active durable scope, so entries recorded by\n * OTHER watchers (queries, cache, outbound HTTP) during a workflow turn or a step body carry the\n * run's trace id.\n *\n * Wire it as `TelescopeModule.forRoot({ traceContext: durableTraceContext() })`.\n *\n * This exists because Telescope resolves an entry's trace id from an ambient OTel span, and\n * `@opentelemetry/api` on its own propagates nothing: without a registered context manager\n * `context.with(...)` is a no-op and `getActiveSpan()` is always undefined. An app that runs a full\n * OTel SDK needs none of this — it should pass `OtelTraceContextProvider` (optionally as the\n * `delegate` below, which is then consulted first so the app's real spans win), and the scope's\n * spans hang off the run's trace anyway. An app that runs no OTel at all — the common case — would\n * otherwise get correlated batches and no trace ids, which is half the feature.\n */\nexport function durableTraceContext(delegate?: TraceContextLike): TraceContextLike {\n return {\n current() {\n // The host's own tracing wins where it has an answer: inside a durable scope it will report a\n // span from this run's trace anyway, and outside one it is the only source there is.\n const fromDelegate = delegate?.current() ?? null;\n if (fromDelegate) return fromDelegate;\n const scope = activeScope.getStore();\n return scope ? { traceId: scope.traceId, spanId: scope.spanId } : null;\n },\n };\n}\n","import { createHash } from 'node:crypto';\nimport { type Context, TraceFlags, context, trace } from '@opentelemetry/api';\n\n/**\n * The trace a run's spans hang under, DERIVED FROM THE RUN ID rather than held as a live root span.\n *\n * The obvious implementation — open a root span on `run.started`, keep it in a map, end it when the\n * run settles — cannot work for a durable workflow. A run suspends: `ctx.step` dispatches and the\n * turn unwinds, and the step body then executes minutes (or days) later, on a different turn, very\n * often in a different process. A live root span would either have to be ended at every suspension —\n * fragmenting one run into a trace per turn, so the step that failed is in a different trace from\n * the turn that dispatched it — or held open across the suspension, which pins a span object for the\n * lifetime of a workflow that is explicitly designed to wait for days.\n *\n * Hashing the run id sidesteps both. The trace id is a pure function of the run, so every turn and\n * every step of a run lands in ONE trace with no state held anywhere, and a worker in another\n * process computes the same id from the same run id — which is what makes a remotely-executed step\n * correlate with the run that dispatched it at all.\n *\n * The parent span context is marked `isRemote` because that is what it is: a reference to a trace\n * whose \"root\" is the run itself, not a span this process ever recorded.\n */\nexport function runTraceId(runId: string): string {\n // 16 bytes of hex, as the W3C trace-context spec requires. sha256 is not used as a security\n // primitive here — only as a well-distributed map from an arbitrary run id into that space.\n return createHash('sha256').update(`durable:run:${runId}`).digest('hex').slice(0, 32);\n}\n\n/** The synthetic parent span id for a run's trace — derived the same way, from a different prefix. */\nfunction runSpanId(runId: string): string {\n return createHash('sha256').update(`durable:run-span:${runId}`).digest('hex').slice(0, 16);\n}\n\n/**\n * An OTel {@link Context} whose active span context is the run's synthetic root. Spans started\n * against it are children of the run, and Telescope's trace-context provider stamps every entry\n * recorded inside it with {@link runTraceId}.\n *\n * `TraceFlags.SAMPLED` is set deliberately: a non-sampled parent leaves the derived span\n * non-recording, and its span context would then never reach an exporter — the run would be\n * traceable in Telescope but invisible in any OTel backend, which is the confusing half-state.\n */\nexport function runTraceContext(runId: string, parent: Context = context.active()): Context {\n return trace.setSpanContext(parent, {\n traceId: runTraceId(runId),\n spanId: runSpanId(runId),\n traceFlags: TraceFlags.SAMPLED,\n isRemote: true,\n });\n}\n","import { defineTelescopeExtension } from '@dudousxd/nestjs-telescope';\nimport { durableDashboard } from './durable-dashboard.spec-data';\nimport {\n durableDurationProvider,\n durableRecentFailuresProvider,\n durableRunsOverTimeProvider,\n durableStateBreakdownProvider,\n durableStateProvider,\n durableSuccessRateProvider,\n durableThroughputProvider,\n durableTimeseriesProvider,\n durableWorkerHealthProvider,\n durableWorkerStatusProvider,\n} from './durable-data-providers';\nimport { DurableTelescopeWatcher } from './durable-telescope.watcher';\n\n/** The first-class Telescope extension for nestjs-durable: watcher + Workflows dashboard. */\nexport function durableTelescopeExtension(\n opts: { runHref?: string; recentFailuresWindowMs?: number } = {},\n) {\n return defineTelescopeExtension({\n name: 'durable',\n watchers: () => [new DurableTelescopeWatcher()],\n entryTypes: () => [{ id: 'durable', label: 'Workflows', dot: 'bg-amber-400' }],\n dashboards: () => [durableDashboard(opts)],\n dataProviders: () => [\n durableStateProvider(),\n durableTimeseriesProvider(),\n durableRecentFailuresProvider(),\n durableWorkerHealthProvider(),\n durableWorkerStatusProvider(),\n durableDurationProvider(),\n durableRunsOverTimeProvider(),\n durableSuccessRateProvider(),\n durableThroughputProvider(),\n durableStateBreakdownProvider(),\n ],\n });\n}\n","import type { DashboardSpec } from '@dudousxd/nestjs-telescope';\n\n/**\n * The \"Workflows\" health dashboard — golden-signals sections layout.\n *\n * `runHref` is a URL template for deep-linking a run out to the durable dashboard\n * (e.g. '/durable/runs/{runId}'). `recentFailuresWindowMs` bounds the \"Stuck runs\"\n * table (default 24h) so a healthy system shows an empty table instead of\n * days-old failures; pass `0` to show all.\n */\nexport function durableDashboard(\n opts: { runHref?: string; recentFailuresWindowMs?: number } = {},\n): DashboardSpec {\n const runHref = opts.runHref ?? '/durable/runs/{runId}';\n const windowMs = opts.recentFailuresWindowMs ?? 24 * 60 * 60 * 1000;\n return {\n id: 'durable.workflows',\n label: 'Workflows',\n panels: [],\n sections: [\n {\n title: 'Health',\n cols: 4,\n panels: [\n {\n kind: 'gauge',\n title: 'Success rate',\n data: { provider: 'durable.successRate' },\n max: 1,\n format: 'percent',\n thresholds: { warn: 0.98, bad: 0.95, direction: 'down-bad' },\n },\n {\n kind: 'stat',\n title: 'Duration p95',\n data: { provider: 'durable.duration', query: { metric: 'p95' } },\n format: 'duration',\n spark: false,\n thresholds: { warn: 2000, bad: 5000, direction: 'up-bad' },\n },\n {\n kind: 'stat',\n title: 'Backlog',\n data: { provider: 'durable.state', query: { status: 'pending' } },\n spark: false,\n thresholds: { warn: 50, bad: 200, direction: 'up-bad' },\n },\n {\n kind: 'stat',\n title: 'Throughput',\n data: { provider: 'durable.throughput' },\n format: 'rate',\n spark: true,\n },\n ],\n },\n {\n title: 'Needs attention',\n cols: 3,\n panels: [\n {\n kind: 'topN',\n title: 'Top failing workflows',\n data: { provider: 'durable.timeseries', query: { metric: 'topFailures' } },\n limit: 8,\n },\n {\n kind: 'table',\n title: 'Stuck runs',\n data: { provider: 'durable.recentFailures', query: { windowMs } },\n columns: [\n { key: 'updatedAt', label: 'Updated' },\n { key: 'workflow', label: 'Workflow' },\n { key: 'runId', label: 'Run', link: { href: runHref } },\n { key: 'error', label: 'Error' },\n ],\n },\n {\n kind: 'table',\n // Lists ALL worker groups (starved ones sorted first); the Status column\n // flags STARVED (queued > 0 with zero live workers) vs ok. Titling it\n // \"Starved worker groups\" read as if every listed group were starved.\n title: 'Worker health',\n data: { provider: 'durable.workerHealth' },\n columns: [\n { key: 'group', label: 'Group' },\n { key: 'queued', label: 'Queued' },\n { key: 'liveWorkers', label: 'Workers' },\n { key: 'status', label: 'Status' },\n ],\n },\n ],\n },\n {\n title: 'Workers',\n // One column, because this section holds exactly one panel and that panel is the widest\n // table on the dashboard. A section renders as a fixed `grid-cols-N` with no `colSpan`, so\n // `cols: 2` gave the widest table on the dashboard half the viewport and left the cell\n // beside it empty — it scrolled sideways inside its own card while a hole sat next to it.\n cols: 1,\n panels: [\n {\n kind: 'table',\n // One row per LIVE worker (flattened from each group's heartbeats), exposing the live\n // WorkerStatus the heartbeat carries: fixed vs. adaptive limit, in-flight saturation,\n // RAM%/CPU%, throughput, p95, and why the adaptive controller last moved. A worker from\n // an older SDK with no status still lists, reading 'n/a' — distinct from the '—' of a\n // worker that reports fine but has nothing to measure yet, which is what an idle\n // deployment looks like (throughput and p95 come off a rolling window of completions,\n // so they are empty until a step finishes inside it). The\n // adaptive min–max range rides the In-flight column's row value (`minMax` key) for those\n // that want it; the visible columns stay readable.\n title: 'Workers',\n data: { provider: 'durable.workerStatus' },\n columns: [\n { key: 'group', label: 'Group' },\n { key: 'instanceId', label: 'Worker' },\n { key: 'mode', label: 'Mode' },\n // No separate `limit` column: In-flight renders `<inFlight>/<limit>`, so a Limit\n // column beside it repeats its own denominator in every row.\n { key: 'saturation', label: 'In-flight' },\n { key: 'queued', label: 'Queued' },\n { key: 'rssPct', label: 'RAM %' },\n { key: 'cpuPct', label: 'CPU %' },\n { key: 'throughputPerMin', label: 'Thrpt/min' },\n { key: 'p95Ms', label: 'p95 ms' },\n { key: 'lastAdjust', label: 'Last adjust' },\n ],\n },\n ],\n },\n {\n title: 'Trends',\n cols: 3,\n panels: [\n {\n kind: 'timeseries',\n title: 'Runs over time',\n data: { provider: 'durable.runsOverTime' },\n series: ['done', 'failed'],\n style: 'stacked',\n },\n {\n kind: 'distribution',\n title: 'Duration distribution',\n data: { provider: 'durable.duration' },\n markers: ['p50', 'p95', 'p99'],\n format: 'duration',\n },\n {\n kind: 'breakdown',\n title: 'Runs by state',\n data: { provider: 'durable.stateBreakdown' },\n style: 'donut',\n },\n ],\n },\n ],\n };\n}\n","import { STATE_STORE_CANONICAL, WorkflowEngine } from '@dudousxd/nestjs-durable-core';\nimport type { RunStatus, StateStore, WorkerStatus } from '@dudousxd/nestjs-durable-core';\nimport type { DataProvider, ExtensionContext } from '@dudousxd/nestjs-telescope';\nimport { TELESCOPE_STORAGE } from '@dudousxd/nestjs-telescope';\n\n// ─── Shared helpers ───────────────────────────────────────────────────────────\n\n/** Compute the p-th percentile of a SORTED array. */\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[Math.max(0, idx)] ?? 0;\n}\n\ntype StorageEntry = { content?: unknown; createdAt?: Date };\n\n/** Fetch the durable entry page from TELESCOPE_STORAGE. */\nasync function fetchEntries(ctx: ExtensionContext): Promise<StorageEntry[]> {\n const storage = ctx.moduleRef.get(TELESCOPE_STORAGE, { strict: false }) as {\n get(q: { type?: string; limit?: number }): Promise<{ data: StorageEntry[] }>;\n };\n const page = await storage.get({ type: 'durable', limit: 5_000 });\n return page.data;\n}\n\n/**\n * Split entries into current/previous equal-length windows.\n * current: (now - windowMs, now]\n * previous: (now - 2*windowMs, now - windowMs]\n */\nfunction splitWindows(\n entries: StorageEntry[],\n windowMs: number,\n now: number,\n): { current: StorageEntry[]; previous: StorageEntry[] } {\n const start = now - windowMs;\n const prevStart = start - windowMs;\n return {\n current: entries.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > start && t <= now;\n }),\n previous: entries.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > prevStart && t <= start;\n }),\n };\n}\n\n/** Compute success rate (completed / total) for a list of entries, returns 1 when no data. */\nfunction successRateOf(entries: StorageEntry[]): number {\n let completed = 0;\n let failed = 0;\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string };\n if (c.event === 'run.completed') completed += 1;\n else if (c.event === 'run.failed') failed += 1;\n }\n const total = completed + failed;\n return total === 0 ? 1 : completed / total;\n}\n\n/** Run-level lifecycle events — each can legitimately occur at most once per runId. */\nconst RUN_LIFECYCLE_EVENTS = new Set([\n 'run.started',\n 'run.completed',\n 'run.failed',\n 'run.suspended',\n]);\n\n/**\n * Deduplicate run-level lifecycle entries by `${event}:${runId}`, keeping the FIRST occurrence.\n *\n * The durable engine emits each run lifecycle event on every pod (1 worker + N api pods) and the\n * watcher records on each, so a single `run.completed` for a given runId is captured multiple times\n * with identical content but distinct entry ids / createdAt (±ms) — inflating completed/failed\n * counts. A given runId can legitimately have at most ONE `run.started` and ONE terminal\n * `run.completed`/`run.failed`/`run.suspended`, so collapsing by `${event}:${runId}` is provably\n * safe. Step events and any entry without a run-level event + runId pass through untouched — a step\n * can legitimately repeat across attempts, and the watcher's step content carries no stepId/attempt\n * to key on.\n */\nfunction dedupeRunEvents(entries: StorageEntry[]): StorageEntry[] {\n const seen = new Set<string>();\n const out: StorageEntry[] = [];\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string };\n if (c.event && c.runId && RUN_LIFECYCLE_EVENTS.has(c.event)) {\n const key = `${c.event}:${c.runId}`;\n if (seen.has(key)) continue;\n seen.add(key);\n }\n out.push(e);\n }\n return out;\n}\n\nconst STATE_CAP = 10_000;\n\n/**\n * How many runs are in `status`: an exact `COUNT` through the store's `countRuns`, or, for a store\n * without one, the length of a listing capped at {@link STATE_CAP}. A wide `ctx.all` fan can put\n * tens of thousands of runs in one status, which the capped listing would both under-count and pay\n * for by loading every row.\n */\nfunction countByStatus(store: StateStore, status: RunStatus): Promise<number> {\n if (store.countRuns) return store.countRuns({ status });\n return store.listRuns({ status, limit: STATE_CAP }).then((runs) => runs.length);\n}\n\n// ─── Existing providers ───────────────────────────────────────────────────────\n\n/** Source C: current-state gauge from the durable store. query.status selects which (default 'dead'). */\nexport function durableStateProvider(): DataProvider {\n return {\n name: 'durable.state',\n async resolve(query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const status = (query?.status as RunStatus) ?? 'dead';\n return { value: await countByStatus(store, status) };\n },\n };\n}\n\n/**\n * Source A: rollups from captured `durable` entries in Telescope's own storage.\n * Reads recent run.* lifecycle entries and aggregates. Bounded by Telescope's prune\n * window (by design — this is the \"history\" series, not the source of truth).\n */\nexport function durableTimeseriesProvider(): DataProvider {\n return {\n name: 'durable.timeseries',\n async resolve(query, ctx: ExtensionContext) {\n // TELESCOPE_STORAGE.get(query) → { data: Entry[]; nextCursor }. Entry = { content, createdAt, ... }.\n const storage = ctx.moduleRef.get(TELESCOPE_STORAGE, { strict: false }) as {\n get(q: { type?: string; limit?: number }): Promise<{\n data: Array<{ content?: unknown; createdAt?: Date }>;\n }>;\n };\n const limit = Math.min(5_000, Math.max(100, Number(query?.limit ?? 2_000)));\n const page = await storage.get({ type: 'durable', limit });\n\n let completed = 0;\n let failed = 0;\n const failByWorkflow = new Map<string, number>();\n // Dedup triplicated lifecycle events (one event recorded on every pod) before counting.\n for (const e of dedupeRunEvents(page.data)) {\n const c = (e.content ?? {}) as { event?: string; workflow?: string };\n if (c.event === 'run.completed') completed += 1;\n else if (c.event === 'run.failed') {\n failed += 1;\n const wf = c.workflow ?? 'unknown';\n failByWorkflow.set(wf, (failByWorkflow.get(wf) ?? 0) + 1);\n }\n }\n const total = completed + failed;\n const metric = (query?.metric as string) ?? 'successRate';\n if (metric === 'successRate') return { value: total === 0 ? 1 : completed / total };\n if (metric === 'failed') return { value: failed };\n if (metric === 'total') return { value: total };\n if (metric === 'topFailures') {\n const items = [...failByWorkflow.entries()]\n .map(([label, value]) => ({ label, value }))\n .sort((a, b) => b.value - a.value);\n return { items };\n }\n return { value: total };\n },\n };\n}\n\n/**\n * Source C: recent failed + dead runs as table rows (newest first), time-bounded.\n * Only failures updated within `query.windowMs` (default 24h) are returned, so a\n * healthy system shows an EMPTY table instead of surfacing days-old failures as if\n * they were a live incident. `windowMs: 0` disables the window (return all). Each\n * row carries a compact `updatedAt` stamp so recency is visible in the table.\n */\nexport function durableRecentFailuresProvider(): DataProvider {\n return {\n name: 'durable.recentFailures',\n async resolve(query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50)));\n const windowMs = query?.windowMs === undefined ? 24 * 60 * 60 * 1000 : Number(query.windowMs);\n const cutoff = windowMs > 0 ? Date.now() - windowMs : 0;\n const [failed, dead] = await Promise.all([\n store.listRuns({ status: 'failed', limit }),\n store.listRuns({ status: 'dead', limit }),\n ]);\n const rows = [...failed, ...dead]\n .filter((r) => +new Date(r.updatedAt) >= cutoff)\n .sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt))\n .slice(0, limit)\n .map((r) => ({\n // Compact UTC stamp (\"YYYY-MM-DD HH:mm Z\"); the generic table renderer\n // prints column values as-is, so we format here.\n updatedAt: `${new Date(r.updatedAt).toISOString().replace('T', ' ').slice(0, 16)}Z`,\n workflow: r.workflow,\n runId: r.id,\n error: r.error?.message ?? '',\n }));\n return { rows };\n },\n };\n}\n\n/**\n * Source D: per-group worker health from the engine (`WorkflowEngine.workerHealth()` — queue depth\n * vs. live worker heartbeats). `query.metric: 'starvedCount'` returns the number of groups with work\n * queued and zero live workers (the \"alive but not consuming\" alert state) as a stat; otherwise a\n * table, starved groups sorted first. Empty when the transport can't report health (in-process).\n */\nexport function durableWorkerHealthProvider(): DataProvider {\n return {\n name: 'durable.workerHealth',\n async resolve(query, ctx: ExtensionContext) {\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false }) as WorkflowEngine;\n const health = await engine.workerHealth();\n const isStarved = (g: { depth: number; liveWorkers: unknown[] }) =>\n g.depth > 0 && g.liveWorkers.length === 0;\n if ((query?.metric as string) === 'starvedCount') {\n return { value: health.filter(isStarved).length };\n }\n const rows = health\n .slice()\n // Starved groups first, then deepest backlog — the rows that need attention rise to the top.\n .sort((a, b) => Number(isStarved(b)) - Number(isStarved(a)) || b.depth - a.depth)\n .map((g) => ({\n group: g.group,\n queued: g.depth,\n liveWorkers: g.liveWorkers.length,\n status: isStarved(g) ? 'STARVED' : 'ok',\n }));\n return { rows };\n },\n };\n}\n\n/** Compact \"Nm ago\" relative stamp for an epoch-ms instant (worker last-adjust recency). */\nfunction relAgo(atMs: number): string {\n const s = Math.max(0, Math.round((Date.now() - atMs) / 1000));\n if (s < 60) return `${s}s ago`;\n const m = Math.round(s / 60);\n if (m < 60) return `${m}m ago`;\n const h = Math.round(m / 60);\n if (h < 24) return `${h}h ago`;\n return `${Math.round(h / 24)}d ago`;\n}\n\n/**\n * What a cell says when it has no number, and the two cases it has to keep apart.\n *\n * A blank cell used to mean both \"this worker's SDK is too old to report anything\" and \"this worker\n * reports fine, it just has nothing to measure yet\" — and the second is the common one on an idle\n * deployment, where the adaptive controller's rolling window is empty because no step has completed\n * inside it. Reading one as the other sends someone hunting for a broken worker that is merely idle,\n * or ignoring a fleet that has genuinely stopped reporting.\n *\n * `n/a` is the heartbeat carrying no {@link WorkerStatus} at all; `—` is a status that carries no\n * value for this field yet.\n */\nconst NOT_REPORTED = 'n/a';\nconst NOT_YET_MEASURED = '—';\n\n/**\n * Render a worker's last limit change as \"shrink 8→5 2m ago\".\n *\n * `—` here means the adaptive controller has not moved the limit yet — the normal state of a worker\n * that has been sitting at its floor. A fixed-mode worker never adjusts at all, which is what its\n * `Mode` column already says.\n */\nfunction fmtLastAdjust(status: WorkerStatus | undefined): string {\n if (!status) return NOT_REPORTED;\n const a = status.lastAdjust;\n if (!a) return NOT_YET_MEASURED;\n return `${a.reason} ${a.from}→${a.to} ${relAgo(a.at)}`;\n}\n\n/** Round a 0..100(+) percent to a whole number for table display. */\nfunction fmtPct(n: number | undefined, status: WorkerStatus | undefined): number | string {\n if (typeof n === 'number') return Math.round(n);\n return status ? NOT_YET_MEASURED : NOT_REPORTED;\n}\n\n/** Round a rate/latency to a whole number for table display. */\nfunction fmtNum(n: number | undefined, status: WorkerStatus | undefined): number | string {\n if (typeof n === 'number') return Math.round(n);\n return status ? NOT_YET_MEASURED : NOT_REPORTED;\n}\n\n/**\n * Source D (per-worker): flatten `WorkflowEngine.workerHealth()` to ONE row per live worker, exposing\n * the live {@link WorkerStatus} each heartbeat now carries (mode/limit/min-max, in-flight saturation,\n * RAM%/CPU%, throughput, p95, last adjust). A worker from an older SDK with no `status` still renders\n * — its measured fields fall back to '—', so a mixed fleet stays visible. Rows sort backlog-bearing\n * groups first, then by group, then instanceId. Empty when the transport can't report health.\n */\nexport function durableWorkerStatusProvider(): DataProvider {\n return {\n name: 'durable.workerStatus',\n async resolve(_query, ctx: ExtensionContext) {\n const engine = ctx.moduleRef.get(WorkflowEngine, { strict: false }) as WorkflowEngine;\n const health = await engine.workerHealth();\n const rows = health\n .slice()\n // Groups with a backlog first (the workers worth scrutinising), then by group name.\n .sort((a, b) => Number(b.depth > 0) - Number(a.depth > 0) || a.group.localeCompare(b.group))\n .flatMap((group) =>\n group.liveWorkers\n .slice()\n .sort((a, b) => a.instanceId.localeCompare(b.instanceId))\n .map((worker) => {\n const status = worker.status;\n const concurrency = status?.concurrency;\n // Mode/limit/in-flight are not measurements — a status either declares them or the\n // heartbeat carried no status at all, so their absence is always `n/a`.\n const mode = concurrency?.mode ?? NOT_REPORTED;\n const limit = concurrency?.limit ?? NOT_REPORTED;\n const inFlight = status?.inFlight ?? NOT_REPORTED;\n const minMax =\n concurrency?.mode === 'adaptive'\n ? `${concurrency.min ?? 1}–${concurrency.max ?? 32}`\n : NOT_REPORTED;\n const saturation =\n concurrency && status ? `${status.inFlight}/${concurrency.limit}` : NOT_REPORTED;\n return {\n group: group.group,\n instanceId: worker.instanceId,\n mode,\n limit,\n minMax,\n inFlight,\n saturation,\n queued: group.depth,\n rssPct: fmtPct(status?.rssPct, status),\n cpuPct: fmtPct(status?.cpuPct, status),\n throughputPerMin: fmtNum(status?.throughputPerMin, status),\n p95Ms: fmtNum(status?.p95Ms, status),\n lastAdjust: fmtLastAdjust(status),\n };\n }),\n );\n return { rows };\n },\n };\n}\n\n// ─── New golden-signals providers ────────────────────────────────────────────\n\n/**\n * Duration percentiles (p50/p95/p99) + a histogram of ~8 buckets.\n * Durations are read from `content.durationMs` on `run.completed`/`run.failed`; when absent,\n * computed by pairing a `run.started` and its corresponding `run.completed`/`run.failed` entry\n * by `runId` using their `createdAt` timestamps.\n *\n * With `query.metric === 'p50'|'p95'|'p99'` returns `{ value }` for use as a stat panel.\n */\nexport function durableDurationProvider(): DataProvider {\n return {\n name: 'durable.duration',\n async resolve(query, ctx: ExtensionContext) {\n // Dedup triplicated lifecycle events so paired durations aren't double-counted.\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n\n // Index run.started entries by runId for the pairing fallback.\n const startedAt = new Map<string, number>();\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string };\n if (c.event === 'run.started' && c.runId && e.createdAt) {\n startedAt.set(c.runId, +new Date(e.createdAt));\n }\n }\n\n const durs: number[] = [];\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string; runId?: string; durationMs?: number };\n if (c.event === 'run.completed' || c.event === 'run.failed') {\n if (typeof c.durationMs === 'number') {\n durs.push(c.durationMs);\n } else if (c.runId) {\n const start = startedAt.get(c.runId);\n const end = e.createdAt ? +new Date(e.createdAt) : undefined;\n if (start !== undefined && end !== undefined && end >= start) {\n durs.push(end - start);\n }\n }\n }\n }\n durs.sort((a, b) => a - b);\n\n const p50 = percentile(durs, 50);\n const p95 = percentile(durs, 95);\n const p99 = percentile(durs, 99);\n\n // Stat shortcut: return a single value for use in stat panels.\n const metric = (query as Record<string, unknown>)?.metric as string | undefined;\n if (metric === 'p50') return { value: p50 };\n if (metric === 'p95') return { value: p95 };\n if (metric === 'p99') return { value: p99 };\n\n // Build ~8 equal-width buckets between 0 and max.\n const max = durs.at(-1) ?? 0;\n const bucketCount = 8;\n const size = Math.max(1, Math.ceil((max + 1) / bucketCount));\n const buckets = Array.from({ length: bucketCount }, (_, i) => ({\n label: `${Math.round((i * size) / 100) / 10}s`,\n count: 0,\n }));\n for (const d of durs) {\n const bi = Math.min(bucketCount - 1, Math.floor(d / size));\n const bucket = buckets[bi];\n if (bucket) bucket.count += 1;\n }\n\n return { buckets, p50, p95, p99 };\n },\n };\n}\n\n/**\n * Buckets `run.completed` (-> done) and `run.failed` (-> failed) entries by `createdAt` into\n * N=`query.buckets ?? 24` equal-width time buckets. Returns `{ rows: Array<{ label; done; failed }> }`.\n */\nexport function durableRunsOverTimeProvider(): DataProvider {\n return {\n name: 'durable.runsOverTime',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const n = Number((query as Record<string, unknown>)?.buckets ?? 24);\n const now = Date.now();\n\n // Determine the time window from the oldest entry or default to 24h.\n let minT = now;\n for (const e of entries) {\n if (e.createdAt) {\n const t = +new Date(e.createdAt);\n if (t < minT) minT = t;\n }\n }\n const span = Math.max(now - minT, 1);\n const bucketSize = span / n;\n\n const rows: Array<{ label: string; done: number; failed: number }> = Array.from(\n { length: n },\n (_, i) => {\n const bucketStart = new Date(minT + i * bucketSize);\n const label = bucketStart.toISOString().slice(11, 16); // \"HH:mm\"\n return { label, done: 0, failed: 0 };\n },\n );\n\n for (const e of entries) {\n const c = (e.content ?? {}) as { event?: string };\n if (c.event !== 'run.completed' && c.event !== 'run.failed') continue;\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n const idx = Math.min(n - 1, Math.floor((t - minT) / bucketSize));\n const row = rows[idx];\n if (row) {\n if (c.event === 'run.completed') row.done += 1;\n else row.failed += 1;\n }\n }\n\n return { rows };\n },\n };\n}\n\n/**\n * Success rate over the last `query.windowMs` (default 24h), with a `delta` vs. the previous\n * equal-length window and a `spark` array of per-sub-bucket success rates.\n */\nexport function durableSuccessRateProvider(): DataProvider {\n return {\n name: 'durable.successRate',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const windowMs = Number((query as Record<string, unknown>)?.windowMs ?? 24 * 60 * 60 * 1000);\n const now = Date.now();\n const { current, previous } = splitWindows(entries, windowMs, now);\n\n const value = successRateOf(current);\n const prevRate = successRateOf(previous);\n const delta = previous.length > 0 ? value - prevRate : undefined;\n\n // Spark: split the current window into 8 sub-buckets.\n const sparkBuckets = 8;\n const bucketSize = windowMs / sparkBuckets;\n const sparkStart = now - windowMs;\n const spark = Array.from({ length: sparkBuckets }, (_, i) => {\n const bStart = sparkStart + i * bucketSize;\n const bEnd = bStart + bucketSize;\n const bEntries = current.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > bStart && t <= bEnd;\n });\n return successRateOf(bEntries);\n });\n\n return { value, delta, spark };\n },\n };\n}\n\n/**\n * Completed runs per hour over the last `query.windowMs` (default 24h), with a `delta` vs. the\n * previous equal-length window and a `spark` array of per-sub-bucket throughput values.\n */\nexport function durableThroughputProvider(): DataProvider {\n return {\n name: 'durable.throughput',\n async resolve(query, ctx: ExtensionContext) {\n const entries = dedupeRunEvents(await fetchEntries(ctx));\n const windowMs = Number((query as Record<string, unknown>)?.windowMs ?? 24 * 60 * 60 * 1000);\n const now = Date.now();\n const { current, previous } = splitWindows(entries, windowMs, now);\n\n const countCompleted = (es: StorageEntry[]) =>\n es.filter((e) => ((e.content ?? {}) as { event?: string }).event === 'run.completed')\n .length;\n\n const windowHours = windowMs / (60 * 60 * 1000);\n const value = countCompleted(current) / windowHours;\n const prevValue = countCompleted(previous) / windowHours;\n const delta = previous.length > 0 ? value - prevValue : undefined;\n\n // Spark: 8 sub-buckets of current window.\n const sparkBuckets = 8;\n const bucketSize = windowMs / sparkBuckets;\n const bucketHours = bucketSize / (60 * 60 * 1000);\n const sparkStart = now - windowMs;\n const spark = Array.from({ length: sparkBuckets }, (_, i) => {\n const bStart = sparkStart + i * bucketSize;\n const bEnd = bStart + bucketSize;\n const bEntries = current.filter((e) => {\n const t = e.createdAt ? +new Date(e.createdAt) : 0;\n return t > bStart && t <= bEnd;\n });\n return countCompleted(bEntries) / bucketHours;\n });\n\n return { value, delta, spark };\n },\n };\n}\n\n// Palette aligned index-for-index with STATE_BREAKDOWN_STATUSES so each status reads with the\n// semantically-correct color: running=cyan, pending=amber, cancelling=orange, completed=green,\n// failed=red, dead=purple.\nconst STATE_BREAKDOWN_STATUSES: RunStatus[] = [\n 'running',\n 'pending',\n 'cancelling',\n 'completed',\n 'failed',\n 'dead',\n];\n/** Color palette for the state breakdown pie segments (aligned to STATE_BREAKDOWN_STATUSES). */\nconst STATE_BREAKDOWN_PALETTE = [\n '#38bdf8', // running → cyan/blue\n '#fbbf24', // pending → amber\n '#f59e0b', // cancelling → orange\n '#34d399', // completed → green\n '#f87171', // failed → red\n '#a78bfa', // dead → purple\n];\n\n/**\n * Counts per status (see {@link countByStatus}) for each of the statuses and returns pie\n * segments with the standard palette: `{ segments: Array<{ label, value, color }> }`.\n */\nexport function durableStateBreakdownProvider(): DataProvider {\n return {\n name: 'durable.stateBreakdown',\n async resolve(_query, ctx: ExtensionContext) {\n const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, { strict: false }) as StateStore;\n const counts = await Promise.all(\n STATE_BREAKDOWN_STATUSES.map((status) => countByStatus(store, status)),\n );\n const segments = STATE_BREAKDOWN_STATUSES.map((label, i) => ({\n label,\n value: counts[i],\n color: STATE_BREAKDOWN_PALETTE[i],\n }));\n return { segments };\n },\n };\n}\n\n// NOTE: durableRetryHotspotsProvider is NOT implemented.\n// StateStore only exposes `listCheckpoints(runId: string)` (per-run lookup), with no cross-run\n// query by step `attempts`. There is no API to enumerate all step checkpoints across runs without\n// first listing every run — which would be O(N*M) and impractical. Task 13 should omit that panel.\n"],"mappings":";;;;AAAA,SAEEA,gBACAC,eACAC,2BACK;AAEP,SAASC,kBAAAA,iBAAgBC,WAAAA,UAASC,SAAAA,cAAa;;;ACP/C,SAASC,yBAAyB;AAClC,SAASC,mBAAmB;AAG5B,SAASC,gBAAgBC,WAAAA,UAASC,SAAAA,cAAa;;;ACJ/C,SAASC,kBAAkB;AAC3B,SAAuBC,YAAYC,SAASC,aAAa;AAqBlD,SAASC,WAAWC,OAAa;AAGtC,SAAOC,WAAW,QAAA,EAAUC,OAAO,eAAeF,KAAAA,EAAO,EAAEG,OAAO,KAAA,EAAOC,MAAM,GAAG,EAAA;AACpF;AAJgBL;AAOhB,SAASM,UAAUL,OAAa;AAC9B,SAAOC,WAAW,QAAA,EAAUC,OAAO,oBAAoBF,KAAAA,EAAO,EAAEG,OAAO,KAAA,EAAOC,MAAM,GAAG,EAAA;AACzF;AAFSC;AAaF,SAASC,gBAAgBN,OAAeO,SAAkBC,QAAQC,OAAM,GAAE;AAC/E,SAAOC,MAAMC,eAAeJ,QAAQ;IAClCK,SAASb,WAAWC,KAAAA;IACpBa,QAAQR,UAAUL,KAAAA;IAClBc,YAAYC,WAAWC;IACvBC,UAAU;EACZ,CAAA;AACF;AAPgBX;;;ADThB,IAAMY,cAAc,IAAIC,kBAAAA;AAGjB,SAASC,sBAAAA;AACd,SAAOF,YAAYG,SAAQ;AAC7B;AAFgBD;AAKhB,IAAME,uBAAuB;AAE7B,SAASC,SAASC,WAA2B;AAC3C,MAAIA,UAAUC,SAAS,OAAQ,QAAO,QAAQD,UAAUE,QAAQ,GAAA;AAChE,SAAO,YAAYF,UAAUG,YAAY,GAAA;AAC3C;AAHSJ;AAKT,SAASK,eAAeJ,WAA2B;AACjD,QAAMK,aAA8C;IAAE,kBAAkBL,UAAUM;EAAM;AACxF,MAAIN,UAAUG,aAAaI,OAAWF,YAAW,kBAAA,IAAsBL,UAAUG;AACjF,MAAIH,UAAUE,SAASK,OAAWF,YAAW,mBAAA,IAAuBL,UAAUE;AAC9E,MAAIF,UAAUQ,QAAQD,OAAWF,YAAW,kBAAA,IAAsBL,UAAUQ;AAC5E,MAAIR,UAAUS,YAAYF,OAAWF,YAAW,sBAAA,IAA0BL,UAAUS;AACpF,SAAOJ;AACT;AAPSD;AA4DF,SAASM,sBAAsBC,KAAmB;AACvD,QAAMC,SAASC,OAAMC,UAAU,oCAAA;AAC/B,QAAMC,QAAQ,CAAC;AAEf,SAAO,CAACf,WAAWgB,SAAAA;AAEjB,UAAMC,OAAOvB,YAAYG,SAAQ;AACjC,QAAIoB,MAAMF,UAAUA,SAASE,KAAKX,UAAUN,UAAUM,MAAO,QAAOU,KAAAA;AAGpE,QAAIL,IAAIO,OAAOC,YAAY,MAAO,QAAOH,KAAAA;AAEzC,QAAI;AACF,YAAMI,SAASC,gBAAgBrB,UAAUM,KAAK;AAC9C,YAAMgB,OAAOV,OAAOW,UAClBxB,SAASC,SAAAA,GACT;QAAEK,YAAYD,eAAeJ,SAAAA;MAAW,GACxCoB,MAAAA;AAKF,YAAMI,cAAcF,KAAKE,YAAW;AACpC,YAAMC,QAAsB;QAC1BnB,OAAON,UAAUM;QACjBoB,SAASC,WAAW3B,UAAUM,KAAK;QACnCsB,QAAQN,KAAKO,YAAW,IAAKL,YAAYI,SAASE,YAAY,CAAA,EAAGC,SAAS,KAAA;QAC1EhB;MACF;AAEA,aAAOiB,SAAQC,KAAKpB,OAAMqB,QAAQd,QAAQE,IAAAA,GAAO,MAC/C5B,YAAYyC,IAAIV,OAAO,MACrBd,IAAIyB,WAAWtC,sBAAsBkB,IAAAA,EAAMqB,KACzC,CAACC,UAAAA;AACChB,aAAKiB,IAAG;AACR,eAAOD;MACT,GACA,CAACE,UAAAA;AACClB,aAAKmB,UAAU;UACbC,MAAMC,eAAeC;UACrB,GAAIJ,iBAAiBK,QAAQ;YAAEC,SAASN,MAAMM;UAAQ,IAAI,CAAC;QAC7D,CAAA;AACAxB,aAAKiB,IAAG;AACR,cAAMC;MACR,CAAA,CAAA,CAAA;IAIR,QAAQ;AAKN,aAAOxB,KAAAA;IACT;EACF;AACF;AAxDgBN;AA+ET,SAASqC,oBAAoBC,UAA2B;AAC7D,SAAO;IACLC,UAAAA;AAGE,YAAMC,eAAeF,UAAUC,QAAAA,KAAa;AAC5C,UAAIC,aAAc,QAAOA;AACzB,YAAMzB,QAAQ/B,YAAYG,SAAQ;AAClC,aAAO4B,QAAQ;QAAEC,SAASD,MAAMC;QAASE,QAAQH,MAAMG;MAAO,IAAI;IACpE;EACF;AACF;AAXgBmB;;;AD7IT,IAAMI,0BAAN,MAAMA;EA9Cb,OA8CaA;;;EACFC,OAAO;;EAERC;EAERC,SAASC,KAA2B;AAKlC,SAAKF,eAAY;AACjB,SAAKA,eAAeG,oBAAoBC,sBAAsBF,GAAAA,CAAAA;AAE9D,UAAMG,SAASH,IAAII,UAAUC,IAAIC,gBAAgB;MAAEC,QAAQ;IAAM,CAAA;AAMjE,QAAI,OAAQJ,QAAoCK,cAAc,WAAY;AAC1E,UAAMC,SAASC,OAAMC,UAAU,oCAAA;AAE/BR,WAAOK,UAAU,CAACI,UAAAA;AAChB,YAAMC,SAAS,6BACbb,IAAIa,OAAO;QACThB,MAAM,KAAKA;;;;;;;QAOXiB,SAASC,WAAWH,MAAMI,KAAK;QAC/BC,SAAS;UACPL,OAAOA,MAAMf;UACbqB,UAAUN,MAAMM;UAChBF,OAAOJ,MAAMI;UACbG,aAAaC,cAAcR,MAAMI,KAAK;UACtCK,KAAKT,MAAMS;UACXC,MAAMV,MAAMU;UACZC,MAAMX,MAAMW;UACZC,QAAQZ,MAAMY;UACdC,OAAOb,MAAMa;UACbC,YAAYd,MAAMc;QACpB;QACAC,MAAM,KAAKA,KAAKf,KAAAA;MAClB,CAAA,GAvBa;AA4Bf,YAAMgB,OAAOC,gBAAgBjB,MAAMI,KAAK;AAKxC,UAAIJ,MAAMU,QAAQ,QAAQV,MAAMS,OAAO,MAAM;AAC3C,cAAMS,OAAOrB,OAAOsB,UAClB,QAAQnB,MAAMU,IAAI,IAClB;UACEU,YAAY;YACV,kBAAkBpB,MAAMI;YACxB,oBAAoBJ,MAAMS;YAC1B,qBAAqBT,MAAMW;UAC7B;QACF,GACAK,IAAAA;AAEF,YAAIhB,MAAMf,SAAS,eAAe;AAChC,gBAAMoC,UAAUrB,MAAMa,OAAOQ;AAC7BH,eAAKI,UACHD,YAAYE,SACR;YAAEC,MAAMC,gBAAeC;YAAOL;UAAQ,IACtC;YAAEG,MAAMC,gBAAeC;UAAM,CAAA;QAErC;AACAC,QAAAA,SAAQC,KAAK9B,OAAM+B,QAAQb,MAAME,IAAAA,GAAOjB,MAAAA;AACxCiB,aAAKY,IAAG;MACV,OAAO;AACLH,QAAAA,SAAQC,KAAKZ,MAAMf,MAAAA;MACrB;IACF,CAAA;EACF;;;;;;;EAQA8B,aAAmB;AACjB,SAAK7C,eAAY;AACjB,SAAKA,eAAeqC;EACtB;EAEQR,KAAKf,OAA8B;AACzC,UAAMe,OAAO;MAAC;MAAWf,MAAMf;MAAM,OAAOe,MAAMI,KAAK;;AAGvD,UAAM4B,SAASxB,cAAcR,MAAMI,KAAK;AACxC,QAAI4B,OAAQjB,MAAKkB,KAAK,UAAUD,MAAAA,EAAQ;AACxC,QAAIhC,MAAMM,SAAUS,MAAKkB,KAAK,YAAYjC,MAAMM,QAAQ,EAAE;AAC1D,QAAIN,MAAMW,KAAMI,MAAKkB,KAAK,QAAQjC,MAAMW,IAAI,EAAE;AAC9C,QAAIX,MAAMf,SAAS,aAAc8B,MAAKkB,KAAK,QAAA;AAC3C,WAAOlB;EACT;AACF;;;AGxJA,SAASmB,gCAAgC;;;ACUlC,SAASC,iBACdC,OAA8D,CAAC,GAAC;AAEhE,QAAMC,UAAUD,KAAKC,WAAW;AAChC,QAAMC,WAAWF,KAAKG,0BAA0B,KAAK,KAAK,KAAK;AAC/D,SAAO;IACLC,IAAI;IACJC,OAAO;IACPC,QAAQ,CAAA;IACRC,UAAU;MACR;QACEC,OAAO;QACPC,MAAM;QACNH,QAAQ;UACN;YACEI,MAAM;YACNF,OAAO;YACPG,MAAM;cAAEC,UAAU;YAAsB;YACxCC,KAAK;YACLC,QAAQ;YACRC,YAAY;cAAEC,MAAM;cAAMC,KAAK;cAAMC,WAAW;YAAW;UAC7D;UACA;YACER,MAAM;YACNF,OAAO;YACPG,MAAM;cAAEC,UAAU;cAAoBO,OAAO;gBAAEC,QAAQ;cAAM;YAAE;YAC/DN,QAAQ;YACRO,OAAO;YACPN,YAAY;cAAEC,MAAM;cAAMC,KAAK;cAAMC,WAAW;YAAS;UAC3D;UACA;YACER,MAAM;YACNF,OAAO;YACPG,MAAM;cAAEC,UAAU;cAAiBO,OAAO;gBAAEG,QAAQ;cAAU;YAAE;YAChED,OAAO;YACPN,YAAY;cAAEC,MAAM;cAAIC,KAAK;cAAKC,WAAW;YAAS;UACxD;UACA;YACER,MAAM;YACNF,OAAO;YACPG,MAAM;cAAEC,UAAU;YAAqB;YACvCE,QAAQ;YACRO,OAAO;UACT;;MAEJ;MACA;QACEb,OAAO;QACPC,MAAM;QACNH,QAAQ;UACN;YACEI,MAAM;YACNF,OAAO;YACPG,MAAM;cAAEC,UAAU;cAAsBO,OAAO;gBAAEC,QAAQ;cAAc;YAAE;YACzEG,OAAO;UACT;UACA;YACEb,MAAM;YACNF,OAAO;YACPG,MAAM;cAAEC,UAAU;cAA0BO,OAAO;gBAAEjB;cAAS;YAAE;YAChEsB,SAAS;cACP;gBAAEC,KAAK;gBAAapB,OAAO;cAAU;cACrC;gBAAEoB,KAAK;gBAAYpB,OAAO;cAAW;cACrC;gBAAEoB,KAAK;gBAASpB,OAAO;gBAAOqB,MAAM;kBAAEC,MAAM1B;gBAAQ;cAAE;cACtD;gBAAEwB,KAAK;gBAASpB,OAAO;cAAQ;;UAEnC;UACA;YACEK,MAAM;;;;YAINF,OAAO;YACPG,MAAM;cAAEC,UAAU;YAAuB;YACzCY,SAAS;cACP;gBAAEC,KAAK;gBAASpB,OAAO;cAAQ;cAC/B;gBAAEoB,KAAK;gBAAUpB,OAAO;cAAS;cACjC;gBAAEoB,KAAK;gBAAepB,OAAO;cAAU;cACvC;gBAAEoB,KAAK;gBAAUpB,OAAO;cAAS;;UAErC;;MAEJ;MACA;QACEG,OAAO;;;;;QAKPC,MAAM;QACNH,QAAQ;UACN;YACEI,MAAM;;;;;;;;;;YAUNF,OAAO;YACPG,MAAM;cAAEC,UAAU;YAAuB;YACzCY,SAAS;cACP;gBAAEC,KAAK;gBAASpB,OAAO;cAAQ;cAC/B;gBAAEoB,KAAK;gBAAcpB,OAAO;cAAS;cACrC;gBAAEoB,KAAK;gBAAQpB,OAAO;cAAO;;;cAG7B;gBAAEoB,KAAK;gBAAcpB,OAAO;cAAY;cACxC;gBAAEoB,KAAK;gBAAUpB,OAAO;cAAS;cACjC;gBAAEoB,KAAK;gBAAUpB,OAAO;cAAQ;cAChC;gBAAEoB,KAAK;gBAAUpB,OAAO;cAAQ;cAChC;gBAAEoB,KAAK;gBAAoBpB,OAAO;cAAY;cAC9C;gBAAEoB,KAAK;gBAASpB,OAAO;cAAS;cAChC;gBAAEoB,KAAK;gBAAcpB,OAAO;cAAc;;UAE9C;;MAEJ;MACA;QACEG,OAAO;QACPC,MAAM;QACNH,QAAQ;UACN;YACEI,MAAM;YACNF,OAAO;YACPG,MAAM;cAAEC,UAAU;YAAuB;YACzCgB,QAAQ;cAAC;cAAQ;;YACjBC,OAAO;UACT;UACA;YACEnB,MAAM;YACNF,OAAO;YACPG,MAAM;cAAEC,UAAU;YAAmB;YACrCkB,SAAS;cAAC;cAAO;cAAO;;YACxBhB,QAAQ;UACV;UACA;YACEJ,MAAM;YACNF,OAAO;YACPG,MAAM;cAAEC,UAAU;YAAyB;YAC3CiB,OAAO;UACT;;MAEJ;;EAEJ;AACF;AArJgB9B;;;ACVhB,SAASgC,uBAAuBC,kBAAAA,uBAAsB;AAGtD,SAASC,yBAAyB;AAKlC,SAASC,WAAWC,QAAkBC,GAAS;AAC7C,MAAID,OAAOE,WAAW,EAAG,QAAO;AAChC,QAAMC,MAAMC,KAAKC,IAAIL,OAAOE,SAAS,GAAGE,KAAKE,KAAML,IAAI,MAAOD,OAAOE,MAAM,IAAI,CAAA;AAC/E,SAAOF,OAAOI,KAAKG,IAAI,GAAGJ,GAAAA,CAAAA,KAAS;AACrC;AAJSJ;AAST,eAAeS,aAAaC,KAAqB;AAC/C,QAAMC,UAAUD,IAAIE,UAAUC,IAAIC,mBAAmB;IAAEC,QAAQ;EAAM,CAAA;AAGrE,QAAMC,OAAO,MAAML,QAAQE,IAAI;IAAEI,MAAM;IAAWC,OAAO;EAAM,CAAA;AAC/D,SAAOF,KAAKG;AACd;AANeV;AAaf,SAASW,aACPC,SACAC,UACAC,KAAW;AAEX,QAAMC,QAAQD,MAAMD;AACpB,QAAMG,YAAYD,QAAQF;AAC1B,SAAO;IACLI,SAASL,QAAQM,OAAO,CAACC,MAAAA;AACvB,YAAMC,IAAID,EAAEE,YAAY,CAAC,IAAIC,KAAKH,EAAEE,SAAS,IAAI;AACjD,aAAOD,IAAIL,SAASK,KAAKN;IAC3B,CAAA;IACAS,UAAUX,QAAQM,OAAO,CAACC,MAAAA;AACxB,YAAMC,IAAID,EAAEE,YAAY,CAAC,IAAIC,KAAKH,EAAEE,SAAS,IAAI;AACjD,aAAOD,IAAIJ,aAAaI,KAAKL;IAC/B,CAAA;EACF;AACF;AAjBSJ;AAoBT,SAASa,cAAcZ,SAAuB;AAC5C,MAAIa,YAAY;AAChB,MAAIC,SAAS;AACb,aAAWP,KAAKP,SAAS;AACvB,UAAMe,IAAKR,EAAES,WAAW,CAAC;AACzB,QAAID,EAAEE,UAAU,gBAAiBJ,cAAa;aACrCE,EAAEE,UAAU,aAAcH,WAAU;EAC/C;AACA,QAAMI,QAAQL,YAAYC;AAC1B,SAAOI,UAAU,IAAI,IAAIL,YAAYK;AACvC;AAVSN;AAaT,IAAMO,uBAAuB,oBAAIC,IAAI;EACnC;EACA;EACA;EACA;CACD;AAcD,SAASC,gBAAgBrB,SAAuB;AAC9C,QAAMsB,OAAO,oBAAIF,IAAAA;AACjB,QAAMG,MAAsB,CAAA;AAC5B,aAAWhB,KAAKP,SAAS;AACvB,UAAMe,IAAKR,EAAES,WAAW,CAAC;AACzB,QAAID,EAAEE,SAASF,EAAES,SAASL,qBAAqBM,IAAIV,EAAEE,KAAK,GAAG;AAC3D,YAAMS,MAAM,GAAGX,EAAEE,KAAK,IAAIF,EAAES,KAAK;AACjC,UAAIF,KAAKG,IAAIC,GAAAA,EAAM;AACnBJ,WAAKK,IAAID,GAAAA;IACX;AACAH,QAAIK,KAAKrB,CAAAA;EACX;AACA,SAAOgB;AACT;AAbSF;AAeT,IAAMQ,YAAY;AAQlB,SAASC,cAAcC,OAAmBC,QAAiB;AACzD,MAAID,MAAME,UAAW,QAAOF,MAAME,UAAU;IAAED;EAAO,CAAA;AACrD,SAAOD,MAAMG,SAAS;IAAEF;IAAQnC,OAAOgC;EAAU,CAAA,EAAGM,KAAK,CAACC,SAASA,KAAKtD,MAAM;AAChF;AAHSgD;AAQF,SAASO,uBAAAA;AACd,SAAO;IACLC,MAAM;IACN,MAAMC,QAAQC,OAAOnD,KAAqB;AACxC,YAAM0C,QAAQ1C,IAAIE,UAAUC,IAAIiD,uBAAuB;QAAE/C,QAAQ;MAAM,CAAA;AACvE,YAAMsC,SAAUQ,OAAOR,UAAwB;AAC/C,aAAO;QAAEU,OAAO,MAAMZ,cAAcC,OAAOC,MAAAA;MAAQ;IACrD;EACF;AACF;AATgBK;AAgBT,SAASM,4BAAAA;AACd,SAAO;IACLL,MAAM;IACN,MAAMC,QAAQC,OAAOnD,KAAqB;AAExC,YAAMC,UAAUD,IAAIE,UAAUC,IAAIC,mBAAmB;QAAEC,QAAQ;MAAM,CAAA;AAKrE,YAAMG,QAAQb,KAAKC,IAAI,KAAOD,KAAKG,IAAI,KAAKyD,OAAOJ,OAAO3C,SAAS,GAAA,CAAA,CAAA;AACnE,YAAMF,OAAO,MAAML,QAAQE,IAAI;QAAEI,MAAM;QAAWC;MAAM,CAAA;AAExD,UAAIgB,YAAY;AAChB,UAAIC,SAAS;AACb,YAAM+B,iBAAiB,oBAAIC,IAAAA;AAE3B,iBAAWvC,KAAKc,gBAAgB1B,KAAKG,IAAI,GAAG;AAC1C,cAAMiB,IAAKR,EAAES,WAAW,CAAC;AACzB,YAAID,EAAEE,UAAU,gBAAiBJ,cAAa;iBACrCE,EAAEE,UAAU,cAAc;AACjCH,oBAAU;AACV,gBAAMiC,KAAKhC,EAAEiC,YAAY;AACzBH,yBAAeI,IAAIF,KAAKF,eAAerD,IAAIuD,EAAAA,KAAO,KAAK,CAAA;QACzD;MACF;AACA,YAAM7B,QAAQL,YAAYC;AAC1B,YAAMoC,SAAUV,OAAOU,UAAqB;AAC5C,UAAIA,WAAW,cAAe,QAAO;QAAER,OAAOxB,UAAU,IAAI,IAAIL,YAAYK;MAAM;AAClF,UAAIgC,WAAW,SAAU,QAAO;QAAER,OAAO5B;MAAO;AAChD,UAAIoC,WAAW,QAAS,QAAO;QAAER,OAAOxB;MAAM;AAC9C,UAAIgC,WAAW,eAAe;AAC5B,cAAMC,QAAQ;aAAIN,eAAe7C,QAAO;UACrCoD,IAAI,CAAC,CAACC,OAAOX,KAAAA,OAAY;UAAEW;UAAOX;QAAM,EAAA,EACxCY,KAAK,CAACC,GAAGC,MAAMA,EAAEd,QAAQa,EAAEb,KAAK;AACnC,eAAO;UAAES;QAAM;MACjB;AACA,aAAO;QAAET,OAAOxB;MAAM;IACxB;EACF;AACF;AAxCgByB;AAiDT,SAASc,gCAAAA;AACd,SAAO;IACLnB,MAAM;IACN,MAAMC,QAAQC,OAAOnD,KAAqB;AACxC,YAAM0C,QAAQ1C,IAAIE,UAAUC,IAAIiD,uBAAuB;QAAE/C,QAAQ;MAAM,CAAA;AACvE,YAAMG,QAAQb,KAAKC,IAAI,KAAKD,KAAKG,IAAI,IAAIyD,OAAOJ,OAAO3C,SAAS,EAAA,CAAA,CAAA;AAChE,YAAMI,WAAWuC,OAAOvC,aAAayD,SAAY,KAAK,KAAK,KAAK,MAAOd,OAAOJ,MAAMvC,QAAQ;AAC5F,YAAM0D,SAAS1D,WAAW,IAAIS,KAAKR,IAAG,IAAKD,WAAW;AACtD,YAAM,CAACa,QAAQ8C,IAAAA,IAAQ,MAAMC,QAAQC,IAAI;QACvC/B,MAAMG,SAAS;UAAEF,QAAQ;UAAUnC;QAAM,CAAA;QACzCkC,MAAMG,SAAS;UAAEF,QAAQ;UAAQnC;QAAM,CAAA;OACxC;AACD,YAAMkE,OAAO;WAAIjD;WAAW8C;QACzBtD,OAAO,CAAC0D,MAAM,CAAC,IAAItD,KAAKsD,EAAEC,SAAS,KAAKN,MAAAA,EACxCL,KAAK,CAACC,GAAGC,MAAM,CAAC,IAAI9C,KAAK8C,EAAES,SAAS,IAAI,CAAC,IAAIvD,KAAK6C,EAAEU,SAAS,CAAA,EAC7DC,MAAM,GAAGrE,KAAAA,EACTuD,IAAI,CAACY,OAAO;;;QAGXC,WAAW,GAAG,IAAIvD,KAAKsD,EAAEC,SAAS,EAAEE,YAAW,EAAGC,QAAQ,KAAK,GAAA,EAAKF,MAAM,GAAG,EAAA,CAAA;QAC7ElB,UAAUgB,EAAEhB;QACZxB,OAAOwC,EAAEK;QACTC,OAAON,EAAEM,OAAOC,WAAW;MAC7B,EAAA;AACF,aAAO;QAAER;MAAK;IAChB;EACF;AACF;AA3BgBN;AAmCT,SAASe,8BAAAA;AACd,SAAO;IACLlC,MAAM;IACN,MAAMC,QAAQC,OAAOnD,KAAqB;AACxC,YAAMoF,SAASpF,IAAIE,UAAUC,IAAIkF,iBAAgB;QAAEhF,QAAQ;MAAM,CAAA;AACjE,YAAMiF,SAAS,MAAMF,OAAOG,aAAY;AACxC,YAAMC,YAAY,wBAACC,MACjBA,EAAEC,QAAQ,KAAKD,EAAEE,YAAYlG,WAAW,GADxB;AAElB,UAAK0D,OAAOU,WAAsB,gBAAgB;AAChD,eAAO;UAAER,OAAOiC,OAAOrE,OAAOuE,SAAAA,EAAW/F;QAAO;MAClD;AACA,YAAMiF,OAAOY,OACVT,MAAK,EAELZ,KAAK,CAACC,GAAGC,MAAMZ,OAAOiC,UAAUrB,CAAAA,CAAAA,IAAMZ,OAAOiC,UAAUtB,CAAAA,CAAAA,KAAOC,EAAEuB,QAAQxB,EAAEwB,KAAK,EAC/E3B,IAAI,CAAC0B,OAAO;QACXG,OAAOH,EAAEG;QACTC,QAAQJ,EAAEC;QACVC,aAAaF,EAAEE,YAAYlG;QAC3BkD,QAAQ6C,UAAUC,CAAAA,IAAK,YAAY;MACrC,EAAA;AACF,aAAO;QAAEf;MAAK;IAChB;EACF;AACF;AAxBgBS;AA2BhB,SAASW,OAAOC,MAAY;AAC1B,QAAMC,IAAIrG,KAAKG,IAAI,GAAGH,KAAKsG,OAAO5E,KAAKR,IAAG,IAAKkF,QAAQ,GAAA,CAAA;AACvD,MAAIC,IAAI,GAAI,QAAO,GAAGA,CAAAA;AACtB,QAAME,IAAIvG,KAAKsG,MAAMD,IAAI,EAAA;AACzB,MAAIE,IAAI,GAAI,QAAO,GAAGA,CAAAA;AACtB,QAAMC,IAAIxG,KAAKsG,MAAMC,IAAI,EAAA;AACzB,MAAIC,IAAI,GAAI,QAAO,GAAGA,CAAAA;AACtB,SAAO,GAAGxG,KAAKsG,MAAME,IAAI,EAAA,CAAA;AAC3B;AARSL;AAsBT,IAAMM,eAAe;AACrB,IAAMC,mBAAmB;AASzB,SAASC,cAAc3D,QAAgC;AACrD,MAAI,CAACA,OAAQ,QAAOyD;AACpB,QAAMlC,IAAIvB,OAAO4D;AACjB,MAAI,CAACrC,EAAG,QAAOmC;AACf,SAAO,GAAGnC,EAAEsC,MAAM,IAAItC,EAAEuC,IAAI,SAAIvC,EAAEwC,EAAE,IAAIZ,OAAO5B,EAAEyC,EAAE,CAAA;AACrD;AALSL;AAQT,SAASM,OAAOC,GAAuBlE,QAAgC;AACrE,MAAI,OAAOkE,MAAM,SAAU,QAAOlH,KAAKsG,MAAMY,CAAAA;AAC7C,SAAOlE,SAAS0D,mBAAmBD;AACrC;AAHSQ;AAMT,SAASE,OAAOD,GAAuBlE,QAAgC;AACrE,MAAI,OAAOkE,MAAM,SAAU,QAAOlH,KAAKsG,MAAMY,CAAAA;AAC7C,SAAOlE,SAAS0D,mBAAmBD;AACrC;AAHSU;AAYF,SAASC,8BAAAA;AACd,SAAO;IACL9D,MAAM;IACN,MAAMC,QAAQ8D,QAAQhH,KAAqB;AACzC,YAAMoF,SAASpF,IAAIE,UAAUC,IAAIkF,iBAAgB;QAAEhF,QAAQ;MAAM,CAAA;AACjE,YAAMiF,SAAS,MAAMF,OAAOG,aAAY;AACxC,YAAMb,OAAOY,OACVT,MAAK,EAELZ,KAAK,CAACC,GAAGC,MAAMZ,OAAOY,EAAEuB,QAAQ,CAAA,IAAKnC,OAAOW,EAAEwB,QAAQ,CAAA,KAAMxB,EAAE0B,MAAMqB,cAAc9C,EAAEyB,KAAK,CAAA,EACzFsB,QAAQ,CAACtB,UACRA,MAAMD,YACHd,MAAK,EACLZ,KAAK,CAACC,GAAGC,MAAMD,EAAEiD,WAAWF,cAAc9C,EAAEgD,UAAU,CAAA,EACtDpD,IAAI,CAACqD,WAAAA;AACJ,cAAMzE,SAASyE,OAAOzE;AACtB,cAAM0E,cAAc1E,QAAQ0E;AAG5B,cAAMC,OAAOD,aAAaC,QAAQlB;AAClC,cAAM5F,QAAQ6G,aAAa7G,SAAS4F;AACpC,cAAMmB,WAAW5E,QAAQ4E,YAAYnB;AACrC,cAAMoB,SACJH,aAAaC,SAAS,aAClB,GAAGD,YAAYzH,OAAO,CAAA,SAAKyH,YAAYvH,OAAO,EAAA,KAC9CsG;AACN,cAAMqB,aACJJ,eAAe1E,SAAS,GAAGA,OAAO4E,QAAQ,IAAIF,YAAY7G,KAAK,KAAK4F;AACtE,eAAO;UACLR,OAAOA,MAAMA;UACbuB,YAAYC,OAAOD;UACnBG;UACA9G;UACAgH;UACAD;UACAE;UACA5B,QAAQD,MAAMF;UACdgC,QAAQd,OAAOjE,QAAQ+E,QAAQ/E,MAAAA;UAC/BgF,QAAQf,OAAOjE,QAAQgF,QAAQhF,MAAAA;UAC/BiF,kBAAkBd,OAAOnE,QAAQiF,kBAAkBjF,MAAAA;UACnDkF,OAAOf,OAAOnE,QAAQkF,OAAOlF,MAAAA;UAC7B4D,YAAYD,cAAc3D,MAAAA;QAC5B;MACF,CAAA,CAAA;AAEN,aAAO;QAAE+B;MAAK;IAChB;EACF;AACF;AAhDgBqC;AA4DT,SAASe,0BAAAA;AACd,SAAO;IACL7E,MAAM;IACN,MAAMC,QAAQC,OAAOnD,KAAqB;AAExC,YAAMW,UAAUqB,gBAAgB,MAAMjC,aAAaC,GAAAA,CAAAA;AAGnD,YAAM+H,YAAY,oBAAItE,IAAAA;AACtB,iBAAWvC,KAAKP,SAAS;AACvB,cAAMe,IAAKR,EAAES,WAAW,CAAC;AACzB,YAAID,EAAEE,UAAU,iBAAiBF,EAAES,SAASjB,EAAEE,WAAW;AACvD2G,oBAAUnE,IAAIlC,EAAES,OAAO,CAAC,IAAId,KAAKH,EAAEE,SAAS,CAAA;QAC9C;MACF;AAEA,YAAM4G,OAAiB,CAAA;AACvB,iBAAW9G,KAAKP,SAAS;AACvB,cAAMe,IAAKR,EAAES,WAAW,CAAC;AACzB,YAAID,EAAEE,UAAU,mBAAmBF,EAAEE,UAAU,cAAc;AAC3D,cAAI,OAAOF,EAAEuG,eAAe,UAAU;AACpCD,iBAAKzF,KAAKb,EAAEuG,UAAU;UACxB,WAAWvG,EAAES,OAAO;AAClB,kBAAMrB,QAAQiH,UAAU5H,IAAIuB,EAAES,KAAK;AACnC,kBAAM+F,MAAMhH,EAAEE,YAAY,CAAC,IAAIC,KAAKH,EAAEE,SAAS,IAAIiD;AACnD,gBAAIvD,UAAUuD,UAAa6D,QAAQ7D,UAAa6D,OAAOpH,OAAO;AAC5DkH,mBAAKzF,KAAK2F,MAAMpH,KAAAA;YAClB;UACF;QACF;MACF;AACAkH,WAAK/D,KAAK,CAACC,GAAGC,MAAMD,IAAIC,CAAAA;AAExB,YAAMgE,MAAM7I,WAAW0I,MAAM,EAAA;AAC7B,YAAMI,MAAM9I,WAAW0I,MAAM,EAAA;AAC7B,YAAMK,MAAM/I,WAAW0I,MAAM,EAAA;AAG7B,YAAMnE,SAAUV,OAAmCU;AACnD,UAAIA,WAAW,MAAO,QAAO;QAAER,OAAO8E;MAAI;AAC1C,UAAItE,WAAW,MAAO,QAAO;QAAER,OAAO+E;MAAI;AAC1C,UAAIvE,WAAW,MAAO,QAAO;QAAER,OAAOgF;MAAI;AAG1C,YAAMvI,MAAMkI,KAAKrB,GAAG,EAAC,KAAM;AAC3B,YAAM2B,cAAc;AACpB,YAAMC,OAAO5I,KAAKG,IAAI,GAAGH,KAAKE,MAAMC,MAAM,KAAKwI,WAAAA,CAAAA;AAC/C,YAAME,UAAUC,MAAMhC,KAAK;QAAEhH,QAAQ6I;MAAY,GAAG,CAACI,GAAGC,OAAO;QAC7D3E,OAAO,GAAGrE,KAAKsG,MAAO0C,IAAIJ,OAAQ,GAAA,IAAO,EAAA;QACzCK,OAAO;MACT,EAAA;AACA,iBAAWC,KAAKb,MAAM;AACpB,cAAMc,KAAKnJ,KAAKC,IAAI0I,cAAc,GAAG3I,KAAKoJ,MAAMF,IAAIN,IAAAA,CAAAA;AACpD,cAAMS,SAASR,QAAQM,EAAAA;AACvB,YAAIE,OAAQA,QAAOJ,SAAS;MAC9B;AAEA,aAAO;QAAEJ;QAASL;QAAKC;QAAKC;MAAI;IAClC;EACF;AACF;AA5DgBP;AAkET,SAASmB,8BAAAA;AACd,SAAO;IACLhG,MAAM;IACN,MAAMC,QAAQC,OAAOnD,KAAqB;AACxC,YAAMW,UAAUqB,gBAAgB,MAAMjC,aAAaC,GAAAA,CAAAA;AACnD,YAAM6G,IAAItD,OAAQJ,OAAmCqF,WAAW,EAAA;AAChE,YAAM3H,MAAMQ,KAAKR,IAAG;AAGpB,UAAIqI,OAAOrI;AACX,iBAAWK,KAAKP,SAAS;AACvB,YAAIO,EAAEE,WAAW;AACf,gBAAMD,IAAI,CAAC,IAAIE,KAAKH,EAAEE,SAAS;AAC/B,cAAID,IAAI+H,KAAMA,QAAO/H;QACvB;MACF;AACA,YAAMgI,OAAOxJ,KAAKG,IAAIe,MAAMqI,MAAM,CAAA;AAClC,YAAME,aAAaD,OAAOtC;AAE1B,YAAMnC,OAA+D+D,MAAMhC,KACzE;QAAEhH,QAAQoH;MAAE,GACZ,CAAC6B,GAAGC,MAAAA;AACF,cAAMU,cAAc,IAAIhI,KAAK6H,OAAOP,IAAIS,UAAAA;AACxC,cAAMpF,QAAQqF,YAAYvE,YAAW,EAAGD,MAAM,IAAI,EAAA;AAClD,eAAO;UAAEb;UAAOsF,MAAM;UAAG7H,QAAQ;QAAE;MACrC,CAAA;AAGF,iBAAWP,KAAKP,SAAS;AACvB,cAAMe,IAAKR,EAAES,WAAW,CAAC;AACzB,YAAID,EAAEE,UAAU,mBAAmBF,EAAEE,UAAU,aAAc;AAC7D,cAAMT,IAAID,EAAEE,YAAY,CAAC,IAAIC,KAAKH,EAAEE,SAAS,IAAI;AACjD,cAAM1B,MAAMC,KAAKC,IAAIiH,IAAI,GAAGlH,KAAKoJ,OAAO5H,IAAI+H,QAAQE,UAAAA,CAAAA;AACpD,cAAMG,MAAM7E,KAAKhF,GAAAA;AACjB,YAAI6J,KAAK;AACP,cAAI7H,EAAEE,UAAU,gBAAiB2H,KAAID,QAAQ;cACxCC,KAAI9H,UAAU;QACrB;MACF;AAEA,aAAO;QAAEiD;MAAK;IAChB;EACF;AACF;AA3CgBuE;AAiDT,SAASO,6BAAAA;AACd,SAAO;IACLvG,MAAM;IACN,MAAMC,QAAQC,OAAOnD,KAAqB;AACxC,YAAMW,UAAUqB,gBAAgB,MAAMjC,aAAaC,GAAAA,CAAAA;AACnD,YAAMY,WAAW2C,OAAQJ,OAAmCvC,YAAY,KAAK,KAAK,KAAK,GAAA;AACvF,YAAMC,MAAMQ,KAAKR,IAAG;AACpB,YAAM,EAAEG,SAASM,SAAQ,IAAKZ,aAAaC,SAASC,UAAUC,GAAAA;AAE9D,YAAMwC,QAAQ9B,cAAcP,OAAAA;AAC5B,YAAMyI,WAAWlI,cAAcD,QAAAA;AAC/B,YAAMoI,QAAQpI,SAAS7B,SAAS,IAAI4D,QAAQoG,WAAWpF;AAGvD,YAAMsF,eAAe;AACrB,YAAMP,aAAaxI,WAAW+I;AAC9B,YAAMC,aAAa/I,MAAMD;AACzB,YAAMiJ,QAAQpB,MAAMhC,KAAK;QAAEhH,QAAQkK;MAAa,GAAG,CAACjB,GAAGC,MAAAA;AACrD,cAAMmB,SAASF,aAAajB,IAAIS;AAChC,cAAMW,OAAOD,SAASV;AACtB,cAAMY,WAAWhJ,QAAQC,OAAO,CAACC,MAAAA;AAC/B,gBAAMC,IAAID,EAAEE,YAAY,CAAC,IAAIC,KAAKH,EAAEE,SAAS,IAAI;AACjD,iBAAOD,IAAI2I,UAAU3I,KAAK4I;QAC5B,CAAA;AACA,eAAOxI,cAAcyI,QAAAA;MACvB,CAAA;AAEA,aAAO;QAAE3G;QAAOqG;QAAOG;MAAM;IAC/B;EACF;AACF;AA9BgBL;AAoCT,SAASS,4BAAAA;AACd,SAAO;IACLhH,MAAM;IACN,MAAMC,QAAQC,OAAOnD,KAAqB;AACxC,YAAMW,UAAUqB,gBAAgB,MAAMjC,aAAaC,GAAAA,CAAAA;AACnD,YAAMY,WAAW2C,OAAQJ,OAAmCvC,YAAY,KAAK,KAAK,KAAK,GAAA;AACvF,YAAMC,MAAMQ,KAAKR,IAAG;AACpB,YAAM,EAAEG,SAASM,SAAQ,IAAKZ,aAAaC,SAASC,UAAUC,GAAAA;AAE9D,YAAMqJ,iBAAiB,wBAACC,OACtBA,GAAGlJ,OAAO,CAACC,OAAQA,EAAES,WAAW,CAAC,GAA0BC,UAAU,eAAA,EAClEnC,QAFkB;AAIvB,YAAM2K,cAAcxJ,YAAY,KAAK,KAAK;AAC1C,YAAMyC,QAAQ6G,eAAelJ,OAAAA,IAAWoJ;AACxC,YAAMC,YAAYH,eAAe5I,QAAAA,IAAY8I;AAC7C,YAAMV,QAAQpI,SAAS7B,SAAS,IAAI4D,QAAQgH,YAAYhG;AAGxD,YAAMsF,eAAe;AACrB,YAAMP,aAAaxI,WAAW+I;AAC9B,YAAMW,cAAclB,cAAc,KAAK,KAAK;AAC5C,YAAMQ,aAAa/I,MAAMD;AACzB,YAAMiJ,QAAQpB,MAAMhC,KAAK;QAAEhH,QAAQkK;MAAa,GAAG,CAACjB,GAAGC,MAAAA;AACrD,cAAMmB,SAASF,aAAajB,IAAIS;AAChC,cAAMW,OAAOD,SAASV;AACtB,cAAMY,WAAWhJ,QAAQC,OAAO,CAACC,MAAAA;AAC/B,gBAAMC,IAAID,EAAEE,YAAY,CAAC,IAAIC,KAAKH,EAAEE,SAAS,IAAI;AACjD,iBAAOD,IAAI2I,UAAU3I,KAAK4I;QAC5B,CAAA;AACA,eAAOG,eAAeF,QAAAA,IAAYM;MACpC,CAAA;AAEA,aAAO;QAAEjH;QAAOqG;QAAOG;MAAM;IAC/B;EACF;AACF;AApCgBI;AAyChB,IAAMM,2BAAwC;EAC5C;EACA;EACA;EACA;EACA;EACA;;AAGF,IAAMC,0BAA0B;EAC9B;EACA;EACA;EACA;EACA;EACA;;AAOK,SAASC,gCAAAA;AACd,SAAO;IACLxH,MAAM;IACN,MAAMC,QAAQ8D,QAAQhH,KAAqB;AACzC,YAAM0C,QAAQ1C,IAAIE,UAAUC,IAAIiD,uBAAuB;QAAE/C,QAAQ;MAAM,CAAA;AACvE,YAAMqK,SAAS,MAAMlG,QAAQC,IAC3B8F,yBAAyBxG,IAAI,CAACpB,WAAWF,cAAcC,OAAOC,MAAAA,CAAAA,CAAAA;AAEhE,YAAMgI,WAAWJ,yBAAyBxG,IAAI,CAACC,OAAO2E,OAAO;QAC3D3E;QACAX,OAAOqH,OAAO/B,CAAAA;QACdiC,OAAOJ,wBAAwB7B,CAAAA;MACjC,EAAA;AACA,aAAO;QAAEgC;MAAS;IACpB;EACF;AACF;AAhBgBF;;;AF3iBT,SAASI,0BACdC,OAA8D,CAAC,GAAC;AAEhE,SAAOC,yBAAyB;IAC9BC,MAAM;IACNC,UAAU,6BAAM;MAAC,IAAIC,wBAAAA;OAAX;IACVC,YAAY,6BAAM;MAAC;QAAEC,IAAI;QAAWC,OAAO;QAAaC,KAAK;MAAe;OAAhE;IACZC,YAAY,6BAAM;MAACC,iBAAiBV,IAAAA;OAAxB;IACZW,eAAe,6BAAM;MACnBC,qBAAAA;MACAC,0BAAAA;MACAC,8BAAAA;MACAC,4BAAAA;MACAC,4BAAAA;MACAC,wBAAAA;MACAC,4BAAAA;MACAC,2BAAAA;MACAC,0BAAAA;MACAC,8BAAAA;OAVa;EAYjB,CAAA;AACF;AArBgBtB;","names":["WorkflowEngine","parentRunIdOf","useDurableExecution","SpanStatusCode","context","trace","AsyncLocalStorage","randomBytes","SpanStatusCode","context","trace","createHash","TraceFlags","context","trace","runTraceId","runId","createHash","update","digest","slice","runSpanId","runTraceContext","parent","context","active","trace","setSpanContext","traceId","spanId","traceFlags","TraceFlags","SAMPLED","isRemote","activeScope","AsyncLocalStorage","currentDurableScope","getStore","DURABLE_BATCH_ORIGIN","spanName","execution","unit","name","workflow","spanAttributes","attributes","runId","undefined","seq","attempt","durableExecutionScope","ctx","tracer","trace","getTracer","owner","next","open","config","enabled","parent","runTraceContext","span","startSpan","spanContext","scope","traceId","runTraceId","spanId","isRecording","randomBytes","toString","context","with","setSpan","run","runInBatch","then","value","end","error","setStatus","code","SpanStatusCode","ERROR","Error","message","durableTraceContext","delegate","current","fromDelegate","DurableTelescopeWatcher","type","disposeScope","register","ctx","useDurableExecution","durableExecutionScope","engine","moduleRef","get","WorkflowEngine","strict","subscribe","tracer","trace","getTracer","event","record","traceId","runTraceId","runId","content","workflow","parentRunId","parentRunIdOf","seq","name","kind","output","error","durationMs","tags","base","runTraceContext","step","startSpan","attributes","message","setStatus","undefined","code","SpanStatusCode","ERROR","context","with","setSpan","end","unregister","parent","push","defineTelescopeExtension","durableDashboard","opts","runHref","windowMs","recentFailuresWindowMs","id","label","panels","sections","title","cols","kind","data","provider","max","format","thresholds","warn","bad","direction","query","metric","spark","status","limit","columns","key","link","href","series","style","markers","STATE_STORE_CANONICAL","WorkflowEngine","TELESCOPE_STORAGE","percentile","sorted","p","length","idx","Math","min","ceil","max","fetchEntries","ctx","storage","moduleRef","get","TELESCOPE_STORAGE","strict","page","type","limit","data","splitWindows","entries","windowMs","now","start","prevStart","current","filter","e","t","createdAt","Date","previous","successRateOf","completed","failed","c","content","event","total","RUN_LIFECYCLE_EVENTS","Set","dedupeRunEvents","seen","out","runId","has","key","add","push","STATE_CAP","countByStatus","store","status","countRuns","listRuns","then","runs","durableStateProvider","name","resolve","query","STATE_STORE_CANONICAL","value","durableTimeseriesProvider","Number","failByWorkflow","Map","wf","workflow","set","metric","items","map","label","sort","a","b","durableRecentFailuresProvider","undefined","cutoff","dead","Promise","all","rows","r","updatedAt","slice","toISOString","replace","id","error","message","durableWorkerHealthProvider","engine","WorkflowEngine","health","workerHealth","isStarved","g","depth","liveWorkers","group","queued","relAgo","atMs","s","round","m","h","NOT_REPORTED","NOT_YET_MEASURED","fmtLastAdjust","lastAdjust","reason","from","to","at","fmtPct","n","fmtNum","durableWorkerStatusProvider","_query","localeCompare","flatMap","instanceId","worker","concurrency","mode","inFlight","minMax","saturation","rssPct","cpuPct","throughputPerMin","p95Ms","durableDurationProvider","startedAt","durs","durationMs","end","p50","p95","p99","bucketCount","size","buckets","Array","_","i","count","d","bi","floor","bucket","durableRunsOverTimeProvider","minT","span","bucketSize","bucketStart","done","row","durableSuccessRateProvider","prevRate","delta","sparkBuckets","sparkStart","spark","bStart","bEnd","bEntries","durableThroughputProvider","countCompleted","es","windowHours","prevValue","bucketHours","STATE_BREAKDOWN_STATUSES","STATE_BREAKDOWN_PALETTE","durableStateBreakdownProvider","counts","segments","color","durableTelescopeExtension","opts","defineTelescopeExtension","name","watchers","DurableTelescopeWatcher","entryTypes","id","label","dot","dashboards","durableDashboard","dataProviders","durableStateProvider","durableTimeseriesProvider","durableRecentFailuresProvider","durableWorkerHealthProvider","durableWorkerStatusProvider","durableDurationProvider","durableRunsOverTimeProvider","durableSuccessRateProvider","durableThroughputProvider","durableStateBreakdownProvider"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-durable-telescope",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Telescope watcher for nestjs-durable — workflow runs and steps as Telescope entries",
5
5
  "license": "MIT",
6
6
  "author": "Davide Carvalho",
@@ -9,6 +9,15 @@
9
9
  "url": "https://github.com/DavideCarvalho/nestjs-durable.git",
10
10
  "directory": "packages/telescope"
11
11
  },
12
+ "homepage": "https://davidecarvalho.github.io/aviary/docs/durable",
13
+ "bugs": {
14
+ "url": "https://github.com/DavideCarvalho/nestjs-durable/issues"
15
+ },
16
+ "keywords": [
17
+ "nestjs",
18
+ "durable",
19
+ "telescope"
20
+ ],
12
21
  "type": "module",
13
22
  "main": "./dist/index.cjs",
14
23
  "types": "./dist/index.d.ts",
@@ -18,16 +27,16 @@
18
27
  "peerDependencies": {
19
28
  "@dudousxd/nestjs-durable-core": ">=0.1.0 <1.0.0",
20
29
  "@dudousxd/nestjs-telescope": ">=1.0.0 <2.0.0",
21
- "@nestjs/core": "^10.0.0 || ^11.0.0",
30
+ "@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.0.0",
22
31
  "@opentelemetry/api": "^1.0.0"
23
32
  },
24
33
  "devDependencies": {
25
- "@dudousxd/nestjs-telescope": "1.26.0",
26
- "@nestjs/core": "11.1.26",
27
- "@opentelemetry/api": "1.9.0",
34
+ "@dudousxd/nestjs-telescope": "1.29.1",
35
+ "@nestjs/core": "12.0.1",
36
+ "@opentelemetry/api": "1.9.1",
28
37
  "typescript": "5.9.3",
29
38
  "tsup": "8.3.5",
30
- "@dudousxd/nestjs-durable-core": "^0.64.0"
39
+ "@dudousxd/nestjs-durable-core": "^0.74.0"
31
40
  },
32
41
  "module": "./dist/index.js",
33
42
  "exports": {