callman-core 1.32.0 → 1.33.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.
Files changed (93) hide show
  1. package/ai-packs/scenario-nodes.md +57 -2
  2. package/dist/ai/prompts/scenario.d.ts +1 -1
  3. package/dist/ai/prompts/scenario.d.ts.map +1 -1
  4. package/dist/ai/prompts/scenario.js +3 -1
  5. package/dist/ai/prompts/scenario.js.map +1 -1
  6. package/dist/ai/prompts/scenarioNodeDefaults.d.ts.map +1 -1
  7. package/dist/ai/prompts/scenarioNodeDefaults.js +9 -0
  8. package/dist/ai/prompts/scenarioNodeDefaults.js.map +1 -1
  9. package/dist/ai/prompts/scenarioNodeReference.d.ts.map +1 -1
  10. package/dist/ai/prompts/scenarioNodeReference.js +80 -2
  11. package/dist/ai/prompts/scenarioNodeReference.js.map +1 -1
  12. package/dist/host/index.d.ts +2 -0
  13. package/dist/host/index.d.ts.map +1 -1
  14. package/dist/host/index.js +3 -0
  15. package/dist/host/index.js.map +1 -1
  16. package/dist/host/temporal/temporalService.d.ts +67 -0
  17. package/dist/host/temporal/temporalService.d.ts.map +1 -0
  18. package/dist/host/temporal/temporalService.js +1182 -0
  19. package/dist/host/temporal/temporalService.js.map +1 -0
  20. package/dist/host/temporal/types.d.ts +13 -0
  21. package/dist/host/temporal/types.d.ts.map +1 -0
  22. package/dist/host/temporal/types.js +5 -0
  23. package/dist/host/temporal/types.js.map +1 -0
  24. package/dist/scenario-runner/adapters/connection-config.d.ts +17 -1
  25. package/dist/scenario-runner/adapters/connection-config.d.ts.map +1 -1
  26. package/dist/scenario-runner/adapters/index.d.ts +3 -2
  27. package/dist/scenario-runner/adapters/index.d.ts.map +1 -1
  28. package/dist/scenario-runner/adapters/index.js +7 -0
  29. package/dist/scenario-runner/adapters/index.js.map +1 -1
  30. package/dist/scenario-runner/adapters/temporal-executor.d.ts +10 -0
  31. package/dist/scenario-runner/adapters/temporal-executor.d.ts.map +1 -0
  32. package/dist/scenario-runner/adapters/temporal-executor.js +186 -0
  33. package/dist/scenario-runner/adapters/temporal-executor.js.map +1 -0
  34. package/dist/scenario-runner/adapters/types.d.ts +16 -5
  35. package/dist/scenario-runner/adapters/types.d.ts.map +1 -1
  36. package/dist/scenario-runner/condition.d.ts.map +1 -1
  37. package/dist/scenario-runner/condition.js +73 -0
  38. package/dist/scenario-runner/condition.js.map +1 -1
  39. package/dist/scenario-runner/graphTraversal.d.ts +6 -0
  40. package/dist/scenario-runner/graphTraversal.d.ts.map +1 -1
  41. package/dist/scenario-runner/graphTraversal.js +9 -0
  42. package/dist/scenario-runner/graphTraversal.js.map +1 -1
  43. package/dist/scenario-runner/index.d.ts +1 -0
  44. package/dist/scenario-runner/index.d.ts.map +1 -1
  45. package/dist/scenario-runner/index.js +3 -0
  46. package/dist/scenario-runner/index.js.map +1 -1
  47. package/dist/scenario-runner/nodePolicy.d.ts +2 -2
  48. package/dist/scenario-runner/nodePolicy.d.ts.map +1 -1
  49. package/dist/scenario-runner/nodePolicy.js +3 -0
  50. package/dist/scenario-runner/nodePolicy.js.map +1 -1
  51. package/dist/scenario-runner/runScenario.d.ts.map +1 -1
  52. package/dist/scenario-runner/runScenario.js +76 -0
  53. package/dist/scenario-runner/runScenario.js.map +1 -1
  54. package/dist/scenario-runner/temporal.d.ts +73 -0
  55. package/dist/scenario-runner/temporal.d.ts.map +1 -0
  56. package/dist/scenario-runner/temporal.js +377 -0
  57. package/dist/scenario-runner/temporal.js.map +1 -0
  58. package/dist/scenario-runner/types.d.ts +206 -4
  59. package/dist/scenario-runner/types.d.ts.map +1 -1
  60. package/dist/ui-test/commands.d.ts.map +1 -1
  61. package/dist/ui-test/commands.js +71 -0
  62. package/dist/ui-test/commands.js.map +1 -1
  63. package/dist-cjs/ai/prompts/scenario.js +3 -1
  64. package/dist-cjs/ai/prompts/scenario.js.map +1 -1
  65. package/dist-cjs/ai/prompts/scenarioNodeDefaults.js +9 -0
  66. package/dist-cjs/ai/prompts/scenarioNodeDefaults.js.map +1 -1
  67. package/dist-cjs/ai/prompts/scenarioNodeReference.js +79 -1
  68. package/dist-cjs/ai/prompts/scenarioNodeReference.js.map +1 -1
  69. package/dist-cjs/host/index.js +20 -2
  70. package/dist-cjs/host/index.js.map +1 -1
  71. package/dist-cjs/host/temporal/temporalService.js +1200 -0
  72. package/dist-cjs/host/temporal/temporalService.js.map +1 -0
  73. package/dist-cjs/host/temporal/types.js +6 -0
  74. package/dist-cjs/host/temporal/types.js.map +1 -0
  75. package/dist-cjs/scenario-runner/adapters/index.js +9 -1
  76. package/dist-cjs/scenario-runner/adapters/index.js.map +1 -1
  77. package/dist-cjs/scenario-runner/adapters/temporal-executor.js +190 -0
  78. package/dist-cjs/scenario-runner/adapters/temporal-executor.js.map +1 -0
  79. package/dist-cjs/scenario-runner/condition.js +73 -0
  80. package/dist-cjs/scenario-runner/condition.js.map +1 -1
  81. package/dist-cjs/scenario-runner/graphTraversal.js +10 -1
  82. package/dist-cjs/scenario-runner/graphTraversal.js.map +1 -1
  83. package/dist-cjs/scenario-runner/index.js +3 -0
  84. package/dist-cjs/scenario-runner/index.js.map +1 -1
  85. package/dist-cjs/scenario-runner/nodePolicy.js +3 -0
  86. package/dist-cjs/scenario-runner/nodePolicy.js.map +1 -1
  87. package/dist-cjs/scenario-runner/runScenario.js +76 -0
  88. package/dist-cjs/scenario-runner/runScenario.js.map +1 -1
  89. package/dist-cjs/scenario-runner/temporal.js +386 -0
  90. package/dist-cjs/scenario-runner/temporal.js.map +1 -0
  91. package/dist-cjs/ui-test/commands.js +71 -0
  92. package/dist-cjs/ui-test/commands.js.map +1 -1
  93. package/package.json +2 -1
