@supernovae-st/nika 0.118.7 → 0.120.2

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 CHANGED
@@ -72,12 +72,19 @@ var NikaOperationError = class extends NikaError {
72
72
  };
73
73
  var NikaEventBufferOverflowError = class extends NikaError {
74
74
  runId;
75
+ /** The bound that was exceeded: the view's `bufferSize`. */
75
76
  limit;
76
- constructor(runId, limit) {
77
- super(`Event subscriber for run ${runId} exceeded its ${limit}-event buffer`);
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");
78
80
  this.name = "NikaEventBufferOverflowError";
79
81
  this.runId = runId;
80
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
+ }
81
88
  }
82
89
  };
83
90
  var NikaRunOwnershipError = class extends NikaError {
@@ -87,9 +94,6 @@ var NikaRunOwnershipError = class extends NikaError {
87
94
  }
88
95
  };
89
96
 
90
- // src/lib/http-transport.ts
91
- import { randomUUID } from "crypto";
92
-
93
97
  // src/lib/binary/error.ts
94
98
  var NikaEngineUnavailable = class extends NikaError {
95
99
  code = "NIKA_ENGINE_UNAVAILABLE";
@@ -578,6 +582,184 @@ function incompatible2(message, cause) {
578
582
  );
579
583
  }
580
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
+
581
763
  // src/lib/sse/parser.ts
