@supernovae-st/nika 0.71.0 → 0.120.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3502 @@
1
+ // src/errors.ts
2
+ var NikaError = class extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = "NikaError";
6
+ }
7
+ };
8
+ var NikaConfigurationError = class extends NikaError {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "NikaConfigurationError";
12
+ }
13
+ };
14
+ var NikaTransportError = class extends NikaError {
15
+ transport;
16
+ constructor(transport, message, options) {
17
+ super(message, options);
18
+ this.name = "NikaTransportError";
19
+ this.transport = transport;
20
+ }
21
+ };
22
+ var NikaCompatibilityError = class extends NikaError {
23
+ capability;
24
+ transport;
25
+ constructor(capability, transport, message) {
26
+ super(message);
27
+ this.name = "NikaCompatibilityError";
28
+ this.capability = capability;
29
+ this.transport = transport;
30
+ }
31
+ };
32
+ var NikaProtocolError = class extends NikaTransportError {
33
+ constructor(transport, message, options) {
34
+ super(transport, message, options);
35
+ this.name = "NikaProtocolError";
36
+ }
37
+ };
38
+ var NikaObservationInterrupted = class extends NikaTransportError {
39
+ runId;
40
+ lastSequence;
41
+ attempts;
42
+ constructor(transport, runId, lastSequence, attempts) {
43
+ super(
44
+ transport,
45
+ `HTTP observation interrupted after ${attempts} retries; run ${runId} last acknowledged sequence ${lastSequence}`
46
+ );
47
+ this.name = "NikaObservationInterrupted";
48
+ this.runId = runId;
49
+ this.lastSequence = lastSequence;
50
+ this.attempts = attempts;
51
+ }
52
+ };
53
+ var NikaOperationError = class extends NikaError {
54
+ operation;
55
+ code;
56
+ transport;
57
+ status;
58
+ findings;
59
+ currentRevision;
60
+ machineCode;
61
+ constructor(operation, transport, code, message, details) {
62
+ super(message);
63
+ this.name = "NikaOperationError";
64
+ this.operation = operation;
65
+ this.code = code;
66
+ this.transport = transport;
67
+ this.status = details.status;
68
+ this.findings = details.findings;
69
+ this.currentRevision = details.currentRevision;
70
+ this.machineCode = details.machineCode;
71
+ }
72
+ };
73
+ var NikaEventBufferOverflowError = class extends NikaError {
74
+ runId;
75
+ /** The bound that was exceeded: the view's `bufferSize`. */
76
+ limit;
77
+ reason;
78
+ constructor(runId, limit, replay) {
79
+ super(replay === void 0 ? `Event subscriber for run ${runId} exceeded its ${limit}-event buffer` : `Run ${runId} had produced ${replay.observed} events when this view was opened, more than the ${limit} it can replay (${replay.retained} still retained). The run and its result are unaffected. ` + (replay.retained === replay.observed ? `Open the view with bufferSize >= ${replay.observed}` : `Set eventBufferSize >= ${replay.observed} for the next run`) + ", or observe the run live");
80
+ this.name = "NikaEventBufferOverflowError";
81
+ this.runId = runId;
82
+ this.limit = limit;
83
+ this.reason = replay === void 0 ? "live_backpressure" : "replay_truncated";
84
+ if (replay !== void 0) {
85
+ this.observed = replay.observed;
86
+ this.retained = replay.retained;
87
+ }
88
+ }
89
+ };
90
+ var NikaRunOwnershipError = class extends NikaError {
91
+ constructor() {
92
+ super("The NikaRun was not created by this Nika client");
93
+ this.name = "NikaRunOwnershipError";
94
+ }
95
+ };
96
+
97
+ // src/lib/binary/error.ts
98
+ var NikaEngineUnavailable = class extends NikaError {
99
+ code = "NIKA_ENGINE_UNAVAILABLE";
100
+ platform;
101
+ arch;
102
+ packageName;
103
+ constructor(platform, arch, packageName) {
104
+ const target = `${platform}-${arch}`;
105
+ const detail = packageName ? `the optional payload ${packageName} is not installed (npm leaves it UNMET OPTIONAL when the registry has no version matching this client)` : "there is no packaged payload for this host";
106
+ super(
107
+ `Nika engine unavailable for ${target}: ${detail}. A nika found on PATH is deliberately not used. Set NIKA_BIN=/absolute/path/to/nika, pass config.bin, or install the matching payload package.`
108
+ );
109
+ this.name = "NikaEngineUnavailable";
110
+ this.platform = platform;
111
+ this.arch = arch;
112
+ this.packageName = packageName;
113
+ }
114
+ };
115
+
116
+ // src/lib/binary/packages.ts
117
+ var NATIVE_PACKAGES = {
118
+ "darwin-arm64": "@supernovae-st/nika-darwin-arm64",
119
+ "darwin-x64": "@supernovae-st/nika-darwin-x64",
120
+ "linux-x64": "@supernovae-st/nika-linux-x64",
121
+ "linux-arm64": "@supernovae-st/nika-linux-arm64"
122
+ };
123
+ function packageForHost(platform, arch, glibc) {
124
+ if (platform === "linux" && !glibc) return void 0;
125
+ return NATIVE_PACKAGES[`${platform}-${arch}`];
126
+ }
127
+
128
+ // src/lib/binary/resolve.ts
129
+ import { createRequire } from "module";
130
+ import path from "path";
131
+ var requireFromBundle = createRequire(
132
+ typeof import.meta.url === "string" ? import.meta.url : __filename
133
+ );
134
+ function resolveNikaEngine(configuredBin, host = {}) {
135
+ if (configuredBin !== void 0) {
136
+ return { bin: absoluteEnginePath(configuredBin, "bin") };
137
+ }
138
+ const envBin = (host.env ?? process.env).NIKA_BIN;
139
+ if (envBin !== void 0) {
140
+ return { bin: absoluteEnginePath(envBin, "NIKA_BIN") };
141
+ }
142
+ const platform = host.platform ?? process.platform;
143
+ const arch = host.arch ?? process.arch;
144
+ const packageName = packageForHost(
145
+ platform,
146
+ arch,
147
+ host.glibc ?? runtimeIsGlibc(platform)
148
+ );
149
+ if (!packageName) throw new NikaEngineUnavailable(platform, arch);
150
+ try {
151
+ const packageJson = (host.resolvePackageJson ?? requireFromBundle.resolve)(
152
+ `${packageName}/package.json`
153
+ );
154
+ const packageRoot = path.dirname(packageJson);
155
+ return {
156
+ bin: path.join(packageRoot, "bin", "nika"),
157
+ packageName,
158
+ packageRoot
159
+ };
160
+ } catch (cause) {
161
+ if (isMissingModule(cause)) {
162
+ throw new NikaEngineUnavailable(platform, arch, packageName);
163
+ }
164
+ throw cause;
165
+ }
166
+ }
167
+ function absoluteEnginePath(value, source) {
168
+ if (value.length === 0) {
169
+ throw new NikaConfigurationError(`${source} must be a non-empty string`);
170
+ }
171
+ if (!path.isAbsolute(value)) {
172
+ throw new NikaConfigurationError(
173
+ `${source} must be an absolute path to a nika engine (got "${value}"); a bare name or a relative path would be resolved through PATH or the working directory, which this client refuses`
174
+ );
175
+ }
176
+ return value;
177
+ }
178
+ function runtimeIsGlibc(platform) {
179
+ if (platform !== "linux") return true;
180
+ const report = process.report?.getReport();
181
+ return typeof report?.header?.glibcVersionRuntime === "string";
182
+ }
183
+ function isMissingModule(cause) {
184
+ return cause instanceof Error && "code" in cause && cause.code === "MODULE_NOT_FOUND";
185
+ }
186
+
187
+ // src/lib/binary/verify.ts
188
+ import { createHash } from "crypto";
189
+ import { createReadStream } from "fs";
190
+ import { readFile, stat } from "fs/promises";
191
+ import path2 from "path";
192
+
193
+ // src/lib/engine-capture.ts
194
+ import { spawn } from "child_process";
195
+ function captureEngine(bin, args, options) {
196
+ return new Promise((resolve, reject) => {
197
+ if (options.signal?.aborted) {
198
+ reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
199
+ return;
200
+ }
201
+ const child = spawn(bin, args, {
202
+ cwd: options.cwd,
203
+ shell: false,
204
+ stdio: ["ignore", "pipe", "pipe"]
205
+ });
206
+ let stdout = "";
207
+ let stderr = "";
208
+ let overflow = false;
209
+ let spawnError;
210
+ const append = (stream, chunk) => {
211
+ if (overflow) return;
212
+ if (stream === "stdout") stdout += chunk;
213
+ else stderr += chunk;
214
+ if (Buffer.byteLength(stdout) > options.bufferBytes || Buffer.byteLength(stderr) > options.bufferBytes) {
215
+ overflow = true;
216
+ child.kill("SIGTERM");
217
+ }
218
+ };
219
+ child.stdout.setEncoding("utf8");
220
+ child.stderr.setEncoding("utf8");
221
+ child.stdout.on("data", (chunk) => append("stdout", chunk));
222
+ child.stderr.on("data", (chunk) => append("stderr", chunk));
223
+ const abort = () => child.kill("SIGTERM");
224
+ options.signal?.addEventListener("abort", abort, { once: true });
225
+ child.once("error", (cause) => {
226
+ spawnError = cause;
227
+ });
228
+ child.once("close", (code) => {
229
+ options.signal?.removeEventListener("abort", abort);
230
+ if (options.signal?.aborted) {
231
+ reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
232
+ } else if (spawnError) {
233
+ reject(new NikaTransportError(
234
+ options.transport,
235
+ `Cannot spawn ${bin} for ${options.label}: ${spawnError.message}`,
236
+ { cause: spawnError }
237
+ ));
238
+ } else if (overflow) {
239
+ reject(new NikaProtocolError(
240
+ options.transport,
241
+ `${options.label} exceeded ${options.bufferBytes} bytes`
242
+ ));
243
+ } else {
244
+ resolve({ exitCode: code ?? 3, stdout, stderr });
245
+ }
246
+ });
247
+ });
248
+ }
249
+
250
+ // src/lib/settlement.ts
251
+ var TALLY_FIELDS = [
252
+ "total",
253
+ "ok",
254
+ "failed",
255
+ "recovered",
256
+ "skipped",
257
+ "cancelled",
258
+ "never_started"
259
+ ];
260
+ function readSettlement(value, transport, expectedStatus) {
261
+ const fault = (detail) => {
262
+ throw new NikaProtocolError(transport, `Engine settlement was malformed: ${detail}`);
263
+ };
264
+ const settlement = record(value) ?? fault("not an object");
265
+ for (const field of ["status", "cause"]) {
266
+ if (settlement[field] !== void 0 && typeof settlement[field] !== "string") {
267
+ fault(`${field} was not a string`);
268
+ }
269
+ }
270
+ if (settlement.status !== void 0 && expectedStatus !== void 0 && settlement.status !== expectedStatus) {
271
+ fault(`status ${String(settlement.status)} contradicts the record's ${expectedStatus}`);
272
+ }
273
+ count("elapsed_ms", settlement.elapsed_ms, fault);
274
+ if (settlement.tasks !== void 0) {
275
+ const tasks = record(settlement.tasks) ?? fault("tasks was not an object");
276
+ for (const field of TALLY_FIELDS) count(`tasks.${field}`, tasks[field], fault);
277
+ }
278
+ if (settlement.spend !== void 0) {
279
+ const spend = record(settlement.spend) ?? fault("spend was not an object");
280
+ if (spend.qualifier !== void 0 && typeof spend.qualifier !== "string") {
281
+ fault("spend.qualifier was not a string");
282
+ }
283
+ if (spend.pricing_as_of !== void 0 && spend.pricing_as_of !== null && typeof spend.pricing_as_of !== "string") {
284
+ fault("spend.pricing_as_of was not a string");
285
+ }
286
+ count("spend.priced_calls", spend.priced_calls, fault);
287
+ count("spend.unpriced_calls", spend.unpriced_calls, fault);
288
+ if (spend.total_cost_usd !== void 0 && spend.total_cost_usd !== null) {
289
+ amount("spend.total_cost_usd", spend.total_cost_usd, fault);
290
+ }
291
+ if (spend.by_source !== void 0) {
292
+ const bySource = record(spend.by_source) ?? fault("spend.by_source was not an object");
293
+ for (const [source, cost] of Object.entries(bySource)) {
294
+ amount(`spend.by_source.${source}`, cost, fault);
295
+ }
296
+ }
297
+ }
298
+ if (settlement.error !== void 0) {
299
+ const error = record(settlement.error) ?? fault("error was not an object");
300
+ if (typeof error.code !== "string" || typeof error.message !== "string") {
301
+ fault("error omitted its code or message");
302
+ }
303
+ if (error.task !== void 0 && typeof error.task !== "string") {
304
+ fault("error.task was not a string");
305
+ }
306
+ }
307
+ return settlement;
308
+ }
309
+ function record(value) {
310
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
311
+ }
312
+ function count(name, value, fault) {
313
+ if (value === void 0) return;
314
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
315
+ fault(`${name} was not a non-negative integer`);
316
+ }
317
+ }
318
+ function amount(name, value, fault) {
319
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
320
+ fault(`${name} was not a non-negative number`);
321
+ }
322
+ }
323
+
324
+ // src/lib/machine.ts
325
+ function machineObject(value) {
326
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
327
+ }
328
+ function parseMachineObject(text, transport) {
329
+ let value;
330
+ try {
331
+ value = JSON.parse(text);
332
+ } catch (cause) {
333
+ throw new NikaProtocolError(transport, "Engine machine output was not valid JSON", {
334
+ cause: cause instanceof Error ? cause : void 0
335
+ });
336
+ }
337
+ const object = machineObject(value);
338
+ if (!object) {
339
+ throw new NikaProtocolError(transport, "Engine machine output was not a JSON object");
340
+ }
341
+ return object;
342
+ }
343
+ function eventStatus(event) {
344
+ if (typeof event?.status === "string") return event.status;
345
+ const fields = Array.isArray(event?.fields) ? event.fields : [];
346
+ for (const field of fields) {
347
+ const row = machineObject(field);
348
+ if (row?.key === "status" && typeof row.value === "string") return row.value;
349
+ }
350
+ return void 0;
351
+ }
352
+ function eventSettlement(event, transport = "http") {
353
+ if (!event) return void 0;
354
+ const record2 = event;
355
+ if (record2.settlement !== void 0) {
356
+ return readSettlement(record2.settlement, transport, eventStatus(event));
357
+ }
358
+ if (["cause", "elapsed_ms", "tasks", "spend"].every((key) => record2[key] === void 0)) return void 0;
359
+ const fields = ["status", "cause", "elapsed_ms", "tasks", "spend", "error"];
360
+ return readSettlement(Object.fromEntries(fields.filter((key) => record2[key] !== void 0).map((key) => [key, record2[key]])), transport, eventStatus(event));
361
+ }
362
+ function eventReceipt(event) {
363
+ const direct = machineObject(event?.receipt);
364
+ if (direct) return Object.freeze(direct);
365
+ const fields = Array.isArray(event?.fields) ? event.fields : [];
366
+ for (const field of fields) {
367
+ const row = machineObject(field);
368
+ if (row?.key === "receipt") {
369
+ const receipt = machineObject(row.value);
370
+ if (receipt) return Object.freeze(receipt);
371
+ }
372
+ }
373
+ return void 0;
374
+ }
375
+ function eventOutputs(event) {
376
+ const direct = machineObject(event?.outputs);
377
+ if (direct) return direct;
378
+ const fields = Array.isArray(event?.fields) ? event.fields : [];
379
+ for (const field of fields) {
380
+ const row = machineObject(field);
381
+ if (row?.key === "outputs") return machineObject(row.value);
382
+ }
383
+ return void 0;
384
+ }
385
+ function eventError(event) {
386
+ const settled = machineObject(machineObject(event?.settlement)?.error);
387
+ if (settled) return settled;
388
+ const direct = machineObject(event?.error);
389
+ if (direct) return direct;
390
+ if (typeof event?.code === "string" || typeof event?.message === "string") {
391
+ return {
392
+ ...typeof event.code === "string" ? { code: event.code } : {},
393
+ ...typeof event.message === "string" ? { message: event.message } : {}
394
+ };
395
+ }
396
+ return fieldsError(event);
397
+ }
398
+ function fieldsError(event) {
399
+ if (event?.kind !== "task_failed") return void 0;
400
+ const fields = Array.isArray(event.fields) ? event.fields : [];
401
+ let detail;
402
+ let task;
403
+ for (const field of fields) {
404
+ const row = machineObject(field);
405
+ if (row?.key === "detail" && typeof row.value === "string") detail = row.value;
406
+ if (row?.key === "task" && typeof row.value === "string") task = row.value;
407
+ }
408
+ if (detail === void 0) return void 0;
409
+ const match = detail.trim().match(/^(NIKA-[A-Z0-9-]+)\s*·?\s*([\s\S]*)$/);
410
+ const code = match?.[1];
411
+ const message = (match ? match[2] : detail).trim();
412
+ return {
413
+ ...code ? { code } : {},
414
+ ...message.length > 0 ? { message } : {},
415
+ ...task ? { task } : {}
416
+ };
417
+ }
418
+ function receiptTraceLocator(receipt) {
419
+ const value = receipt.trace_path;
420
+ return typeof value === "string" && value.length > 0 ? value : void 0;
421
+ }
422
+
423
+ // src/lib/engine-identity.ts
424
+ var MACHINE_PROTOCOL_VERSION = 1;
425
+ var REQUIRED_CAPABILITIES = ["check", "executionSnapshot", "eventStream"];
426
+ var COMPATIBILITY_CLOCKS = [
427
+ "machineProtocolVersion",
428
+ "snapshotFormatVersion",
429
+ "checkReportVersion",
430
+ "eventFormatVersion"
431
+ ];
432
+ function compatibleEngineIdentity(value, transport, peer) {
433
+ const identity = machineObject(value);
434
+ if (!identity) throw incompatible(transport, "Engine identity is not an object");
435
+ if (typeof identity.engineVersion !== "string" || identity.engineVersion.length === 0) {
436
+ throw incompatible(transport, "Engine identity is missing engineVersion");
437
+ }
438
+ for (const clock of COMPATIBILITY_CLOCKS) {
439
+ if (!Number.isSafeInteger(identity[clock])) {
440
+ throw incompatible(transport, `Engine identity is missing ${clock}`);
441
+ }
442
+ }
443
+ if (identity.machineProtocolVersion !== MACHINE_PROTOCOL_VERSION) {
444
+ throw incompatible(
445
+ transport,
446
+ `Engine machine protocol ${String(identity.machineProtocolVersion)} is incompatible with SDK protocol ${MACHINE_PROTOCOL_VERSION}`
447
+ );
448
+ }
449
+ if (!Array.isArray(identity.supportedCapabilities) || !identity.supportedCapabilities.every((item) => typeof item === "string")) {
450
+ throw incompatible(transport, "Engine identity is missing supportedCapabilities");
451
+ }
452
+ for (const capability of REQUIRED_CAPABILITIES) {
453
+ if (!identity.supportedCapabilities.includes(capability)) {
454
+ throw incompatible(transport, `Engine identity does not support ${capability}`);
455
+ }
456
+ }
457
+ const checked = identity;
458
+ if (peer) {
459
+ for (const clock of COMPATIBILITY_CLOCKS) {
460
+ if (checked[clock] !== peer[clock]) {
461
+ throw incompatible(
462
+ transport,
463
+ `Remote ${clock} ${checked[clock]} is incompatible with local ${clock} ${peer[clock]}`
464
+ );
465
+ }
466
+ }
467
+ }
468
+ return checked;
469
+ }
470
+ function incompatible(transport, message) {
471
+ return new NikaCompatibilityError("engineIdentity", transport, message);
472
+ }
473
+
474
+ // src/lib/binary/verify.ts
475
+ var IDENTITY_BUFFER_BYTES = 16 * 1024;
476
+ async function verifyNikaEngine(engine) {
477
+ const expectedVersion = engine.packageRoot ? await verifyManagedPayload(engine) : void 0;
478
+ const identity = await probeIdentity(engine.bin);
479
+ if (expectedVersion !== void 0 && identity.engineVersion !== expectedVersion) {
480
+ throw incompatible2(
481
+ `Packaged engine version ${identity.engineVersion} does not match payload version ${expectedVersion}`
482
+ );
483
+ }
484
+ return identity;
485
+ }
486
+ async function verifyManagedPayload(engine) {
487
+ const packageRoot = engine.packageRoot;
488
+ if (!packageRoot || !engine.packageName) {
489
+ throw incompatible2("Managed engine resolution is missing package identity");
490
+ }
491
+ const manifest = await readJson(path2.join(packageRoot, "package.json"), "payload manifest");
492
+ if (manifest.name !== engine.packageName || typeof manifest.version !== "string") {
493
+ throw incompatible2(`Invalid manifest for ${engine.packageName}`);
494
+ }
495
+ const integrity = await readJson(
496
+ path2.join(packageRoot, "INTEGRITY.json"),
497
+ "payload integrity metadata"
498
+ );
499
+ if (integrity.algorithm !== "sha256" || integrity.executable?.file !== "bin/nika" || !isSha256(integrity.executable?.sha256)) {
500
+ throw incompatible2(`Invalid INTEGRITY.json for ${engine.packageName}`);
501
+ }
502
+ let metadata;
503
+ try {
504
+ metadata = await stat(engine.bin);
505
+ } catch (cause) {
506
+ throw incompatible2(`Cannot read packaged engine ${engine.bin}`, cause);
507
+ }
508
+ if (!metadata.isFile()) {
509
+ throw incompatible2(`Packaged engine is not a file: ${engine.bin}`);
510
+ }
511
+ const actual = await sha256(engine.bin);
512
+ if (actual !== integrity.executable.sha256) {
513
+ throw incompatible2(`Packaged engine checksum mismatch for ${engine.packageName}`);
514
+ }
515
+ return manifest.version;
516
+ }
517
+ async function probeIdentity(bin) {
518
+ const notAnEngine = `is ${bin} a nika engine? (run "${bin} --sdk-identity" by hand)`;
519
+ const captured = await captureEngine(bin, ["--sdk-identity"], {
520
+ bufferBytes: IDENTITY_BUFFER_BYTES,
521
+ transport: "native-process",
522
+ label: "Engine identity probe"
523
+ }).catch((cause) => {
524
+ throw incompatible2(
525
+ `Engine identity probe of ${bin} failed`,
526
+ cause
527
+ );
528
+ });
529
+ if (captured.exitCode !== 0) {
530
+ throw incompatible2(
531
+ `Engine identity probe of ${bin} exited with code ${captured.exitCode}; ${notAnEngine}`
532
+ );
533
+ }
534
+ let value;
535
+ try {
536
+ value = JSON.parse(captured.stdout.trim());
537
+ } catch (cause) {
538
+ throw incompatible2(
539
+ `Engine identity probe of ${bin} did not emit one JSON object; ${notAnEngine}`,
540
+ cause
541
+ );
542
+ }
543
+ if (captured.stderr.trim()) {
544
+ throw incompatible2(`Engine identity probe of ${bin} wrote unexpected diagnostics`);
545
+ }
546
+ try {
547
+ return compatibleEngineIdentity(value, "native-process");
548
+ } catch (cause) {
549
+ if (cause instanceof NikaCompatibilityError) throw cause;
550
+ throw incompatible2("Engine identity probe was incompatible", cause);
551
+ }
552
+ }
553
+ async function readJson(file, label) {
554
+ try {
555
+ const parsed = JSON.parse(await readFile(file, "utf8"));
556
+ if (!isRecord(parsed)) throw new TypeError(`${label} is not an object`);
557
+ return parsed;
558
+ } catch (cause) {
559
+ throw incompatible2(`Cannot verify ${label} at ${file}`, cause);
560
+ }
561
+ }
562
+ function sha256(file) {
563
+ return new Promise((resolve, reject) => {
564
+ const hash = createHash("sha256");
565
+ const stream = createReadStream(file);
566
+ stream.on("data", (chunk) => hash.update(chunk));
567
+ stream.once("error", reject);
568
+ stream.once("end", () => resolve(hash.digest("hex")));
569
+ });
570
+ }
571
+ function isRecord(value) {
572
+ return typeof value === "object" && value !== null && !Array.isArray(value);
573
+ }
574
+ function isSha256(value) {
575
+ return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
576
+ }
577
+ function incompatible2(message, cause) {
578
+ return new NikaCompatibilityError(
579
+ "engineIdentity",
580
+ "native-process",
581
+ cause instanceof Error ? `${message}: ${cause.message}` : message
582
+ );
583
+ }
584
+
585
+ // src/lib/literal-inputs.ts
586
+ import { types } from "util";
587
+ var LITERAL_INPUTS_MAX_BYTES = 1024 * 1024;
588
+ function literalInputs(options) {
589
+ if (options.inputs === void 0) return void 0;
590
+ if (options.vars !== void 0) {
591
+ throw new NikaConfigurationError(
592
+ "run({ inputs, vars }): inputs and the deprecated vars alias cannot be combined; move every value into inputs"
593
+ );
594
+ }
595
+ return encodeLiteralInputs(options.inputs);
596
+ }
597
+ var STRICT_JSON = "inputs must be strict JSON (null, booleans, finite numbers, strings, plain arrays and plain objects)";
598
+ var NATIVE_OBJECT = Function.prototype.toString.call(Object);
599
+ var NATIVE_ARRAY = Function.prototype.toString.call(Array);
600
+ var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
601
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
602
+ var PATH_LIMIT = 240;
603
+ function encodeLiteralInputs(inputs) {
604
+ if (containerKind(inputs) !== "object") {
605
+ throw new NikaConfigurationError(
606
+ `run({ inputs }): inputs must be a plain object mapping declared workflow input names to strict JSON values; received ${describe(inputs)}`
607
+ );
608
+ }
609
+ const parts = [];
610
+ let bytes = 0;
611
+ const write = (chunk) => {
612
+ bytes += Buffer.byteLength(chunk);
613
+ if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
614
+ parts.push(chunk);
615
+ };
616
+ const stack = [];
617
+ const open = /* @__PURE__ */ new Set();
618
+ const refuse = (segment, problem) => {
619
+ const path3 = stack.map((frame) => frame.segment).join("") + segment;
620
+ const shown = path3.length > PATH_LIMIT ? `${path3.slice(0, PATH_LIMIT / 2)}\u2026${path3.slice(-PATH_LIMIT / 2)}` : path3;
621
+ return new NikaConfigurationError(`run({ inputs }): ${shown} ${problem}; ${STRICT_JSON}`);
622
+ };
623
+ const enter = (value, segment, kind) => {
624
+ open.add(value);
625
+ if (kind === "array") {
626
+ write("[");
627
+ stack.push({ value, segment, length: value.length, index: 0 });
628
+ } else {
629
+ write("{");
630
+ const keys = Reflect.ownKeys(value);
631
+ stack.push({ value, segment, keys, length: keys.length, index: 0 });
632
+ }
633
+ };
634
+ enter(inputs, "inputs", "object");
635
+ for (let frame = stack.at(-1); frame; frame = stack.at(-1)) {
636
+ if (frame.index === frame.length) {
637
+ if (!frame.keys) {
638
+ const named = Reflect.ownKeys(frame.value).find((key2) => !isArrayMember(key2, frame.length));
639
+ if (named !== void 0) {
640
+ stack.pop();
641
+ throw refuse(frame.segment + keySegment(named), typeof named === "symbol" ? "is a symbol key, which JSON cannot carry" : "is a named array property, which JSON cannot carry");
642
+ }
643
+ }
644
+ write(frame.keys ? "}" : "]");
645
+ open.delete(frame.value);
646
+ stack.pop();
647
+ continue;
648
+ }
649
+ const position = frame.index;
650
+ frame.index += 1;
651
+ if (position > 0) write(",");
652
+ const key = frame.keys ? frame.keys[position] : String(position);
653
+ const segment = frame.keys ? keySegment(key) : `[${position}]`;
654
+ if (typeof key === "symbol") throw refuse(segment, "is a symbol key, which JSON cannot carry");
655
+ const member = Object.getOwnPropertyDescriptor(frame.value, key);
656
+ if (!member) {
657
+ throw refuse(segment, frame.keys ? "is not an own data property" : "is an array hole, which JSON cannot carry");
658
+ }
659
+ if ("get" in member || "set" in member) {
660
+ throw refuse(segment, "is an accessor property, which JSON cannot carry");
661
+ }
662
+ if (frame.keys) {
663
+ if (!member.enumerable) {
664
+ throw refuse(segment, "is a non-enumerable property, which JSON cannot carry");
665
+ }
666
+ if (LONE_SURROGATE.test(key)) {
667
+ throw refuse(segment, "is a key with a lone surrogate, which is not valid Unicode");
668
+ }
669
+ write(`${JSON.stringify(key)}:`);
670
+ }
671
+ const value = member.value;
672
+ switch (typeof value) {
673
+ case "string":
674
+ if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
675
+ if (LONE_SURROGATE.test(value)) {
676
+ throw refuse(segment, "contains a lone surrogate, which is not valid Unicode");
677
+ }
678
+ write(JSON.stringify(value));
679
+ break;
680
+ case "number":
681
+ if (!Number.isFinite(value)) {
682
+ throw refuse(segment, "is a non-finite number, which JSON cannot carry");
683
+ }
684
+ write(JSON.stringify(value));
685
+ break;
686
+ case "boolean":
687
+ write(value ? "true" : "false");
688
+ break;
689
+ case "object": {
690
+ if (value === null) {
691
+ write("null");
692
+ break;
693
+ }
694
+ if (open.has(value)) throw refuse(segment, "is a cycle, which JSON cannot carry");
695
+ const kind = containerKind(value);
696
+ if (!kind) throw refuse(segment, refusedObject(value));
697
+ enter(value, segment, kind);
698
+ break;
699
+ }
700
+ default:
701
+ throw refuse(segment, `is ${typeof value === "undefined" ? "" : "a "}${typeof value}, which JSON cannot carry`);
702
+ }
703
+ }
704
+ return { json: parts.join(""), bytes };
705
+ }
706
+ function tooLarge() {
707
+ return new NikaConfigurationError(
708
+ `run({ inputs }): the serialized inputs map exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB), the bound both transports share with the native engine`
709
+ );
710
+ }
711
+ function containerKind(value) {
712
+ if (value === null || typeof value !== "object" || types.isProxy(value)) return void 0;
713
+ const prototype = Object.getPrototypeOf(value);
714
+ if (Array.isArray(value)) return isRealmPrototype(prototype, NATIVE_ARRAY) ? "array" : void 0;
715
+ if (prototype === null) return "object";
716
+ return isRealmPrototype(prototype, NATIVE_OBJECT) ? "object" : void 0;
717
+ }
718
+ function isRealmPrototype(prototype, source) {
719
+ if (prototype === null || types.isProxy(prototype)) return false;
720
+ const constructor = ownValue(prototype, "constructor");
721
+ if (typeof constructor !== "function" || types.isProxy(constructor)) return false;
722
+ let native;
723
+ try {
724
+ native = Function.prototype.toString.call(constructor) === source;
725
+ } catch {
726
+ return false;
727
+ }
728
+ return native && ownValue(constructor, "prototype") === prototype;
729
+ }
730
+ function ownValue(target, key) {
731
+ return Object.getOwnPropertyDescriptor(target, key)?.value;
732
+ }
733
+ function isArrayMember(key, length) {
734
+ if (typeof key === "symbol") return false;
735
+ if (key === "length") return true;
736
+ const index = Number(key);
737
+ return Number.isInteger(index) && index >= 0 && index < length && String(index) === key;
738
+ }
739
+ function keySegment(key) {
740
+ if (typeof key === "symbol") return `[${String(key)}]`;
741
+ return IDENTIFIER.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
742
+ }
743
+ function describe(value) {
744
+ if (value === null) return "null";
745
+ if (typeof value === "undefined") return "undefined";
746
+ if (typeof value !== "object") return `a ${typeof value}`;
747
+ if (types.isProxy(value)) return "a Proxy";
748
+ if (Array.isArray(value)) return "an array";
749
+ return refusedObject(value).replace(/^is /, "").replace(/, not a plain object or array$/, "");
750
+ }
751
+ function refusedObject(value) {
752
+ if (types.isProxy(value)) return "is a Proxy, which cannot be read without running its traps";
753
+ const custom = "is an object with a custom prototype, not a plain object or array";
754
+ const prototype = Object.getPrototypeOf(value);
755
+ if (prototype === null || types.isProxy(prototype)) return custom;
756
+ const constructor = ownValue(prototype, "constructor");
757
+ if (typeof constructor !== "function" || types.isProxy(constructor)) return custom;
758
+ if (ownValue(constructor, "prototype") !== prototype) return custom;
759
+ const name = ownValue(constructor, "name");
760
+ return typeof name === "string" && IDENTIFIER.test(name) && name.length <= 64 ? `is a ${name} instance, not a plain object or array` : custom;
761
+ }
762
+
763
+ // src/lib/sse/parser.ts
764
+ var SseParseError = class extends Error {
765
+ constructor(message, options) {
766
+ super(message, options);
767
+ this.name = "SseParseError";
768
+ }
769
+ };
770
+ var SseParser = class {
771
+ constructor(limits) {
772
+ this.limits = limits;
773
+ for (const [name, value] of Object.entries(limits)) {
774
+ if (!Number.isSafeInteger(value) || value < 1) {
775
+ throw new RangeError(`${name} must be a positive integer`);
776
+ }
777
+ }
778
+ }
779
+ pending = new Uint8Array(0);
780
+ pendingBytes = 0;
781
+ frameBytes = 0;
782
+ dataLines = [];
783
+ id;
784
+ retry;
785
+ firstLine = true;
786
+ push(chunk) {
787
+ const frames = [];
788
+ let offset = 0;
789
+ while (offset < chunk.length) {
790
+ const newline = chunk.indexOf(10, offset);
791
+ if (newline < 0) {
792
+ this.append(chunk.subarray(offset));
793
+ break;
794
+ }
795
+ this.append(chunk.subarray(offset, newline));
796
+ const frame = this.processPendingLine();
797
+ if (frame) frames.push(frame);
798
+ offset = newline + 1;
799
+ }
800
+ return frames;
801
+ }
802
+ finish() {
803
+ if (this.pendingBytes > 0) this.processPendingLine(false);
804
+ if (this.dataLines.length > 0) {
805
+ throw new SseParseError("SSE stream ended with an incomplete data frame");
806
+ }
807
+ this.resetFrame();
808
+ return [];
809
+ }
810
+ append(bytes) {
811
+ const length = this.pendingBytes + bytes.length;
812
+ if (length > this.limits.maxBufferBytes) {
813
+ throw new SseParseError(`SSE buffer exceeded ${this.limits.maxBufferBytes} bytes`);
814
+ }
815
+ if (length > this.limits.maxLineBytes) {
816
+ throw new SseParseError(`SSE line exceeded ${this.limits.maxLineBytes} bytes`);
817
+ }
818
+ if (bytes.length === 0) return;
819
+ this.ensurePendingCapacity(length);
820
+ this.pending.set(bytes, this.pendingBytes);
821
+ this.pendingBytes = length;
822
+ }
823
+ ensurePendingCapacity(length) {
824
+ if (this.pending.byteLength >= length) return;
825
+ const ceiling = Math.min(this.limits.maxBufferBytes, this.limits.maxLineBytes);
826
+ const capacity = Math.min(
827
+ ceiling,
828
+ Math.max(length, Math.max(1, this.pending.byteLength) * 2)
829
+ );
830
+ const pending = new Uint8Array(capacity);
831
+ pending.set(this.pending.subarray(0, this.pendingBytes));
832
+ this.pending = pending;
833
+ }
834
+ processPendingLine(terminated = true) {
835
+ const rawLength = this.pendingBytes + (terminated ? 1 : 0);
836
+ if (this.frameBytes + rawLength > this.limits.maxFrameBytes) {
837
+ throw new SseParseError(`SSE frame exceeded ${this.limits.maxFrameBytes} bytes`);
838
+ }
839
+ this.frameBytes += rawLength;
840
+ let bytes = this.pending.subarray(0, this.pendingBytes);
841
+ this.pendingBytes = 0;
842
+ if (bytes.at(-1) === 13) bytes = bytes.subarray(0, -1);
843
+ let line;
844
+ try {
845
+ line = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
846
+ } catch (cause) {
847
+ throw new SseParseError("SSE line was not valid UTF-8", {
848
+ cause: cause instanceof Error ? cause : void 0
849
+ });
850
+ }
851
+ if (this.firstLine) {
852
+ this.firstLine = false;
853
+ if (line.startsWith("\uFEFF")) line = line.slice(1);
854
+ }
855
+ if (line.length === 0) return this.dispatch();
856
+ if (line.startsWith(":")) return void 0;
857
+ const colon = line.indexOf(":");
858
+ const field = colon < 0 ? line : line.slice(0, colon);
859
+ let value = colon < 0 ? "" : line.slice(colon + 1);
860
+ if (value.startsWith(" ")) value = value.slice(1);
861
+ switch (field) {
862
+ case "data":
863
+ this.dataLines.push(value);
864
+ break;
865
+ case "id":
866
+ if (!value.includes("\0")) this.id = value;
867
+ break;
868
+ case "retry":
869
+ if (/^\d+$/.test(value)) {
870
+ const parsed = Number(value);
871
+ if (Number.isSafeInteger(parsed)) this.retry = parsed;
872
+ }
873
+ break;
874
+ default:
875
+ break;
876
+ }
877
+ return void 0;
878
+ }
879
+ dispatch() {
880
+ const data = this.dataLines.join("\n");
881
+ const frame = {
882
+ ...data.length > 0 ? { data } : {},
883
+ ...this.id !== void 0 ? { id: this.id } : {},
884
+ ...this.retry !== void 0 ? { retry: this.retry } : {}
885
+ };
886
+ this.resetFrame();
887
+ return Object.keys(frame).length > 0 ? frame : void 0;
888
+ }
889
+ resetFrame() {
890
+ this.frameBytes = 0;
891
+ this.dataLines = [];
892
+ this.id = void 0;
893
+ this.retry = void 0;
894
+ }
895
+ };
896
+ async function* decodeSse(stream, limits, signal) {
897
+ const reader = stream.getReader();
898
+ const parser = new SseParser(limits);
899
+ const abort = () => {
900
+ void reader.cancel().catch(() => {
901
+ });
902
+ };
903
+ if (signal?.aborted) abort();
904
+ else signal?.addEventListener("abort", abort, { once: true });
905
+ try {
906
+ while (true) {
907
+ const { done, value } = await reader.read();
908
+ if (done) break;
909
+ for (const frame of parser.push(value)) yield frame;
910
+ }
911
+ for (const frame of parser.finish()) yield frame;
912
+ } finally {
913
+ signal?.removeEventListener("abort", abort);
914
+ await reader.cancel().catch(() => {
915
+ });
916
+ reader.releaseLock();
917
+ }
918
+ }
919
+
920
+ // src/lib/workflow-name.ts
921
+ var WORKFLOW_SUFFIX = ".nika";
922
+ var LEGACY_WORKFLOW_SUFFIXES = [".nika.yaml", ".nika.yml"];
923
+ var CONTAINED_CHAR = /[A-Za-z0-9/._-]/;
924
+ function workflowBasename(path3) {
925
+ const parts = path3.split(/[\\/]/);
926
+ return parts[parts.length - 1] ?? "";
927
+ }
928
+ function hasControlChar(value) {
929
+ for (const ch of value) {
930
+ const c = ch.codePointAt(0) ?? 0;
931
+ if (c <= 31 || c === 127) {
932
+ return true;
933
+ }
934
+ }
935
+ return false;
936
+ }
937
+ function isLegacyWorkflowPath(path3) {
938
+ if (typeof path3 !== "string" || path3.length === 0) {
939
+ return false;
940
+ }
941
+ if (path3.endsWith("/") || path3.endsWith("\\")) {
942
+ return false;
943
+ }
944
+ const base = workflowBasename(path3);
945
+ return LEGACY_WORKFLOW_SUFFIXES.some((suffix) => base.endsWith(suffix));
946
+ }
947
+ function isCanonicalWorkflowFilename(basename) {
948
+ if (typeof basename !== "string" || basename.length === 0) {
949
+ return false;
950
+ }
951
+ if (hasControlChar(basename)) {
952
+ return false;
953
+ }
954
+ if (basename.includes("/") || basename.includes("\\")) {
955
+ return false;
956
+ }
957
+ if (LEGACY_WORKFLOW_SUFFIXES.some((suffix) => basename.endsWith(suffix))) {
958
+ return false;
959
+ }
960
+ if (!basename.endsWith(WORKFLOW_SUFFIX)) {
961
+ return false;
962
+ }
963
+ return basename.length > WORKFLOW_SUFFIX.length;
964
+ }
965
+ function isCanonicalWorkflowPath(path3) {
966
+ if (typeof path3 !== "string" || path3.length === 0) {
967
+ return false;
968
+ }
969
+ if (hasControlChar(path3)) {
970
+ return false;
971
+ }
972
+ if (path3.endsWith("/") || path3.endsWith("\\")) {
973
+ return false;
974
+ }
975
+ return isCanonicalWorkflowFilename(workflowBasename(path3));
976
+ }
977
+ function containedRelativeShape(value) {
978
+ if (value.includes("\\") || value.startsWith("/")) {
979
+ return false;
980
+ }
981
+ for (const ch of value) {
982
+ if (!CONTAINED_CHAR.test(ch)) {
983
+ return false;
984
+ }
985
+ }
986
+ const segments = value.split("/");
987
+ return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
988
+ }
989
+ function isContainedWorkflowName(value) {
990
+ return typeof value === "string" && isCanonicalWorkflowPath(value) && containedRelativeShape(value);
991
+ }
992
+ function isRetiredByNameAttempt(value) {
993
+ if (typeof value !== "string" || value.length === 0) {
994
+ return false;
995
+ }
996
+ if (value.startsWith("/") || value.startsWith("./") || value.includes("\\")) {
997
+ return false;
998
+ }
999
+ return isLegacyWorkflowPath(value);
1000
+ }
1001
+ function renameLegacyWorkflow(name) {
1002
+ if (name.endsWith(".nika.yaml")) {
1003
+ return `${name.slice(0, -".nika.yaml".length)}${WORKFLOW_SUFFIX}`;
1004
+ }
1005
+ if (name.endsWith(".nika.yml")) {
1006
+ return `${name.slice(0, -".nika.yml".length)}${WORKFLOW_SUFFIX}`;
1007
+ }
1008
+ return name;
1009
+ }
1010
+ function legacyWorkflowRenameMessage(name) {
1011
+ return `workflow names use ${WORKFLOW_SUFFIX}; rename ${name} to ${renameLegacyWorkflow(name)}`;
1012
+ }
1013
+
1014
+ // src/lib/http-transport.ts
1015
+ var JOB_STATUSES = /* @__PURE__ */ new Set([
1016
+ "queued",
1017
+ "running",
1018
+ "interrupted",
1019
+ "paused",
1020
+ "succeeded",
1021
+ "failed",
1022
+ "cancelled"
1023
+ ]);
1024
+ var MAX_OBSERVATION_RETRIES = 5;
1025
+ var POSITIVE_TRACE_VERDICTS = /* @__PURE__ */ new Set(["VERIFIED", "OK", "SEALED", "ANCHORED", "REPLAYED"]);
1026
+ var RETRY_BASE_MILLISECONDS = 100;
1027
+ var RETRY_MIN_MILLISECONDS = 25;
1028
+ var RETRY_MAX_MILLISECONDS = 5e3;
1029
+ var WORKFLOW_REFUSAL_STATUSES = /* @__PURE__ */ new Set([404, 422]);
1030
+ var JOB_INPUTS = "jobInputs";
1031
+ var HttpTransport = class {
1032
+ constructor(options) {
1033
+ this.options = options;
1034
+ }
1035
+ kind = "http";
1036
+ serverIdentity;
1037
+ localIdentity;
1038
+ resolvedEngine;
1039
+ remoteIdentity;
1040
+ async check(workflow, options) {
1041
+ if (options.model !== void 0 || options.nativeStrict === true) {
1042
+ throw this.gap(
1043
+ "checkOptions",
1044
+ "nika serve admission has no request envelope for model or nativeStrict overrides"
1045
+ );
1046
+ }
1047
+ refuseLegacyContainedName(workflow);
1048
+ if (isContainedWorkflowName(workflow)) return this.checkByName(workflow, options.signal);
1049
+ const captured = await this.captureSnapshot(workflow, options.signal, true);
1050
+ if (captured.bytes === void 0) return captured.report;
1051
+ const snapshot = captured.identity;
1052
+ if (!snapshot) {
1053
+ throw this.gap("executionSnapshot", "Local engine omitted execution snapshot identity");
1054
+ }
1055
+ const acknowledged = snapshotAck(await this.json("/v1/check", {
1056
+ method: "POST",
1057
+ headers: { "Content-Type": "application/json" },
1058
+ body: captured.bytes,
1059
+ signal: options.signal
1060
+ }, true, [200], "check"));
1061
+ if (!acknowledged || acknowledged.snapshot_digest !== snapshot.digest || acknowledged.root !== snapshot.root || acknowledged.units !== snapshot.units) {
1062
+ throw new NikaProtocolError(this.kind, "Check admission did not acknowledge the snapshot");
1063
+ }
1064
+ return captured.report;
1065
+ }
1066
+ async startRun(workflow, options) {
1067
+ const inputs = literalInputs(options);
1068
+ if (options.vars && Object.keys(options.vars).length > 0 || options.model !== void 0 || options.maxCostUsd !== void 0) {
1069
+ throw this.gap(
1070
+ "runOptions",
1071
+ "nika serve admission has no request envelope for vars, model, or maxCostUsd; literal workflow values ride inputs, and vars is the native --var operator channel"
1072
+ );
1073
+ }
1074
+ refuseLegacyContainedName(workflow);
1075
+ const byName = isContainedWorkflowName(workflow);
1076
+ if (inputs && !byName) {
1077
+ throw this.gap(
1078
+ "snapshotInputs",
1079
+ "An execution snapshot freezes its inputs and takes no request overlay; run({ inputs }) over HTTP binds a workflow by its served name (listWorkflows())"
1080
+ );
1081
+ }
1082
+ const idempotencyKey = options.idempotencyKey;
1083
+ if (idempotencyKey === void 0) {
1084
+ throw new NikaConfigurationError(
1085
+ "HTTP run() requires a caller-owned idempotencyKey; reuse the same key and request when retrying an uncertain admission"
1086
+ );
1087
+ }
1088
+ if (Buffer.byteLength(idempotencyKey) < 1 || Buffer.byteLength(idempotencyKey) > 255) {
1089
+ throw new NikaTransportError(this.kind, "Idempotency-Key must be 1-255 bytes");
1090
+ }
1091
+ if (byName) {
1092
+ await this.ensureServerIdentity();
1093
+ if (inputs) this.requireJobInputs();
1094
+ const body = inputs ? `{"workflow":${JSON.stringify(workflow)},"inputs":${inputs.json}}` : JSON.stringify({ workflow });
1095
+ const job2 = await this.admitJob(body, idempotencyKey);
1096
+ return this.httpRun(job2.id, 0, job2);
1097
+ }
1098
+ const captured = await this.captureSnapshot(workflow);
1099
+ if (captured.bytes === void 0) {
1100
+ throw this.gap("executionSnapshot", "Local workflow check was not clean; run was not admitted");
1101
+ }
1102
+ if (!captured.identity) {
1103
+ throw this.gap("executionSnapshot", "Local engine omitted execution snapshot identity");
1104
+ }
1105
+ const job = await this.admitJob(captured.bytes, idempotencyKey);
1106
+ return this.httpRun(job.id, 0, job, captured.identity.digest);
1107
+ }
1108
+ /**
1109
+ * The by-name form of `POST /v1/check` (ADR-131): the resident captures
1110
+ * the world of a workflow its registry lists and answers the compact
1111
+ * acknowledgement; nothing is hashed or spawned here. A refusal that
1112
+ * judges the workflow is a red result, as it is on the native transport:
1113
+ * 404 (the name is not served) and 422 (the capture or admission refused
1114
+ * it). Authorization, deadline, envelope and availability refusals still
1115
+ * throw, because they judge the request, not the workflow.
1116
+ */
1117
+ async checkByName(workflow, signal) {
1118
+ await this.ensureServerIdentity();
1119
+ const outcome = await this.jsonOutcome("/v1/check", {
1120
+ method: "POST",
1121
+ headers: { "Content-Type": "application/json" },
1122
+ body: JSON.stringify({ workflow }),
1123
+ signal
1124
+ }, true, [200], "check", true);
1125
+ if ("refusal" in outcome) {
1126
+ if (!WORKFLOW_REFUSAL_STATUSES.has(outcome.status)) throw this.refused("/v1/check", outcome);
1127
+ return { clean: false, error: outcome.refusal };
1128
+ }
1129
+ const acknowledged = snapshotAck(outcome.object);
1130
+ if (!acknowledged) {
1131
+ throw new NikaProtocolError(this.kind, "Check admission did not acknowledge the workflow");
1132
+ }
1133
+ return { clean: true, ...acknowledged };
1134
+ }
1135
+ async admitJob(body, idempotencyKey) {
1136
+ const admitted = await this.json("/v1/jobs", {
1137
+ method: "POST",
1138
+ headers: {
1139
+ "Content-Type": "application/json",
1140
+ "Idempotency-Key": idempotencyKey
1141
+ },
1142
+ body
1143
+ }, true, [200, 202], "run");
1144
+ const id = typeof admitted.id === "string" ? admitted.id : void 0;
1145
+ if (!id) {
1146
+ throw new NikaProtocolError(this.kind, "Job admission response omitted its id");
1147
+ }
1148
+ return durableJob(admitted, id, this.kind);
1149
+ }
1150
+ async attachRun(id, options) {
1151
+ await this.ensureServerIdentity();
1152
+ const object = await this.json(`/v1/jobs/${encodeURIComponent(id)}`, {
1153
+ method: "GET",
1154
+ headers: { Accept: "application/json" }
1155
+ }, true, [200], "attachRun");
1156
+ const durable = durableJob(object, id, this.kind);
1157
+ return this.httpRun(id, options.lastEventId ?? 0, durable);
1158
+ }
1159
+ async listWorkflows() {
1160
+ await this.ensureServerIdentity();
1161
+ const object = await this.json("/v1/workflows", {
1162
+ method: "GET",
1163
+ headers: { Accept: "application/json" }
1164
+ }, true, [200], "listWorkflows");
1165
+ if (Object.keys(object).some((key) => key !== "workflows") || !Array.isArray(object.workflows) || object.workflows.some((name) => !isContainedWorkflowName(name)) || new Set(object.workflows).size !== object.workflows.length) {
1166
+ throw new NikaProtocolError(this.kind, "Workflow catalog response was malformed");
1167
+ }
1168
+ return Object.freeze([...object.workflows]);
1169
+ }
1170
+ async workflow(name) {
1171
+ refuseLegacyContainedName(name);
1172
+ await this.ensureServerIdentity();
1173
+ const object = await this.json(`/v1/workflows/${workflowPath(name)}`, {
1174
+ method: "GET",
1175
+ headers: { Accept: "application/json" }
1176
+ }, true, [200], "workflow");
1177
+ if (Object.keys(object).some((key) => key !== "workflow") || !isContainedWorkflowName(object.workflow) || object.workflow !== name) {
1178
+ throw new NikaProtocolError(this.kind, "Workflow metadata response was malformed");
1179
+ }
1180
+ return object;
1181
+ }
1182
+ async schedule(workflow, options) {
1183
+ refuseLegacyContainedName(workflow);
1184
+ await this.ensureScheduleCapability();
1185
+ const path3 = `/v1/schedules/${encodeURIComponent(options.id)}`;
1186
+ const headers = new Headers({ "Content-Type": "application/json" });
1187
+ if (options.revision === void 0) {
1188
+ headers.set("If-None-Match", "*");
1189
+ } else {
1190
+ headers.set("If-Match", `"${options.revision}"`);
1191
+ }
1192
+ const object = await this.operationJson("schedule", path3, {
1193
+ method: "PUT",
1194
+ headers,
1195
+ body: JSON.stringify(scheduleBody(workflow, options))
1196
+ });
1197
+ if (object.applied !== true || typeof object.changed !== "boolean" || !scheduleStatusProjection(object.status)) {
1198
+ throw new NikaProtocolError(this.kind, "Schedule apply response was not an acknowledgement");
1199
+ }
1200
+ return object;
1201
+ }
1202
+ async scheduleStatus(id) {
1203
+ await this.ensureScheduleCapability();
1204
+ const path3 = `/v1/schedules/${encodeURIComponent(id)}`;
1205
+ const object = await this.operationJson("scheduleStatus", path3, {
1206
+ method: "GET",
1207
+ headers: { Accept: "application/json" }
1208
+ });
1209
+ if (!scheduleStatusProjection(object)) {
1210
+ throw new NikaProtocolError(this.kind, "Schedule status response omitted machine fields");
1211
+ }
1212
+ return object;
1213
+ }
1214
+ async traceVerify(receipt, options) {
1215
+ await this.ensureServerIdentity();
1216
+ const jobId2 = receipt.job_id;
1217
+ if (typeof jobId2 !== "string" || jobId2.length === 0) {
1218
+ throw this.gap("traceVerify", "The engine receipt does not carry a remote job_id");
1219
+ }
1220
+ const object = await this.json(
1221
+ `/v1/jobs/${encodeURIComponent(jobId2)}/trace/verify`,
1222
+ { method: "GET", signal: options.signal },
1223
+ true,
1224
+ [200],
1225
+ "traceVerify"
1226
+ );
1227
+ if (typeof object.verdict !== "string" || object.reason !== void 0 && typeof object.reason !== "string" || object.trace_id !== void 0 && typeof object.trace_id !== "string") {
1228
+ throw new NikaProtocolError(this.kind, "Trace verification verdict was malformed");
1229
+ }
1230
+ const traceBound = typeof object.trace_id === "string" && object.trace_id === receipt.trace_id;
1231
+ return {
1232
+ ...object,
1233
+ verified: POSITIVE_TRACE_VERDICTS.has(object.verdict.toUpperCase()) && traceBound
1234
+ };
1235
+ }
1236
+ httpRun(id, initialSequence = 0, attachedState, expectedSnapshotDigest) {
1237
+ const controller = new AbortController();
1238
+ let terminalObserved = false;
1239
+ let resolveDone;
1240
+ let rejectDone;
1241
+ const done = new Promise((resolve, reject) => {
1242
+ resolveDone = resolve;
1243
+ rejectDone = reject;
1244
+ });
1245
+ done.catch(() => {
1246
+ });
1247
+ const settle = (source) => {
1248
+ const event = "sequence" in source ? source : void 0;
1249
+ const durable = event ? void 0 : source;
1250
+ const receipt = event ? eventReceipt(event) : durable?.receipt;
1251
+ if (durable?.execution_id !== void 0 && attachedState?.execution_id !== void 0 && durable.execution_id !== attachedState.execution_id) {
1252
+ throw new NikaProtocolError(this.kind, "Durable execution identity changed after attach");
1253
+ }
1254
+ if (durable?.trace_id !== void 0 && attachedState?.trace_id !== void 0 && durable.trace_id !== attachedState.trace_id) {
1255
+ throw new NikaProtocolError(this.kind, "Durable trace identity changed after attach");
1256
+ }
1257
+ const knownExecutionId = attachedState?.execution_id ?? durable?.execution_id;
1258
+ const knownTraceId = attachedState?.trace_id ?? durable?.trace_id;
1259
+ if (receipt) {
1260
+ assertReceiptIdentity(
1261
+ receipt,
1262
+ id,
1263
+ this.kind,
1264
+ knownExecutionId,
1265
+ knownTraceId,
1266
+ expectedSnapshotDigest
1267
+ );
1268
+ }
1269
+ const executionId = knownExecutionId ?? (typeof receipt?.execution_id === "string" ? receipt.execution_id : void 0);
1270
+ const traceId = knownTraceId ?? (typeof receipt?.trace_id === "string" ? receipt.trace_id : void 0);
1271
+ terminalObserved = true;
1272
+ const outputs = event ? eventOutputs(event) : durable?.outputs;
1273
+ const error = event ? eventError(event) : durable?.settlement?.error ?? durable?.error;
1274
+ const settlement = event ? eventSettlement(event, this.kind) : durable?.settlement;
1275
+ const evidence = event ? eventEvidence(event, this.kind) : durable?.evidence;
1276
+ resolveDone({
1277
+ id,
1278
+ status: source.status,
1279
+ transport: this.kind,
1280
+ ...executionId ? { execution_id: executionId } : {},
1281
+ ...traceId ? { trace_id: traceId } : {},
1282
+ ...outputs ? { outputs } : {},
1283
+ ...receipt ? { receipt } : {},
1284
+ ...error ? { error } : {},
1285
+ ...settlement ? { settlement } : {},
1286
+ ...evidence ? { evidence } : {}
1287
+ });
1288
+ };
1289
+ if (attachedState && isObservationEnded(attachedState)) settle(attachedState);
1290
+ const events = {
1291
+ [Symbol.asyncIterator]: async function* () {
1292
+ try {
1293
+ for await (const event of this.observeJob(
1294
+ id,
1295
+ controller.signal,
1296
+ settle,
1297
+ initialSequence
1298
+ )) {
1299
+ yield event;
1300
+ }
1301
+ } catch (cause) {
1302
+ const error = cause instanceof Error ? cause : new NikaTransportError(this.kind, String(cause));
1303
+ if (terminalObserved && controller.signal.aborted) return;
1304
+ if (!terminalObserved) rejectDone(error);
1305
+ throw error;
1306
+ }
1307
+ }.bind(this)
1308
+ };
1309
+ return {
1310
+ id,
1311
+ events,
1312
+ done,
1313
+ status: async () => {
1314
+ const object = await this.json(`/v1/jobs/${encodeURIComponent(id)}/status`, {
1315
+ method: "GET",
1316
+ headers: { Accept: "application/json" }
1317
+ }, true, [200], "status");
1318
+ if (typeof object.status !== "string" || !JOB_STATUSES.has(object.status)) {
1319
+ throw new NikaProtocolError(this.kind, "Job status response was malformed");
1320
+ }
1321
+ return object.status;
1322
+ },
1323
+ cancel: async () => {
1324
+ const { object, status } = await this.jsonWithStatus(
1325
+ `/v1/jobs/${encodeURIComponent(id)}/cancel`,
1326
+ { method: "POST" },
1327
+ true,
1328
+ [200, 202],
1329
+ "cancel"
1330
+ );
1331
+ const durable = durableJob(object, id, this.kind);
1332
+ if (status === 202) {
1333
+ if (isExecutionTerminal(durable.status)) {
1334
+ throw new NikaProtocolError(this.kind, "Pending cancellation returned a terminal job");
1335
+ }
1336
+ return {
1337
+ runId: id,
1338
+ accepted: true,
1339
+ status: "cancellation_requested",
1340
+ transport: this.kind
1341
+ };
1342
+ }
1343
+ if (durable.status !== "paused" && !isExecutionTerminal(durable.status)) {
1344
+ throw new NikaProtocolError(this.kind, "Cancellation did not return an ended observation");
1345
+ }
1346
+ settle(durable);
1347
+ const accepted = durable.status === "cancelled";
1348
+ return {
1349
+ runId: id,
1350
+ accepted,
1351
+ status: accepted ? "cancelled" : "already_settled",
1352
+ transport: this.kind
1353
+ };
1354
+ },
1355
+ cleanup: async () => {
1356
+ controller.abort();
1357
+ }
1358
+ };
1359
+ }
1360
+ async *observeJob(id, signal, settle, initialSequence = 0) {
1361
+ const state = {
1362
+ lastSequence: initialSequence,
1363
+ attempt: 0,
1364
+ terminalObserved: false
1365
+ };
1366
+ const jobPath = `/v1/jobs/${encodeURIComponent(id)}`;
1367
+ const eventsPath = `/v1/jobs/${encodeURIComponent(id)}/events`;
1368
+ const settlement = { jobPath, id, settle };
1369
+ while (!state.terminalObserved) {
1370
+ const headers = new Headers({ Accept: "text/event-stream" });
1371
+ if (state.lastSequence > 0) headers.set("Last-Event-ID", String(state.lastSequence));
1372
+ let response;
1373
+ try {
1374
+ response = await this.observationRequest(eventsPath, { headers, signal });
1375
+ } catch {
1376
+ await this.retryObservation(state, signal, {
1377
+ retry: true
1378
+ }, settlement);
1379
+ continue;
1380
+ }
1381
+ if (response.status === 404) {
1382
+ await discardResponse(response);
1383
+ const durable2 = await this.inspectDurableJob(jobPath, id, signal);
1384
+ if (isRetryObservation(durable2)) {
1385
+ await this.retryObservation(state, signal, durable2, settlement);
1386
+ continue;
1387
+ }
1388
+ if (isObservationEnded(durable2)) {
1389
+ state.terminalObserved = true;
1390
+ settle(durable2);
1391
+ return;
1392
+ }
1393
+ await this.retryObservation(state, signal, { retry: true }, settlement);
1394
+ continue;
1395
+ }
1396
+ if (isRetryableStatus(response.status)) {
1397
+ const retryAfterMilliseconds = retryAfter(response.headers.get("Retry-After"));
1398
+ await discardResponse(response);
1399
+ await this.retryObservation(state, signal, {
1400
+ retry: true,
1401
+ ...retryAfterMilliseconds !== void 0 ? { retryAfterMilliseconds } : {}
1402
+ }, settlement);
1403
+ continue;
1404
+ }
1405
+ this.assertObservationResponse(response, eventsPath, "text/event-stream");
1406
+ const body = response.body;
1407
+ if (!body) {
1408
+ throw new NikaProtocolError(this.kind, "SSE event response omitted its body");
1409
+ }
1410
+ try {
1411
+ const limits = this.sseLimits();
1412
+ for await (const frame of decodeSse(body, limits, signal)) {
1413
+ if (frame.retry !== void 0) {
1414
+ state.serverRetryMilliseconds = boundedDelay(frame.retry);
1415
+ }
1416
+ if (frame.data === void 0) continue;
1417
+ const event = this.nikaEvent(id, frame.id, frame.data);
1418
+ const sequence = event.sequence;
1419
+ if (sequence === state.lastSequence) {
1420
+ if (frame.data === state.lastData) continue;
1421
+ throw new NikaProtocolError(
1422
+ this.kind,
1423
+ `SSE sequence ${sequence} replayed with different data`
1424
+ );
1425
+ }
1426
+ if (sequence !== state.lastSequence + 1) {
1427
+ const relation = sequence > state.lastSequence ? "gap" : "out-of-order replay";
1428
+ throw new NikaProtocolError(
1429
+ this.kind,
1430
+ `SSE ${relation}: expected sequence ${state.lastSequence + 1}, received ${sequence}`
1431
+ );
1432
+ }
1433
+ state.lastSequence = sequence;
1434
+ state.lastData = frame.data;
1435
+ state.attempt = 0;
1436
+ if (isObservationEnded(event)) {
1437
+ state.terminalObserved = true;
1438
+ settle(event);
1439
+ }
1440
+ yield event;
1441
+ if (state.terminalObserved) return;
1442
+ }
1443
+ } catch (cause) {
1444
+ if (cause instanceof SseParseError) {
1445
+ throw new NikaProtocolError(this.kind, cause.message, { cause });
1446
+ }
1447
+ if (cause instanceof NikaProtocolError) throw cause;
1448
+ }
1449
+ const durable = await this.inspectDurableJob(jobPath, id, signal);
1450
+ if (isRetryObservation(durable)) {
1451
+ await this.retryObservation(state, signal, durable, settlement);
1452
+ continue;
1453
+ }
1454
+ if (isObservationEnded(durable)) {
1455
+ state.terminalObserved = true;
1456
+ settle(durable);
1457
+ return;
1458
+ }
1459
+ await this.retryObservation(state, signal, { retry: true }, settlement);
1460
+ }
1461
+ }
1462
+ nikaEvent(jobId2, id, data) {
1463
+ if (!id || !/^[1-9]\d*$/.test(id)) {
1464
+ throw new NikaProtocolError(
1465
+ this.kind,
1466
+ "SSE event id must be a canonical positive decimal sequence"
1467
+ );
1468
+ }
1469
+ const sequence = Number(id);
1470
+ if (!Number.isSafeInteger(sequence)) {
1471
+ throw new NikaProtocolError(this.kind, "SSE event id exceeded the safe integer range");
1472
+ }
1473
+ let value;
1474
+ try {
1475
+ value = JSON.parse(data);
1476
+ } catch (cause) {
1477
+ throw new NikaProtocolError(this.kind, "SSE data was not valid JSON", {
1478
+ cause: cause instanceof Error ? cause : void 0
1479
+ });
1480
+ }
1481
+ const event = machineObject(value);
1482
+ if (!event) throw new NikaProtocolError(this.kind, "SSE data was not an object");
1483
+ const allowed = /* @__PURE__ */ new Set([
1484
+ "sequence",
1485
+ "at",
1486
+ "kind",
1487
+ "status",
1488
+ "code",
1489
+ "message",
1490
+ "outputs",
1491
+ "receipt",
1492
+ "settlement",
1493
+ "evidence"
1494
+ ]);
1495
+ if (Object.keys(event).some((key) => !allowed.has(key))) {
1496
+ throw new NikaProtocolError(this.kind, "SSE data contained fields outside the public projection");
1497
+ }
1498
+ if (event.sequence !== sequence) {
1499
+ throw new NikaProtocolError(
1500
+ this.kind,
1501
+ `SSE id ${id} did not equal JSON data.sequence`
1502
+ );
1503
+ }
1504
+ if (!Object.hasOwn(event, "kind") || event.kind !== null && typeof event.kind !== "string") {
1505
+ throw new NikaProtocolError(this.kind, "SSE data.kind was not a string or null");
1506
+ }
1507
+ if (!Object.hasOwn(event, "status") || event.status !== null && (typeof event.status !== "string" || !JOB_STATUSES.has(event.status))) {
1508
+ throw new NikaProtocolError(this.kind, "SSE data.status was not a known job status or null");
1509
+ }
1510
+ for (const field of ["code", "message"]) {
1511
+ if (event[field] !== void 0 && typeof event[field] !== "string") {
1512
+ throw new NikaProtocolError(this.kind, `SSE data.${field} was not a string`);
1513
+ }
1514
+ }
1515
+ if (event.at !== void 0 && (typeof event.at !== "string" || !isCanonicalTimestamp(event.at))) {
1516
+ throw new NikaProtocolError(this.kind, "SSE data.at was not an RFC 3339 timestamp");
1517
+ }
1518
+ if (event.evidence !== void 0) journalEvidence(event.evidence, this.kind, "SSE data.evidence");
1519
+ if (event.outputs !== void 0 && !machineObject(event.outputs)) {
1520
+ throw new NikaProtocolError(this.kind, "SSE data.outputs was not an object");
1521
+ }
1522
+ if (event.settlement !== void 0) {
1523
+ readSettlement(
1524
+ event.settlement,
1525
+ this.kind,
1526
+ typeof event.status === "string" ? event.status : void 0
1527
+ );
1528
+ }
1529
+ if (event.receipt !== void 0) {
1530
+ const receipt = machineObject(event.receipt);
1531
+ if (!receipt) {
1532
+ throw new NikaProtocolError(this.kind, "SSE data.receipt was not an object");
1533
+ }
1534
+ assertReceiptIdentity(receipt, jobId2, this.kind);
1535
+ }
1536
+ return event;
1537
+ }
1538
+ async inspectDurableJob(path3, expectedId, signal) {
1539
+ let response;
1540
+ try {
1541
+ response = await this.observationRequest(path3, { headers: { Accept: "application/json" }, signal });
1542
+ } catch {
1543
+ return { retry: true };
1544
+ }
1545
+ if (response.status === 404) {
1546
+ await discardResponse(response);
1547
+ throw new NikaTransportError(this.kind, `HTTP 404 for ${path3}; job was durably absent`);
1548
+ }
1549
+ if (isRetryableStatus(response.status)) {
1550
+ const retryAfterMilliseconds = retryAfter(response.headers.get("Retry-After"));
1551
+ await discardResponse(response);
1552
+ return {
1553
+ retry: true,
1554
+ ...retryAfterMilliseconds !== void 0 ? { retryAfterMilliseconds } : {}
1555
+ };
1556
+ }
1557
+ this.assertObservationResponse(response, path3, "application/json");
1558
+ try {
1559
+ const object = await this.readObservationObject(response, path3);
1560
+ return durableJob(object, expectedId, this.kind);
1561
+ } catch (cause) {
1562
+ if (cause instanceof NikaTransportError && !(cause instanceof NikaProtocolError)) {
1563
+ return { retry: true };
1564
+ }
1565
+ throw cause;
1566
+ }
1567
+ }
1568
+ assertObservationResponse(response, path3, contentType) {
1569
+ if (response.status === 204) {
1570
+ void discardResponse(response);
1571
+ throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${path3}`);
1572
+ }
1573
+ if (response.status === 401 || response.status === 403) {
1574
+ void discardResponse(response);
1575
+ throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${path3}`);
1576
+ }
1577
+ if (response.status !== 200) {
1578
+ void discardResponse(response);
1579
+ throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${path3}`);
1580
+ }
1581
+ const actual = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1582
+ if (actual !== contentType) {
1583
+ void discardResponse(response);
1584
+ throw new NikaProtocolError(
1585
+ this.kind,
1586
+ `HTTP ${path3} returned an invalid content-type`
1587
+ );
1588
+ }
1589
+ }
1590
+ async readObservationObject(response, path3, signal) {
1591
+ if (!response.body) {
1592
+ throw new NikaProtocolError(this.kind, `HTTP ${path3} omitted its JSON body`);
1593
+ }
1594
+ const reader = response.body.getReader();
1595
+ const chunks = [];
1596
+ let bytes = 0;
1597
+ let rejectBoundary;
1598
+ const boundary = new Promise((_resolve, reject) => {
1599
+ rejectBoundary = reject;
1600
+ });
1601
+ const abort = () => {
1602
+ rejectBoundary(new NikaTransportError(this.kind, `HTTP ${path3} response body was aborted`));
1603
+ void reader.cancel().catch(() => {
1604
+ });
1605
+ };
1606
+ signal?.addEventListener("abort", abort, { once: true });
1607
+ const timer = setTimeout(() => {
1608
+ rejectBoundary(new NikaTransportError(
1609
+ this.kind,
1610
+ `HTTP response body timed out after ${this.options.requestTimeout}ms`
1611
+ ));
1612
+ void reader.cancel().catch(() => {
1613
+ });
1614
+ }, this.options.requestTimeout);
1615
+ try {
1616
+ if (signal?.aborted) abort();
1617
+ while (true) {
1618
+ const { done, value: value2 } = await Promise.race([reader.read(), boundary]);
1619
+ if (done) break;
1620
+ bytes += value2.byteLength;
1621
+ if (bytes > this.options.machineBufferBytes) {
1622
+ throw new NikaProtocolError(
1623
+ this.kind,
1624
+ `HTTP ${path3} JSON exceeded ${this.options.machineBufferBytes} bytes`
1625
+ );
1626
+ }
1627
+ chunks.push(value2);
1628
+ }
1629
+ } catch (cause) {
1630
+ if (cause instanceof NikaProtocolError) throw cause;
1631
+ throw new NikaTransportError(this.kind, `HTTP ${path3} response body reset`);
1632
+ } finally {
1633
+ clearTimeout(timer);
1634
+ signal?.removeEventListener("abort", abort);
1635
+ await reader.cancel().catch(() => {
1636
+ });
1637
+ reader.releaseLock();
1638
+ }
1639
+ const body = new Uint8Array(bytes);
1640
+ let offset = 0;
1641
+ for (const chunk of chunks) {
1642
+ body.set(chunk, offset);
1643
+ offset += chunk.byteLength;
1644
+ }
1645
+ let text;
1646
+ try {
1647
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
1648
+ } catch (cause) {
1649
+ throw new NikaProtocolError(this.kind, `HTTP ${path3} JSON was not valid UTF-8`, {
1650
+ cause: cause instanceof Error ? cause : void 0
1651
+ });
1652
+ }
1653
+ let value;
1654
+ try {
1655
+ value = JSON.parse(text);
1656
+ } catch (cause) {
1657
+ throw new NikaProtocolError(this.kind, `HTTP ${path3} did not return valid JSON`, {
1658
+ cause: cause instanceof Error ? cause : void 0
1659
+ });
1660
+ }
1661
+ const object = machineObject(value);
1662
+ if (!object) throw new NikaProtocolError(this.kind, `HTTP ${path3} did not return an object`);
1663
+ return object;
1664
+ }
1665
+ async observationRequest(path3, init, withTimeout = true) {
1666
+ const controller = withTimeout ? new AbortController() : void 0;
1667
+ const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
1668
+ const callerSignal = init.signal;
1669
+ const abort = () => controller?.abort(callerSignal?.reason);
1670
+ callerSignal?.addEventListener("abort", abort, { once: true });
1671
+ const headers = new Headers(init.headers);
1672
+ headers.set("Authorization", `Bearer ${this.options.token}`);
1673
+ try {
1674
+ return await this.options.fetch(`${this.options.url}${path3}`, {
1675
+ ...init,
1676
+ headers,
1677
+ signal: controller?.signal ?? callerSignal
1678
+ });
1679
+ } catch {
1680
+ if (controller?.signal.aborted && !callerSignal?.aborted) {
1681
+ throw new NikaTransportError(
1682
+ this.kind,
1683
+ `HTTP observation timed out after ${this.options.requestTimeout}ms`
1684
+ );
1685
+ }
1686
+ throw new NikaTransportError(this.kind, "HTTP observation transport failed");
1687
+ } finally {
1688
+ if (timer) clearTimeout(timer);
1689
+ callerSignal?.removeEventListener("abort", abort);
1690
+ }
1691
+ }
1692
+ async retryObservation(state, signal, retry, settlement) {
1693
+ if (state.attempt >= MAX_OBSERVATION_RETRIES) {
1694
+ const durable = await this.inspectDurableJob(
1695
+ settlement.jobPath,
1696
+ settlement.id,
1697
+ signal
1698
+ );
1699
+ if (!isRetryObservation(durable) && isObservationEnded(durable)) {
1700
+ state.terminalObserved = true;
1701
+ settlement.settle(durable);
1702
+ return;
1703
+ }
1704
+ throw new NikaObservationInterrupted(
1705
+ this.kind,
1706
+ settlement.id,
1707
+ state.lastSequence,
1708
+ state.attempt
1709
+ );
1710
+ }
1711
+ state.attempt += 1;
1712
+ const exponential = Math.min(
1713
+ RETRY_BASE_MILLISECONDS * 2 ** (state.attempt - 1),
1714
+ RETRY_MAX_MILLISECONDS
1715
+ );
1716
+ const milliseconds = retry.retryAfterMilliseconds ?? state.serverRetryMilliseconds ?? exponential;
1717
+ await (this.options.retryDelay ?? abortableDelay)(boundedDelay(milliseconds), signal);
1718
+ }
1719
+ sseLimits() {
1720
+ return {
1721
+ maxLineBytes: this.options.machineBufferBytes,
1722
+ maxFrameBytes: this.options.machineBufferBytes,
1723
+ maxBufferBytes: this.options.machineBufferBytes
1724
+ };
1725
+ }
1726
+ gap(capability, message) {
1727
+ return new NikaCompatibilityError(capability, this.kind, message);
1728
+ }
1729
+ ensureServerIdentity() {
1730
+ this.serverIdentity ??= this.probeServerIdentity();
1731
+ return this.serverIdentity;
1732
+ }
1733
+ async probeServerIdentity() {
1734
+ const health = await this.json("/health", { method: "GET" }, false);
1735
+ if (health.status !== "ok" || health.service !== "nika-serve") {
1736
+ throw new NikaCompatibilityError(
1737
+ "engineIdentity",
1738
+ this.kind,
1739
+ "GET /health did not identify a live nika-serve engine"
1740
+ );
1741
+ }
1742
+ this.remoteIdentity = compatibleEngineIdentity(health, this.kind);
1743
+ return this.remoteIdentity;
1744
+ }
1745
+ ensureLocalEngine() {
1746
+ this.localIdentity ??= this.verifyLocalEngine();
1747
+ return this.localIdentity;
1748
+ }
1749
+ localEngine() {
1750
+ this.resolvedEngine ??= this.options.resolveEngine();
1751
+ return this.resolvedEngine;
1752
+ }
1753
+ async verifyLocalEngine() {
1754
+ const local = await verifyNikaEngine(this.localEngine());
1755
+ const remote = await this.ensureServerIdentity();
1756
+ compatibleEngineIdentity(remote, this.kind, local);
1757
+ return local;
1758
+ }
1759
+ async ensureScheduleCapability() {
1760
+ await this.ensureServerIdentity();
1761
+ if (!this.remoteIdentity?.supportedCapabilities.includes("schedule")) {
1762
+ throw this.gap(
1763
+ "schedule",
1764
+ "The connected nika serve process did not advertise resident schedule authority"
1765
+ );
1766
+ }
1767
+ }
1768
+ /**
1769
+ * The resident must advertise named-input admission before a map is sent
1770
+ * (nika#1642). A resident from before the envelope answered 202 to an extra
1771
+ * `inputs` field and applied nothing, so an accepted POST negotiates nothing:
1772
+ * only the capability `/health` advertised does. Called after
1773
+ * `ensureServerIdentity()`.
1774
+ */
1775
+ requireJobInputs() {
1776
+ const identity = this.remoteIdentity;
1777
+ if (identity?.supportedCapabilities.includes(JOB_INPUTS)) return;
1778
+ throw this.gap(
1779
+ JOB_INPUTS,
1780
+ `The connected nika serve ${identity?.engineVersion ?? "(unidentified)"} does not advertise ${JOB_INPUTS} (advertised: ${identity?.supportedCapabilities.join(", ") ?? "nothing"}); run({ inputs }) needs a resident that binds declared inputs by name. Nothing was posted: an older resident accepts the field and ignores its values`
1781
+ );
1782
+ }
1783
+ async captureSnapshot(workflow, signal, teach = false) {
1784
+ const identity = await this.ensureLocalEngine();
1785
+ const captured = await captureEngine(
1786
+ this.localEngine().bin,
1787
+ ["check", workflow, "--json", "--sdk-snapshot"],
1788
+ {
1789
+ cwd: this.options.cwd,
1790
+ signal,
1791
+ bufferBytes: this.options.machineBufferBytes,
1792
+ transport: this.kind,
1793
+ label: "SDK snapshot capture"
1794
+ }
1795
+ );
1796
+ let outer;
1797
+ try {
1798
+ outer = JSON.parse(captured.stdout.trim());
1799
+ } catch (cause) {
1800
+ throw new NikaCompatibilityError(
1801
+ "executionSnapshot",
1802
+ this.kind,
1803
+ `Local engine did not emit the required snapshot report (exit ${captured.exitCode})`
1804
+ );
1805
+ }
1806
+ const report = { ...outer, exitCode: captured.exitCode };
1807
+ delete report.execution_snapshot;
1808
+ if (captured.exitCode !== 0 || outer.clean !== true) {
1809
+ report.clean = false;
1810
+ if (teach && !Array.isArray(outer.findings)) {
1811
+ return { report: await this.teachingReport(workflow, report, signal) };
1812
+ }
1813
+ return { report };
1814
+ }
1815
+ compatibleEngineIdentity(outer, this.kind, identity);
1816
+ if (outer.engineVersion !== identity.engineVersion || outer.report_version !== identity.checkReportVersion) {
1817
+ throw this.gap(
1818
+ "executionSnapshot",
1819
+ "Local snapshot report identity changed after the engine probe"
1820
+ );
1821
+ }
1822
+ if (captured.stderr.trim()) {
1823
+ throw this.gap("executionSnapshot", "Local snapshot capture wrote unexpected diagnostics");
1824
+ }
1825
+ const bytes = outer.execution_snapshot;
1826
+ if (typeof bytes !== "string" || bytes.length === 0) {
1827
+ throw this.gap("executionSnapshot", "Local engine omitted execution_snapshot bytes");
1828
+ }
1829
+ return {
1830
+ report,
1831
+ bytes,
1832
+ identity: snapshotIdentity(bytes, this.kind)
1833
+ };
1834
+ }
1835
+ /**
1836
+ * A red workflow has no exportable snapshot, so the capture returns one
1837
+ * error line and no findings. Ask the same local engine for its plain
1838
+ * teaching report, so `findings[]` reads the same on both transports. The
1839
+ * refused capture line is preserved as `snapshot_error` rather than
1840
+ * overwriting whatever the engine put in `error`. No bytes leave the
1841
+ * machine on this path: the caller still sees `clean: false`.
1842
+ */
1843
+ async teachingReport(workflow, refused, signal) {
1844
+ const captured = await captureEngine(
1845
+ this.localEngine().bin,
1846
+ ["check", workflow, "--json"],
1847
+ {
1848
+ cwd: this.options.cwd,
1849
+ signal,
1850
+ bufferBytes: this.options.machineBufferBytes,
1851
+ transport: this.kind,
1852
+ label: "SDK teaching check"
1853
+ }
1854
+ );
1855
+ let parsed;
1856
+ try {
1857
+ parsed = JSON.parse(captured.stdout.trim());
1858
+ } catch {
1859
+ return refused;
1860
+ }
1861
+ const plain = machineObject(parsed);
1862
+ if (!plain) return refused;
1863
+ const report = {
1864
+ ...plain,
1865
+ clean: false,
1866
+ exitCode: captured.exitCode,
1867
+ ...refused.error === void 0 ? {} : { snapshot_error: refused.error }
1868
+ };
1869
+ delete report.execution_snapshot;
1870
+ return report;
1871
+ }
1872
+ async json(path3, init, authenticated = true, acceptedStatuses = [200], operation) {
1873
+ const { object } = await this.jsonWithStatus(
1874
+ path3,
1875
+ init,
1876
+ authenticated,
1877
+ acceptedStatuses,
1878
+ operation
1879
+ );
1880
+ return object;
1881
+ }
1882
+ /**
1883
+ * The JSON object an accepted status carries, with that status, for the
1884
+ * routes whose accepted statuses mean different things (cancel: 200 is a
1885
+ * settled job, 202 an accepted request).
1886
+ */
1887
+ async jsonWithStatus(path3, init, authenticated, acceptedStatuses, operation) {
1888
+ const outcome = await this.jsonOutcome(path3, init, authenticated, acceptedStatuses, operation);
1889
+ if ("refusal" in outcome) throw this.refused(path3, outcome);
1890
+ return outcome;
1891
+ }
1892
+ async jsonOutcome(path3, init, authenticated, acceptedStatuses, operation, strictRefusal = false) {
1893
+ const response = await this.fetchResponse(path3, init, true, authenticated, false);
1894
+ if (!acceptedStatuses.includes(response.status)) {
1895
+ if (response.ok) {
1896
+ await discardResponse(response);
1897
+ throw new NikaProtocolError(
1898
+ this.kind,
1899
+ `HTTP ${path3} returned non-contract status ${response.status}`
1900
+ );
1901
+ }
1902
+ const refusal = operation === void 0 ? void 0 : await this.readRefusal(response, path3, strictRefusal);
1903
+ if (operation !== void 0 && refusal) {
1904
+ return { operation, status: response.status, refusal };
1905
+ }
1906
+ await discardResponse(response);
1907
+ throw new NikaTransportError(
1908
+ this.kind,
1909
+ `HTTP ${response.status} for ${path3}: [REDACTED]`
1910
+ );
1911
+ }
1912
+ const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1913
+ if (contentType !== "application/json") {
1914
+ await discardResponse(response);
1915
+ throw new NikaProtocolError(this.kind, `HTTP ${path3} returned an invalid content-type`);
1916
+ }
1917
+ return {
1918
+ object: await this.readObservationObject(response, path3, init.signal ?? void 0),
1919
+ status: response.status
1920
+ };
1921
+ }
1922
+ refused(path3, outcome) {
1923
+ const { code, message } = outcome.refusal;
1924
+ return new NikaOperationError(
1925
+ outcome.operation,
1926
+ this.kind,
1927
+ code,
1928
+ `HTTP ${outcome.status} for ${path3}: ${code}` + (message ? ` (${message})` : ""),
1929
+ { status: outcome.status, machineCode: code }
1930
+ );
1931
+ }
1932
+ /**
1933
+ * Read a non-2xx body only when the server typed it as an engine refusal.
1934
+ * Anything else (plain text, oversized, malformed, a code that is not a
1935
+ * plain identifier, a code that carries the bearer token) is discarded and
1936
+ * the caller falls back to the redacted transport error.
1937
+ */
1938
+ async readRefusal(response, path3, strict = false) {
1939
+ const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1940
+ if (contentType !== "application/json") {
1941
+ await discardResponse(response);
1942
+ return void 0;
1943
+ }
1944
+ let object;
1945
+ try {
1946
+ object = await this.readObservationObject(response, path3);
1947
+ } catch {
1948
+ return void 0;
1949
+ }
1950
+ const error = machineObject(object.error);
1951
+ if (strict && (Object.keys(object).some((key) => key !== "error") || !error || Object.keys(error).some((key) => key !== "code" && key !== "message") || typeof error.message !== "string")) return void 0;
1952
+ const code = error?.code;
1953
+ if (typeof code !== "string" || !/^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/.test(code) || code.includes(this.options.token)) {
1954
+ return void 0;
1955
+ }
1956
+ const message = typeof error?.message === "string" && error.message.length > 0 ? this.redact(error.message) : void 0;
1957
+ return { code, ...strict ? { message: this.redact(error?.message) } : message ? { message } : {} };
1958
+ }
1959
+ /** Engine messages are already path-free; a reflected bearer token never survives. */
1960
+ redact(text) {
1961
+ const clean = text.replace(/[\u0000-\u001f\u007f]+/g, " ").split(this.options.token).join("[REDACTED]");
1962
+ return clean.length > 240 ? `${clean.slice(0, 240)}...` : clean;
1963
+ }
1964
+ async operationJson(operation, path3, init) {
1965
+ const response = await this.fetchResponse(path3, init, true, true, false);
1966
+ const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
1967
+ if (contentType !== "application/json") {
1968
+ await discardResponse(response);
1969
+ throw new NikaProtocolError(this.kind, `HTTP ${path3} returned an invalid content-type`);
1970
+ }
1971
+ const object = await this.readObservationObject(response, path3);
1972
+ if (!response.ok) throw this.operationFailure(operation, response.status, object);
1973
+ return object;
1974
+ }
1975
+ operationFailure(operation, status, object) {
1976
+ if (status === 412 && operation === "schedule") {
1977
+ const machine2 = machineObject(object.error);
1978
+ const currentRevision = machine2?.currentRevision;
1979
+ return new NikaOperationError(
1980
+ operation,
1981
+ this.kind,
1982
+ "schedule_conflict",
1983
+ typeof machine2?.message === "string" ? machine2.message : "Schedule revision conflict",
1984
+ {
1985
+ status,
1986
+ machineCode: typeof machine2?.code === "string" ? machine2.code : void 0,
1987
+ ...typeof currentRevision === "string" || currentRevision === null ? { currentRevision } : {}
1988
+ }
1989
+ );
1990
+ }
1991
+ if (Array.isArray(object.findings)) {
1992
+ const findings = operationFindings(object.findings);
1993
+ if (!findings) {
1994
+ throw new NikaProtocolError(this.kind, "Schedule refusal findings were malformed");
1995
+ }
1996
+ return new NikaOperationError(
1997
+ operation,
1998
+ this.kind,
1999
+ "schedule_refused",
2000
+ "Schedule was refused by the resident authority",
2001
+ { status, findings }
2002
+ );
2003
+ }
2004
+ const machine = machineObject(object.error);
2005
+ if (typeof machine?.code !== "string" || typeof machine.message !== "string") {
2006
+ throw new NikaProtocolError(this.kind, `HTTP ${pathForOperation(operation)} error was malformed`);
2007
+ }
2008
+ return new NikaOperationError(
2009
+ operation,
2010
+ this.kind,
2011
+ machine.code,
2012
+ machine.message,
2013
+ { status, machineCode: machine.code }
2014
+ );
2015
+ }
2016
+ async fetchResponse(path3, init, withTimeout, authenticated = true, requireOk = true) {
2017
+ const controller = withTimeout ? new AbortController() : void 0;
2018
+ const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
2019
+ const callerSignal = init.signal;
2020
+ const abort = () => controller?.abort(callerSignal?.reason);
2021
+ callerSignal?.addEventListener("abort", abort, { once: true });
2022
+ const headers = new Headers(init.headers);
2023
+ if (authenticated) headers.set("Authorization", `Bearer ${this.options.token}`);
2024
+ try {
2025
+ const response = await this.options.fetch(`${this.options.url}${path3}`, {
2026
+ ...init,
2027
+ headers,
2028
+ signal: controller?.signal ?? callerSignal
2029
+ });
2030
+ if (requireOk && !response.ok) {
2031
+ await discardResponse(response);
2032
+ throw new NikaTransportError(
2033
+ this.kind,
2034
+ `HTTP ${response.status} for ${path3}: [REDACTED]`
2035
+ );
2036
+ }
2037
+ return response;
2038
+ } catch (cause) {
2039
+ if (cause instanceof NikaTransportError) throw cause;
2040
+ if (controller?.signal.aborted && !callerSignal?.aborted) {
2041
+ throw new NikaTransportError(
2042
+ this.kind,
2043
+ `HTTP request timed out after ${this.options.requestTimeout}ms`,
2044
+ { cause: cause instanceof Error ? cause : void 0 }
2045
+ );
2046
+ }
2047
+ throw new NikaTransportError(this.kind, "HTTP transport failed", {
2048
+ cause: cause instanceof Error ? cause : void 0
2049
+ });
2050
+ } finally {
2051
+ if (timer) clearTimeout(timer);
2052
+ callerSignal?.removeEventListener("abort", abort);
2053
+ }
2054
+ }
2055
+ };
2056
+ function refuseLegacyContainedName(workflow) {
2057
+ if (isRetiredByNameAttempt(workflow)) {
2058
+ throw new NikaConfigurationError(legacyWorkflowRenameMessage(workflow));
2059
+ }
2060
+ }
2061
+ function workflowPath(name) {
2062
+ const segments = name.split("/");
2063
+ if (name.startsWith("/") || name.includes("\\") || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
2064
+ throw new TypeError("workflow name must be a contained slash-separated path");
2065
+ }
2066
+ return segments.map(encodeURIComponent).join("/");
2067
+ }
2068
+ function scheduleBody(workflow, options) {
2069
+ return {
2070
+ workflow,
2071
+ when: options.when,
2072
+ maxCostUsd: options.maxCostUsd,
2073
+ missed: options.missed,
2074
+ ...options.maxLatenessSeconds !== void 0 ? { maxLatenessSeconds: options.maxLatenessSeconds } : {},
2075
+ ...options.overlap !== void 0 ? { overlap: options.overlap } : {},
2076
+ ...options.afterSkip !== void 0 ? { afterSkip: options.afterSkip } : {},
2077
+ ...options.jitter !== void 0 ? { jitter: options.jitter } : {},
2078
+ ...options.tolerance !== void 0 ? { tolerance: options.tolerance } : {},
2079
+ ...options.active !== void 0 ? { active: options.active } : {},
2080
+ ...options.pauseReason !== void 0 ? { pauseReason: options.pauseReason } : {},
2081
+ ...options.pauseUntil !== void 0 ? { pauseUntil: options.pauseUntil } : {}
2082
+ };
2083
+ }
2084
+ function scheduleStatusProjection(value) {
2085
+ const status = machineObject(value);
2086
+ const definition = machineObject(status?.definition);
2087
+ const when = machineObject(definition?.when);
2088
+ const due = machineObject(status?.due);
2089
+ const finding = machineObject(status?.finding);
2090
+ return !!status && !!definition && typeof definition.id === "string" && typeof definition.workflow === "string" && !!when && typeof when.kind === "string" && typeof definition.maxCostUsd === "number" && typeof definition.missed === "string" && (definition.maxLatenessSeconds === null || typeof definition.maxLatenessSeconds === "number") && typeof definition.overlap === "string" && typeof definition.afterSkip === "string" && (definition.jitter === null || typeof definition.jitter === "string") && (definition.tolerance === null || typeof definition.tolerance === "string") && typeof definition.active === "boolean" && (definition.pauseReason === null || typeof definition.pauseReason === "string") && (definition.pauseUntil === null || typeof definition.pauseUntil === "string") && typeof status.origin === "string" && typeof status.revision === "string" && typeof status.active === "boolean" && (status.pause === null || !!machineObject(status.pause)) && Array.isArray(status.next) && status.next.every(scheduleSlotProjection) && (status.earliestWakeHint === null || typeof status.earliestWakeHint === "string") && (status.lastDecision === null || !!machineObject(status.lastDecision)) && (typeof due?.kind === "string" && status.finding === void 0 || status.due === void 0 && scheduleFindingProjection(finding));
2091
+ }
2092
+ function scheduleSlotProjection(value) {
2093
+ const slot = machineObject(value);
2094
+ return typeof slot?.slotId === "string" && typeof slot.scheduledFor === "string" && (slot.requestedCivil === null || typeof slot.requestedCivil === "string") && typeof slot.shift === "string";
2095
+ }
2096
+ function scheduleFindingProjection(value) {
2097
+ const finding = machineObject(value);
2098
+ return typeof finding?.code === "string" && typeof finding.detail === "string";
2099
+ }
2100
+ function operationFindings(values) {
2101
+ const findings = [];
2102
+ for (const value of values) {
2103
+ const finding = machineObject(value);
2104
+ if (typeof finding?.code !== "string" || typeof finding.detail !== "string") {
2105
+ return void 0;
2106
+ }
2107
+ findings.push(finding);
2108
+ }
2109
+ return findings;
2110
+ }
2111
+ function pathForOperation(operation) {
2112
+ if (operation === "schedule") return "schedule apply";
2113
+ if (operation === "scheduleStatus") return "schedule status";
2114
+ return operation;
2115
+ }
2116
+ function isObservationEnded(source) {
2117
+ return isExecutionTerminal(source.status) || source.status === "paused" && machineObject(source.settlement)?.status === "paused";
2118
+ }
2119
+ function isExecutionTerminal(status) {
2120
+ return status === "succeeded" || status === "failed" || status === "interrupted" || status === "cancelled";
2121
+ }
2122
+ function isRetryableStatus(status) {
2123
+ return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
2124
+ }
2125
+ function retryAfter(value) {
2126
+ if (!value) return void 0;
2127
+ const trimmed = value.trim();
2128
+ if (/^\d+$/.test(trimmed)) {
2129
+ const seconds = Number(trimmed);
2130
+ return Number.isSafeInteger(seconds) ? boundedDelay(seconds * 1e3) : void 0;
2131
+ }
2132
+ const date = Date.parse(trimmed);
2133
+ if (!Number.isFinite(date)) return void 0;
2134
+ return boundedDelay(Math.max(0, date - Date.now()));
2135
+ }
2136
+ function boundedDelay(milliseconds) {
2137
+ return Math.max(
2138
+ RETRY_MIN_MILLISECONDS,
2139
+ Math.min(Math.floor(milliseconds), RETRY_MAX_MILLISECONDS)
2140
+ );
2141
+ }
2142
+ function abortableDelay(milliseconds, signal) {
2143
+ if (signal.aborted) return Promise.reject(signal.reason ?? new Error("Observation aborted"));
2144
+ return new Promise((resolve, reject) => {
2145
+ const timer = setTimeout(done, milliseconds);
2146
+ function done() {
2147
+ signal.removeEventListener("abort", abort);
2148
+ resolve();
2149
+ }
2150
+ function abort() {
2151
+ clearTimeout(timer);
2152
+ signal.removeEventListener("abort", abort);
2153
+ reject(signal.reason ?? new Error("Observation aborted"));
2154
+ }
2155
+ signal.addEventListener("abort", abort, { once: true });
2156
+ });
2157
+ }
2158
+ async function discardResponse(response) {
2159
+ await response.body?.cancel().catch(() => {
2160
+ });
2161
+ }
2162
+ function isRetryObservation(value) {
2163
+ return "retry" in value;
2164
+ }
2165
+ function durableJob(value, expectedId, transport) {
2166
+ const allowed = /* @__PURE__ */ new Set([
2167
+ "id",
2168
+ "status",
2169
+ "execution_id",
2170
+ "trace_id",
2171
+ "outputs",
2172
+ "receipt",
2173
+ "error",
2174
+ "settlement",
2175
+ "evidence"
2176
+ ]);
2177
+ if (Object.keys(value).some((key) => !allowed.has(key))) {
2178
+ throw new NikaProtocolError(transport, "Durable job response contained unknown fields");
2179
+ }
2180
+ if (value.id !== expectedId || typeof value.status !== "string" || !JOB_STATUSES.has(value.status)) {
2181
+ throw new NikaProtocolError(transport, "Durable job response had an invalid id or status");
2182
+ }
2183
+ for (const field of ["execution_id", "trace_id"]) {
2184
+ if (value[field] !== void 0 && typeof value[field] !== "string") {
2185
+ throw new NikaProtocolError(transport, `Durable job response ${field} was not a string`);
2186
+ }
2187
+ }
2188
+ let error;
2189
+ if (value.error !== void 0) {
2190
+ const object = machineObject(value.error);
2191
+ if (!object || Object.keys(object).some((key) => key !== "code" && key !== "message") || typeof object.code !== "string" || typeof object.message !== "string") {
2192
+ throw new NikaProtocolError(transport, "Durable job response error was malformed");
2193
+ }
2194
+ error = { code: object.code, message: object.message };
2195
+ }
2196
+ const outputs = value.outputs === void 0 ? void 0 : machineObject(value.outputs);
2197
+ const receipt = value.receipt === void 0 ? void 0 : machineObject(value.receipt);
2198
+ const settlement = value.settlement === void 0 ? void 0 : readSettlement(value.settlement, transport, value.status);
2199
+ const evidence = value.evidence === void 0 ? void 0 : journalEvidence(value.evidence, transport, "Durable job response evidence");
2200
+ if (value.outputs !== void 0 && !outputs) {
2201
+ throw new NikaProtocolError(transport, "Durable job response outputs were malformed");
2202
+ }
2203
+ if (value.receipt !== void 0 && !receipt) {
2204
+ throw new NikaProtocolError(transport, "Durable job response receipt was malformed");
2205
+ }
2206
+ if (receipt) {
2207
+ assertReceiptIdentity(
2208
+ receipt,
2209
+ expectedId,
2210
+ transport,
2211
+ typeof value.execution_id === "string" ? value.execution_id : void 0,
2212
+ typeof value.trace_id === "string" ? value.trace_id : void 0
2213
+ );
2214
+ }
2215
+ return {
2216
+ id: value.id,
2217
+ status: value.status,
2218
+ ...typeof value.execution_id === "string" ? { execution_id: value.execution_id } : {},
2219
+ ...typeof value.trace_id === "string" ? { trace_id: value.trace_id } : {},
2220
+ ...outputs ? { outputs } : {},
2221
+ ...receipt ? { receipt: Object.freeze(receipt) } : {},
2222
+ ...error ? { error } : {},
2223
+ ...settlement ? { settlement } : {},
2224
+ ...evidence ? { evidence } : {}
2225
+ };
2226
+ }
2227
+ var JOURNAL_EVIDENCE_REASONS = /* @__PURE__ */ new Set(["write_failed", "record_refused"]);
2228
+ function journalEvidence(value, transport, where) {
2229
+ const evidence = machineObject(value);
2230
+ if (!evidence || Object.keys(evidence).length !== 2 || evidence.status !== "mirror_lost" || typeof evidence.reason !== "string" || !JOURNAL_EVIDENCE_REASONS.has(evidence.reason)) {
2231
+ throw new NikaProtocolError(transport, `${where} was not the resident's journal evidence`);
2232
+ }
2233
+ return Object.freeze({
2234
+ status: "mirror_lost",
2235
+ reason: evidence.reason
2236
+ });
2237
+ }
2238
+ function eventEvidence(event, transport) {
2239
+ return event.evidence === void 0 ? void 0 : journalEvidence(event.evidence, transport, "SSE data.evidence");
2240
+ }
2241
+ function assertReceiptIdentity(receipt, expectedJobId, transport, expectedExecutionId, expectedTraceId, expectedSnapshotDigest) {
2242
+ const allowed = /* @__PURE__ */ new Set([
2243
+ "job_id",
2244
+ "execution_id",
2245
+ "trace_id",
2246
+ "snapshot_digest",
2247
+ "origin",
2248
+ "chain_head"
2249
+ ]);
2250
+ if (Object.keys(receipt).some((key) => !allowed.has(key))) {
2251
+ throw new NikaProtocolError(transport, "Receipt contained unknown fields");
2252
+ }
2253
+ if (receipt.job_id !== expectedJobId) {
2254
+ throw new NikaProtocolError(transport, "Receipt job identity did not match its run");
2255
+ }
2256
+ if (typeof receipt.execution_id !== "string" || receipt.execution_id.length === 0) {
2257
+ throw new NikaProtocolError(transport, "Receipt omitted its execution identity");
2258
+ }
2259
+ if (typeof receipt.trace_id !== "string" || receipt.trace_id.length === 0) {
2260
+ throw new NikaProtocolError(transport, "Receipt omitted its trace identity");
2261
+ }
2262
+ if (typeof receipt.snapshot_digest !== "string" || !/^[0-9a-f]{64}$/.test(receipt.snapshot_digest)) {
2263
+ throw new NikaProtocolError(transport, "Receipt omitted its canonical snapshot digest");
2264
+ }
2265
+ if (receipt.chain_head !== void 0 && (typeof receipt.chain_head !== "string" || receipt.chain_head.length === 0)) {
2266
+ throw new NikaProtocolError(transport, "Receipt chain head was malformed");
2267
+ }
2268
+ if (receipt.origin !== void 0) validateReceiptOrigin(receipt.origin, transport);
2269
+ if (expectedExecutionId !== void 0 && receipt.execution_id !== expectedExecutionId) {
2270
+ throw new NikaProtocolError(transport, "Receipt execution identity did not match its job");
2271
+ }
2272
+ if (expectedTraceId !== void 0 && receipt.trace_id !== expectedTraceId) {
2273
+ throw new NikaProtocolError(transport, "Receipt trace identity did not match its job");
2274
+ }
2275
+ if (expectedSnapshotDigest !== void 0 && receipt.snapshot_digest !== expectedSnapshotDigest) {
2276
+ throw new NikaProtocolError(transport, "Receipt snapshot digest did not match its admission");
2277
+ }
2278
+ }
2279
+ function snapshotAck(value) {
2280
+ const allowed = /* @__PURE__ */ new Set(["status", "snapshot_digest", "root", "units"]);
2281
+ if (Object.keys(value).some((key) => !allowed.has(key)) || value.status !== "accepted" || typeof value.snapshot_digest !== "string" || !/^[0-9a-f]{64}$/.test(value.snapshot_digest) || typeof value.root !== "string" || value.root.length === 0 || typeof value.units !== "number" || !Number.isSafeInteger(value.units) || value.units < 1) {
2282
+ return void 0;
2283
+ }
2284
+ return {
2285
+ status: "accepted",
2286
+ snapshot_digest: value.snapshot_digest,
2287
+ root: value.root,
2288
+ units: value.units
2289
+ };
2290
+ }
2291
+ function snapshotIdentity(bytes, transport) {
2292
+ let value;
2293
+ try {
2294
+ value = JSON.parse(bytes);
2295
+ } catch (cause) {
2296
+ throw new NikaCompatibilityError(
2297
+ "executionSnapshot",
2298
+ transport,
2299
+ "Local engine emitted malformed execution_snapshot bytes"
2300
+ );
2301
+ }
2302
+ const snapshot = machineObject(value);
2303
+ if (!snapshot || typeof snapshot.digest !== "string" || !/^[0-9a-f]{64}$/.test(snapshot.digest) || typeof snapshot.root !== "string" || snapshot.root.length === 0 || !Array.isArray(snapshot.units) || snapshot.units.length < 1) {
2304
+ throw new NikaCompatibilityError(
2305
+ "executionSnapshot",
2306
+ transport,
2307
+ "Local engine emitted an invalid execution_snapshot identity"
2308
+ );
2309
+ }
2310
+ return { digest: snapshot.digest, root: snapshot.root, units: snapshot.units.length };
2311
+ }
2312
+ function validateReceiptOrigin(value, transport) {
2313
+ const origin = machineObject(value);
2314
+ if (!origin || typeof origin.kind !== "string") {
2315
+ throw new NikaProtocolError(transport, "Receipt origin was malformed");
2316
+ }
2317
+ if (origin.kind === "manual") {
2318
+ if (Object.keys(origin).some((key) => key !== "kind")) {
2319
+ throw new NikaProtocolError(transport, "Manual receipt origin contained unknown fields");
2320
+ }
2321
+ return;
2322
+ }
2323
+ const required = [
2324
+ "kind",
2325
+ "schedule_origin",
2326
+ "schedule_id",
2327
+ "schedule_revision",
2328
+ "slot_id",
2329
+ "decision",
2330
+ "scheduled_for",
2331
+ "fired_at",
2332
+ "arm_generation"
2333
+ ];
2334
+ if (origin.kind !== "schedule" || Object.keys(origin).some((key) => !required.includes(key)) || required.some((key) => !Object.hasOwn(origin, key)) || !["project", "api"].includes(String(origin.schedule_origin)) || !["scheduled", "catch_up"].includes(String(origin.decision)) || required.slice(2).some((key) => typeof origin[key] !== "string" || origin[key].length === 0) || Buffer.byteLength(String(origin.schedule_id)) > 255 || !/^sha256:[0-9a-f]{64}$/.test(String(origin.schedule_revision)) || !/^[0-9a-f]{64}$/.test(String(origin.slot_id)) || !/^[0-9a-f]{64}$/.test(String(origin.arm_generation)) || !isCanonicalTimestamp(String(origin.scheduled_for)) || !isCanonicalTimestamp(String(origin.fired_at))) {
2335
+ throw new NikaProtocolError(transport, "Scheduled receipt origin was malformed");
2336
+ }
2337
+ }
2338
+ function isCanonicalTimestamp(value) {
2339
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
2340
+ if (!match) return false;
2341
+ const [
2342
+ ,
2343
+ yearText,
2344
+ monthText,
2345
+ dayText,
2346
+ hourText,
2347
+ minuteText,
2348
+ secondText,
2349
+ offsetSign,
2350
+ offsetHourText,
2351
+ offsetMinuteText
2352
+ ] = match;
2353
+ const year = Number(yearText);
2354
+ const month = Number(monthText);
2355
+ const day = Number(dayText);
2356
+ const hour = Number(hourText);
2357
+ const minute = Number(minuteText);
2358
+ const second = Number(secondText);
2359
+ const offsetHour = offsetSign ? Number(offsetHourText) : 0;
2360
+ const offsetMinute = offsetSign ? Number(offsetMinuteText) : 0;
2361
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
2362
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
2363
+ return year > 0 && month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1] && hour <= 23 && minute <= 59 && second <= 59 && offsetHour <= 23 && offsetMinute <= 59 && Number.isFinite(Date.parse(value));
2364
+ }
2365
+
2366
+ // src/lib/native-process-transport.ts
2367
+ import { spawn as spawn2 } from "child_process";
2368
+ import { randomUUID } from "crypto";
2369
+
2370
+ // src/lib/legacy-run-refusals.ts
2371
+ var REFUSAL_EXITS = /* @__PURE__ */ new Set([2, 3]);
2372
+ var ENGINE_PREFIX = /^nika(?: [a-z-]+)?:\s*/;
2373
+ function legacyTeachingLine(line) {
2374
+ const machineCode = leadingCode(line);
2375
+ return machineCode ? { machineCode, message: line } : void 0;
2376
+ }
2377
+ async function legacyPrettyReport(opening, lines, limit, transport) {
2378
+ if (!opening.startsWith("{")) return void 0;
2379
+ let text = opening;
2380
+ let bytes = Buffer.byteLength(opening);
2381
+ for (; ; ) {
2382
+ const next = await lines.next();
2383
+ if (next.done) break;
2384
+ text += `
2385
+ ${next.value}`;
2386
+ bytes += Buffer.byteLength(next.value) + 1;
2387
+ if (bytes > limit) {
2388
+ throw new NikaProtocolError(transport, `Native pre-run report exceeded ${limit} bytes`);
2389
+ }
2390
+ }
2391
+ let value;
2392
+ try {
2393
+ value = JSON.parse(text);
2394
+ } catch {
2395
+ return { text };
2396
+ }
2397
+ const frame = machineObject(value);
2398
+ return frame ? { frame, text } : { text };
2399
+ }
2400
+ function legacyStderrRefusal(stderr, exitCode) {
2401
+ if (!REFUSAL_EXITS.has(exitCode)) return void 0;
2402
+ for (const raw of stderr.split("\n")) {
2403
+ const line = raw.trim().replace(ENGINE_PREFIX, "");
2404
+ const machineCode = leadingCode(line);
2405
+ if (machineCode) return { machineCode, message: line };
2406
+ }
2407
+ return void 0;
2408
+ }
2409
+ function leadingCode(text) {
2410
+ const code = /^NIKA-[A-Z0-9_-]+/.exec(text)?.[0].replace(/-+$/, "");
2411
+ return code && /\d/.test(code) ? code : void 0;
2412
+ }
2413
+
2414
+ // src/lib/run-refusal.ts
2415
+ var RUN_REFUSED = "run_refused";
2416
+ var UNEXPLAINED = "The engine refused this workflow before admission";
2417
+ function isRunEvent(frame) {
2418
+ return typeof frame.kind === "string";
2419
+ }
2420
+ function preRunRefusal(frame, transport) {
2421
+ if (isRunEvent(frame)) return void 0;
2422
+ if (frame.clean === false && Array.isArray(frame.findings)) {
2423
+ return checkRefusal(frame.findings, transport);
2424
+ }
2425
+ const error = machineObject(frame.error);
2426
+ if (!error || typeof error.message !== "string" || error.message.length === 0) {
2427
+ return void 0;
2428
+ }
2429
+ const machineCode = typeof error.code === "string" && error.code.length > 0 ? error.code : void 0;
2430
+ return { ...machineCode ? { machineCode } : {}, message: error.message };
2431
+ }
2432
+ function runRefusalError(transport, refusal, exitCode) {
2433
+ return new NikaOperationError(
2434
+ "run",
2435
+ transport,
2436
+ refusal.machineCode ?? RUN_REFUSED,
2437
+ refusal.message,
2438
+ {
2439
+ status: exitCode,
2440
+ ...refusal.findings ? { findings: refusal.findings } : {},
2441
+ ...refusal.machineCode ? { machineCode: refusal.machineCode } : {}
2442
+ }
2443
+ );
2444
+ }
2445
+ function checkRefusal(values, transport) {
2446
+ const findings = [];
2447
+ for (const value of values) {
2448
+ const finding = machineObject(value);
2449
+ if (!finding) {
2450
+ throw new NikaProtocolError(transport, "Pre-run refusal findings were malformed");
2451
+ }
2452
+ findings.push(finding);
2453
+ }
2454
+ const named = findings.find((finding) => typeof finding.code === "string" && finding.code) ?? findings[0];
2455
+ const machineCode = typeof named?.code === "string" && named.code ? named.code : void 0;
2456
+ const said = typeof named?.message === "string" && named.message ? named.message : void 0;
2457
+ const others = findings.length > 1 ? ` (+${findings.length - 1} more findings)` : "";
2458
+ return {
2459
+ ...machineCode ? { machineCode } : {},
2460
+ message: `${[machineCode, said ?? UNEXPLAINED].filter(Boolean).join(" \xB7 ")}${others}`,
2461
+ findings
2462
+ };
2463
+ }
2464
+
2465
+ // src/lib/native-process-transport.ts
2466
+ var NativeProcessTransport = class {
2467
+ constructor(options) {
2468
+ this.options = options;
2469
+ }
2470
+ kind = "native-process";
2471
+ ready;
2472
+ async check(workflow, options) {
2473
+ await this.ensureReady();
2474
+ const args = ["check", workflow, "--json"];
2475
+ if (options.model) args.push("--model", options.model);
2476
+ if (options.nativeStrict) args.push("--native-strict");
2477
+ const captured = await this.capture(args, options.signal);
2478
+ const report = reportObject(captured.stdout) ?? reportObject(stripEnginePrefix(captured.stderr));
2479
+ if (!report) {
2480
+ const excerpt = diagnosticExcerpt(captured.stderr);
2481
+ throw new NikaCompatibilityError(
2482
+ "check",
2483
+ this.kind,
2484
+ `This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (excerpt ? `: ${excerpt}` : "")
2485
+ );
2486
+ }
2487
+ return { ...report, exitCode: captured.exitCode };
2488
+ }
2489
+ async startRun(workflow, options) {
2490
+ const inputs = literalInputs(options);
2491
+ const identity = await this.ensureReady();
2492
+ if (options.idempotencyKey !== void 0) {
2493
+ throw new NikaCompatibilityError(
2494
+ "idempotencyKey",
2495
+ this.kind,
2496
+ "idempotencyKey is an HTTP admission option"
2497
+ );
2498
+ }
2499
+ if (inputs && !identity.supportedCapabilities.includes(INPUTS_LITERAL)) {
2500
+ throw new NikaCompatibilityError(
2501
+ INPUTS_LITERAL,
2502
+ this.kind,
2503
+ `Engine ${identity.engineVersion} at ${this.options.engine.bin} does not advertise ${INPUTS_LITERAL} (advertised: ${identity.supportedCapabilities.join(", ")}); run({ inputs }) needs the literal input channel \`nika run --inputs-json -\`. The SDK never falls back to --var, which reads @env: and coerces by declared type`
2504
+ );
2505
+ }
2506
+ const args = ["run", workflow, "--json", ...runFlags(options, inputs !== void 0)];
2507
+ const spawnOptions = { cwd: this.options.cwd, shell: false };
2508
+ const child = inputs ? spawn2(this.options.engine.bin, args, { ...spawnOptions, stdio: ["pipe", "pipe", "pipe"] }) : spawn2(this.options.engine.bin, args, { ...spawnOptions, stdio: ["ignore", "pipe", "pipe"] });
2509
+ if (inputs && child.stdin) writeLiteralInputs(child.stdin, inputs.json);
2510
+ const engine = this.nativeProcess(child);
2511
+ let first;
2512
+ try {
2513
+ first = await this.admission(engine);
2514
+ } catch (cause) {
2515
+ await engine.stop();
2516
+ throw cause;
2517
+ }
2518
+ return this.processRun(randomUUID(), engine, first);
2519
+ }
2520
+ async attachRun(_id, _options) {
2521
+ throw new NikaCompatibilityError(
2522
+ "attachRun",
2523
+ this.kind,
2524
+ "Only a resident nika serve authority owns durable jobs across client processes"
2525
+ );
2526
+ }
2527
+ async listWorkflows() {
2528
+ throw new NikaCompatibilityError(
2529
+ "workflowCatalog",
2530
+ this.kind,
2531
+ "The contained workflow catalog belongs to a resident nika serve authority"
2532
+ );
2533
+ }
2534
+ async workflow(_name) {
2535
+ throw new NikaCompatibilityError(
2536
+ "workflowCatalog",
2537
+ this.kind,
2538
+ "The contained workflow catalog belongs to a resident nika serve authority"
2539
+ );
2540
+ }
2541
+ async schedule(_workflow, _options) {
2542
+ throw new NikaCompatibilityError(
2543
+ "schedule",
2544
+ this.kind,
2545
+ "A direct native process is not a proven resident schedule authority; connect to nika serve"
2546
+ );
2547
+ }
2548
+ async scheduleStatus(_id) {
2549
+ throw new NikaCompatibilityError(
2550
+ "schedule",
2551
+ this.kind,
2552
+ "A direct native process is not a proven resident schedule authority; connect to nika serve"
2553
+ );
2554
+ }
2555
+ async traceVerify(receipt, options) {
2556
+ await this.ensureReady();
2557
+ const locator = receiptTraceLocator(receipt);
2558
+ if (!locator) {
2559
+ throw new NikaCompatibilityError(
2560
+ "traceVerify",
2561
+ this.kind,
2562
+ "The engine receipt does not carry a native trace locator"
2563
+ );
2564
+ }
2565
+ const captured = await this.capture(["trace", "verify", locator, "--plain"], options.signal);
2566
+ if (captured.exitCode !== 0) {
2567
+ return {
2568
+ verified: false,
2569
+ verdict: "invalid",
2570
+ exitCode: captured.exitCode,
2571
+ output: captured.stdout + captured.stderr
2572
+ };
2573
+ }
2574
+ const evidence = await this.capture(
2575
+ ["trace", "evidence", locator, "--json", "--plain"],
2576
+ options.signal
2577
+ );
2578
+ let manifest;
2579
+ try {
2580
+ manifest = parseMachineObject(evidence.stdout.trim(), this.kind);
2581
+ } catch (cause) {
2582
+ throw new NikaCompatibilityError(
2583
+ "traceVerify",
2584
+ this.kind,
2585
+ `This engine did not emit the required evidence manifest (exit ${evidence.exitCode})`
2586
+ );
2587
+ }
2588
+ const mismatch = receiptMismatch(receipt, manifest);
2589
+ return {
2590
+ verified: mismatch === void 0,
2591
+ // The same vocabulary the HTTP verdict speaks, so a caller reads one
2592
+ // field on both transports instead of inferring it from exitCode.
2593
+ verdict: mismatch === void 0 ? "verified" : "invalid",
2594
+ ...mismatch === void 0 ? {} : { reason: "receipt_mismatch" },
2595
+ exitCode: mismatch === void 0 ? 0 : 2,
2596
+ output: captured.stdout + captured.stderr + (mismatch === void 0 ? "" : `
2597
+ RECEIPT MISMATCH: ${mismatch}
2598
+ `)
2599
+ };
2600
+ }
2601
+ nativeProcess(child) {
2602
+ let settled = false;
2603
+ let stderr = "";
2604
+ child.stderr.setEncoding("utf8");
2605
+ child.stderr.on("data", (chunk) => {
2606
+ stderr = boundedAppend(stderr, chunk, this.options.machineBufferBytes);
2607
+ });
2608
+ const closed = new Promise((resolve, reject) => {
2609
+ child.once("error", (cause) => {
2610
+ reject(new NikaTransportError(
2611
+ this.kind,
2612
+ `Cannot spawn ${this.options.engine.bin}: ${cause.message}`,
2613
+ { cause }
2614
+ ));
2615
+ });
2616
+ child.once("close", (code, signal) => {
2617
+ settled = true;
2618
+ resolve(code === null && signal ? 130 : code ?? 3);
2619
+ });
2620
+ });
2621
+ closed.catch(() => {
2622
+ });
2623
+ const lines = machineLines(child.stdout, this.options.machineBufferBytes, this.kind);
2624
+ return {
2625
+ child,
2626
+ lines,
2627
+ closed,
2628
+ settled: () => settled,
2629
+ diagnostics: () => stderr,
2630
+ stop: async () => {
2631
+ void lines.return(void 0).catch(() => {
2632
+ });
2633
+ child.stdin?.destroy();
2634
+ if (settled) return;
2635
+ child.kill("SIGTERM");
2636
+ if (await endsWithin(closed, STOP_GRACE_MILLISECONDS)) return;
2637
+ child.kill("SIGKILL");
2638
+ await endsWithin(closed, STOP_GRACE_MILLISECONDS);
2639
+ }
2640
+ };
2641
+ }
2642
+ /**
2643
+ * The admission law (issue #121): resolve with the run's first event, or
2644
+ * reject before any Run exists. The first machine frame decides. A run
2645
+ * event is the engine's own admission evidence; an object without a `kind`
2646
+ * is its one pre-run refusal object; anything else proves neither and stays
2647
+ * a protocol fault. The SDK reads which one the engine wrote and judges
2648
+ * nothing itself.
2649
+ */
2650
+ async admission(engine) {
2651
+ const { lines } = engine;
2652
+ const kind = this.kind;
2653
+ const opening = await lines.next();
2654
+ if (opening.done) {
2655
+ const exitCode2 = await engine.closed;
2656
+ const taught = legacyStderrRefusal(engine.diagnostics(), exitCode2);
2657
+ if (taught) throw runRefusalError(kind, taught, exitCode2);
2658
+ throw new NikaProtocolError(kind, quoted(
2659
+ `Engine exited without a machine frame (exit ${exitCode2})`,
2660
+ engine.diagnostics()
2661
+ ));
2662
+ }
2663
+ const line = opening.value;
2664
+ let frame;
2665
+ let unreadable;
2666
+ try {
2667
+ frame = parseMachineObject(line, kind);
2668
+ } catch (cause) {
2669
+ unreadable = cause;
2670
+ }
2671
+ let text = line;
2672
+ let refusal;
2673
+ if (frame) {
2674
+ if (isRunEvent(frame)) return frame;
2675
+ refusal = preRunRefusal(frame, kind);
2676
+ } else {
2677
+ refusal = legacyTeachingLine(line);
2678
+ if (!refusal) {
2679
+ const report = await legacyPrettyReport(
2680
+ line,
2681
+ lines,
2682
+ this.options.machineBufferBytes,
2683
+ kind
2684
+ );
2685
+ text = report?.text ?? line;
2686
+ if (!report?.frame) throw quotedProtocolError(unreadable, text, kind);
2687
+ refusal = preRunRefusal(report.frame, kind);
2688
+ }
2689
+ }
2690
+ if (!refusal) {
2691
+ throw new NikaProtocolError(kind, quoted(
2692
+ "The first machine frame was neither a run event nor a pre-run refusal object",
2693
+ text
2694
+ ));
2695
+ }
2696
+ const extra = await lines.next();
2697
+ if (!extra.done) {
2698
+ throw new NikaProtocolError(kind, quoted(
2699
+ "Engine wrote more machine output after its pre-run refusal",
2700
+ extra.value
2701
+ ));
2702
+ }
2703
+ const exitCode = await engine.closed;
2704
+ if (exitCode === 0) {
2705
+ throw new NikaProtocolError(kind, quoted(
2706
+ "Engine wrote a pre-run refusal but exited 0",
2707
+ refusal.message
2708
+ ));
2709
+ }
2710
+ throw runRefusalError(kind, refusal, exitCode);
2711
+ }
2712
+ processRun(id, engine, first) {
2713
+ const { child, lines, closed } = engine;
2714
+ let lastEvent;
2715
+ let receipt;
2716
+ let outputs;
2717
+ let machineError;
2718
+ let settlement;
2719
+ let streamError;
2720
+ let resolveDrained;
2721
+ const drained = new Promise((resolve) => {
2722
+ resolveDrained = resolve;
2723
+ });
2724
+ const kind = this.kind;
2725
+ const observe = (event) => {
2726
+ lastEvent = event;
2727
+ receipt = eventReceipt(event) ?? receipt;
2728
+ outputs = eventOutputs(event) ?? outputs;
2729
+ const currentSettlement = eventSettlement(event, kind);
2730
+ machineError = currentSettlement ? eventError(event) : eventError(event) ?? machineError;
2731
+ settlement = currentSettlement ?? settlement;
2732
+ return event;
2733
+ };
2734
+ const events = {
2735
+ [Symbol.asyncIterator]: async function* () {
2736
+ try {
2737
+ yield observe(first);
2738
+ for (; ; ) {
2739
+ const next = await lines.next();
2740
+ if (next.done) break;
2741
+ yield observe(machineLine(next.value, kind));
2742
+ }
2743
+ } catch (cause) {
2744
+ streamError = cause instanceof Error ? cause : new Error(String(cause));
2745
+ if (!engine.settled()) child.kill("SIGTERM");
2746
+ throw streamError;
2747
+ } finally {
2748
+ void lines.return(void 0).catch(() => {
2749
+ });
2750
+ resolveDrained();
2751
+ }
2752
+ }
2753
+ };
2754
+ const done = Promise.all([closed, drained]).then(([exitCode]) => {
2755
+ if (streamError) throw streamError;
2756
+ const status = eventStatus(lastEvent) ?? statusForExit(exitCode, lastEvent?.kind);
2757
+ const stderr = engine.diagnostics();
2758
+ return {
2759
+ id,
2760
+ status,
2761
+ transport: this.kind,
2762
+ exitCode,
2763
+ ...outputs ? { outputs } : {},
2764
+ ...receipt ? { receipt } : {},
2765
+ ...machineError ? { error: machineError } : {},
2766
+ ...settlement ? { settlement } : {},
2767
+ ...stderr ? { diagnostics: stderr } : {}
2768
+ };
2769
+ });
2770
+ done.catch(() => {
2771
+ });
2772
+ let cancelPromise;
2773
+ return {
2774
+ id,
2775
+ events,
2776
+ done,
2777
+ status: async () => {
2778
+ throw new NikaCompatibilityError(
2779
+ "runStatus",
2780
+ this.kind,
2781
+ "A direct native process has no independent durable status authority; await run.result()"
2782
+ );
2783
+ },
2784
+ cancel: () => {
2785
+ cancelPromise ??= Promise.resolve(
2786
+ engine.settled() ? {
2787
+ runId: id,
2788
+ accepted: false,
2789
+ status: "already_settled",
2790
+ transport: this.kind
2791
+ } : {
2792
+ runId: id,
2793
+ accepted: child.kill("SIGTERM"),
2794
+ status: "cancellation_requested",
2795
+ transport: this.kind
2796
+ }
2797
+ );
2798
+ return cancelPromise;
2799
+ },
2800
+ cleanup: () => engine.stop()
2801
+ };
2802
+ }
2803
+ capture(args, signal) {
2804
+ return new Promise((resolve, reject) => {
2805
+ if (signal?.aborted) {
2806
+ reject(new NikaTransportError(this.kind, "Operation aborted by caller"));
2807
+ return;
2808
+ }
2809
+ const child = spawn2(this.options.engine.bin, args, {
2810
+ cwd: this.options.cwd,
2811
+ stdio: ["ignore", "pipe", "pipe"],
2812
+ shell: false
2813
+ });
2814
+ let stdout = "";
2815
+ let stderr = "";
2816
+ child.stdout.setEncoding("utf8");
2817
+ child.stderr.setEncoding("utf8");
2818
+ child.stdout.on("data", (chunk) => {
2819
+ stdout = boundedAppend(stdout, chunk, this.options.machineBufferBytes);
2820
+ });
2821
+ child.stderr.on("data", (chunk) => {
2822
+ stderr = boundedAppend(stderr, chunk, this.options.machineBufferBytes);
2823
+ });
2824
+ const abort = () => child.kill("SIGTERM");
2825
+ signal?.addEventListener("abort", abort, { once: true });
2826
+ child.once("error", (cause) => {
2827
+ signal?.removeEventListener("abort", abort);
2828
+ reject(new NikaTransportError(
2829
+ this.kind,
2830
+ `Cannot spawn ${this.options.engine.bin}: ${cause.message}`,
2831
+ { cause }
2832
+ ));
2833
+ });
2834
+ child.once("close", (code) => {
2835
+ signal?.removeEventListener("abort", abort);
2836
+ if (signal?.aborted) {
2837
+ reject(new NikaTransportError(this.kind, "Operation aborted by caller"));
2838
+ return;
2839
+ }
2840
+ resolve({ exitCode: code ?? 3, stdout, stderr });
2841
+ });
2842
+ });
2843
+ }
2844
+ /** The verified engine's identity is kept: its capabilities gate what a run may ask. */
2845
+ ensureReady() {
2846
+ this.ready ??= verifyNikaEngine(this.options.engine);
2847
+ return this.ready;
2848
+ }
2849
+ };
2850
+ var INPUTS_LITERAL = "inputsLiteral";
2851
+ function writeLiteralInputs(stdin, json) {
2852
+ stdin.on("error", () => {
2853
+ });
2854
+ stdin.end(json);
2855
+ }
2856
+ var ENGINE_PREFIX2 = /^nika:\s*/;
2857
+ var DIAGNOSTIC_EXCERPT_LIMIT = 240;
2858
+ var STOP_GRACE_MILLISECONDS = 2e3;
2859
+ function endsWithin(settling, milliseconds) {
2860
+ return new Promise((resolve) => {
2861
+ const timer = setTimeout(() => resolve(false), milliseconds);
2862
+ const ended = () => {
2863
+ clearTimeout(timer);
2864
+ resolve(true);
2865
+ };
2866
+ settling.then(ended, ended);
2867
+ });
2868
+ }
2869
+ async function* machineLines(stdout, limit, kind) {
2870
+ const bounded = (raw) => {
2871
+ if (Buffer.byteLength(raw) > limit) {
2872
+ throw new NikaProtocolError(
2873
+ kind,
2874
+ `Native machine line exceeded ${limit} bytes; machineBufferBytes bounds one machine frame`
2875
+ );
2876
+ }
2877
+ return raw.trim();
2878
+ };
2879
+ let buffer = "";
2880
+ stdout.setEncoding("utf8");
2881
+ for await (const chunk of stdout) {
2882
+ buffer += String(chunk);
2883
+ let newline;
2884
+ while ((newline = buffer.indexOf("\n")) >= 0) {
2885
+ const line = bounded(buffer.slice(0, newline));
2886
+ buffer = buffer.slice(newline + 1);
2887
+ if (line) yield line;
2888
+ }
2889
+ bounded(buffer);
2890
+ }
2891
+ const tail = bounded(buffer);
2892
+ if (tail) yield tail;
2893
+ }
2894
+ function reportObject(text) {
2895
+ const trimmed = text.trim();
2896
+ if (!trimmed) return void 0;
2897
+ let value;
2898
+ try {
2899
+ value = JSON.parse(trimmed);
2900
+ } catch {
2901
+ return void 0;
2902
+ }
2903
+ return machineObject(value);
2904
+ }
2905
+ function stripEnginePrefix(text) {
2906
+ return text.trim().replace(ENGINE_PREFIX2, "");
2907
+ }
2908
+ function diagnosticExcerpt(text, limit = DIAGNOSTIC_EXCERPT_LIMIT) {
2909
+ const single = stripEnginePrefix(text).replace(/\s+/g, " ").trim();
2910
+ if (!single) return void 0;
2911
+ return single.length > limit ? `${single.slice(0, limit)}\u2026` : single;
2912
+ }
2913
+ function machineLine(line, kind) {
2914
+ try {
2915
+ return parseMachineObject(line, kind);
2916
+ } catch (cause) {
2917
+ throw quotedProtocolError(cause, line, kind);
2918
+ }
2919
+ }
2920
+ function quotedProtocolError(cause, text, kind) {
2921
+ const verdict = cause instanceof Error ? cause.message : "Engine machine output was unreadable";
2922
+ return new NikaProtocolError(
2923
+ kind,
2924
+ quoted(verdict, text),
2925
+ { cause: cause instanceof Error ? cause : void 0 }
2926
+ );
2927
+ }
2928
+ function quoted(verdict, text) {
2929
+ const excerpt = diagnosticExcerpt(text);
2930
+ return excerpt ? `${verdict}: ${excerpt}` : verdict;
2931
+ }
2932
+ function runFlags(options, literal) {
2933
+ const flags = [];
2934
+ if (literal) flags.push("--inputs-json", "-");
2935
+ for (const [key, value] of Object.entries(options.vars ?? {})) {
2936
+ flags.push("--var", `${key}=${String(value)}`);
2937
+ }
2938
+ if (options.model) flags.push("--model", options.model);
2939
+ if (options.maxCostUsd !== void 0) flags.push("--max-cost-usd", String(options.maxCostUsd));
2940
+ return flags;
2941
+ }
2942
+ function statusForExit(exitCode, kind) {
2943
+ if (kind === "workflow_completed") return "succeeded";
2944
+ if (kind === "workflow_failed") return "failed";
2945
+ if (kind === "workflow_paused") return "paused";
2946
+ if (kind === "workflow_cancelled") return "cancelled";
2947
+ switch (exitCode) {
2948
+ case 0:
2949
+ return "succeeded";
2950
+ case 4:
2951
+ return "paused";
2952
+ case 130:
2953
+ return "interrupted";
2954
+ default:
2955
+ return "failed";
2956
+ }
2957
+ }
2958
+ function boundedAppend(current, chunk, limit) {
2959
+ const combined = current + chunk;
2960
+ if (Buffer.byteLength(combined) <= limit) return combined;
2961
+ return Buffer.from(combined).subarray(-limit).toString("utf8");
2962
+ }
2963
+ function receiptMismatch(receipt, manifest) {
2964
+ const trace = machineObject(manifest.trace);
2965
+ const seal = machineObject(manifest.seal);
2966
+ const covers = machineObject(seal?.covers);
2967
+ const binding = machineObject(covers?.sdk_receipt);
2968
+ if (trace?.chain !== "intact" || seal?.present !== true || seal.verifies !== true || seal.covers_chain !== true || !binding) return "the evidence manifest has no verified signed SDK receipt binding";
2969
+ const claims = ["receipt_format", "execution_id", "trace_id", "snapshot_digest"];
2970
+ for (const claim of claims) {
2971
+ if (receipt[claim] !== binding[claim]) return `${claim} is not bound to this trace`;
2972
+ }
2973
+ if (receipt.chain_head !== trace.head) return "chain_head is not this journal head";
2974
+ if (receipt.chain_len !== trace.events) return "chain_len is not this journal length";
2975
+ if (receipt.sealed !== true) return "sealed is not true for this verified seal";
2976
+ return void 0;
2977
+ }
2978
+
2979
+ // src/lib/run-events.ts
2980
+ var STARTED = /* @__PURE__ */ new Set(["workflow_started", "execution.started"]);
2981
+ var TASKS = /* @__PURE__ */ new Map([
2982
+ ["task_scheduled", "task.scheduled"],
2983
+ ["task_started", "task.started"],
2984
+ ["task_completed", "task.completed"],
2985
+ ["task_failed", "task.failed"]
2986
+ ]);
2987
+ var SETTLEMENT = /* @__PURE__ */ new Set(["run_settled", "execution.settled"]);
2988
+ var SETTLEMENT_WORDS = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
2989
+ var SETTLED = /* @__PURE__ */ new Map([
2990
+ ["run_settled", SETTLEMENT_WORDS],
2991
+ ["execution.settled", SETTLEMENT_WORDS],
2992
+ ["execution.cancelled", /* @__PURE__ */ new Set(["cancelled"])],
2993
+ ["execution.refused", /* @__PURE__ */ new Set(["failed"])]
2994
+ ]);
2995
+ var INTERRUPTED = /* @__PURE__ */ new Set([
2996
+ "workflow_interrupted",
2997
+ "execution.interrupted",
2998
+ "interrupted"
2999
+ ]);
3000
+ function semanticRunEvent(raw, transport) {
3001
+ const stated = eventStatus(raw);
3002
+ const kind = semanticKind(raw.kind, stated);
3003
+ const named = kind !== "engine.event";
3004
+ const status = named ? stated : void 0;
3005
+ const task = kind.startsWith("task.") ? eventTask(raw) : void 0;
3006
+ const failed = kind === "task.failed" || kind === "run.settled" && status === "failed";
3007
+ const error = failed ? eventError(raw) : void 0;
3008
+ const sequence = transport === "http" && Number.isSafeInteger(raw.sequence) ? raw.sequence : void 0;
3009
+ return Object.freeze({
3010
+ kind,
3011
+ transport,
3012
+ ...status !== void 0 ? { status } : {},
3013
+ ...sequence !== void 0 ? { sequence } : {},
3014
+ ...task !== void 0 ? { task } : {},
3015
+ ...error !== void 0 ? { error } : {},
3016
+ raw
3017
+ });
3018
+ }
3019
+ function semanticKind(kind, status) {
3020
+ if (typeof kind !== "string") return "engine.event";
3021
+ if (STARTED.has(kind)) return "run.started";
3022
+ const task = TASKS.get(kind);
3023
+ if (task !== void 0) return task;
3024
+ if (kind === "run_sealed") return "run.sealed";
3025
+ if (INTERRUPTED.has(kind)) return status === "interrupted" ? "run.interrupted" : "engine.event";
3026
+ if (status === void 0) return "engine.event";
3027
+ if (status === "paused") return SETTLEMENT.has(kind) ? "run.waiting" : "engine.event";
3028
+ return SETTLED.get(kind)?.has(status) === true ? "run.settled" : "engine.event";
3029
+ }
3030
+ function eventTask(raw) {
3031
+ if (typeof raw.task === "string") return raw.task;
3032
+ const fields = Array.isArray(raw.fields) ? raw.fields : [];
3033
+ for (const field of fields) {
3034
+ const row = machineObject(field);
3035
+ if (row?.key === "task" && typeof row.value === "string") return row.value;
3036
+ }
3037
+ return void 0;
3038
+ }
3039
+
3040
+ // src/lib/run-session.ts
3041
+ var RunSession = class {
3042
+ constructor(source, eventBufferSize, transport) {
3043
+ this.source = source;
3044
+ this.eventBufferSize = eventBufferSize;
3045
+ this.transport = transport;
3046
+ this.done = new Promise((resolve, reject) => {
3047
+ this.resolveDone = resolve;
3048
+ this.rejectDone = reject;
3049
+ });
3050
+ this.done.catch(() => {
3051
+ });
3052
+ this.run = Object.freeze({
3053
+ id: source.id,
3054
+ events: (options) => this.semanticEvents(options),
3055
+ result: () => this.done,
3056
+ status: () => this.status(),
3057
+ cancel: () => this.cancel(),
3058
+ done: this.done
3059
+ });
3060
+ void this.observeSettlement();
3061
+ void this.pump();
3062
+ }
3063
+ run;
3064
+ history = [];
3065
+ subscribers = /* @__PURE__ */ new Set();
3066
+ done;
3067
+ resolveDone;
3068
+ rejectDone;
3069
+ terminal = false;
3070
+ cancelPromise;
3071
+ /** Every frame the pump delivered; `history` holds at most the last `eventBufferSize`. */
3072
+ observed = 0;
3073
+ /**
3074
+ * The lifecycle vocabulary over the same bounded view: one history, one
3075
+ * queue, and a pure per-frame projection at the edge. No second event bus.
3076
+ */
3077
+ semanticEvents(options = {}) {
3078
+ const view = this.events(options);
3079
+ const transport = this.transport;
3080
+ const lifecycle = {
3081
+ [Symbol.asyncIterator]: () => lifecycle,
3082
+ next: async () => {
3083
+ const step = await view.next();
3084
+ if (step.done) return { value: void 0, done: true };
3085
+ return { value: semanticRunEvent(step.value, transport), done: false };
3086
+ },
3087
+ return: async () => {
3088
+ await view.return?.();
3089
+ return { value: void 0, done: true };
3090
+ }
3091
+ };
3092
+ return lifecycle;
3093
+ }
3094
+ /** The protocol frames, exactly as the transport delivered them. */
3095
+ events(options = {}) {
3096
+ const requested = options.bufferSize ?? this.eventBufferSize;
3097
+ if (!Number.isInteger(requested) || requested < 1 || requested > this.eventBufferSize) {
3098
+ throw new RangeError(
3099
+ `events bufferSize must be an integer from 1 to ${this.eventBufferSize}`
3100
+ );
3101
+ }
3102
+ if (this.observed > requested) {
3103
+ throw new NikaEventBufferOverflowError(this.source.id, requested, {
3104
+ observed: this.observed,
3105
+ retained: this.history.length
3106
+ });
3107
+ }
3108
+ const subscription = new EventSubscription(
3109
+ this.source.id,
3110
+ requested,
3111
+ this.history.slice(-requested),
3112
+ options.signal,
3113
+ () => this.subscribers.delete(subscription)
3114
+ );
3115
+ if (this.terminal || options.signal?.aborted) subscription.close();
3116
+ else this.subscribers.add(subscription);
3117
+ return subscription;
3118
+ }
3119
+ cancel() {
3120
+ this.cancelPromise ??= this.requestCancel().catch((error) => {
3121
+ this.cancelPromise = void 0;
3122
+ throw error;
3123
+ });
3124
+ return this.cancelPromise;
3125
+ }
3126
+ status() {
3127
+ return this.source.status();
3128
+ }
3129
+ async requestCancel() {
3130
+ const cancellation = await this.source.cancel();
3131
+ if (cancellation.status === "cancellation_requested") return cancellation;
3132
+ const result = await this.source.done;
3133
+ if (!this.terminal && this.subscribers.size === 0) {
3134
+ this.terminal = true;
3135
+ this.resolveDone(result);
3136
+ for (const subscriber of [...this.subscribers]) subscriber.close();
3137
+ this.subscribers.clear();
3138
+ await this.source.cleanup().catch(() => {
3139
+ });
3140
+ }
3141
+ return cancellation;
3142
+ }
3143
+ async observeSettlement() {
3144
+ try {
3145
+ this.resolveDone(await this.source.done);
3146
+ } catch (cause) {
3147
+ const error = cause instanceof Error ? cause : new Error(String(cause));
3148
+ this.rejectDone(error);
3149
+ }
3150
+ }
3151
+ async pump() {
3152
+ try {
3153
+ for await (const event of this.source.events) {
3154
+ this.observed += 1;
3155
+ this.history.push(event);
3156
+ if (this.history.length > this.eventBufferSize) this.history.shift();
3157
+ for (const subscriber of [...this.subscribers]) subscriber.push(event);
3158
+ }
3159
+ this.resolveDone(await this.source.done);
3160
+ } catch (cause) {
3161
+ const error = cause instanceof Error ? cause : new Error(String(cause));
3162
+ this.rejectDone(error);
3163
+ for (const subscriber of [...this.subscribers]) subscriber.fail(error);
3164
+ } finally {
3165
+ this.terminal = true;
3166
+ for (const subscriber of [...this.subscribers]) subscriber.close();
3167
+ this.subscribers.clear();
3168
+ await this.source.cleanup().catch(() => {
3169
+ });
3170
+ }
3171
+ }
3172
+ };
3173
+ var EventSubscription = class {
3174
+ constructor(runId, limit, initial, signal, detach) {
3175
+ this.runId = runId;
3176
+ this.limit = limit;
3177
+ this.detach = detach;
3178
+ this.queue = [...initial];
3179
+ this.signal = signal;
3180
+ this.abort = () => this.close();
3181
+ if (signal?.aborted) this.closed = true;
3182
+ else signal?.addEventListener("abort", this.abort, { once: true });
3183
+ }
3184
+ queue;
3185
+ waiter;
3186
+ closed = false;
3187
+ error;
3188
+ abort;
3189
+ signal;
3190
+ [Symbol.asyncIterator]() {
3191
+ return this;
3192
+ }
3193
+ next() {
3194
+ const event = this.queue.shift();
3195
+ if (event) return Promise.resolve({ value: event, done: false });
3196
+ if (this.error) return Promise.reject(this.error);
3197
+ if (this.closed) return Promise.resolve({ value: void 0, done: true });
3198
+ if (this.waiter) {
3199
+ return Promise.reject(new Error("Concurrent next() calls are not supported"));
3200
+ }
3201
+ return new Promise((resolve, reject) => {
3202
+ this.waiter = { resolve, reject };
3203
+ });
3204
+ }
3205
+ return() {
3206
+ this.close();
3207
+ return Promise.resolve({ value: void 0, done: true });
3208
+ }
3209
+ push(event) {
3210
+ if (this.closed || this.error) return;
3211
+ if (this.waiter) {
3212
+ const waiter = this.waiter;
3213
+ this.waiter = void 0;
3214
+ waiter.resolve({ value: event, done: false });
3215
+ return;
3216
+ }
3217
+ if (this.queue.length >= this.limit) {
3218
+ this.fail(new NikaEventBufferOverflowError(this.runId, this.limit));
3219
+ return;
3220
+ }
3221
+ this.queue.push(event);
3222
+ }
3223
+ close() {
3224
+ if (this.closed) return;
3225
+ this.closed = true;
3226
+ this.signal?.removeEventListener("abort", this.abort);
3227
+ this.detach();
3228
+ if (this.waiter) {
3229
+ const waiter = this.waiter;
3230
+ this.waiter = void 0;
3231
+ waiter.resolve({ value: void 0, done: true });
3232
+ }
3233
+ }
3234
+ fail(error) {
3235
+ if (this.closed || this.error) return;
3236
+ this.error = error;
3237
+ this.queue.length = 0;
3238
+ this.signal?.removeEventListener("abort", this.abort);
3239
+ this.detach();
3240
+ if (this.waiter) {
3241
+ const waiter = this.waiter;
3242
+ this.waiter = void 0;
3243
+ waiter.reject(error);
3244
+ }
3245
+ }
3246
+ };
3247
+
3248
+ // src/results.ts
3249
+ function isNikaRunSucceeded(result) {
3250
+ return result.status === "succeeded";
3251
+ }
3252
+
3253
+ // src/events.ts
3254
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
3255
+ "succeeded",
3256
+ "failed",
3257
+ "interrupted",
3258
+ "cancelled"
3259
+ ]);
3260
+ function isNikaRunSettledEvent(event) {
3261
+ return event.kind === "run_settled" || event.kind === "execution.settled";
3262
+ }
3263
+ function isNikaTerminalEvent(event) {
3264
+ return typeof event.status === "string" && TERMINAL_STATUSES.has(event.status);
3265
+ }
3266
+ function isNikaRunSealedEvent(event) {
3267
+ return event.kind === "run_sealed";
3268
+ }
3269
+
3270
+ // src/index.ts
3271
+ var DEFAULT_EVENT_BUFFER_SIZE = 4096;
3272
+ var DEFAULT_MACHINE_BUFFER_BYTES = 64 * 1024;
3273
+ var DEFAULT_REQUEST_TIMEOUT = 3e4;
3274
+ var Nika = class {
3275
+ transportKind;
3276
+ transport;
3277
+ sessions = /* @__PURE__ */ new WeakMap();
3278
+ eventBufferSize;
3279
+ constructor(config = {}) {
3280
+ this.eventBufferSize = positiveInteger(
3281
+ config.eventBufferSize ?? DEFAULT_EVENT_BUFFER_SIZE,
3282
+ "eventBufferSize"
3283
+ );
3284
+ const machineBufferBytes = positiveInteger(
3285
+ config.machineBufferBytes ?? DEFAULT_MACHINE_BUFFER_BYTES,
3286
+ "machineBufferBytes"
3287
+ );
3288
+ if (config.url !== void 0) {
3289
+ if (typeof config.token !== "string" || config.token.length === 0) {
3290
+ throw new NikaConfigurationError("A non-empty token is required with a Nika URL");
3291
+ }
3292
+ const url = checkedUrl(config.url, config.allowInsecureHttp === true);
3293
+ this.transport = new HttpTransport({
3294
+ url,
3295
+ token: checkedToken(config.token),
3296
+ fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
3297
+ requestTimeout: positiveInteger(
3298
+ config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT,
3299
+ "requestTimeout"
3300
+ ),
3301
+ machineBufferBytes,
3302
+ // Lazy: HTTP observation needs no local engine. Resolution (and its
3303
+ // typed NikaEngineUnavailable refusal) is deferred to caller-owned
3304
+ // source capture in check/run.
3305
+ resolveEngine: () => resolveNikaEngine(config.bin),
3306
+ cwd: config.cwd
3307
+ });
3308
+ } else {
3309
+ const remoteOnly = config;
3310
+ if (remoteOnly.token !== void 0 || remoteOnly.allowInsecureHttp !== void 0 || remoteOnly.requestTimeout !== void 0 || remoteOnly.fetch !== void 0) {
3311
+ throw new NikaConfigurationError("token, allowInsecureHttp, requestTimeout, and fetch require url");
3312
+ }
3313
+ const engine = resolveNikaEngine(config.bin);
3314
+ this.transport = new NativeProcessTransport({
3315
+ engine,
3316
+ cwd: config.cwd,
3317
+ machineBufferBytes
3318
+ });
3319
+ }
3320
+ this.transportKind = this.transport.kind;
3321
+ }
3322
+ check(workflow, options = {}) {
3323
+ return this.transport.check(workflowName(workflow), options);
3324
+ }
3325
+ /**
3326
+ * Resolves with the run's handle once the engine admitted it, and rejects
3327
+ * without one when the engine refused it. The handle owns the lifecycle:
3328
+ * `run.events()`, `run.result()`, `run.status()`, `run.cancel()`.
3329
+ *
3330
+ * `Outputs` is the caller's projection of the engine-emitted outputs map;
3331
+ * the SDK transports outputs without validating their shape.
3332
+ */
3333
+ async run(workflow, options = {}) {
3334
+ const source = await this.transport.startRun(workflowName(workflow), options);
3335
+ return this.own(source);
3336
+ }
3337
+ /**
3338
+ * The one recovery door: reattach this client process to an
3339
+ * already-admitted durable HTTP job and get a full `NikaRun` back. Pass the
3340
+ * last `event.sequence` you fully processed as `lastEventId`. A native
3341
+ * process is process-bound and refuses with a typed compatibility error.
3342
+ */
3343
+ async attachRun(id, options = {}) {
3344
+ const lastEventId = options.lastEventId ?? 0;
3345
+ if (!Number.isSafeInteger(lastEventId) || lastEventId < 0) {
3346
+ throw new RangeError("lastEventId must be a non-negative safe integer");
3347
+ }
3348
+ const source = await this.transport.attachRun(jobId(id), { lastEventId });
3349
+ return this.own(source);
3350
+ }
3351
+ /** List contained workflow names from a resident HTTP authority. */
3352
+ listWorkflows() {
3353
+ return this.transport.listWorkflows();
3354
+ }
3355
+ /** Read path-free metadata for one contained workflow. */
3356
+ workflow(name) {
3357
+ return this.transport.workflow(workflowName(name));
3358
+ }
3359
+ /**
3360
+ * The run's events in the protocol vocabulary of its transport
3361
+ * (`workflow_*` · `task_*` · `run_*` natively, `execution.*` over HTTP).
3362
+ *
3363
+ * @deprecated Use `run.events()`: one lifecycle vocabulary on both
3364
+ * transports, with this same frame kept on `event.raw`. This wrapper is a
3365
+ * compatibility door for one release train, counted from publication: it
3366
+ * ships unchanged in the first published train that carries `run.events()`,
3367
+ * and the earliest train that may remove it is the one after, as announced
3368
+ * in that train's release notes. It accepts only a run this client created.
3369
+ */
3370
+ events(run, options = {}) {
3371
+ return this.session(run).events(options);
3372
+ }
3373
+ /**
3374
+ * @deprecated Use `run.cancel()`; both return the one memoized request.
3375
+ * Kept through the same compatibility window as `events(run)`. It accepts
3376
+ * only a run this client created.
3377
+ */
3378
+ cancel(run) {
3379
+ return this.session(run).cancel();
3380
+ }
3381
+ /**
3382
+ * Read the current durable status without waiting for terminal settlement.
3383
+ *
3384
+ * @deprecated Use `run.status()`. Kept through the same compatibility
3385
+ * window as `events(run)`. It accepts only a run this client created.
3386
+ */
3387
+ status(run) {
3388
+ return this.session(run).status();
3389
+ }
3390
+ schedule(workflow, options) {
3391
+ scheduleId(options?.id);
3392
+ return this.transport.schedule(workflowName(workflow), options);
3393
+ }
3394
+ scheduleStatus(id) {
3395
+ return this.transport.scheduleStatus(scheduleId(id));
3396
+ }
3397
+ traceVerify(receipt, options = {}) {
3398
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
3399
+ throw new TypeError("traceVerify() accepts an engine-issued NikaReceipt object only");
3400
+ }
3401
+ return this.transport.traceVerify(receipt, options);
3402
+ }
3403
+ /** One session per admitted run, owned by this client and by no registry. */
3404
+ own(source) {
3405
+ const session = new RunSession(source, this.eventBufferSize, this.transportKind);
3406
+ this.sessions.set(session.run, session);
3407
+ return session.run;
3408
+ }
3409
+ session(run) {
3410
+ const session = this.sessions.get(run);
3411
+ if (!session) throw new NikaRunOwnershipError();
3412
+ return session;
3413
+ }
3414
+ };
3415
+ function checkedUrl(value, allowInsecureHttp) {
3416
+ let url;
3417
+ try {
3418
+ url = new URL(value);
3419
+ } catch {
3420
+ throw new NikaConfigurationError(`Invalid Nika URL: ${value}`);
3421
+ }
3422
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
3423
+ throw new NikaConfigurationError("Nika URL must use https: or http:");
3424
+ }
3425
+ if (url.protocol === "http:") {
3426
+ if (!allowInsecureHttp) {
3427
+ throw new NikaConfigurationError(
3428
+ "Plain HTTP requires allowInsecureHttp: true; prefer HTTPS for remote engines"
3429
+ );
3430
+ }
3431
+ if (!isLoopbackHost(url.hostname)) {
3432
+ throw new NikaConfigurationError(
3433
+ `Plain HTTP is limited to loopback hosts (localhost, 127.0.0.0/8, [::1]); use HTTPS for ${url.hostname}`
3434
+ );
3435
+ }
3436
+ }
3437
+ if (url.username || url.password || url.search || url.hash) {
3438
+ throw new NikaConfigurationError("Nika URL cannot contain credentials, a query, or a fragment");
3439
+ }
3440
+ return url.toString().replace(/\/$/, "");
3441
+ }
3442
+ var LOOPBACK_IPV4 = /^127(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
3443
+ var IPV4_MAPPED_IPV6 = /^::ffff:([0-9a-f]{1,4}):[0-9a-f]{1,4}$/;
3444
+ function isLoopbackHost(hostname) {
3445
+ const host = hostname.toLowerCase();
3446
+ if (host === "localhost") return true;
3447
+ if (LOOPBACK_IPV4.test(host)) return true;
3448
+ const literal = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
3449
+ if (literal === "::1") return true;
3450
+ const mapped = literal.match(IPV4_MAPPED_IPV6);
3451
+ return mapped !== null && Number.parseInt(mapped[1], 16) >>> 8 === 127;
3452
+ }
3453
+ function checkedToken(value) {
3454
+ const bytes = Buffer.byteLength(value);
3455
+ if (bytes < 32 || bytes > 512 || !/^[\x21-\x7e]+$/.test(value)) {
3456
+ throw new NikaConfigurationError(
3457
+ "Nika bearer token must be 32-512 visible ASCII bytes without whitespace"
3458
+ );
3459
+ }
3460
+ return value;
3461
+ }
3462
+ function positiveInteger(value, name) {
3463
+ if (!Number.isSafeInteger(value) || value < 1) {
3464
+ throw new NikaConfigurationError(`${name} must be a positive integer`);
3465
+ }
3466
+ return value;
3467
+ }
3468
+ function workflowName(value) {
3469
+ if (typeof value !== "string" || value.length === 0) {
3470
+ throw new TypeError("workflow must be a non-empty string");
3471
+ }
3472
+ return value;
3473
+ }
3474
+ function scheduleId(value) {
3475
+ if (typeof value !== "string" || value.length === 0) {
3476
+ throw new TypeError("schedule id must be a non-empty string");
3477
+ }
3478
+ return value;
3479
+ }
3480
+ function jobId(value) {
3481
+ if (typeof value !== "string" || value.length === 0) {
3482
+ throw new TypeError("job id must be a non-empty string");
3483
+ }
3484
+ return value;
3485
+ }
3486
+ export {
3487
+ Nika,
3488
+ NikaCompatibilityError,
3489
+ NikaConfigurationError,
3490
+ NikaEngineUnavailable,
3491
+ NikaError,
3492
+ NikaEventBufferOverflowError,
3493
+ NikaObservationInterrupted,
3494
+ NikaOperationError,
3495
+ NikaProtocolError,
3496
+ NikaRunOwnershipError,
3497
+ NikaTransportError,
3498
+ isNikaRunSealedEvent,
3499
+ isNikaRunSettledEvent,
3500
+ isNikaRunSucceeded,
3501
+ isNikaTerminalEvent
3502
+ };