@@ -0,0 +1,1182 @@
1
+ // Temporal client service — the ONE implementation both hosts call (the
2
+ // backend scenario worker and the desktop Electron main process), following
3
+ // the db-adapter pattern: `@temporalio/client` is an optional dependency
4
+ // loaded lazily, with a test seam to inject a fake driver.
5
+ //
6
+ // Responsibilities:
7
+ // - build `Connection.connect(...)` options from a TemporalConnectionConfig
8
+ // (TLS / mTLS / Temporal Cloud API key / extra gRPC metadata);
9
+ // - cache one Connection + Client per `${runId}:${connectionId}:${configHash}`
10
+ // so a scenario reuses the gRPC channel across nodes, released per run;
11
+ // - execute one resolved operation and normalize its outcome into
12
+ // ScenarioTemporalResult (every key present);
13
+ // - summarize an event history (event counts, activity timeline, signals,
14
+ // timers, failure) and decode payloads with the default data converter.
15
+ //
16
+ // Abort: the SDK offers no per-call cancellation. A cancelled wait closes and
17
+ // evicts that cache entry, so a parallel branch sharing the connection simply
18
+ // reconnects on its next call instead of touching a closed client.
19
+ import { createHash } from "node:crypto";
20
+ import { loadOptionalModule } from "../lazy.js";
21
+ import { DEFAULT_TEMPORAL_HISTORY_LIMIT, DEFAULT_TEMPORAL_LIST_PAGE_SIZE, MAX_TEMPORAL_HISTORY_EVENTS, MAX_TEMPORAL_LIST_PAGE_SIZE, } from "../../scenario-runner/graphTraversal.js";
22
+ import { createEmptyTemporalResult } from "../../scenario-runner/temporal.js";
23
+ // --- Driver loading ---------------------------------------------------------
24
+ let temporalDriverOverride = null;
25
+ /**
26
+ * Test-only seam: inject a fake `@temporalio/client` module shape
27
+ * (`{ Connection, Client, ... }`) — core tests run under `node --test` with
28
+ * no module mocking. Never set in production.
29
+ */
30
+ export const __setTemporalDriverForTests = (driver) => {
31
+ temporalDriverOverride = driver;
32
+ };
33
+ const loadTemporalDriver = async () => {
34
+ if (temporalDriverOverride) {
35
+ return temporalDriverOverride;
36
+ }
37
+ try {
38
+ const mod = await loadOptionalModule("@temporalio/client");
39
+ return mod && mod.default && !mod.Connection ? mod.default : mod;
40
+ }
41
+ catch {
42
+ throw new Error("Temporal client (`@temporalio/client`) is not installed. Install `@temporalio/client` to use Temporal connections.");
43
+ }
44
+ };
45
+ // --- Connection options -----------------------------------------------------
46
+ const requireAddress = (config) => {
47
+ const address = (config.address ?? "").trim();
48
+ if (!address) {
49
+ throw new Error("Temporal connection has no address (host:port).");
50
+ }
51
+ return address;
52
+ };
53
+ export const resolveTemporalNamespace = (config, override) => (override ?? "").trim() || (config.namespace ?? "").trim() || "default";
54
+ /** `Connection.connect` options for a connection config (secrets included). */
55
+ export const buildTemporalConnectionOptions = (config) => {
56
+ const address = requireAddress(config);
57
+ const hasClientPair = Boolean(config.clientCert?.trim() && config.clientKey?.trim());
58
+ const useTls = config.tls === true || Boolean(config.apiKey?.trim()) || hasClientPair;
59
+ let tls = false;
60
+ if (useTls) {
61
+ const tlsOptions = {};
62
+ if (config.caCert?.trim()) {
63
+ tlsOptions.serverRootCACertificate = Buffer.from(config.caCert, "utf8");
64
+ }
65
+ if (hasClientPair) {
66
+ tlsOptions.clientCertPair = {
67
+ crt: Buffer.from(config.clientCert ?? "", "utf8"),
68
+ key: Buffer.from(config.clientKey ?? "", "utf8"),
69
+ };
70
+ }
71
+ if (config.serverNameOverride?.trim()) {
72
+ tlsOptions.serverNameOverride = config.serverNameOverride.trim();
73
+ }
74
+ tls = Object.keys(tlsOptions).length > 0 ? tlsOptions : true;
75
+ }
76
+ const metadata = {};
77
+ for (const [key, value] of Object.entries(config.metadata ?? {})) {
78
+ if (key.trim() && typeof value === "string") {
79
+ metadata[key.trim()] = value;
80
+ }
81
+ }
82
+ return {
83
+ address,
84
+ tls,
85
+ ...(config.apiKey?.trim() ? { apiKey: config.apiKey.trim() } : {}),
86
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
87
+ connectTimeout: typeof config.connectTimeoutMs === "number" && config.connectTimeoutMs > 0
88
+ ? Math.round(config.connectTimeoutMs)
89
+ : 10_000,
90
+ };
91
+ };
92
+ const clientCache = new Map();
93
+ const hashConfig = (config) => createHash("sha1")
94
+ .update(JSON.stringify({
95
+ address: config.address,
96
+ namespace: config.namespace,
97
+ tls: config.tls ?? false,
98
+ caCert: config.caCert ?? "",
99
+ clientCert: config.clientCert ?? "",
100
+ clientKey: config.clientKey ?? "",
101
+ serverNameOverride: config.serverNameOverride ?? "",
102
+ apiKey: config.apiKey ?? "",
103
+ metadata: config.metadata ?? {},
104
+ identity: config.identity ?? "",
105
+ }))
106
+ .digest("hex");
107
+ const cacheKey = (runId, connectionId, config) => `${runId}:${connectionId}:${hashConfig(config)}`;
108
+ const openClient = async (config, namespace) => {
109
+ const driver = await loadTemporalDriver();
110
+ const connection = await driver.Connection.connect(buildTemporalConnectionOptions(config));
111
+ const client = new driver.Client({
112
+ connection,
113
+ namespace,
114
+ ...(config.identity?.trim() ? { identity: config.identity.trim() } : {}),
115
+ });
116
+ return { connection, client, namespace };
117
+ };
118
+ const getCachedClient = async (runId, connectionId, config, namespace) => {
119
+ const key = `${cacheKey(runId, connectionId, config)}:${namespace}`;
120
+ let pending = clientCache.get(key);
121
+ if (!pending) {
122
+ pending = openClient(config, namespace);
123
+ clientCache.set(key, pending);
124
+ pending.catch(() => clientCache.delete(key));
125
+ }
126
+ return { key, entry: await pending };
127
+ };
128
+ const closeEntry = async (key) => {
129
+ const pending = clientCache.get(key);
130
+ clientCache.delete(key);
131
+ if (!pending) {
132
+ return;
133
+ }
134
+ try {
135
+ const entry = await pending;
136
+ await entry.connection.close?.();
137
+ }
138
+ catch {
139
+ // Already closed / never opened — nothing to release.
140
+ }
141
+ };
142
+ /** Close every connection opened under this scenario run. */
143
+ export const releaseTemporalConnectionsForRun = async (runId) => {
144
+ const prefix = `${runId}:`;
145
+ await Promise.all([...clientCache.keys()].filter((key) => key.startsWith(prefix)).map((key) => closeEntry(key)));
146
+ };
147
+ /** Process shutdown: close everything. */
148
+ export const closeAllTemporalConnections = async () => {
149
+ await Promise.all([...clientCache.keys()].map((key) => closeEntry(key)));
150
+ };
151
+ export const __getTemporalConnectionCountForTests = () => clientCache.size;
152
+ // --- Payload / value helpers ------------------------------------------------
153
+ const toIso = (value) => {
154
+ if (value === null || value === undefined)
155
+ return null;
156
+ if (value instanceof Date)
157
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
158
+ if (typeof value === "string") {
159
+ const parsed = new Date(value);
160
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toISOString();
161
+ }
162
+ if (typeof value === "number")
163
+ return new Date(value).toISOString();
164
+ // google.protobuf.Timestamp { seconds: Long|number, nanos }
165
+ if (typeof value === "object" && value !== null && "seconds" in value) {
166
+ const seconds = Number(value.seconds ?? 0);
167
+ const nanos = Number(value.nanos ?? 0);
168
+ if (Number.isFinite(seconds)) {
169
+ return new Date(seconds * 1000 + Math.floor(nanos / 1_000_000)).toISOString();
170
+ }
171
+ }
172
+ return null;
173
+ };
174
+ const toNumber = (value) => {
175
+ if (typeof value === "number")
176
+ return Number.isFinite(value) ? value : null;
177
+ if (typeof value === "bigint")
178
+ return Number(value);
179
+ if (typeof value === "string" && value.trim()) {
180
+ const parsed = Number(value);
181
+ return Number.isFinite(parsed) ? parsed : null;
182
+ }
183
+ // Long.js { low, high, unsigned }
184
+ if (typeof value === "object" && value !== null && typeof value.toNumber === "function") {
185
+ const parsed = value.toNumber();
186
+ return Number.isFinite(parsed) ? parsed : null;
187
+ }
188
+ return null;
189
+ };
190
+ const durationToMs = (value) => {
191
+ if (value === null || value === undefined)
192
+ return null;
193
+ if (typeof value === "number")
194
+ return value;
195
+ // google.protobuf.Duration { seconds, nanos }
196
+ if (typeof value === "object" && "seconds" in value) {
197
+ const seconds = toNumber(value.seconds) ?? 0;
198
+ const nanos = toNumber(value.nanos) ?? 0;
199
+ return seconds * 1000 + Math.floor(nanos / 1_000_000);
200
+ }
201
+ return null;
202
+ };
203
+ const msToDuration = (ms) => typeof ms === "number" && ms > 0 ? `${Math.round(ms)}ms` : undefined;
204
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
205
+ // Strips the proto enum prefix: EVENT_TYPE_ACTIVITY_TASK_COMPLETED →
206
+ // ActivityTaskCompleted; WORKFLOW_EXECUTION_STATUS_RUNNING → RUNNING.
207
+ const enumName = (value, prefix) => {
208
+ if (typeof value === "number")
209
+ return null;
210
+ if (typeof value !== "string")
211
+ return null;
212
+ return value.startsWith(prefix) ? value.slice(prefix.length) : value;
213
+ };
214
+ const toPascal = (upperSnake) => upperSnake
215
+ .toLowerCase()
216
+ .split("_")
217
+ .filter(Boolean)
218
+ .map((part) => part[0].toUpperCase() + part.slice(1))
219
+ .join("");
220
+ const WORKFLOW_STATUS_BY_NUMBER = {
221
+ 1: "RUNNING",
222
+ 2: "COMPLETED",
223
+ 3: "FAILED",
224
+ 4: "CANCELED",
225
+ 5: "TERMINATED",
226
+ 6: "CONTINUED_AS_NEW",
227
+ 7: "TIMED_OUT",
228
+ };
229
+ export const normalizeWorkflowStatus = (value) => {
230
+ if (typeof value === "number")
231
+ return WORKFLOW_STATUS_BY_NUMBER[value] ?? "UNKNOWN";
232
+ if (typeof value !== "string")
233
+ return "UNKNOWN";
234
+ const name = value.replace(/^WORKFLOW_EXECUTION_STATUS_/, "").toUpperCase();
235
+ const known = [
236
+ "RUNNING",
237
+ "COMPLETED",
238
+ "FAILED",
239
+ "CANCELED",
240
+ "TERMINATED",
241
+ "TIMED_OUT",
242
+ "CONTINUED_AS_NEW",
243
+ ];
244
+ return known.includes(name) ? name : "UNKNOWN";
245
+ };
246
+ const EVENT_TYPE_BY_NUMBER = {
247
+ 1: "WorkflowExecutionStarted",
248
+ 2: "WorkflowExecutionCompleted",
249
+ 3: "WorkflowExecutionFailed",
250
+ 4: "WorkflowExecutionTimedOut",
251
+ 5: "WorkflowTaskScheduled",
252
+ 6: "WorkflowTaskStarted",
253
+ 7: "WorkflowTaskCompleted",
254
+ 8: "WorkflowTaskTimedOut",
255
+ 9: "WorkflowTaskFailed",
256
+ 10: "ActivityTaskScheduled",
257
+ 11: "ActivityTaskStarted",
258
+ 12: "ActivityTaskCompleted",
259
+ 13: "ActivityTaskFailed",
260
+ 14: "ActivityTaskTimedOut",
261
+ 15: "ActivityTaskCancelRequested",
262
+ 16: "ActivityTaskCanceled",
263
+ 17: "TimerStarted",
264
+ 18: "TimerFired",
265
+ 19: "TimerCanceled",
266
+ 20: "WorkflowExecutionCancelRequested",
267
+ 21: "WorkflowExecutionCanceled",
268
+ 22: "RequestCancelExternalWorkflowExecutionInitiated",
269
+ 23: "RequestCancelExternalWorkflowExecutionFailed",
270
+ 24: "ExternalWorkflowExecutionCancelRequested",
271
+ 25: "MarkerRecorded",
272
+ 26: "WorkflowExecutionSignaled",
273
+ 27: "WorkflowExecutionTerminated",
274
+ 28: "WorkflowExecutionContinuedAsNew",
275
+ 29: "StartChildWorkflowExecutionInitiated",
276
+ 30: "StartChildWorkflowExecutionFailed",
277
+ 31: "ChildWorkflowExecutionStarted",
278
+ 32: "ChildWorkflowExecutionCompleted",
279
+ 33: "ChildWorkflowExecutionFailed",
280
+ 34: "ChildWorkflowExecutionCanceled",
281
+ 35: "ChildWorkflowExecutionTimedOut",
282
+ 36: "ChildWorkflowExecutionTerminated",
283
+ 37: "SignalExternalWorkflowExecutionInitiated",
284
+ 38: "SignalExternalWorkflowExecutionFailed",
285
+ 39: "ExternalWorkflowExecutionSignaled",
286
+ 40: "UpsertWorkflowSearchAttributes",
287
+ 41: "WorkflowExecutionUpdateAccepted",
288
+ 42: "WorkflowExecutionUpdateRejected",
289
+ 43: "WorkflowExecutionUpdateCompleted",
290
+ 44: "WorkflowPropertiesModifiedExternally",
291
+ 45: "ActivityPropertiesModifiedExternally",
292
+ 46: "WorkflowPropertiesModified",
293
+ 47: "WorkflowExecutionUpdateAdmitted",
294
+ 48: "NexusOperationScheduled",
295
+ 49: "NexusOperationStarted",
296
+ 50: "NexusOperationCompleted",
297
+ 51: "NexusOperationFailed",
298
+ 52: "NexusOperationCanceled",
299
+ 53: "NexusOperationTimedOut",
300
+ 54: "NexusOperationCancelRequested",
301
+ 55: "WorkflowExecutionOptionsUpdated",
302
+ };
303
+ export const normalizeEventType = (value) => {
304
+ if (typeof value === "number")
305
+ return EVENT_TYPE_BY_NUMBER[value] ?? `EventType${value}`;
306
+ const name = enumName(value, "EVENT_TYPE_");
307
+ if (!name)
308
+ return "Unknown";
309
+ return /^[A-Z0-9_]+$/.test(name) ? toPascal(name) : name;
310
+ };
311
+ // Payload decoding: default data converter (JSON / null / binary). Anything
312
+ // it cannot decode is kept as `{ encoding, data (base64) }` so the step never
313
+ // fails on an exotic payload (custom data converters are not supported).
314
+ let payloadConverter = null;
315
+ const getPayloadConverter = async () => {
316
+ if (payloadConverter)
317
+ return payloadConverter;
318
+ try {
319
+ const driver = await loadTemporalDriver();
320
+ payloadConverter = driver.defaultPayloadConverter ?? null;
321
+ }
322
+ catch {
323
+ payloadConverter = null;
324
+ }
325
+ return payloadConverter;
326
+ };
327
+ const bytesToString = (value) => {
328
+ if (value instanceof Uint8Array)
329
+ return Buffer.from(value).toString("utf8");
330
+ if (typeof value === "string")
331
+ return Buffer.from(value, "base64").toString("utf8");
332
+ return null;
333
+ };
334
+ const decodeOnePayload = (payload, converter) => {
335
+ if (!isRecord(payload))
336
+ return payload;
337
+ if (converter && typeof converter.fromPayload === "function") {
338
+ try {
339
+ return converter.fromPayload(payload);
340
+ }
341
+ catch {
342
+ // fall through to the manual decode
343
+ }
344
+ }
345
+ const metadata = isRecord(payload.metadata) ? payload.metadata : {};
346
+ const encoding = bytesToString(metadata.encoding) ?? "unknown";
347
+ const data = payload.data;
348
+ if (encoding === "binary/null")
349
+ return null;
350
+ if (encoding === "json/plain" || encoding === "json/protobuf") {
351
+ const text = bytesToString(data);
352
+ if (text !== null) {
353
+ try {
354
+ return JSON.parse(text);
355
+ }
356
+ catch {
357
+ return text;
358
+ }
359
+ }
360
+ }
361
+ const base64 = data instanceof Uint8Array
362
+ ? Buffer.from(data).toString("base64")
363
+ : typeof data === "string"
364
+ ? data
365
+ : null;
366
+ return { encoding, data: base64 };
367
+ };
368
+ /** Decode a `Payloads` ({ payloads: [...] }) or a single Payload into JS values. */
369
+ export const decodeTemporalPayloads = async (value) => {
370
+ if (value === null || value === undefined)
371
+ return [];
372
+ const converter = await getPayloadConverter();
373
+ const list = isRecord(value) && Array.isArray(value.payloads) ? value.payloads : [value];
374
+ return list.map((entry) => decodeOnePayload(entry, converter));
375
+ };
376
+ const decodeFirst = async (value) => {
377
+ const decoded = await decodeTemporalPayloads(value);
378
+ return decoded.length === 0 ? null : decoded.length === 1 ? decoded[0] : decoded;
379
+ };
380
+ /** Decode a `{ fields: { key: Payload } }` map (memo / search attributes). */
381
+ const decodeFieldMap = async (value) => {
382
+ if (!isRecord(value))
383
+ return null;
384
+ const fields = isRecord(value.fields) ? value.fields : value;
385
+ const converter = await getPayloadConverter();
386
+ const out = {};
387
+ for (const [key, payload] of Object.entries(fields)) {
388
+ out[key] = isRecord(payload) && "metadata" in payload ? decodeOnePayload(payload, converter) : payload;
389
+ }
390
+ return out;
391
+ };
392
+ // --- Failure mapping --------------------------------------------------------
393
+ const failureTypeOf = (failure) => {
394
+ if (isRecord(failure.applicationFailureInfo)) {
395
+ const type = failure.applicationFailureInfo.type;
396
+ return typeof type === "string" && type ? type : "ApplicationFailure";
397
+ }
398
+ if (failure.activityFailureInfo)
399
+ return "ActivityFailure";
400
+ if (failure.childWorkflowExecutionFailureInfo)
401
+ return "ChildWorkflowFailure";
402
+ if (failure.canceledFailureInfo)
403
+ return "CanceledFailure";
404
+ if (failure.timeoutFailureInfo)
405
+ return "TimeoutFailure";
406
+ if (failure.terminatedFailureInfo)
407
+ return "TerminatedFailure";
408
+ if (failure.serverFailureInfo)
409
+ return "ServerFailure";
410
+ if (failure.nexusOperationExecutionFailureInfo)
411
+ return "NexusOperationFailure";
412
+ return null;
413
+ };
414
+ /** Proto `Failure` message → ScenarioTemporalFailure (cause chain flattened, depth 3). */
415
+ export const summarizeProtoFailure = (value, depth = 0) => {
416
+ if (!isRecord(value))
417
+ return null;
418
+ const message = typeof value.message === "string" ? value.message : "Workflow failed";
419
+ const cause = [];
420
+ let current = value.cause;
421
+ let level = 0;
422
+ while (isRecord(current) && level < 3) {
423
+ cause.push({
424
+ message: typeof current.message === "string" ? current.message : "",
425
+ type: failureTypeOf(current),
426
+ });
427
+ current = current.cause;
428
+ level += 1;
429
+ }
430
+ return {
431
+ message,
432
+ type: failureTypeOf(value),
433
+ stackTrace: typeof value.stackTrace === "string" && value.stackTrace ? value.stackTrace : null,
434
+ ...(cause.length > 0 ? { cause } : {}),
435
+ };
436
+ };
437
+ /** SDK error (WorkflowFailedError, ApplicationFailure…) → ScenarioTemporalFailure. */
438
+ export const summarizeSdkError = (error) => {
439
+ if (!(error instanceof Error)) {
440
+ return { message: String(error), type: null };
441
+ }
442
+ const cause = [];
443
+ let current = error.cause;
444
+ let level = 0;
445
+ while (current instanceof Error && level < 3) {
446
+ const typed = current;
447
+ cause.push({
448
+ message: current.message,
449
+ type: typeof typed.type === "string"
450
+ ? typed.type
451
+ : current.name && current.name !== "Error"
452
+ ? current.name
453
+ : null,
454
+ });
455
+ current = current.cause;
456
+ level += 1;
457
+ }
458
+ const first = cause[0];
459
+ return {
460
+ message: first?.message ? `${error.message}: ${first.message}` : error.message,
461
+ type: first?.type ?? (error.name !== "Error" ? error.name : null),
462
+ stackTrace: null,
463
+ ...(cause.length > 0 ? { cause } : {}),
464
+ };
465
+ };
466
+ const attributesOf = (event) => {
467
+ for (const [key, value] of Object.entries(event)) {
468
+ if (key.endsWith("EventAttributes") && isRecord(value)) {
469
+ return value;
470
+ }
471
+ }
472
+ return null;
473
+ };
474
+ const CLOSE_STATUS_BY_EVENT = {
475
+ WorkflowExecutionCompleted: "COMPLETED",
476
+ WorkflowExecutionFailed: "FAILED",
477
+ WorkflowExecutionTimedOut: "TIMED_OUT",
478
+ WorkflowExecutionCanceled: "CANCELED",
479
+ WorkflowExecutionTerminated: "TERMINATED",
480
+ WorkflowExecutionContinuedAsNew: "CONTINUED_AS_NEW",
481
+ };
482
+ /**
483
+ * Turn raw history events (proto JSON as `fetchHistory()` returns them) into
484
+ * the report-friendly summary. `eventTypes` filters the kept `events[]`
485
+ * (counts / activities / signals always cover the whole history); `limit`
486
+ * caps `events[]` (the last N events win — the tail is where failures are).
487
+ */
488
+ export const summarizeTemporalHistory = async (rawEvents, options = {}) => {
489
+ const limit = Math.min(MAX_TEMPORAL_HISTORY_EVENTS, Math.max(1, options.limit ?? DEFAULT_TEMPORAL_HISTORY_LIMIT));
490
+ const wanted = new Set((options.eventTypes ?? []).map((entry) => entry.toLowerCase()));
491
+ const converter = await getPayloadConverter();
492
+ // The counters people assert on are zero-seeded so `equals 0` is a real
493
+ // check (a missing key would make the rule fail, not pass).
494
+ const eventCounts = {
495
+ WorkflowExecutionStarted: 0,
496
+ WorkflowExecutionCompleted: 0,
497
+ WorkflowExecutionFailed: 0,
498
+ WorkflowExecutionCanceled: 0,
499
+ WorkflowExecutionTerminated: 0,
500
+ WorkflowExecutionTimedOut: 0,
501
+ WorkflowExecutionSignaled: 0,
502
+ WorkflowTaskFailed: 0,
503
+ ActivityTaskScheduled: 0,
504
+ ActivityTaskStarted: 0,
505
+ ActivityTaskCompleted: 0,
506
+ ActivityTaskFailed: 0,
507
+ ActivityTaskTimedOut: 0,
508
+ ActivityTaskCanceled: 0,
509
+ TimerStarted: 0,
510
+ TimerFired: 0,
511
+ ChildWorkflowExecutionCompleted: 0,
512
+ ChildWorkflowExecutionFailed: 0,
513
+ };
514
+ const activitiesByScheduledId = new Map();
515
+ const activityOrder = [];
516
+ const signals = [];
517
+ const timersByStartedId = new Map();
518
+ const timerOrder = [];
519
+ let failure = null;
520
+ let result = null;
521
+ let status = null;
522
+ const kept = [];
523
+ for (const raw of rawEvents) {
524
+ if (!isRecord(raw))
525
+ continue;
526
+ const eventType = normalizeEventType(raw.eventType);
527
+ const eventId = toNumber(raw.eventId) ?? 0;
528
+ const eventTime = toIso(raw.eventTime);
529
+ const attributes = attributesOf(raw);
530
+ eventCounts[eventType] = (eventCounts[eventType] ?? 0) + 1;
531
+ switch (eventType) {
532
+ case "ActivityTaskScheduled": {
533
+ const activityType = isRecord(attributes?.activityType)
534
+ ? String(attributes.activityType.name ?? "")
535
+ : "";
536
+ const activity = {
537
+ activityId: String(attributes?.activityId ?? eventId),
538
+ activityType,
539
+ status: "scheduled",
540
+ attempt: 1,
541
+ scheduledAt: eventTime,
542
+ startedAt: null,
543
+ closedAt: null,
544
+ durationMs: null,
545
+ failure: null,
546
+ };
547
+ activitiesByScheduledId.set(eventId, activity);
548
+ activityOrder.push(activity);
549
+ break;
550
+ }
551
+ case "ActivityTaskStarted": {
552
+ const activity = activitiesByScheduledId.get(toNumber(attributes?.scheduledEventId) ?? -1);
553
+ if (activity) {
554
+ activity.status = "started";
555
+ activity.startedAt = eventTime;
556
+ activity.attempt = toNumber(attributes?.attempt) ?? activity.attempt;
557
+ const lastFailure = summarizeProtoFailure(attributes?.lastFailure);
558
+ if (lastFailure)
559
+ activity.failure = lastFailure.message;
560
+ }
561
+ break;
562
+ }
563
+ case "ActivityTaskCompleted":
564
+ case "ActivityTaskFailed":
565
+ case "ActivityTaskTimedOut":
566
+ case "ActivityTaskCanceled": {
567
+ const activity = activitiesByScheduledId.get(toNumber(attributes?.scheduledEventId) ?? -1);
568
+ if (activity) {
569
+ activity.status =
570
+ eventType === "ActivityTaskCompleted"
571
+ ? "completed"
572
+ : eventType === "ActivityTaskFailed"
573
+ ? "failed"
574
+ : eventType === "ActivityTaskTimedOut"
575
+ ? "timed_out"
576
+ : "canceled";
577
+ activity.closedAt = eventTime;
578
+ const start = activity.startedAt ?? activity.scheduledAt;
579
+ if (start && eventTime) {
580
+ activity.durationMs = Math.max(0, Date.parse(eventTime) - Date.parse(start));
581
+ }
582
+ if (eventType === "ActivityTaskCompleted") {
583
+ activity.result = attributes?.result ? decodeFirstSync(attributes.result, converter) : null;
584
+ }
585
+ else {
586
+ const summarized = summarizeProtoFailure(attributes?.failure);
587
+ if (summarized)
588
+ activity.failure = summarized.message;
589
+ }
590
+ }
591
+ break;
592
+ }
593
+ case "WorkflowExecutionSignaled": {
594
+ signals.push({
595
+ name: String(attributes?.signalName ?? ""),
596
+ eventId,
597
+ eventTime,
598
+ });
599
+ break;
600
+ }
601
+ case "TimerStarted": {
602
+ const timer = {
603
+ timerId: String(attributes?.timerId ?? eventId),
604
+ fired: false,
605
+ startedAt: eventTime,
606
+ };
607
+ timersByStartedId.set(eventId, timer);
608
+ timerOrder.push(timer);
609
+ break;
610
+ }
611
+ case "TimerFired": {
612
+ const timer = timersByStartedId.get(toNumber(attributes?.startedEventId) ?? -1);
613
+ if (timer)
614
+ timer.fired = true;
615
+ break;
616
+ }
617
+ case "WorkflowExecutionCompleted": {
618
+ status = "COMPLETED";
619
+ result = attributes?.result ? decodeFirstSync(attributes.result, converter) : null;
620
+ break;
621
+ }
622
+ case "WorkflowExecutionFailed": {
623
+ status = "FAILED";
624
+ failure = summarizeProtoFailure(attributes?.failure) ?? { message: "Workflow failed", type: null };
625
+ break;
626
+ }
627
+ case "WorkflowExecutionTimedOut":
628
+ case "WorkflowExecutionCanceled":
629
+ case "WorkflowExecutionTerminated":
630
+ case "WorkflowExecutionContinuedAsNew": {
631
+ status = CLOSE_STATUS_BY_EVENT[eventType] ?? status;
632
+ if (eventType === "WorkflowExecutionTerminated" || eventType === "WorkflowExecutionCanceled") {
633
+ const reason = typeof attributes?.reason === "string" ? attributes.reason : null;
634
+ failure = {
635
+ message: reason ? `${eventType.replace("WorkflowExecution", "")}: ${reason}` : eventType.replace("WorkflowExecution", "Workflow "),
636
+ type: eventType === "WorkflowExecutionCanceled" ? "CanceledFailure" : "TerminatedFailure",
637
+ };
638
+ }
639
+ else if (eventType === "WorkflowExecutionTimedOut") {
640
+ failure = { message: "Workflow timed out", type: "TimeoutFailure" };
641
+ }
642
+ break;
643
+ }
644
+ default:
645
+ break;
646
+ }
647
+ if (wanted.size === 0 || wanted.has(eventType.toLowerCase())) {
648
+ kept.push({
649
+ eventId,
650
+ eventType,
651
+ eventTime,
652
+ ...(attributes ? { attributes: decodeAttributesSync(attributes, converter) } : {}),
653
+ });
654
+ }
655
+ }
656
+ if (status === null && rawEvents.length > 0) {
657
+ status = "RUNNING";
658
+ }
659
+ const truncated = kept.length > limit;
660
+ return {
661
+ events: truncated ? kept.slice(kept.length - limit) : kept,
662
+ eventCounts,
663
+ activities: activityOrder,
664
+ signals,
665
+ timers: timerOrder,
666
+ failure,
667
+ result,
668
+ status,
669
+ historyLength: rawEvents.length,
670
+ truncated,
671
+ };
672
+ };
673
+ const decodeFirstSync = (value, converter) => {
674
+ const list = isRecord(value) && Array.isArray(value.payloads) ? value.payloads : [value];
675
+ const decoded = list.map((entry) => decodeOnePayload(entry, converter));
676
+ return decoded.length === 0 ? null : decoded.length === 1 ? decoded[0] : decoded;
677
+ };
678
+ // Replace Payload(s) objects inside an attribute bag with decoded values so
679
+ // the report shows `input: {...}` rather than base64 blobs. Shallow on purpose
680
+ // — nested proto messages stay as they are.
681
+ const decodeAttributesSync = (attributes, converter) => {
682
+ const out = {};
683
+ for (const [key, value] of Object.entries(attributes)) {
684
+ if (isRecord(value) && Array.isArray(value.payloads)) {
685
+ out[key] = decodeFirstSync(value, converter);
686
+ }
687
+ else if (isRecord(value) && "metadata" in value && "data" in value) {
688
+ out[key] = decodeOnePayload(value, converter);
689
+ }
690
+ else if (key === "failure" || key === "lastFailure") {
691
+ out[key] = summarizeProtoFailure(value);
692
+ }
693
+ else if (typeof value === "bigint") {
694
+ out[key] = Number(value);
695
+ }
696
+ else {
697
+ out[key] = value;
698
+ }
699
+ }
700
+ return out;
701
+ };
702
+ // --- Abort plumbing ---------------------------------------------------------
703
+ class TemporalAbortError extends Error {
704
+ constructor() {
705
+ super("Temporal operation cancelled");
706
+ this.name = "TemporalAbortError";
707
+ }
708
+ }
709
+ // Races a pending gRPC call against the scenario's abort signal. On abort the
710
+ // cache entry is closed + evicted (the SDK has no per-call cancel).
711
+ const raceAbort = async (task, signal, onAbort) => {
712
+ if (!signal)
713
+ return task;
714
+ if (signal.aborted) {
715
+ await onAbort();
716
+ throw new TemporalAbortError();
717
+ }
718
+ return await new Promise((resolve, reject) => {
719
+ const handleAbort = () => {
720
+ void onAbort().finally(() => reject(new TemporalAbortError()));
721
+ };
722
+ signal.addEventListener?.("abort", handleAbort, { once: true });
723
+ task.then((value) => {
724
+ signal.removeEventListener?.("abort", handleAbort);
725
+ resolve(value);
726
+ }, (error) => {
727
+ signal.removeEventListener?.("abort", handleAbort);
728
+ reject(error);
729
+ });
730
+ });
731
+ };
732
+ // --- Operation execution ----------------------------------------------------
733
+ const workflowIdOf = (op) => {
734
+ if (!op.workflowId) {
735
+ throw new Error("Temporal operation needs a workflow id.");
736
+ }
737
+ return op.workflowId;
738
+ };
739
+ const toStartOptions = (op) => {
740
+ const start = op.startOptions;
741
+ return {
742
+ taskQueue: op.taskQueue,
743
+ workflowId: workflowIdOf(op),
744
+ args: op.args,
745
+ ...(start.idReusePolicy ? { workflowIdReusePolicy: start.idReusePolicy } : {}),
746
+ ...(start.idConflictPolicy ? { workflowIdConflictPolicy: start.idConflictPolicy } : {}),
747
+ ...(start.memo ? { memo: start.memo } : {}),
748
+ ...(start.searchAttributes ? { searchAttributes: start.searchAttributes } : {}),
749
+ ...(msToDuration(start.workflowExecutionTimeoutMs)
750
+ ? { workflowExecutionTimeout: msToDuration(start.workflowExecutionTimeoutMs) }
751
+ : {}),
752
+ ...(msToDuration(start.workflowRunTimeoutMs)
753
+ ? { workflowRunTimeout: msToDuration(start.workflowRunTimeoutMs) }
754
+ : {}),
755
+ ...(msToDuration(start.workflowTaskTimeoutMs)
756
+ ? { workflowTaskTimeout: msToDuration(start.workflowTaskTimeoutMs) }
757
+ : {}),
758
+ ...(msToDuration(start.startDelayMs) ? { startDelay: msToDuration(start.startDelayMs) } : {}),
759
+ ...(start.retryPolicy
760
+ ? {
761
+ retry: {
762
+ ...(start.retryPolicy.maximumAttempts
763
+ ? { maximumAttempts: start.retryPolicy.maximumAttempts }
764
+ : {}),
765
+ ...(msToDuration(start.retryPolicy.initialIntervalMs)
766
+ ? { initialInterval: msToDuration(start.retryPolicy.initialIntervalMs) }
767
+ : {}),
768
+ ...(start.retryPolicy.backoffCoefficient
769
+ ? { backoffCoefficient: start.retryPolicy.backoffCoefficient }
770
+ : {}),
771
+ ...(msToDuration(start.retryPolicy.maximumIntervalMs)
772
+ ? { maximumInterval: msToDuration(start.retryPolicy.maximumIntervalMs) }
773
+ : {}),
774
+ },
775
+ }
776
+ : {}),
777
+ };
778
+ };
779
+ const applyDescription = async (result, description) => {
780
+ // WorkflowExecutionDescription (SDK handle.describe()) — camelCase, with
781
+ // `raw` holding the proto DescribeWorkflowExecutionResponse.
782
+ result.workflowId = typeof description.workflowId === "string" ? description.workflowId : result.workflowId;
783
+ result.runId = typeof description.runId === "string" ? description.runId : result.runId;
784
+ result.status = normalizeWorkflowStatus(isRecord(description.status) ? description.status.name : description.status);
785
+ result.workflowType = typeof description.type === "string" ? description.type : result.workflowType;
786
+ result.taskQueue = typeof description.taskQueue === "string" ? description.taskQueue : result.taskQueue;
787
+ result.startTime = toIso(description.startTime);
788
+ result.closeTime = toIso(description.closeTime);
789
+ result.historyLength = toNumber(description.historyLength);
790
+ result.memo = isRecord(description.memo) ? description.memo : null;
791
+ result.searchAttributes = isRecord(description.searchAttributes)
792
+ ? description.searchAttributes
793
+ : isRecord(description.typedSearchAttributes)
794
+ ? description.typedSearchAttributes
795
+ : null;
796
+ const raw = isRecord(description.raw) ? description.raw : null;
797
+ const pending = raw && Array.isArray(raw.pendingActivities) ? raw.pendingActivities : [];
798
+ result.activities = pending.filter(isRecord).map((entry) => ({
799
+ activityId: String(entry.activityId ?? ""),
800
+ activityType: isRecord(entry.activityType) ? String(entry.activityType.name ?? "") : "",
801
+ status: String(entry.state ?? "").toUpperCase().includes("STARTED") ? "started" : "scheduled",
802
+ attempt: toNumber(entry.attempt) ?? 1,
803
+ scheduledAt: toIso(entry.scheduledTime),
804
+ startedAt: toIso(entry.lastStartedTime),
805
+ closedAt: null,
806
+ durationMs: null,
807
+ failure: summarizeProtoFailure(entry.lastFailure)?.message ?? null,
808
+ }));
809
+ const children = raw && Array.isArray(raw.pendingChildren) ? raw.pendingChildren : [];
810
+ result.pendingChildren = children.filter(isRecord).map((entry) => ({
811
+ workflowId: String(entry.workflowId ?? ""),
812
+ runId: typeof entry.runId === "string" ? entry.runId : null,
813
+ workflowType: typeof entry.workflowTypeName === "string" ? entry.workflowTypeName : null,
814
+ }));
815
+ };
816
+ const applyWorkflowFailure = (result, error) => {
817
+ const failure = summarizeSdkError(error);
818
+ result.failure = failure;
819
+ const type = (failure.type ?? "").toLowerCase();
820
+ const name = error instanceof Error ? error.name : "";
821
+ if (name === "WorkflowFailedError" || type.includes("application") || type.includes("activity")) {
822
+ result.status = type.includes("canceled")
823
+ ? "CANCELED"
824
+ : type.includes("terminated")
825
+ ? "TERMINATED"
826
+ : type.includes("timeout")
827
+ ? "TIMED_OUT"
828
+ : "FAILED";
829
+ }
830
+ else if (type.includes("canceled")) {
831
+ result.status = "CANCELED";
832
+ }
833
+ else if (type.includes("terminated")) {
834
+ result.status = "TERMINATED";
835
+ }
836
+ else if (type.includes("timeout")) {
837
+ result.status = "TIMED_OUT";
838
+ }
839
+ else {
840
+ result.status = "FAILED";
841
+ }
842
+ };
843
+ const isWorkflowClosedError = (error) => error instanceof Error &&
844
+ (error.name === "WorkflowFailedError" ||
845
+ error.name === "WorkflowCanceledError" ||
846
+ error.name === "WorkflowTerminatedError" ||
847
+ error.name === "WorkflowTimedOutError");
848
+ const isNotFound = (error) => error instanceof Error &&
849
+ (error.name === "WorkflowNotFoundError" ||
850
+ /not found|NOT_FOUND/i.test(error.message));
851
+ /**
852
+ * Execute one resolved Temporal operation. Throws for infrastructure /
853
+ * configuration errors (unreachable server, unknown workflow, rejected
854
+ * update…); a workflow that FAILED is a result (`status`, `failure`), not an
855
+ * exception — the node's `expect` rules decide what passes.
856
+ */
857
+ export const executeTemporalOperation = async (input) => {
858
+ const config = input.connection.config;
859
+ const { op, runId, signal } = input;
860
+ const namespace = resolveTemporalNamespace(config, op.namespace);
861
+ const result = createEmptyTemporalResult(op.operation, namespace);
862
+ const startedAtMs = Date.now();
863
+ const { key, entry } = await getCachedClient(runId, input.connection.id, config, namespace);
864
+ const client = entry.client;
865
+ const abortClose = () => closeEntry(key);
866
+ const guarded = (task) => raceAbort(task, signal, abortClose);
867
+ const finish = () => {
868
+ result.durationMs = Date.now() - startedAtMs;
869
+ return result;
870
+ };
871
+ const handleFor = (workflowId, workflowRunId) => workflowRunId ? client.workflow.getHandle(workflowId, workflowRunId) : client.workflow.getHandle(workflowId);
872
+ const awaitHandleResult = async (handle) => {
873
+ try {
874
+ result.result = await guarded(handle.result());
875
+ result.status = "COMPLETED";
876
+ }
877
+ catch (error) {
878
+ if (error instanceof TemporalAbortError)
879
+ throw error;
880
+ if (isWorkflowClosedError(error)) {
881
+ applyWorkflowFailure(result, error);
882
+ }
883
+ else {
884
+ throw error;
885
+ }
886
+ }
887
+ try {
888
+ const description = await guarded(handle.describe());
889
+ if (isRecord(description)) {
890
+ const status = result.status;
891
+ await applyDescription(result, description);
892
+ // The describe status wins only when the SDK gave us nothing better.
893
+ if (status && result.status === "UNKNOWN")
894
+ result.status = status;
895
+ }
896
+ }
897
+ catch {
898
+ // describe is best-effort after the result
899
+ }
900
+ };
901
+ switch (op.operation) {
902
+ case "startWorkflow": {
903
+ const workflowId = workflowIdOf(op);
904
+ let handle;
905
+ try {
906
+ handle = await guarded(client.workflow.start(op.workflowType, toStartOptions(op)));
907
+ }
908
+ catch (error) {
909
+ if (error instanceof Error &&
910
+ error.name === "WorkflowExecutionAlreadyStartedError" &&
911
+ op.startOptions.idConflictPolicy === "USE_EXISTING") {
912
+ handle = client.workflow.getHandle(workflowId);
913
+ }
914
+ else {
915
+ throw error;
916
+ }
917
+ }
918
+ result.workflowId = handle.workflowId ?? workflowId;
919
+ result.runId = typeof handle.firstExecutionRunId === "string" ? handle.firstExecutionRunId : null;
920
+ result.workflowType = op.workflowType;
921
+ result.taskQueue = op.taskQueue;
922
+ result.status = "RUNNING";
923
+ result.requested = true;
924
+ if (op.waitForResult) {
925
+ await awaitHandleResult(handle);
926
+ }
927
+ return finish();
928
+ }
929
+ case "signalWithStart": {
930
+ const handle = await guarded(client.workflow.signalWithStart(op.workflowType, {
931
+ ...toStartOptions(op),
932
+ signal: op.signalName,
933
+ signalArgs: op.signalArgs,
934
+ }));
935
+ result.workflowId = handle.workflowId ?? op.workflowId;
936
+ result.runId = typeof handle.firstExecutionRunId === "string" ? handle.firstExecutionRunId : null;
937
+ result.workflowType = op.workflowType;
938
+ result.taskQueue = op.taskQueue;
939
+ result.status = "RUNNING";
940
+ result.requested = true;
941
+ return finish();
942
+ }
943
+ case "signal": {
944
+ const handle = handleFor(workflowIdOf(op), op.runId);
945
+ await guarded(handle.signal(op.signalName, ...op.args));
946
+ result.workflowId = op.workflowId;
947
+ result.runId = op.runId;
948
+ result.requested = true;
949
+ return finish();
950
+ }
951
+ case "query": {
952
+ const handle = handleFor(workflowIdOf(op), op.runId);
953
+ result.result = await guarded(handle.query(op.queryName, ...op.args));
954
+ result.workflowId = op.workflowId;
955
+ result.runId = op.runId;
956
+ return finish();
957
+ }
958
+ case "update": {
959
+ const handle = handleFor(workflowIdOf(op), op.runId);
960
+ result.workflowId = op.workflowId;
961
+ result.runId = op.runId;
962
+ if (op.waitForStage === "ACCEPTED") {
963
+ const updateHandle = await guarded(handle.startUpdate(op.updateName, { args: op.args, waitForStage: "ACCEPTED" }));
964
+ result.updateId = typeof updateHandle.updateId === "string" ? updateHandle.updateId : null;
965
+ result.requested = true;
966
+ }
967
+ else {
968
+ result.result = await guarded(handle.executeUpdate(op.updateName, { args: op.args }));
969
+ result.requested = true;
970
+ }
971
+ return finish();
972
+ }
973
+ case "awaitResult": {
974
+ const handle = handleFor(workflowIdOf(op), op.followRuns ? null : op.runId);
975
+ result.workflowId = op.workflowId;
976
+ result.runId = op.runId;
977
+ await awaitHandleResult(handle);
978
+ return finish();
979
+ }
980
+ case "describe": {
981
+ const handle = handleFor(workflowIdOf(op), op.runId);
982
+ result.workflowId = op.workflowId;
983
+ result.runId = op.runId;
984
+ const description = await guarded(handle.describe());
985
+ if (isRecord(description)) {
986
+ await applyDescription(result, description);
987
+ }
988
+ return finish();
989
+ }
990
+ case "history": {
991
+ const handle = handleFor(workflowIdOf(op), op.runId);
992
+ result.workflowId = op.workflowId;
993
+ result.runId = op.runId;
994
+ const history = await guarded(handle.fetchHistory());
995
+ const rawEvents = isRecord(history) && Array.isArray(history.events) ? history.events : [];
996
+ const summary = await summarizeTemporalHistory(rawEvents, {
997
+ eventTypes: op.eventTypes,
998
+ limit: op.historyLimit ?? undefined,
999
+ });
1000
+ result.events = summary.events;
1001
+ result.eventCounts = summary.eventCounts;
1002
+ result.activities = summary.activities;
1003
+ result.signals = summary.signals;
1004
+ result.timers = summary.timers;
1005
+ result.failure = summary.failure;
1006
+ result.result = summary.result;
1007
+ result.status = summary.status;
1008
+ result.historyLength = summary.historyLength;
1009
+ result.truncatedHistory = summary.truncated;
1010
+ const first = rawEvents.find(isRecord);
1011
+ const startAttributes = first ? attributesOf(first) : null;
1012
+ if (startAttributes) {
1013
+ result.workflowType = isRecord(startAttributes.workflowType)
1014
+ ? String(startAttributes.workflowType.name ?? "")
1015
+ : null;
1016
+ result.taskQueue = isRecord(startAttributes.taskQueue)
1017
+ ? String(startAttributes.taskQueue.name ?? "")
1018
+ : null;
1019
+ result.startTime = toIso(first?.eventTime);
1020
+ }
1021
+ return finish();
1022
+ }
1023
+ case "cancel":
1024
+ case "terminate": {
1025
+ const handle = handleFor(workflowIdOf(op), op.runId);
1026
+ result.workflowId = op.workflowId;
1027
+ result.runId = op.runId;
1028
+ if (op.operation === "cancel") {
1029
+ await guarded(handle.cancel());
1030
+ }
1031
+ else {
1032
+ await guarded(handle.terminate(op.reason ?? undefined));
1033
+ }
1034
+ result.requested = true;
1035
+ return finish();
1036
+ }
1037
+ case "list": {
1038
+ const pageSize = Math.min(MAX_TEMPORAL_LIST_PAGE_SIZE, Math.max(1, op.pageSize ?? DEFAULT_TEMPORAL_LIST_PAGE_SIZE));
1039
+ const executions = [];
1040
+ const iterable = client.workflow.list({ query: op.query ?? "", pageSize });
1041
+ const collect = (async () => {
1042
+ for await (const info of iterable) {
1043
+ if (!isRecord(info))
1044
+ continue;
1045
+ executions.push({
1046
+ workflowId: String(info.workflowId ?? ""),
1047
+ runId: String(info.runId ?? ""),
1048
+ type: typeof info.type === "string" ? info.type : "",
1049
+ status: normalizeWorkflowStatus(isRecord(info.status) ? info.status.name : info.status),
1050
+ startTime: toIso(info.startTime),
1051
+ closeTime: toIso(info.closeTime),
1052
+ taskQueue: typeof info.taskQueue === "string" ? info.taskQueue : null,
1053
+ });
1054
+ if (executions.length >= pageSize)
1055
+ break;
1056
+ }
1057
+ })();
1058
+ await guarded(collect);
1059
+ result.executions = executions;
1060
+ result.count = executions.length;
1061
+ return finish();
1062
+ }
1063
+ case "count": {
1064
+ const response = await guarded(client.workflow.count
1065
+ ? client.workflow.count(op.query ?? "")
1066
+ : entry.connection.workflowService.countWorkflowExecutions({
1067
+ namespace,
1068
+ query: op.query ?? "",
1069
+ }));
1070
+ result.count = toNumber(isRecord(response) ? response.count : response) ?? 0;
1071
+ const groups = isRecord(response) && Array.isArray(response.groups) ? response.groups : [];
1072
+ result.groups = groups.filter(isRecord).map((group) => ({
1073
+ values: Array.isArray(group.groupValues)
1074
+ ? group.groupValues.map((value) => String(value ?? ""))
1075
+ : [],
1076
+ count: toNumber(group.count) ?? 0,
1077
+ }));
1078
+ return finish();
1079
+ }
1080
+ case "describeTaskQueue": {
1081
+ const response = await guarded(entry.connection.workflowService.describeTaskQueue({
1082
+ namespace,
1083
+ taskQueue: { name: op.taskQueue, kind: "TASK_QUEUE_KIND_NORMAL" },
1084
+ taskQueueType: op.taskQueueType === "activity" ? "TASK_QUEUE_TYPE_ACTIVITY" : "TASK_QUEUE_TYPE_WORKFLOW",
1085
+ }));
1086
+ const pollers = isRecord(response) && Array.isArray(response.pollers) ? response.pollers : [];
1087
+ result.taskQueue = op.taskQueue;
1088
+ result.pollers = pollers.filter(isRecord).map((poller) => ({
1089
+ identity: String(poller.identity ?? ""),
1090
+ lastAccessTime: toIso(poller.lastAccessTime),
1091
+ ratePerSecond: toNumber(poller.ratePerSecond),
1092
+ }));
1093
+ result.hasPollers = result.pollers.length > 0;
1094
+ return finish();
1095
+ }
1096
+ default:
1097
+ throw new Error(`Unsupported Temporal operation: ${String(op.operation)}`);
1098
+ }
1099
+ };
1100
+ /** Map an SDK/gRPC error to the message the node reports. */
1101
+ export const describeTemporalError = (error, op) => {
1102
+ if (error instanceof TemporalAbortError) {
1103
+ return "Temporal operation cancelled";
1104
+ }
1105
+ if (!(error instanceof Error)) {
1106
+ return String(error);
1107
+ }
1108
+ if (isNotFound(error) && op?.workflowId) {
1109
+ return `Workflow not found: ${op.workflowId}${op.runId ? ` (run ${op.runId})` : ""}`;
1110
+ }
1111
+ const code = error.code;
1112
+ if (code === 16 || /UNAUTHENTICATED/i.test(error.message)) {
1113
+ return `Temporal rejected the credentials (UNAUTHENTICATED): ${error.message}`;
1114
+ }
1115
+ if (code === 14 || /UNAVAILABLE|ECONNREFUSED/i.test(error.message)) {
1116
+ return `Temporal server unreachable (UNAVAILABLE): ${error.message}`;
1117
+ }
1118
+ if (error.name === "WorkflowUpdateFailedError") {
1119
+ return `Workflow update rejected: ${summarizeSdkError(error).message}`;
1120
+ }
1121
+ if (error.name === "WorkflowExecutionAlreadyStartedError") {
1122
+ return `Workflow already started: ${error.message}`;
1123
+ }
1124
+ return error.message;
1125
+ };
1126
+ // --- Connectivity check -----------------------------------------------------
1127
+ export const testTemporalConnection = async (config) => {
1128
+ const startedAtMs = Date.now();
1129
+ const namespace = resolveTemporalNamespace(config);
1130
+ let connection = null;
1131
+ try {
1132
+ const driver = await loadTemporalDriver();
1133
+ connection = await driver.Connection.connect(buildTemporalConnectionOptions(config));
1134
+ let serverVersion = null;
1135
+ try {
1136
+ const info = await connection.workflowService.getSystemInfo({});
1137
+ serverVersion = isRecord(info) && typeof info.serverVersion === "string" ? info.serverVersion : null;
1138
+ }
1139
+ catch {
1140
+ serverVersion = null;
1141
+ }
1142
+ const described = await connection.workflowService.describeNamespace({ namespace });
1143
+ const namespaceInfo = isRecord(described) && isRecord(described.namespaceInfo) ? described.namespaceInfo : null;
1144
+ const stateRaw = namespaceInfo?.state;
1145
+ const namespaceState = typeof stateRaw === "string"
1146
+ ? toPascal(stateRaw.replace(/^NAMESPACE_STATE_/, ""))
1147
+ : stateRaw === 1
1148
+ ? "Registered"
1149
+ : stateRaw === 2
1150
+ ? "Deprecated"
1151
+ : stateRaw === 3
1152
+ ? "Deleted"
1153
+ : null;
1154
+ return {
1155
+ ok: true,
1156
+ serverVersion,
1157
+ namespace,
1158
+ namespaceState,
1159
+ durationMs: Date.now() - startedAtMs,
1160
+ message: null,
1161
+ };
1162
+ }
1163
+ catch (error) {
1164
+ return {
1165
+ ok: false,
1166
+ serverVersion: null,
1167
+ namespace,
1168
+ namespaceState: null,
1169
+ durationMs: Date.now() - startedAtMs,
1170
+ message: describeTemporalError(error),
1171
+ };
1172
+ }
1173
+ finally {
1174
+ try {
1175
+ await connection?.close?.();
1176
+ }
1177
+ catch {
1178
+ // ignore
1179
+ }
1180
+ }
1181
+ };
1182
+ //# sourceMappingURL=temporalService.js.map