@pikku/core 0.12.148 → 0.12.151

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.
Files changed (139) hide show
  1. package/dist/function/function-runner.js +23 -0
  2. package/dist/function/trace-queue-service.d.ts +26 -0
  3. package/dist/function/trace-queue-service.js +48 -0
  4. package/dist/mesh.d.ts +2 -2
  5. package/dist/mesh.js +3 -3
  6. package/dist/middleware/index.d.ts +1 -1
  7. package/dist/middleware/index.js +1 -1
  8. package/dist/middleware/mesh-auth.d.ts +3 -2
  9. package/dist/middleware/mesh-auth.js +7 -2
  10. package/dist/middleware/remote-jobs-secret.d.ts +1 -1
  11. package/dist/middleware/require-origin.d.ts +19 -0
  12. package/dist/middleware/require-origin.js +36 -2
  13. package/dist/services/deployment-service.d.ts +1 -1
  14. package/dist/services/incoming-webhook-service.d.ts +4 -2
  15. package/dist/services/incoming-webhook-service.js +4 -3
  16. package/dist/services/queue-webhook-service.js +7 -1
  17. package/dist/services/remote-jobs.d.ts +6 -1
  18. package/dist/services/scheduler-service.d.ts +3 -1
  19. package/dist/services/webhook-service.d.ts +2 -0
  20. package/dist/services/workflow-service.d.ts +4 -1
  21. package/dist/testing/index.d.ts +4 -0
  22. package/dist/testing/index.js +2 -0
  23. package/dist/testing/queue-identity-transport-tests.d.ts +18 -0
  24. package/dist/testing/queue-identity-transport-tests.js +162 -0
  25. package/dist/testing/queue-trace-conformance.d.ts +26 -0
  26. package/dist/testing/queue-trace-conformance.js +259 -0
  27. package/dist/testing/service-tests/channel-store-tests.js +11 -0
  28. package/dist/testing/service-tests/queued-workflow-harness.d.ts +18 -1
  29. package/dist/testing/service-tests/queued-workflow-harness.js +19 -3
  30. package/dist/testing/service-tests/workflow-service-tests.js +12 -0
  31. package/dist/testing/trace-matrix/assertions.d.ts +3 -0
  32. package/dist/testing/trace-matrix/assertions.js +190 -0
  33. package/dist/testing/trace-matrix/chain-harness.d.ts +119 -0
  34. package/dist/testing/trace-matrix/chain-harness.js +777 -0
  35. package/dist/testing/trace-matrix/dimensions.d.ts +55 -0
  36. package/dist/testing/trace-matrix/dimensions.js +305 -0
  37. package/dist/testing/trace-matrix/pairwise.d.ts +20 -0
  38. package/dist/testing/trace-matrix/pairwise.js +75 -0
  39. package/dist/testing/trace-matrix/report.d.ts +1 -0
  40. package/dist/testing/trace-matrix/report.js +16 -0
  41. package/dist/testing/trace-matrix/transports.d.ts +19 -0
  42. package/dist/testing/trace-matrix/transports.js +27 -0
  43. package/dist/testing/trace-matrix/workflow-cases.d.ts +24 -0
  44. package/dist/testing/trace-matrix/workflow-cases.js +147 -0
  45. package/dist/trace.d.ts +66 -0
  46. package/dist/trace.js +97 -0
  47. package/dist/types/core.types.d.ts +6 -2
  48. package/dist/wirings/addon/addon-reach.d.ts +3 -0
  49. package/dist/wirings/addon/addon-reach.js +9 -0
  50. package/dist/wirings/addon/define-plugin.d.ts +16 -0
  51. package/dist/wirings/addon/wire-addon.js +5 -0
  52. package/dist/wirings/addon/wire-remote-addon.d.ts +11 -0
  53. package/dist/wirings/agent/agent-finalize.d.ts +2 -0
  54. package/dist/wirings/agent/agent-finalize.js +1 -1
  55. package/dist/wirings/agent/agent-prepare.d.ts +12 -2
  56. package/dist/wirings/agent/agent-prepare.js +38 -14
  57. package/dist/wirings/agent/agent-rpc.js +2 -1
  58. package/dist/wirings/agent/agent-runner.js +5 -6
  59. package/dist/wirings/agent/agent-stream.js +6 -6
  60. package/dist/wirings/agent/voice-input.d.ts +1 -1
  61. package/dist/wirings/agent/voice-output.d.ts +1 -1
  62. package/dist/wirings/agent-scorer/agent-scorer-live.d.ts +4 -2
  63. package/dist/wirings/agent-scorer/agent-scorer-live.js +2 -2
  64. package/dist/wirings/channel/channel-common.d.ts +4 -1
  65. package/dist/wirings/channel/channel-common.js +3 -1
  66. package/dist/wirings/channel/channel-handler.js +2 -0
  67. package/dist/wirings/channel/channel-host-rpc.d.ts +1 -1
  68. package/dist/wirings/channel/channel-host-rpc.js +2 -2
  69. package/dist/wirings/channel/channel-rpc-responder.d.ts +7 -1
  70. package/dist/wirings/channel/channel-rpc-responder.js +2 -2
  71. package/dist/wirings/channel/channel-rpc-service.d.ts +1 -1
  72. package/dist/wirings/channel/channel-rpc-service.js +2 -2
  73. package/dist/wirings/channel/channel-rpc.types.d.ts +5 -2
  74. package/dist/wirings/channel/channel-rpc.types.js +1 -1
  75. package/dist/wirings/channel/channel-store.d.ts +4 -2
  76. package/dist/wirings/channel/channel-store.js +2 -2
  77. package/dist/wirings/channel/channel.types.d.ts +3 -0
  78. package/dist/wirings/channel/local/local-channel-runner.js +16 -5
  79. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +3 -1
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +27 -4
  82. package/dist/wirings/gateway/gateway-runner.js +3 -1
  83. package/dist/wirings/http/http-runner.d.ts +4 -4
  84. package/dist/wirings/http/http-runner.js +14 -18
  85. package/dist/wirings/http/http.types.d.ts +3 -3
  86. package/dist/wirings/mcp/mcp-runner.js +15 -5
  87. package/dist/wirings/queue/in-memory-queue-service.d.ts +1 -1
  88. package/dist/wirings/queue/in-memory-queue-service.js +8 -2
  89. package/dist/wirings/queue/index.d.ts +1 -0
  90. package/dist/wirings/queue/index.js +1 -0
  91. package/dist/wirings/queue/queue-runner.d.ts +3 -4
  92. package/dist/wirings/queue/queue-runner.js +8 -7
  93. package/dist/wirings/queue/queue-trace.d.ts +18 -0
  94. package/dist/wirings/queue/queue-trace.js +42 -0
  95. package/dist/wirings/queue/queue.types.d.ts +7 -0
  96. package/dist/wirings/queue/remote-queue-job.d.ts +1 -1
  97. package/dist/wirings/queue/remote-queue-job.js +5 -3
  98. package/dist/wirings/rpc/rpc-runner.d.ts +7 -1
  99. package/dist/wirings/rpc/rpc-runner.js +43 -21
  100. package/dist/wirings/rpc/rpc-types.d.ts +2 -0
  101. package/dist/wirings/scheduler/remote-scheduled-job.d.ts +1 -1
  102. package/dist/wirings/scheduler/remote-scheduled-job.js +2 -2
  103. package/dist/wirings/scheduler/scheduler-runner.d.ts +4 -4
  104. package/dist/wirings/scheduler/scheduler-runner.js +7 -6
  105. package/dist/wirings/trigger/pikku-trigger-service.js +7 -4
  106. package/dist/wirings/trigger/webhook-source-runner.d.ts +7 -1
  107. package/dist/wirings/trigger/webhook-source-runner.js +11 -3
  108. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  109. package/dist/wirings/workflow/graph/graph-runner.js +49 -30
  110. package/dist/wirings/workflow/index.d.ts +3 -1
  111. package/dist/wirings/workflow/index.js +1 -0
  112. package/dist/wirings/workflow/pikku-scenario-service.js +2 -0
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +37 -40
  114. package/dist/wirings/workflow/pikku-workflow-service.js +122 -133
  115. package/dist/wirings/workflow/workflow-child-step.d.ts +2 -2
  116. package/dist/wirings/workflow/workflow-child-step.js +11 -4
  117. package/dist/wirings/workflow/workflow-compensation.d.ts +6 -0
  118. package/dist/wirings/workflow/workflow-compensation.js +15 -1
  119. package/dist/wirings/workflow/workflow-constants.d.ts +13 -0
  120. package/dist/wirings/workflow/workflow-constants.js +13 -0
  121. package/dist/wirings/workflow/workflow-dsl-pass.d.ts +2 -0
  122. package/dist/wirings/workflow/workflow-dsl-pass.js +27 -11
  123. package/dist/wirings/workflow/workflow-recovery.d.ts +131 -3
  124. package/dist/wirings/workflow/workflow-recovery.js +233 -7
  125. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +1 -0
  126. package/dist/wirings/workflow/workflow-run-mirror.d.ts +4 -0
  127. package/dist/wirings/workflow/workflow-run-mirror.js +16 -0
  128. package/dist/wirings/workflow/workflow-run-status.d.ts +2 -0
  129. package/dist/wirings/workflow/workflow-run-status.js +13 -0
  130. package/dist/wirings/workflow/workflow-run-trace.d.ts +22 -0
  131. package/dist/wirings/workflow/workflow-run-trace.js +24 -0
  132. package/dist/wirings/workflow/workflow-step-claim.d.ts +18 -0
  133. package/dist/wirings/workflow/workflow-step-claim.js +47 -0
  134. package/dist/wirings/workflow/workflow-step-retry.d.ts +6 -2
  135. package/dist/wirings/workflow/workflow-step-retry.js +8 -3
  136. package/dist/wirings/workflow/workflow-wire.d.ts +12 -0
  137. package/dist/wirings/workflow/workflow-wire.js +33 -0
  138. package/dist/wirings/workflow/workflow.types.d.ts +16 -0
  139. package/package.json +2 -1