582
764
  var SseParseError = class extends Error {
583
765
  constructor(message, options) {
@@ -735,6 +917,100 @@ async function* decodeSse(stream, limits, signal) {
735
917
  }
736
918
  }
737
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
+
738
1014
  // src/lib/http-transport.ts
739
1015
  var JOB_STATUSES = /* @__PURE__ */ new Set([
740
1016
  "queued",
@@ -751,6 +1027,7 @@ var RETRY_BASE_MILLISECONDS = 100;
751
1027
  var RETRY_MIN_MILLISECONDS = 25;
752
1028
  var RETRY_MAX_MILLISECONDS = 5e3;
753
1029
  var WORKFLOW_REFUSAL_STATUSES = /* @__PURE__ */ new Set([404, 422]);
1030
+ var JOB_INPUTS = "jobInputs";
754
1031
  var HttpTransport = class {
755
1032
  constructor(options) {
756
1033
  this.options = options;
@@ -767,6 +1044,7 @@ var HttpTransport = class {
767
1044
  "nika serve admission has no request envelope for model or nativeStrict overrides"
768
1045
  );
769
1046
  }
1047
+ refuseLegacyContainedName(workflow);
770
1048
  if (isContainedWorkflowName(workflow)) return this.checkByName(workflow, options.signal);
771
1049
  const captured = await this.captureSnapshot(workflow, options.signal, true);
772
1050
  if (captured.bytes === void 0) return captured.report;
@@ -786,19 +1064,35 @@ var HttpTransport = class {
786
1064
  return captured.report;
787
1065
  }
788
1066
  async startRun(workflow, options) {
1067
+ const inputs = literalInputs(options);
789
1068
  if (options.vars && Object.keys(options.vars).length > 0 || options.model !== void 0 || options.maxCostUsd !== void 0) {
790
1069
  throw this.gap(
791
1070
  "runOptions",
792
- "nika serve admission has no request envelope for vars, model, or maxCostUsd"
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"
793
1086
  );
794
1087
  }
795
- const idempotencyKey = options.idempotencyKey ?? randomUUID();
796
1088
  if (Buffer.byteLength(idempotencyKey) < 1 || Buffer.byteLength(idempotencyKey) > 255) {
797
1089
  throw new NikaTransportError(this.kind, "Idempotency-Key must be 1-255 bytes");
798
1090
  }
799
- if (isContainedWorkflowName(workflow)) {
1091
+ if (byName) {
800
1092
  await this.ensureServerIdentity();
801
- const job2 = await this.admitJob(JSON.stringify({ workflow }), idempotencyKey);
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);
802
1096
  return this.httpRun(job2.id, 0, job2);
803
1097
  }
804
1098
  const captured = await this.captureSnapshot(workflow);
@@ -874,6 +1168,7 @@ var HttpTransport = class {
874
1168
  return Object.freeze([...object.workflows]);
875
1169
  }
876
1170
  async workflow(name) {
1171
+ refuseLegacyContainedName(name);
877
1172
  await this.ensureServerIdentity();
878
1173
  const object = await this.json(`/v1/workflows/${workflowPath(name)}`, {
879
1174
  method: "GET",
@@ -885,6 +1180,7 @@ var HttpTransport = class {
885
1180
  return object;
886
1181
  }
887
1182
  async schedule(workflow, options) {
1183
+ refuseLegacyContainedName(workflow);
888
1184
  await this.ensureScheduleCapability();
889
1185
  const path3 = `/v1/schedules/${encodeURIComponent(options.id)}`;
890
1186
  const headers = new Headers({ "Content-Type": "application/json" });
@@ -976,6 +1272,7 @@ var HttpTransport = class {
976
1272
  const outputs = event ? eventOutputs(event) : durable?.outputs;
977
1273
  const error = event ? eventError(event) : durable?.settlement?.error ?? durable?.error;
978
1274
  const settlement = event ? eventSettlement(event, this.kind) : durable?.settlement;
1275
+ const evidence = event ? eventEvidence(event, this.kind) : durable?.evidence;
979
1276
  resolveDone({
980
1277
  id,
981
1278
  status: source.status,
@@ -985,7 +1282,8 @@ var HttpTransport = class {
985
1282
  ...outputs ? { outputs } : {},
986
1283
  ...receipt ? { receipt } : {},
987
1284
  ...error ? { error } : {},
988
- ...settlement ? { settlement } : {}
1285
+ ...settlement ? { settlement } : {},
1286
+ ...evidence ? { evidence } : {}
989
1287
  });
990
1288
  };
991
1289
  if (attachedState && isObservationEnded(attachedState)) settle(attachedState);
@@ -1184,13 +1482,15 @@ var HttpTransport = class {
1184
1482
  if (!event) throw new NikaProtocolError(this.kind, "SSE data was not an object");
1185
1483
  const allowed = /* @__PURE__ */ new Set([
1186
1484
  "sequence",
1485
+ "at",
1187
1486
  "kind",
1188
1487
  "status",
1189
1488
  "code",
1190
1489
  "message",
1191
1490
  "outputs",
1192
1491
  "receipt",
1193
- "settlement"
1492
+ "settlement",
1493
+ "evidence"
1194
1494
  ]);
1195
1495
  if (Object.keys(event).some((key) => !allowed.has(key))) {
1196
1496
  throw new NikaProtocolError(this.kind, "SSE data contained fields outside the public projection");
@@ -1212,6 +1512,10 @@ var HttpTransport = class {
1212
1512
  throw new NikaProtocolError(this.kind, `SSE data.${field} was not a string`);
1213
1513
  }
1214
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");
1215
1519
  if (event.outputs !== void 0 && !machineObject(event.outputs)) {
1216
1520
  throw new NikaProtocolError(this.kind, "SSE data.outputs was not an object");
1217
1521
  }
@@ -1461,6 +1765,21 @@ var HttpTransport = class {
1461
1765
  );
1462
1766
  }
1463
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
+ }
1464
1783
  async captureSnapshot(workflow, signal, teach = false) {
1465
1784
  const identity = await this.ensureLocalEngine();
1466
1785
  const captured = await captureEngine(
@@ -1734,6 +2053,11 @@ var HttpTransport = class {
1734
2053
  }
1735
2054
  }
1736
2055
  };
2056
+ function refuseLegacyContainedName(workflow) {
2057
+ if (isRetiredByNameAttempt(workflow)) {
2058
+ throw new NikaConfigurationError(legacyWorkflowRenameMessage(workflow));
2059
+ }
2060
+ }
1737
2061
  function workflowPath(name) {
1738
2062
  const segments = name.split("/");
1739
2063
  if (name.startsWith("/") || name.includes("\\") || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
@@ -1741,15 +2065,6 @@ function workflowPath(name) {
1741
2065
  }
1742
2066
  return segments.map(encodeURIComponent).join("/");
1743
2067
  }
1744
- function isContainedWorkflowName(value) {
1745
- if (typeof value !== "string" || !value.endsWith(".nika.yaml")) return false;
1746
- try {
1747
- workflowPath(value);
1748
- return true;
1749
- } catch {
1750
- return false;
1751
- }
1752
- }
1753
2068
  function scheduleBody(workflow, options) {
1754
2069
  return {
1755
2070
  workflow,
@@ -1856,7 +2171,8 @@ function durableJob(value, expectedId, transport) {
1856
2171
  "outputs",
1857
2172
  "receipt",
1858
2173
  "error",
1859
- "settlement"
2174
+ "settlement",
2175
+ "evidence"
1860
2176
  ]);
1861
2177
  if (Object.keys(value).some((key) => !allowed.has(key))) {
1862
2178
  throw new NikaProtocolError(transport, "Durable job response contained unknown fields");
@@ -1880,6 +2196,7 @@ function durableJob(value, expectedId, transport) {
1880
2196
  const outputs = value.outputs === void 0 ? void 0 : machineObject(value.outputs);
1881
2197
  const receipt = value.receipt === void 0 ? void 0 : machineObject(value.receipt);
1882
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");
1883
2200
  if (value.outputs !== void 0 && !outputs) {
1884
2201
  throw new NikaProtocolError(transport, "Durable job response outputs were malformed");
1885
2202
  }
@@ -1903,9 +2220,24 @@ function durableJob(value, expectedId, transport) {
1903
2220
  ...outputs ? { outputs } : {},
1904
2221
  ...receipt ? { receipt: Object.freeze(receipt) } : {},
1905
2222
  ...error ? { error } : {},
1906
- ...settlement ? { settlement } : {}
2223
+ ...settlement ? { settlement } : {},
2224
+ ...evidence ? { evidence } : {}
1907
2225
  };
1908
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
+ }
1909
2241
  function assertReceiptIdentity(receipt, expectedJobId, transport, expectedExecutionId, expectedTraceId, expectedSnapshotDigest) {
1910
2242
  const allowed = /* @__PURE__ */ new Set([
1911
2243
  "job_id",
@@ -2033,7 +2365,104 @@ function isCanonicalTimestamp(value) {
2033
2365
 
2034
2366
  // src/lib/native-process-transport.ts
2035
2367
  import { spawn as spawn2 } from "child_process";
2036
- import { randomUUID as randomUUID2 } from "crypto";
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
2037
2466
  var NativeProcessTransport = class {
2038
2467
  constructor(options) {
2039
2468
  this.options = options;
@@ -2058,7 +2487,8 @@ var NativeProcessTransport = class {
2058
2487
  return { ...report, exitCode: captured.exitCode };
2059
2488
  }
2060
2489
  async startRun(workflow, options) {
2061
- await this.ensureReady();
2490
+ const inputs = literalInputs(options);
2491
+ const identity = await this.ensureReady();
2062
2492
  if (options.idempotencyKey !== void 0) {
2063
2493
  throw new NikaCompatibilityError(
2064
2494
  "idempotencyKey",
@@ -2066,13 +2496,26 @@ var NativeProcessTransport = class {
2066
2496
  "idempotencyKey is an HTTP admission option"
2067
2497
  );
2068
2498
  }
2069
- const args = ["run", workflow, "--json", ...runFlags(options)];
2070
- const child = spawn2(this.options.engine.bin, args, {
2071
- cwd: this.options.cwd,
2072
- stdio: ["ignore", "pipe", "pipe"],
2073
- shell: false
2074
- });
2075
- return this.processRun(randomUUID2(), child);
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);
2076
2519
  }
2077
2520
  async attachRun(_id, _options) {
2078
2521
  throw new NikaCompatibilityError(
@@ -2155,19 +2598,8 @@ RECEIPT MISMATCH: ${mismatch}
2155
2598
  `)
2156
2599
  };
2157
2600
  }
2158
- processRun(id, child) {
2601
+ nativeProcess(child) {
2159
2602
  let settled = false;
2160
- let lastEvent;
2161
- let receipt;
2162
- let outputs;
2163
- let machineError;
2164
- let settlement;
2165
- let streamError;
2166
- let refusal;
2167
- let resolveDrained;
2168
- const drained = new Promise((resolve) => {
2169
- resolveDrained = resolve;
2170
- });
2171
2603
  let stderr = "";
2172
2604
  child.stderr.setEncoding("utf8");
2173
2605
  child.stderr.on("data", (chunk) => {
@@ -2175,94 +2607,154 @@ RECEIPT MISMATCH: ${mismatch}
2175
2607
  });
2176
2608
  const closed = new Promise((resolve, reject) => {
2177
2609
  child.once("error", (cause) => {
2178
- streamError = new NikaTransportError(
2610
+ reject(new NikaTransportError(
2179
2611
  this.kind,
2180
2612
  `Cannot spawn ${this.options.engine.bin}: ${cause.message}`,
2181
2613
  { cause }
2182
- );
2183
- reject(streamError);
2614
+ ));
2184
2615
  });
2185
2616
  child.once("close", (code, signal) => {
2186
2617
  settled = true;
2187
- if (code === null && signal && !streamError) resolve(130);
2188
- else resolve(code ?? 3);
2618
+ resolve(code === null && signal ? 130 : code ?? 3);
2189
2619
  });
2190
2620
  });
2191
2621
  closed.catch(() => {
2192
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;
2193
2652
  const kind = this.kind;
2194
- const machineBufferBytes = this.options.machineBufferBytes;
2195
- const lineEvent = (line) => {
2196
- const refused = engineRefusal(line);
2197
- if (refused) {
2198
- refusal ??= refused;
2199
- return void 0;
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);
2200
2688
  }
2201
- return machineLine(line, kind);
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;
2202
2733
  };
2203
2734
  const events = {
2204
2735
  [Symbol.asyncIterator]: async function* () {
2205
- let buffer = "";
2206
- child.stdout.setEncoding("utf8");
2207
2736
  try {
2208
- for await (const chunk of child.stdout) {
2209
- buffer += String(chunk);
2210
- let newline;
2211
- while ((newline = buffer.indexOf("\n")) >= 0) {
2212
- const line = buffer.slice(0, newline).trim();
2213
- buffer = buffer.slice(newline + 1);
2214
- if (!line) continue;
2215
- const event = lineEvent(line);
2216
- if (!event) continue;
2217
- lastEvent = event;
2218
- receipt = eventReceipt(event) ?? receipt;
2219
- outputs = eventOutputs(event) ?? outputs;
2220
- const currentSettlement = eventSettlement(event, kind);
2221
- machineError = currentSettlement ? eventError(event) : eventError(event) ?? machineError;
2222
- settlement = currentSettlement ?? settlement;
2223
- yield event;
2224
- }
2225
- if (Buffer.byteLength(buffer) > machineBufferBytes) {
2226
- throw new NikaProtocolError(
2227
- kind,
2228
- `Native machine line exceeded ${machineBufferBytes} bytes`
2229
- );
2230
- }
2231
- }
2232
- const tail = buffer.trim();
2233
- if (tail) {
2234
- const event = lineEvent(tail);
2235
- if (event) {
2236
- lastEvent = event;
2237
- receipt = eventReceipt(event) ?? receipt;
2238
- outputs = eventOutputs(event) ?? outputs;
2239
- const currentSettlement = eventSettlement(event, kind);
2240
- machineError = currentSettlement ? eventError(event) : eventError(event) ?? machineError;
2241
- settlement = currentSettlement ?? settlement;
2242
- yield event;
2243
- }
2737
+ yield observe(first);
2738
+ for (; ; ) {
2739
+ const next = await lines.next();
2740
+ if (next.done) break;
2741
+ yield observe(machineLine(next.value, kind));
2244
2742
  }
2245
2743
  } catch (cause) {
2246
2744
  streamError = cause instanceof Error ? cause : new Error(String(cause));
2247
- if (!settled) child.kill("SIGTERM");
2745
+ if (!engine.settled()) child.kill("SIGTERM");
2248
2746
  throw streamError;
2249
2747
  } finally {
2748
+ void lines.return(void 0).catch(() => {
2749
+ });
2250
2750
  resolveDrained();
2251
2751
  }
2252
2752
  }
2253
2753
  };
2254
2754
  const done = Promise.all([closed, drained]).then(([exitCode]) => {
2255
2755
  if (streamError) throw streamError;
2256
- if (refusal) {
2257
- throw new NikaOperationError(
2258
- "run",
2259
- this.kind,
2260
- refusal.code,
2261
- refusal.line,
2262
- { status: exitCode }
2263
- );
2264
- }
2265
2756
  const status = eventStatus(lastEvent) ?? statusForExit(exitCode, lastEvent?.kind);
2757
+ const stderr = engine.diagnostics();
2266
2758
  return {
2267
2759
  id,
2268
2760
  status,
@@ -2286,12 +2778,12 @@ RECEIPT MISMATCH: ${mismatch}
2286
2778
  throw new NikaCompatibilityError(
2287
2779
  "runStatus",
2288
2780
  this.kind,
2289
- "A direct native process has no independent durable status authority; await run.done"
2781
+ "A direct native process has no independent durable status authority; await run.result()"
2290
2782
  );
2291
2783
  },
2292
2784
  cancel: () => {
2293
2785
  cancelPromise ??= Promise.resolve(
2294
- settled ? {
2786
+ engine.settled() ? {
2295
2787
  runId: id,
2296
2788
  accepted: false,
2297
2789
  status: "already_settled",
@@ -2305,11 +2797,7 @@ RECEIPT MISMATCH: ${mismatch}
2305
2797
  );
2306
2798
  return cancelPromise;
2307
2799
  },
2308
- cleanup: async () => {
2309
- if (!settled) child.kill("SIGTERM");
2310
- await closed.catch(() => {
2311
- });
2312
- }
2800
+ cleanup: () => engine.stop()
2313
2801
  };
2314
2802
  }
2315
2803
  capture(args, signal) {
@@ -2353,14 +2841,56 @@ RECEIPT MISMATCH: ${mismatch}
2353
2841
  });
2354
2842
  });
2355
2843
  }
2844
+ /** The verified engine's identity is kept: its capabilities gate what a run may ask. */
2356
2845
  ensureReady() {
2357
- this.ready ??= verifyNikaEngine(this.options.engine).then(() => void 0);
2846
+ this.ready ??= verifyNikaEngine(this.options.engine);
2358
2847
  return this.ready;
2359
2848
  }
2360
2849
  };
2361
- var ENGINE_PREFIX = /^nika:\s*/;
2362
- var REFUSAL_CODE = /^(NIKA-[A-Z0-9-]+)\b/;
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*/;
2363
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
+ }
2364
2894
  function reportObject(text) {
2365
2895
  const trimmed = text.trim();
2366
2896
  if (!trimmed) return void 0;
@@ -2373,32 +2903,35 @@ function reportObject(text) {
2373
2903
  return machineObject(value);
2374
2904
  }
2375
2905
  function stripEnginePrefix(text) {
2376
- return text.trim().replace(ENGINE_PREFIX, "");
2906
+ return text.trim().replace(ENGINE_PREFIX2, "");
2377
2907
  }
2378
2908
  function diagnosticExcerpt(text, limit = DIAGNOSTIC_EXCERPT_LIMIT) {
2379
2909
  const single = stripEnginePrefix(text).replace(/\s+/g, " ").trim();
2380
2910
  if (!single) return void 0;
2381
2911
  return single.length > limit ? `${single.slice(0, limit)}\u2026` : single;
2382
2912
  }
2383
- function engineRefusal(line) {
2384
- const code = REFUSAL_CODE.exec(line)?.[1];
2385
- return code ? { code, line } : void 0;
2386
- }
2387
2913
  function machineLine(line, kind) {
2388
2914
  try {
2389
2915
  return parseMachineObject(line, kind);
2390
2916
  } catch (cause) {
2391
- const verdict = cause instanceof Error ? cause.message : "Engine machine output was unreadable";
2392
- const excerpt = diagnosticExcerpt(line);
2393
- throw new NikaProtocolError(
2394
- kind,
2395
- excerpt ? `${verdict}: ${excerpt}` : verdict,
2396
- { cause: cause instanceof Error ? cause : void 0 }
2397
- );
2917
+ throw quotedProtocolError(cause, line, kind);
2398
2918
  }
2399
2919
  }
2400
- function runFlags(options) {
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) {
2401
2933
  const flags = [];
2934
+ if (literal) flags.push("--inputs-json", "-");
2402
2935
  for (const [key, value] of Object.entries(options.vars ?? {})) {
2403
2936
  flags.push("--var", `${key}=${String(value)}`);
2404
2937
  }
@@ -2443,18 +2976,87 @@ function receiptMismatch(receipt, manifest) {
2443
2976
  return void 0;
2444
2977
  }
2445
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
+
2446
3040
  // src/lib/run-session.ts
2447
3041
  var RunSession = class {
2448
- constructor(source, eventBufferSize) {
3042
+ constructor(source, eventBufferSize, transport) {
2449
3043
  this.source = source;
2450
3044
  this.eventBufferSize = eventBufferSize;
3045
+ this.transport = transport;
2451
3046
  this.done = new Promise((resolve, reject) => {
2452
3047
  this.resolveDone = resolve;
2453
3048
  this.rejectDone = reject;
2454
3049
  });
2455
3050
  this.done.catch(() => {
2456
3051
  });
2457
- this.run = Object.freeze({ id: source.id, done: this.done });
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
+ });
2458
3060
  void this.observeSettlement();
2459
3061
  void this.pump();
2460
3062
  }
@@ -2466,7 +3068,30 @@ var RunSession = class {
2466
3068
  rejectDone;
2467
3069
  terminal = false;
2468
3070
  cancelPromise;
2469
- historyOverflowed = false;
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. */
2470
3095
  events(options = {}) {
2471
3096
  const requested = options.bufferSize ?? this.eventBufferSize;
2472
3097
  if (!Number.isInteger(requested) || requested < 1 || requested > this.eventBufferSize) {
@@ -2474,8 +3099,11 @@ var RunSession = class {
2474
3099
  `events bufferSize must be an integer from 1 to ${this.eventBufferSize}`
2475
3100
  );
2476
3101
  }
2477
- if (this.historyOverflowed || this.history.length > requested) {
2478
- throw new NikaEventBufferOverflowError(this.source.id, requested);
3102
+ if (this.observed > requested) {
3103
+ throw new NikaEventBufferOverflowError(this.source.id, requested, {
3104
+ observed: this.observed,
3105
+ retained: this.history.length
3106
+ });
2479
3107
  }
2480
3108
  const subscription = new EventSubscription(
2481
3109
  this.source.id,
@@ -2523,11 +3151,9 @@ var RunSession = class {
2523
3151
  async pump() {
2524
3152
  try {
2525
3153
  for await (const event of this.source.events) {
3154
+ this.observed += 1;
2526
3155
  this.history.push(event);
2527
- if (this.history.length > this.eventBufferSize) {
2528
- this.history.shift();
2529
- this.historyOverflowed = true;
2530
- }
3156
+ if (this.history.length > this.eventBufferSize) this.history.shift();
2531
3157
  for (const subscriber of [...this.subscribers]) subscriber.push(event);
2532
3158
  }
2533
3159
  this.resolveDone(await this.source.done);
@@ -2619,6 +3245,11 @@ var EventSubscription = class {
2619
3245
  }
2620
3246
  };
2621
3247
 
3248
+ // src/results.ts
3249
+ function isNikaRunSucceeded(result) {
3250
+ return result.status === "succeeded";
3251
+ }
3252
+
2622
3253
  // src/events.ts
2623
3254
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
2624
3255
  "succeeded",
@@ -2637,7 +3268,7 @@ function isNikaRunSealedEvent(event) {
2637
3268
  }
2638
3269
 
2639
3270
  // src/index.ts
2640
- var DEFAULT_EVENT_BUFFER_SIZE = 256;
3271
+ var DEFAULT_EVENT_BUFFER_SIZE = 4096;
2641
3272
  var DEFAULT_MACHINE_BUFFER_BYTES = 64 * 1024;
2642
3273
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
2643
3274
  var Nika = class {
@@ -2692,25 +3323,30 @@ var Nika = class {
2692
3323
  return this.transport.check(workflowName(workflow), options);
2693
3324
  }
2694
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
+ *
2695
3330
  * `Outputs` is the caller's projection of the engine-emitted outputs map;
2696
3331
  * the SDK transports outputs without validating their shape.
2697
3332
  */
2698
3333
  async run(workflow, options = {}) {
2699
3334
  const source = await this.transport.startRun(workflowName(workflow), options);
2700
- const session = new RunSession(source, this.eventBufferSize);
2701
- this.sessions.set(session.run, session);
2702
- return session.run;
3335
+ return this.own(source);
2703
3336
  }
2704
- /** Reattach this client process to an already-admitted durable HTTP job. */
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
+ */
2705
3343
  async attachRun(id, options = {}) {
2706
3344
  const lastEventId = options.lastEventId ?? 0;
2707
3345
  if (!Number.isSafeInteger(lastEventId) || lastEventId < 0) {
2708
3346
  throw new RangeError("lastEventId must be a non-negative safe integer");
2709
3347
  }
2710
3348
  const source = await this.transport.attachRun(jobId(id), { lastEventId });
2711
- const session = new RunSession(source, this.eventBufferSize);
2712
- this.sessions.set(session.run, session);
2713
- return session.run;
3349
+ return this.own(source);
2714
3350
  }
2715
3351
  /** List contained workflow names from a resident HTTP authority. */
2716
3352
  listWorkflows() {
@@ -2720,13 +3356,34 @@ var Nika = class {
2720
3356
  workflow(name) {
2721
3357
  return this.transport.workflow(workflowName(name));
2722
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
+ */
2723
3370
  events(run, options = {}) {
2724
3371
  return this.session(run).events(options);
2725
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
+ */
2726
3378
  cancel(run) {
2727
3379
  return this.session(run).cancel();
2728
3380
  }
2729
- /** Read the current durable status without waiting for terminal settlement. */
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
+ */
2730
3387
  status(run) {
2731
3388
  return this.session(run).status();
2732
3389
  }
@@ -2743,6 +3400,12 @@ var Nika = class {
2743
3400
  }
2744
3401
  return this.transport.traceVerify(receipt, options);
2745
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
+ }
2746
3409
  session(run) {
2747
3410
  const session = this.sessions.get(run);
2748
3411
  if (!session) throw new NikaRunOwnershipError();
@@ -2834,5 +3497,6 @@ export {
2834
3497
  NikaTransportError,
2835
3498
  isNikaRunSealedEvent,
2836
3499
  isNikaRunSettledEvent,
3500
+ isNikaRunSucceeded,
2837
3501
  isNikaTerminalEvent
2838
3502
  };