@@ -1,5 +1,6 @@
1
1
  import { NotFoundError } from '../../errors/errors.js';
2
2
  import { createWeakUID, isSerializable } from '../../utils.js';
3
+ import { resolveHTTPTrace } from '../../trace.js';
3
4
  import { getSingletonServices, getCreateWireServices, } from '../../pikku-state.js';
4
5
  import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../services/user-session-service.js';
5
6
  import { getErrorResponse } from '../../errors/error-handler.js';
@@ -120,7 +121,7 @@ export const createHTTPWire = (request, response) => {
120
121
  };
121
122
  const executeRoute = async (services, matchedRoute, http, options) => {
122
123
  const { params, route, meta } = matchedRoute;
123
- const { singletonServices, createWireServices, skipUserSession, requestId } = services;
124
+ const { singletonServices, createWireServices, skipUserSession, trace } = services;
124
125
  const userSession = new PikkuSessionService(singletonServices.sessionStore);
125
126
  http?.request?.setParams(params);
126
127
  if (meta.headersSchemaName && http.request && singletonServices.schema) {
@@ -206,7 +207,8 @@ const executeRoute = async (services, matchedRoute, http, options) => {
206
207
  });
207
208
  }
208
209
  const wire = {
209
- traceId: requestId,
210
+ traceId: trace.traceId,
211
+ trace,
210
212
  http,
211
213
  channel,
212
214
  ...createMiddlewareSessionWireProps(userSession),
@@ -295,9 +297,9 @@ export const pikkuFetch = async (request, params = {}) => {
295
297
  *
296
298
  * The lower-level form of {@link fetch} for adapters that bring their own
297
299
  * request and response objects (Express, Fastify, uWS). A web `Request` is
298
- * wrapped with the `maxBodySize` limit. The trace ID is taken from `traceId`,
299
- * then the `x-request-id` header, then `generateRequestId`, and is returned to
300
- * the client as `errorId` on failures.
300
+ * wrapped with the `maxBodySize` limit. The trace comes from a valid `traceId`,
301
+ * then `traceparent` when `trustInboundTraceparent` is set, and is otherwise new.
302
+ * Its id is returned to the client as `errorId` on failures.
301
303
  *
302
304
  * Errors are written to `response`: a 4xx keeps its message and payload, while a
303
305
  * 5xx or an unknown error hides its message unless `exposeErrors` is set outside
@@ -311,22 +313,16 @@ export const pikkuFetch = async (request, params = {}) => {
311
313
  * @throws The original error when `bubbleErrors` is set, after the error response has been written.
312
314
  * @throws {Error} When `skipUserSession` is set on a route that requires auth.
313
315
  */
314
- export const fetchData = async (request, response, { skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceDataFromSchema = true, bubbleErrors = false, exposeErrors = !isProduction(), generateRequestId, traceId: externalTraceId, maxBodySize, } = {}) => {
316
+ export const fetchData = async (request, response, { skipUserSession = false, respondWith404 = true, logWarningsForStatusCodes = [], coerceDataFromSchema = true, bubbleErrors = false, exposeErrors = !isProduction(), traceId, trustInboundTraceparent, maxBodySize, } = {}) => {
315
317
  const singletonServices = getSingletonServices();
316
318
  const createWireServices = getCreateWireServices();
317
319
  let result;
318
320
  const pikkuRequest = request instanceof Request
319
321
  ? new PikkuFetchHTTPRequest(request, { maxBodySize })
320
322
  : request;
321
- let requestId = externalTraceId ?? null;
322
- if (!requestId) {
323
- try {
324
- requestId = pikkuRequest.header('x-request-id');
325
- }
326
- catch { }
327
- }
328
- requestId = requestId || generateRequestId?.() || createWeakUID();
329
- const scopedLogger = singletonServices.logger.scope?.(requestId) ?? singletonServices.logger;
323
+ const trace = resolveHTTPTrace(() => pikkuRequest.header('traceparent'), singletonServices.logger, { traceId, trustInboundTraceparent });
324
+ // Read at the point of logging: mesh auth can continue the caller's trace.
325
+ const scopedLogger = () => singletonServices.logger.scope?.(trace.traceId) ?? singletonServices.logger;
330
326
  const http = createHTTPWire(pikkuRequest, response);
331
327
  const apiType = http.request.method();
332
328
  const apiRoute = http.request.path();
@@ -344,7 +340,7 @@ export const fetchData = async (request, response, { skipUserSession = false, re
344
340
  response.status(204).json(undefined);
345
341
  return;
346
342
  }
347
- scopedLogger.info({
343
+ scopedLogger().info({
348
344
  message: 'Route not found',
349
345
  apiRoute,
350
346
  apiType,
@@ -356,11 +352,11 @@ export const fetchData = async (request, response, { skipUserSession = false, re
356
352
  singletonServices,
357
353
  createWireServices,
358
354
  skipUserSession,
359
- requestId,
355
+ trace,
360
356
  }, matchedRoute, http, { coerceDataFromSchema }));
361
357
  return result;
362
358
  }
363
359
  catch (e) {
364
- handleHTTPError(e, http, requestId, scopedLogger, logWarningsForStatusCodes, respondWith404, bubbleErrors, exposeErrors);
360
+ handleHTTPError(e, http, trace.traceId, scopedLogger(), logWarningsForStatusCodes, respondWith404, bubbleErrors, exposeErrors);
365
361
  }
366
362
  };
@@ -20,7 +20,7 @@ export type AssertHTTPWiringParams<In, HTTPWiring extends string> = ExtractHTTPW
20
20
  * - `coerceDataFromSchema` – coerce query and path strings to the schema's types (default `true`).
21
21
  * - `bubbleErrors` – rethrow after writing the error response.
22
22
  * - `exposeErrors` – include messages and stacks of 5xx errors; ignored in production.
23
- * - `generateRequestId` – produce a trace ID when neither `traceId` nor `x-request-id` is present.
23
+ * - `trustInboundTraceparent` – continue the trace named by `traceparent` on every request. Off by default, because the header is caller-supplied.
24
24
  */
25
25
  export type RunHTTPWiringOptions = Partial<{
26
26
  skipUserSession: boolean;
@@ -29,8 +29,8 @@ export type RunHTTPWiringOptions = Partial<{
29
29
  coerceDataFromSchema: boolean;
30
30
  bubbleErrors: boolean;
31
31
  exposeErrors: boolean;
32
- generateRequestId: () => string;
33
- /** Pre-resolved trace ID (e.g. CF-Ray). Falls back to x-request-id header or generated ID. */
32
+ trustInboundTraceparent: boolean;
33
+ /** A trace ID the runtime already holds: 32 lowercase hex, anything else is ignored. */
34
34
  traceId: string;
35
35
  /** Maximum request body size in bytes, applied when pikku wraps a fetch `Request`. */
36
36
  maxBodySize: number;
@@ -5,6 +5,7 @@ import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
5
5
  import { resolveNamespace } from '../rpc/rpc-runner.js';
6
6
  import { BadRequestError, NotFoundError } from '../../errors/errors.js';
7
7
  import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../services/user-session-service.js';
8
+ import { traceWire, tracedLogger } from '../../trace.js';
8
9
  /**
9
10
  * A JSON-RPC error raised by `runMCPTool`, `runMCPResource` and `runMCPPrompt`; `error` is the
10
11
  * response body to send back to the MCP client.
@@ -154,6 +155,10 @@ async function runMCPPikkuFunc(request, type, name, mcp, pikkuFuncId, { mcp: mcp
154
155
  metaKey = name) {
155
156
  const singletonServices = getSingletonServices();
156
157
  const createWireServices = getCreateWireServices();
158
+ // Here rather than left to the function runner: a call refused before it
159
+ // gets there still has to be found in the logs.
160
+ const trace = traceWire();
161
+ const logger = tracedLogger(singletonServices.logger, trace.traceId);
157
162
  try {
158
163
  if (request.jsonrpc !== '2.0') {
159
164
  throw new BadRequestError('Invalid JSON-RPC version, only supoorted version is 2.0');
@@ -167,6 +172,7 @@ metaKey = name) {
167
172
  singletonServices.logger.debug(`Running MCP ${type}: ${name}`);
168
173
  const mcpSessionService = new PikkuSessionService(singletonServices.sessionStore);
169
174
  const wire = {
175
+ ...trace,
170
176
  mcp: mcpWire,
171
177
  // Only when the transport supplied one. Setting `http: undefined`
172
178
  // explicitly would be the same to a reader and different to a spread.
@@ -212,27 +218,31 @@ metaKey = name) {
212
218
  };
213
219
  }
214
220
  catch (e) {
215
- singletonServices.logger.error(`Error running MCP ${type} '${name}':`, e.constructor);
221
+ logger.error(`Error running MCP ${type} '${name}':`, e.constructor);
216
222
  const errorResponse = getErrorResponse(e);
217
223
  if (errorResponse?.mcpCode) {
218
224
  throw new MCPError({
219
225
  id: request.id,
220
226
  code: errorResponse.mcpCode,
221
227
  message: errorResponse.message,
228
+ data: { errorId: trace.traceId },
222
229
  });
223
230
  }
224
231
  else {
225
232
  if (errorResponse) {
226
- singletonServices.logger.warn(`Got error without a mapping: ${errorResponse.message}`);
233
+ logger.warn(`Got error without a mapping: ${errorResponse.message}`);
227
234
  }
228
235
  throw new MCPError({
229
236
  id: request.id,
230
237
  code: -32603,
231
238
  message: 'Internal error',
232
239
  // knowledge: decisions/security/mcp-internal-error-details-are-double-gated-on-production.md
233
- data: exposeErrors && !isProduction() && e instanceof Error
234
- ? { message: e.message, stack: e.stack }
235
- : undefined,
240
+ data: {
241
+ errorId: trace.traceId,
242
+ ...(exposeErrors && !isProduction() && e instanceof Error
243
+ ? { message: e.message, stack: e.stack }
244
+ : {}),
245
+ },
236
246
  });
237
247
  }
238
248
  }
@@ -4,7 +4,7 @@ import type { QueueService, JobOptions, QueueInspection } from './queue.types.js
4
4
  *
5
5
  * Jobs run after a short delay, payloads are JSON round-tripped as a real
6
6
  * backend would, and failed jobs are retried up to `attempts` with the given
7
- * backoff. Nothing is persisted, `getJob` always returns `null`, and jobs still
7
+ * backoff. A job its worker discarded is not retried. Nothing is persisted, `getJob` always returns `null`, and jobs still
8
8
  * pending when the process exits are lost.
9
9
  */
10
10
  export declare class InMemoryQueueService implements QueueService {
@@ -1,4 +1,4 @@
1
- import { runQueueJob } from './queue-runner.js';
1
+ import { QueueJobDiscardedError, runQueueJob } from './queue-runner.js';
2
2
  const FINISHED_JOBS_KEPT = 100;
3
3
  // knowledge: decisions/internals/the-dev-queue-copies-prod-timing-and-serialization-semantics.md
4
4
  /**
@@ -6,7 +6,7 @@ const FINISHED_JOBS_KEPT = 100;
6
6
  *
7
7
  * Jobs run after a short delay, payloads are JSON round-tripped as a real
8
8
  * backend would, and failed jobs are retried up to `attempts` with the given
9
- * backoff. Nothing is persisted, `getJob` always returns `null`, and jobs still
9
+ * backoff. A job its worker discarded is not retried. Nothing is persisted, `getJob` always returns `null`, and jobs still
10
10
  * pending when the process exits are lost.
11
11
  */
12
12
  export class InMemoryQueueService {
@@ -44,12 +44,18 @@ export class InMemoryQueueService {
44
44
  status: () => 'active',
45
45
  metadata: () => ({ attemptsMade, maxAttempts, createdAt }),
46
46
  pikkuUserId: options?.pikkuUserId,
47
+ traceparent: options?.traceparent,
47
48
  };
48
49
  try {
49
50
  await runQueueJob({ job });
50
51
  this.finish(queueName, record, 'completed', null);
51
52
  }
52
53
  catch (e) {
54
+ // As on a real backend, a discard ends the job and is not a failure.
55
+ if (e instanceof QueueJobDiscardedError) {
56
+ this.finish(queueName, record, 'completed', null);
57
+ return;
58
+ }
53
59
  record.error = e?.message ?? String(e);
54
60
  if (attemptsMade < maxAttempts) {
55
61
  record.status = 'delayed';
@@ -2,6 +2,7 @@ export type { ConfigValidationResult, CoreQueueWorker, GroupConcurrencyConfig, J
2
2
  export { wireQueueWorker, runQueueJob, QueueJobDiscardedError, QueueJobFailedError, } from './queue-runner.js';
3
3
  export type { QueueIdentityBinding } from './queue-identity.js';
4
4
  export { SignedQueueService } from './signed-queue-service.js';
5
+ export { unwrapJobTrace, wrapJobTrace } from './queue-trace.js';
5
6
  export type { QueueConfigMapping } from './validate-worker-config.js';
6
7
  export { registerQueueWorkers } from './register-queue-helper.js';
7
8
  export type { QueueRegistrationCallback } from './register-queue-helper.js';
@@ -1,4 +1,5 @@
1
1
  export { wireQueueWorker, runQueueJob, QueueJobDiscardedError, QueueJobFailedError, } from './queue-runner.js';
2
2
  export { SignedQueueService } from './signed-queue-service.js';
3
+ export { unwrapJobTrace, wrapJobTrace } from './queue-trace.js';
3
4
  export { registerQueueWorkers } from './register-queue-helper.js';
4
5
  export { pikkuRemoteQueueJobFunc } from './remote-queue-job.js';
@@ -37,18 +37,17 @@ export declare function removeQueueWorker(name: string): Promise<void>;
37
37
  * The job's `pikkuUserId` claim is verified before the function runs; an
38
38
  * unsigned or invalid claim is logged and the job runs without one. The function
39
39
  * runs with `auth: false`, the worker's middleware, and a `queue` wire carrying
40
- * `updateProgress`, `fail` and `discard`.
40
+ * `updateProgress`, `fail` and `discard`. It continues the job's `traceparent`,
41
+ * or starts a new trace when the job has no valid one.
41
42
  *
42
43
  * @param params.job - The job as the adapter received it.
43
44
  * @param params.updateProgress - Reports progress to the backend; defaults to logging it.
44
- * @param params.traceId - Defaults to `q-<jobId>`.
45
45
  * @returns What the worker's function returned.
46
46
  * @throws {PikkuMissingMetaError} When no metadata was generated for the queue.
47
47
  * @throws {QueueJobFailedError | QueueJobDiscardedError} When the worker called `fail` or `discard`.
48
48
  * @throws Whatever the worker threw, after logging it, so the adapter can apply its retry policy.
49
49
  */
50
- export declare function runQueueJob({ job, updateProgress, traceId, }: {
50
+ export declare function runQueueJob({ job, updateProgress, }: {
51
51
  job: QueueJob;
52
52
  updateProgress?: (progress: number | string | object) => Promise<void>;
53
- traceId?: string;
54
53
  }): Promise<void>;
@@ -3,6 +3,7 @@ import { PikkuMissingMetaError } from '../../errors/errors.js';
3
3
  import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../pikku-state.js';
4
4
  import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
5
5
  import { resolveQueueJobIdentity } from './queue-identity.js';
6
+ import { continueTrace } from '../../trace.js';
6
7
  class QueueWorkerNotFoundError extends PikkuError {
7
8
  constructor(name) {
8
9
  super(`Queue processor not found: ${name}`);
@@ -75,22 +76,21 @@ export async function removeQueueWorker(name) {
75
76
  * The job's `pikkuUserId` claim is verified before the function runs; an
76
77
  * unsigned or invalid claim is logged and the job runs without one. The function
77
78
  * runs with `auth: false`, the worker's middleware, and a `queue` wire carrying
78
- * `updateProgress`, `fail` and `discard`.
79
+ * `updateProgress`, `fail` and `discard`. It continues the job's `traceparent`,
80
+ * or starts a new trace when the job has no valid one.
79
81
  *
80
82
  * @param params.job - The job as the adapter received it.
81
83
  * @param params.updateProgress - Reports progress to the backend; defaults to logging it.
82
- * @param params.traceId - Defaults to `q-<jobId>`.
83
84
  * @returns What the worker's function returned.
84
85
  * @throws {PikkuMissingMetaError} When no metadata was generated for the queue.
85
86
  * @throws {QueueJobFailedError | QueueJobDiscardedError} When the worker called `fail` or `discard`.
86
87
  * @throws Whatever the worker threw, after logging it, so the adapter can apply its retry policy.
87
88
  */
88
- export async function runQueueJob({ job, updateProgress, traceId, }) {
89
+ export async function runQueueJob({ job, updateProgress, }) {
89
90
  const singletonServices = getSingletonServices();
90
91
  const createWireServices = getCreateWireServices();
91
- const resolvedTraceId = traceId ?? `q-${job.id}`;
92
- const logger = singletonServices.logger.scope?.(resolvedTraceId) ??
93
- singletonServices.logger;
92
+ const trace = continueTrace(job.traceparent);
93
+ const logger = singletonServices.logger.scope?.(trace.traceId) ?? singletonServices.logger;
94
94
  const meta = pikkuState(null, 'queue', 'meta');
95
95
  const processorMeta = meta[job.queueName];
96
96
  if (!processorMeta) {
@@ -126,7 +126,8 @@ export async function runQueueJob({ job, updateProgress, traceId, }) {
126
126
  try {
127
127
  logger.info(`Processing job ${job.id} in queue ${job.queueName}`);
128
128
  const wire = {
129
- traceId: resolvedTraceId,
129
+ traceId: trace.traceId,
130
+ trace,
130
131
  queue,
131
132
  };
132
133
  const result = await runPikkuFunc('queue', job.queueName, processorMeta.pikkuFuncId, {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Puts a job's `traceparent` and signed identity claim next to its payload, for
3
+ * queues with no per-message metadata that survives a retry. Applied by the
4
+ * adapter, below `SignedQueueService`, so the signature still covers `data`.
5
+ * A job with neither is sent as it is, unless its data looks like an envelope,
6
+ * or a worker would unwrap it.
7
+ */
8
+ export declare const wrapJobTrace: (data: unknown, traceparent?: string, pikkuUserId?: string) => any;
9
+ /**
10
+ * Undoes {@link wrapJobTrace}. A payload without the envelope is the job's data
11
+ * as enqueued. `pikkuUserId` is the unverified claim: only `runQueueJob` may
12
+ * turn it into an identity.
13
+ */
14
+ export declare const unwrapJobTrace: (payload: any) => {
15
+ data: any;
16
+ traceparent?: string;
17
+ pikkuUserId?: string;
18
+ };
@@ -0,0 +1,42 @@
1
+ const TRACEPARENT_KEY = '__pikkuTraceparent';
2
+ const IDENTITY_KEY = '__pikkuUserId';
3
+ const ENVELOPE_KEYS = new Set([TRACEPARENT_KEY, IDENTITY_KEY, 'data']);
4
+ const isOptionalString = (value) => value === undefined || typeof value === 'string';
5
+ const isEnvelope = (payload) => typeof payload === 'object' &&
6
+ payload !== null &&
7
+ (payload[TRACEPARENT_KEY] !== undefined ||
8
+ payload[IDENTITY_KEY] !== undefined) &&
9
+ isOptionalString(payload[TRACEPARENT_KEY]) &&
10
+ isOptionalString(payload[IDENTITY_KEY]) &&
11
+ Object.keys(payload).every((key) => ENVELOPE_KEYS.has(key));
12
+ /**
13
+ * Puts a job's `traceparent` and signed identity claim next to its payload, for
14
+ * queues with no per-message metadata that survives a retry. Applied by the
15
+ * adapter, below `SignedQueueService`, so the signature still covers `data`.
16
+ * A job with neither is sent as it is, unless its data looks like an envelope,
17
+ * or a worker would unwrap it.
18
+ */
19
+ // knowledge: decisions/internals/bullmq-and-pg-boss-carry-traceparent-in-a-payload-envelope.md
20
+ export const wrapJobTrace = (data, traceparent, pikkuUserId) => traceparent || pikkuUserId || isEnvelope(data)
21
+ ? {
22
+ [TRACEPARENT_KEY]: traceparent ?? '',
23
+ ...(pikkuUserId ? { [IDENTITY_KEY]: pikkuUserId } : {}),
24
+ data,
25
+ }
26
+ : data;
27
+ /**
28
+ * Undoes {@link wrapJobTrace}. A payload without the envelope is the job's data
29
+ * as enqueued. `pikkuUserId` is the unverified claim: only `runQueueJob` may
30
+ * turn it into an identity.
31
+ */
32
+ export const unwrapJobTrace = (payload) => {
33
+ if (!isEnvelope(payload))
34
+ return { data: payload };
35
+ const traceparent = payload[TRACEPARENT_KEY];
36
+ const pikkuUserId = payload[IDENTITY_KEY];
37
+ return {
38
+ data: payload.data,
39
+ ...(traceparent ? { traceparent } : {}),
40
+ ...(pikkuUserId !== undefined ? { pikkuUserId } : {}),
41
+ };
42
+ };
@@ -91,6 +91,8 @@ export interface QueueJob<T = any, R = any> {
91
91
  waitForCompletion?: (ttl?: number) => Promise<R>;
92
92
  metadata?: () => Promise<QueueJobMetadata> | QueueJobMetadata;
93
93
  pikkuUserId?: string;
94
+ /** The W3C trace the producer was in, when the adapter carried one. */
95
+ traceparent?: string;
94
96
  }
95
97
  /**
96
98
  * Options for {@link QueueService.add}.
@@ -114,6 +116,11 @@ export interface JobOptions {
114
116
  pikkuUserId?: string;
115
117
  /** Counts against the worker's {@link PikkuWorkerConfig.groupConcurrency}. */
116
118
  group?: JobGroup;
119
+ /**
120
+ * The W3C trace the job continues. Stamped from the calling function's wire
121
+ * when left unset, so set it only to move the job onto another trace.
122
+ */
123
+ traceparent?: string;
117
124
  }
118
125
  export type QueueJobCounts = Record<QueueJobStatus, number>;
119
126
  export type QueueJobRecord = {
@@ -8,4 +8,4 @@ import type { RemoteQueueJobData } from '../../services/remote-jobs.js';
8
8
  */
9
9
  export declare function pikkuRemoteQueueJobFunc({ logger }: {
10
10
  logger: Logger;
11
- }, { queueName, data, jobId, traceId }: RemoteQueueJobData): Promise<void>;
11
+ }, { queueName, data, jobId, traceparent, pikkuUserId }: RemoteQueueJobData): Promise<void>;
@@ -6,11 +6,13 @@ import { QueueJobDiscardedError, runQueueJob } from './queue-runner.js';
6
6
  * discarded job answers 4xx so the sender stops; anything else propagates and
7
7
  * is delivered again.
8
8
  */
9
- export async function pikkuRemoteQueueJobFunc({ logger }, { queueName, data, jobId, traceId }) {
9
+ export async function pikkuRemoteQueueJobFunc({ logger }, { queueName, data, jobId, traceparent, pikkuUserId }) {
10
10
  const job = {
11
11
  queueName,
12
12
  data,
13
- id: jobId ?? traceId ?? `remote-${Date.now()}`,
13
+ id: jobId ?? `remote-${globalThis.crypto.randomUUID()}`,
14
+ traceparent,
15
+ pikkuUserId,
14
16
  status: async () => 'active',
15
17
  metadata: () => ({
16
18
  processedAt: new Date(),
@@ -28,7 +30,7 @@ export async function pikkuRemoteQueueJobFunc({ logger }, { queueName, data, job
28
30
  },
29
31
  };
30
32
  try {
31
- await runQueueJob({ job, traceId });
33
+ await runQueueJob({ job });
32
34
  }
33
35
  catch (error) {
34
36
  if (error instanceof PikkuMissingMetaError ||
@@ -41,8 +41,13 @@ export declare class ContextAwareRPCService {
41
41
  requiresAuth?: boolean;
42
42
  sessionService?: SessionService<CoreUserSession>;
43
43
  }, packageName?: string | null);
44
+ get traceparent(): string | undefined;
44
45
  rpcExposed(funcName: string, data: any): Promise<any>;
45
46
  rpc<In = any, Out = any>(funcName: string, data: In): Promise<Out>;
47
+ /** A call runs as a span of its own under the caller's. */
48
+ private callSpan;
49
+ /** The services' logger, on the trace of the wire this service belongs to. */
50
+ private get logger();
46
51
  private mapUsedAddon;
47
52
  /**
48
53
  * Resolution half of an addon RPC call: never executes anything, so an
@@ -63,6 +68,7 @@ export declare class ContextAwareRPCService {
63
68
  type: string;
64
69
  id?: string;
65
70
  parentRunId?: string;
71
+ traceparent?: string;
66
72
  };
67
73
  }): Promise<{
68
74
  runId: string;
@@ -107,7 +113,7 @@ export declare const rpcService: PikkuRPCService<import("../../types/core.types.
107
113
  secrets?: {
108
114
  requireAllowedHosts?: boolean;
109
115
  };
110
- workflow?: import("../workflow/workflow.types.js").WorkflowServiceConfig;
116
+ workflow?: Partial<import("../workflow/workflow.types.js").WorkflowServiceConfig>;
111
117
  webhook?: import("../../services/webhook-service.js").WebhookServiceConfig;
112
118
  postgres?: import("../../types/core.types.js").PostgresConfig;
113
119
  }>, PikkuRPC>;
@@ -1,3 +1,4 @@
1
+ import { childTraceWire, outgoingTraceparent, tracedLogger, } from '../../trace.js';
1
2
  import { AddonNotUsedError, addonCallerOf, addonMayReach, mapAddonCall, } from '../addon/addon-reach.js';
2
3
  import { forwardStepName, isCompensationStepName, } from '../../compensation-name.js';
3
4
  import { addonInstanceForNamespace } from '../addon/addon-runner.js';
@@ -80,7 +81,7 @@ addError(RemoteAddonTimeoutError, {
80
81
  /** Headers a wiring's `headers` hook cannot set: the call's auth, trace and contract. */
81
82
  const RESERVED_REMOTE_HEADERS = new Set([
82
83
  'authorization',
83
- 'x-request-id',
84
+ 'traceparent',
84
85
  REMOTE_CONTRACT_HEADER,
85
86
  'content-type',
86
87
  'accept',
@@ -264,6 +265,9 @@ export class ContextAwareRPCService {
264
265
  this.options = options;
265
266
  this.packageName = packageName;
266
267
  }
268
+ get traceparent() {
269
+ return outgoingTraceparent(this.wire);
270
+ }
267
271
  async rpcExposed(funcName, data) {
268
272
  const addonCaller = addonCallerOf(this.wire);
269
273
  if (addonCaller && !addonMayReach(addonCaller, funcName)) {
@@ -323,14 +327,15 @@ export class ContextAwareRPCService {
323
327
  return await this.rpc(funcName, data);
324
328
  }
325
329
  async rpc(funcName, data) {
326
- const updatedWire = {
327
- ...this.wire,
328
- };
330
+ // A local function is a span of its own. Across a boundary the callee's
331
+ // parent must be a span that exists, so that is the caller's.
332
+ const updatedWire = { ...this.wire };
333
+ const localWire = { ...updatedWire, ...this.callSpan() };
329
334
  if (funcName.includes(':')) {
330
335
  funcName = this.mapUsedAddon(funcName);
331
336
  const addonCall = this.resolveAddonFunction(funcName);
332
337
  if (addonCall !== NOT_RESOLVED) {
333
- return await this.executeAddonFunction(addonCall, data, updatedWire);
338
+ return await this.executeAddonFunction(addonCall, data, addonCall.kind === 'remote' ? updatedWire : localWire);
334
339
  }
335
340
  }
336
341
  let resolved;
@@ -341,7 +346,7 @@ export class ContextAwareRPCService {
341
346
  if (e instanceof RPCNotFoundError) {
342
347
  if (!this.packageName && this.services.deploymentService) {
343
348
  const session = await resolveWireSession(this.wire);
344
- return this.services.deploymentService.invoke(funcName, data, session, this.wire.traceId);
349
+ return this.services.deploymentService.invoke(funcName, data, session, outgoingTraceparent(this.wire));
345
350
  }
346
351
  }
347
352
  throw e;
@@ -353,11 +358,19 @@ export class ContextAwareRPCService {
353
358
  auth: this.options.requiresAuth,
354
359
  singletonServices: this.services,
355
360
  data: () => data,
356
- wire: updatedWire,
361
+ wire: localWire,
357
362
  packageName: resolved.packageName,
358
363
  addonInstance,
359
364
  });
360
365
  }
366
+ /** A call runs as a span of its own under the caller's. */
367
+ callSpan() {
368
+ return this.wire.trace ? childTraceWire(this.wire.trace) : {};
369
+ }
370
+ /** The services' logger, on the trace of the wire this service belongs to. */
371
+ get logger() {
372
+ return tracedLogger(this.services.logger, this.wire.traceId);
373
+ }
361
374
  mapUsedAddon(funcName) {
362
375
  return mapAddonCall(this.wire.addonNamespace, funcName);
363
376
  }
@@ -427,7 +440,7 @@ export class ContextAwareRPCService {
427
440
  */
428
441
  async executeAddonFunction(addonCall, data, wire) {
429
442
  if (addonCall.kind === 'remote') {
430
- return this.invokeRemoteAddonFunction(addonCall.namespace, addonCall.fnName, data, addonCall.addonConfig);
443
+ return this.invokeRemoteAddonFunction(addonCall.namespace, addonCall.fnName, data, addonCall.addonConfig, outgoingTraceparent(wire));
431
444
  }
432
445
  return runPikkuFunc('rpc', addonCall.namespacedFunction, addonCall.pikkuFuncId, {
433
446
  auth: addonCall.auth,
@@ -439,7 +452,7 @@ export class ContextAwareRPCService {
439
452
  addonInstance: addonCall.addonInstance,
440
453
  });
441
454
  }
442
- async invokeRemoteAddonFunction(namespace, fnName, data, cfg) {
455
+ async invokeRemoteAddonFunction(namespace, fnName, data, cfg, traceparent) {
443
456
  const serverUrl = typeof cfg.serverUrl === 'function'
444
457
  ? await cfg.serverUrl(this.services)
445
458
  : cfg.serverUrl;
@@ -476,8 +489,8 @@ export class ContextAwareRPCService {
476
489
  if (token) {
477
490
  headers.authorization = `Bearer ${token}`;
478
491
  }
479
- if (this.wire.traceId) {
480
- headers['x-request-id'] = this.wire.traceId;
492
+ if (traceparent) {
493
+ headers.traceparent = traceparent;
481
494
  }
482
495
  const timeoutMs = cfg.remoteTimeoutMs ?? DEFAULT_REMOTE_ADDON_TIMEOUT_MS;
483
496
  const base = serverUrl.replace(/\/+$/, '');
@@ -496,11 +509,11 @@ export class ContextAwareRPCService {
496
509
  if (e?.name === 'TimeoutError') {
497
510
  throw new RemoteAddonTimeoutError(namespace, remoteFn, timeoutMs);
498
511
  }
499
- this.services.logger.error(`Remote addon '${namespace}:${remoteFn}' could not be reached: ${e instanceof Error ? e.message : e}`);
512
+ this.logger.error(`Remote addon '${namespace}:${remoteFn}' could not be reached: ${e instanceof Error ? e.message : e}`);
500
513
  throw new RemoteAddonFailedError(namespace, remoteFn);
501
514
  }
502
515
  if (!res.ok) {
503
- throw await remoteAddonResponseError(res, namespace, remoteFn, this.services.logger);
516
+ throw await remoteAddonResponseError(res, namespace, remoteFn, this.logger);
504
517
  }
505
518
  if (res.status === 204) {
506
519
  return undefined;
@@ -513,7 +526,7 @@ export class ContextAwareRPCService {
513
526
  return JSON.parse(text);
514
527
  }
515
528
  catch {
516
- this.services.logger.error(`Remote addon '${namespace}:${remoteFn}' answered ${res.status} with a body that is not JSON`);
529
+ this.logger.error(`Remote addon '${namespace}:${remoteFn}' answered ${res.status} with a body that is not JSON`);
517
530
  throw new RemoteAddonFailedError(namespace, remoteFn);
518
531
  }
519
532
  }
@@ -538,7 +551,7 @@ export class ContextAwareRPCService {
538
551
  !this.packageName &&
539
552
  this.services.deploymentService) {
540
553
  const session = await resolveWireSession(mergedWire);
541
- return this.services.deploymentService.invoke(rpcName, data, session, mergedWire.traceId);
554
+ return this.services.deploymentService.invoke(rpcName, data, session, outgoingTraceparent(mergedWire));
542
555
  }
543
556
  throw e;
544
557
  }
@@ -562,11 +575,17 @@ export class ContextAwareRPCService {
562
575
  workflowName = this.mapUsedAddon(workflowName);
563
576
  }
564
577
  const parentRunId = this.wire.workflowStep?.runId ?? this.wire.graph?.runId;
565
- const wire = options?.wire ?? {
566
- type: this.wire.wireType ?? 'unknown',
567
- id: this.wire.wireId,
568
- ...(parentRunId ? { parentRunId } : {}),
569
- ...(this.wire.pikkuUserId ? { pikkuUserId: this.wire.pikkuUserId } : {}),
578
+ const traceparent = options?.wire?.traceparent ?? this.traceparent;
579
+ const wire = {
580
+ ...(options?.wire ?? {
581
+ type: this.wire.wireType ?? 'unknown',
582
+ id: this.wire.wireId,
583
+ ...(parentRunId ? { parentRunId } : {}),
584
+ ...(this.wire.pikkuUserId
585
+ ? { pikkuUserId: this.wire.pikkuUserId }
586
+ : {}),
587
+ }),
588
+ ...(traceparent ? { traceparent } : {}),
570
589
  };
571
590
  return this.services.workflowService.startWorkflow(workflowName, input, wire, this, options);
572
591
  }
@@ -593,7 +612,7 @@ export class ContextAwareRPCService {
593
612
  `Set up a DeploymentService to enable mesh function calls.`);
594
613
  }
595
614
  const session = await resolveWireSession(this.wire);
596
- return this.services.deploymentService.invoke(funcName, data, session, this.wire.traceId);
615
+ return this.services.deploymentService.invoke(funcName, data, session, outgoingTraceparent(this.wire));
597
616
  }
598
617
  }
599
618
  /**
@@ -652,6 +671,9 @@ class ContextRPCView {
652
671
  this.startWorkflow = serviceRPC.startWorkflow.bind(serviceRPC);
653
672
  this.rpcWithWire = serviceRPC.rpcWithWire.bind(serviceRPC);
654
673
  }
674
+ get traceparent() {
675
+ return this.serviceRPC.traceparent;
676
+ }
655
677
  get agent() {
656
678
  return this.serviceRPC.agent;
657
679
  }
@@ -5,6 +5,8 @@ import type { AgentInterruptResult } from '../agent/agent-interrupt.js';
5
5
  export type PikkuRPC<Invoke extends (...args: any[]) => any = (...args: any[]) => any, Mesh extends (...args: any[]) => any = (...args: any[]) => any, startWorkflow extends (...args: any[]) => any = (...args: any[]) => any, AgentRun extends (...args: any[]) => any = (...args: any[]) => any, AgentStream extends (...args: any[]) => any = (...args: any[]) => any> = {
6
6
  depth: number;
7
7
  global: boolean;
8
+ /** The W3C trace of the invocation holding this `rpc`, if it is in one. */
9
+ traceparent?: string;
8
10
  invoke: Invoke;
9
11
  mesh: Mesh;
10
12
  exposed: (name: string, data: any) => Promise<any>;
@@ -8,4 +8,4 @@ import type { RemoteScheduledJobData } from '../../services/remote-jobs.js';
8
8
  */
9
9
  export declare function pikkuRemoteScheduledJobFunc({ logger }: {
10
10
  logger: Logger;
11
- }, { taskName }: RemoteScheduledJobData): Promise<void>;
11
+ }, { taskName, traceparent }: RemoteScheduledJobData): Promise<void>;