@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.cjs CHANGED
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  NikaTransportError: () => NikaTransportError,
44
44
  isNikaRunSealedEvent: () => isNikaRunSealedEvent,
45
45
  isNikaRunSettledEvent: () => isNikaRunSettledEvent,
46
+ isNikaRunSucceeded: () => isNikaRunSucceeded,
46
47
  isNikaTerminalEvent: () => isNikaTerminalEvent
47
48
  });
48
49
  module.exports = __toCommonJS(index_exports);
@@ -121,12 +122,19 @@ var NikaOperationError = class extends NikaError {
121
122
  };
122
123
  var NikaEventBufferOverflowError = class extends NikaError {
123
124
  runId;
125
+ /** The bound that was exceeded: the view's `bufferSize`. */
124
126
  limit;
125
- constructor(runId, limit) {
126
- super(`Event subscriber for run ${runId} exceeded its ${limit}-event buffer`);
127
+ reason;
128
+ constructor(runId, limit, replay) {
129
+ 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");
127
130
  this.name = "NikaEventBufferOverflowError";
128
131
  this.runId = runId;
129
132
  this.limit = limit;
133
+ this.reason = replay === void 0 ? "live_backpressure" : "replay_truncated";
134
+ if (replay !== void 0) {
135
+ this.observed = replay.observed;
136
+ this.retained = replay.retained;
137
+ }
130
138
  }
131
139
  };
132
140
  var NikaRunOwnershipError = class extends NikaError {
@@ -136,9 +144,6 @@ var NikaRunOwnershipError = class extends NikaError {
136
144
  }
137
145
  };
138
146
 
139
- // src/lib/http-transport.ts
140
- var import_node_crypto2 = require("crypto");
141
-
142
147
  // src/lib/binary/error.ts
143
148
  var NikaEngineUnavailable = class extends NikaError {
144
149
  code = "NIKA_ENGINE_UNAVAILABLE";
@@ -628,6 +633,184 @@ function incompatible2(message, cause) {
628
633
  );
629
634
  }
630
635
 
636
+ // src/lib/literal-inputs.ts
637
+ var import_node_util = require("util");
638
+ var LITERAL_INPUTS_MAX_BYTES = 1024 * 1024;
639
+ function literalInputs(options) {
640
+ if (options.inputs === void 0) return void 0;
641
+ if (options.vars !== void 0) {
642
+ throw new NikaConfigurationError(
643
+ "run({ inputs, vars }): inputs and the deprecated vars alias cannot be combined; move every value into inputs"
644
+ );
645
+ }
646
+ return encodeLiteralInputs(options.inputs);
647
+ }
648
+ var STRICT_JSON = "inputs must be strict JSON (null, booleans, finite numbers, strings, plain arrays and plain objects)";
649
+ var NATIVE_OBJECT = Function.prototype.toString.call(Object);
650
+ var NATIVE_ARRAY = Function.prototype.toString.call(Array);
651
+ var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
652
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
653
+ var PATH_LIMIT = 240;
654
+ function encodeLiteralInputs(inputs) {
655
+ if (containerKind(inputs) !== "object") {
656
+ throw new NikaConfigurationError(
657
+ `run({ inputs }): inputs must be a plain object mapping declared workflow input names to strict JSON values; received ${describe(inputs)}`
658
+ );
659
+ }
660
+ const parts = [];
661
+ let bytes = 0;
662
+ const write = (chunk) => {
663
+ bytes += Buffer.byteLength(chunk);
664
+ if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
665
+ parts.push(chunk);
666
+ };
667
+ const stack = [];
668
+ const open = /* @__PURE__ */ new Set();
669
+ const refuse = (segment, problem) => {
670
+ const path3 = stack.map((frame) => frame.segment).join("") + segment;
671
+ const shown = path3.length > PATH_LIMIT ? `${path3.slice(0, PATH_LIMIT / 2)}\u2026${path3.slice(-PATH_LIMIT / 2)}` : path3;
672
+ return new NikaConfigurationError(`run({ inputs }): ${shown} ${problem}; ${STRICT_JSON}`);
673
+ };
674
+ const enter = (value, segment, kind) => {
675
+ open.add(value);
676
+ if (kind === "array") {
677
+ write("[");
678
+ stack.push({ value, segment, length: value.length, index: 0 });
679
+ } else {
680
+ write("{");
681
+ const keys = Reflect.ownKeys(value);
682
+ stack.push({ value, segment, keys, length: keys.length, index: 0 });
683
+ }
684
+ };
685
+ enter(inputs, "inputs", "object");
686
+ for (let frame = stack.at(-1); frame; frame = stack.at(-1)) {
687
+ if (frame.index === frame.length) {
688
+ if (!frame.keys) {
689
+ const named = Reflect.ownKeys(frame.value).find((key2) => !isArrayMember(key2, frame.length));
690
+ if (named !== void 0) {
691
+ stack.pop();
692
+ 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");
693
+ }
694
+ }
695
+ write(frame.keys ? "}" : "]");
696
+ open.delete(frame.value);
697
+ stack.pop();
698
+ continue;
699
+ }
700
+ const position = frame.index;
701
+ frame.index += 1;
702
+ if (position > 0) write(",");
703
+ const key = frame.keys ? frame.keys[position] : String(position);
704
+ const segment = frame.keys ? keySegment(key) : `[${position}]`;
705
+ if (typeof key === "symbol") throw refuse(segment, "is a symbol key, which JSON cannot carry");
706
+ const member = Object.getOwnPropertyDescriptor(frame.value, key);
707
+ if (!member) {
708
+ throw refuse(segment, frame.keys ? "is not an own data property" : "is an array hole, which JSON cannot carry");
709
+ }
710
+ if ("get" in member || "set" in member) {
711
+ throw refuse(segment, "is an accessor property, which JSON cannot carry");
712
+ }
713
+ if (frame.keys) {
714
+ if (!member.enumerable) {
715
+ throw refuse(segment, "is a non-enumerable property, which JSON cannot carry");
716
+ }
717
+ if (LONE_SURROGATE.test(key)) {
718
+ throw refuse(segment, "is a key with a lone surrogate, which is not valid Unicode");
719
+ }
720
+ write(`${JSON.stringify(key)}:`);
721
+ }
722
+ const value = member.value;
723
+ switch (typeof value) {
724
+ case "string":
725
+ if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
726
+ if (LONE_SURROGATE.test(value)) {
727
+ throw refuse(segment, "contains a lone surrogate, which is not valid Unicode");
728
+ }
729
+ write(JSON.stringify(value));
730
+ break;
731
+ case "number":
732
+ if (!Number.isFinite(value)) {
733
+ throw refuse(segment, "is a non-finite number, which JSON cannot carry");
734
+ }
735
+ write(JSON.stringify(value));
736
+ break;
737
+ case "boolean":
738
+ write(value ? "true" : "false");
739
+ break;
740
+ case "object": {
741
+ if (value === null) {
742
+ write("null");
743
+ break;
744
+ }
745
+ if (open.has(value)) throw refuse(segment, "is a cycle, which JSON cannot carry");
746
+ const kind = containerKind(value);
747
+ if (!kind) throw refuse(segment, refusedObject(value));
748
+ enter(value, segment, kind);
749
+ break;
750
+ }
751
+ default:
752
+ throw refuse(segment, `is ${typeof value === "undefined" ? "" : "a "}${typeof value}, which JSON cannot carry`);
753
+ }
754
+ }
755
+ return { json: parts.join(""), bytes };
756
+ }
757
+ function tooLarge() {
758
+ return new NikaConfigurationError(
759
+ `run({ inputs }): the serialized inputs map exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB), the bound both transports share with the native engine`
760
+ );
761
+ }
762
+ function containerKind(value) {
763
+ if (value === null || typeof value !== "object" || import_node_util.types.isProxy(value)) return void 0;
764
+ const prototype = Object.getPrototypeOf(value);
765
+ if (Array.isArray(value)) return isRealmPrototype(prototype, NATIVE_ARRAY) ? "array" : void 0;
766
+ if (prototype === null) return "object";
767
+ return isRealmPrototype(prototype, NATIVE_OBJECT) ? "object" : void 0;
768
+ }
769
+ function isRealmPrototype(prototype, source) {
770
+ if (prototype === null || import_node_util.types.isProxy(prototype)) return false;
771
+ const constructor = ownValue(prototype, "constructor");
772
+ if (typeof constructor !== "function" || import_node_util.types.isProxy(constructor)) return false;
773
+ let native;
774
+ try {
775
+ native = Function.prototype.toString.call(constructor) === source;
776
+ } catch {
777
+ return false;
778
+ }
779
+ return native && ownValue(constructor, "prototype") === prototype;
780
+ }
781
+ function ownValue(target, key) {
782
+ return Object.getOwnPropertyDescriptor(target, key)?.value;
783
+ }
784
+ function isArrayMember(key, length) {
785
+ if (typeof key === "symbol") return false;
786
+ if (key === "length") return true;
787
+ const index = Number(key);
788
+ return Number.isInteger(index) && index >= 0 && index < length && String(index) === key;
789
+ }
790
+ function keySegment(key) {
791
+ if (typeof key === "symbol") return `[${String(key)}]`;
792
+ return IDENTIFIER.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
793
+ }
794
+ function describe(value) {
795
+ if (value === null) return "null";
796
+ if (typeof value === "undefined") return "undefined";
797
+ if (typeof value !== "object") return `a ${typeof value}`;
798
+ if (import_node_util.types.isProxy(value)) return "a Proxy";
799
+ if (Array.isArray(value)) return "an array";
800
+ return refusedObject(value).replace(/^is /, "").replace(/, not a plain object or array$/, "");
801
+ }
802
+ function refusedObject(value) {
803
+ if (import_node_util.types.isProxy(value)) return "is a Proxy, which cannot be read without running its traps";
804
+ const custom = "is an object with a custom prototype, not a plain object or array";
805
+ const prototype = Object.getPrototypeOf(value);
806
+ if (prototype === null || import_node_util.types.isProxy(prototype)) return custom;
807
+ const constructor = ownValue(prototype, "constructor");
808
+ if (typeof constructor !== "function" || import_node_util.types.isProxy(constructor)) return custom;
809
+ if (ownValue(constructor, "prototype") !== prototype) return custom;
810
+ const name = ownValue(constructor, "name");
811
+ return typeof name === "string" && IDENTIFIER.test(name) && name.length <= 64 ? `is a ${name} instance, not a plain object or array` : custom;
812
+ }
813
+
631
814
  // src/lib/sse/parser.ts
632
815
  var SseParseError = class extends Error {
633
816
  constructor(message, options) {
@@ -785,6 +968,100 @@ async function* decodeSse(stream, limits, signal) {
785
968
  }
786
969
  }
787
970
 
971
+ // src/lib/workflow-name.ts
972
+ var WORKFLOW_SUFFIX = ".nika";
973
+ var LEGACY_WORKFLOW_SUFFIXES = [".nika.yaml", ".nika.yml"];
974
+ var CONTAINED_CHAR = /[A-Za-z0-9/._-]/;
975
+ function workflowBasename(path3) {
976
+ const parts = path3.split(/[\\/]/);
977
+ return parts[parts.length - 1] ?? "";
978
+ }
979
+ function hasControlChar(value) {
980
+ for (const ch of value) {
981
+ const c = ch.codePointAt(0) ?? 0;
982
+ if (c <= 31 || c === 127) {
983
+ return true;
984
+ }
985
+ }
986
+ return false;
987
+ }
988
+ function isLegacyWorkflowPath(path3) {
989
+ if (typeof path3 !== "string" || path3.length === 0) {
990
+ return false;
991
+ }
992
+ if (path3.endsWith("/") || path3.endsWith("\\")) {
993
+ return false;
994
+ }
995
+ const base = workflowBasename(path3);
996
+ return LEGACY_WORKFLOW_SUFFIXES.some((suffix) => base.endsWith(suffix));
997
+ }
998
+ function isCanonicalWorkflowFilename(basename) {
999
+ if (typeof basename !== "string" || basename.length === 0) {
1000
+ return false;
1001
+ }
1002
+ if (hasControlChar(basename)) {
1003
+ return false;
1004
+ }
1005
+ if (basename.includes("/") || basename.includes("\\")) {
1006
+ return false;
1007
+ }
1008
+ if (LEGACY_WORKFLOW_SUFFIXES.some((suffix) => basename.endsWith(suffix))) {
1009
+ return false;
1010
+ }
1011
+ if (!basename.endsWith(WORKFLOW_SUFFIX)) {
1012
+ return false;
1013
+ }
1014
+ return basename.length > WORKFLOW_SUFFIX.length;
1015
+ }
1016
+ function isCanonicalWorkflowPath(path3) {
1017
+ if (typeof path3 !== "string" || path3.length === 0) {
1018
+ return false;
1019
+ }
1020
+ if (hasControlChar(path3)) {
1021
+ return false;
1022
+ }
1023
+ if (path3.endsWith("/") || path3.endsWith("\\")) {
1024
+ return false;
1025
+ }
1026
+ return isCanonicalWorkflowFilename(workflowBasename(path3));
1027
+ }
1028
+ function containedRelativeShape(value) {
1029
+ if (value.includes("\\") || value.startsWith("/")) {
1030
+ return false;
1031
+ }
1032
+ for (const ch of value) {
1033
+ if (!CONTAINED_CHAR.test(ch)) {
1034
+ return false;
1035
+ }
1036
+ }
1037
+ const segments = value.split("/");
1038
+ return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
1039
+ }
1040
+ function isContainedWorkflowName(value) {
1041
+ return typeof value === "string" && isCanonicalWorkflowPath(value) && containedRelativeShape(value);
1042
+ }
1043
+ function isRetiredByNameAttempt(value) {
1044
+ if (typeof value !== "string" || value.length === 0) {
1045
+ return false;
1046
+ }
1047
+ if (value.startsWith("/") || value.startsWith("./") || value.includes("\\")) {
1048
+ return false;
1049
+ }
1050
+ return isLegacyWorkflowPath(value);
1051
+ }
1052
+ function renameLegacyWorkflow(name) {
1053
+ if (name.endsWith(".nika.yaml")) {
1054
+ return `${name.slice(0, -".nika.yaml".length)}${WORKFLOW_SUFFIX}`;
1055
+ }
1056
+ if (name.endsWith(".nika.yml")) {
1057
+ return `${name.slice(0, -".nika.yml".length)}${WORKFLOW_SUFFIX}`;
1058
+ }
1059
+ return name;
1060
+ }
1061
+ function legacyWorkflowRenameMessage(name) {
1062
+ return `workflow names use ${WORKFLOW_SUFFIX}; rename ${name} to ${renameLegacyWorkflow(name)}`;
1063
+ }
1064
+
788
1065
  // src/lib/http-transport.ts
789
1066
  var JOB_STATUSES = /* @__PURE__ */ new Set([
790
1067
  "queued",
@@ -801,6 +1078,7 @@ var RETRY_BASE_MILLISECONDS = 100;
801
1078
  var RETRY_MIN_MILLISECONDS = 25;
802
1079
  var RETRY_MAX_MILLISECONDS = 5e3;
803
1080
  var WORKFLOW_REFUSAL_STATUSES = /* @__PURE__ */ new Set([404, 422]);
1081
+ var JOB_INPUTS = "jobInputs";
804
1082
  var HttpTransport = class {
805
1083
  constructor(options) {
806
1084
  this.options = options;
@@ -817,6 +1095,7 @@ var HttpTransport = class {
817
1095
  "nika serve admission has no request envelope for model or nativeStrict overrides"
818
1096
  );
819
1097
  }
1098
+ refuseLegacyContainedName(workflow);
820
1099
  if (isContainedWorkflowName(workflow)) return this.checkByName(workflow, options.signal);
821
1100
  const captured = await this.captureSnapshot(workflow, options.signal, true);
822
1101
  if (captured.bytes === void 0) return captured.report;
@@ -836,19 +1115,35 @@ var HttpTransport = class {
836
1115
  return captured.report;
837
1116
  }
838
1117
  async startRun(workflow, options) {
1118
+ const inputs = literalInputs(options);
839
1119
  if (options.vars && Object.keys(options.vars).length > 0 || options.model !== void 0 || options.maxCostUsd !== void 0) {
840
1120
  throw this.gap(
841
1121
  "runOptions",
842
- "nika serve admission has no request envelope for vars, model, or maxCostUsd"
1122
+ "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"
1123
+ );
1124
+ }
1125
+ refuseLegacyContainedName(workflow);
1126
+ const byName = isContainedWorkflowName(workflow);
1127
+ if (inputs && !byName) {
1128
+ throw this.gap(
1129
+ "snapshotInputs",
1130
+ "An execution snapshot freezes its inputs and takes no request overlay; run({ inputs }) over HTTP binds a workflow by its served name (listWorkflows())"
1131
+ );
1132
+ }
1133
+ const idempotencyKey = options.idempotencyKey;
1134
+ if (idempotencyKey === void 0) {
1135
+ throw new NikaConfigurationError(
1136
+ "HTTP run() requires a caller-owned idempotencyKey; reuse the same key and request when retrying an uncertain admission"
843
1137
  );
844
1138
  }
845
- const idempotencyKey = options.idempotencyKey ?? (0, import_node_crypto2.randomUUID)();
846
1139
  if (Buffer.byteLength(idempotencyKey) < 1 || Buffer.byteLength(idempotencyKey) > 255) {
847
1140
  throw new NikaTransportError(this.kind, "Idempotency-Key must be 1-255 bytes");
848
1141
  }
849
- if (isContainedWorkflowName(workflow)) {
1142
+ if (byName) {
850
1143
  await this.ensureServerIdentity();
851
- const job2 = await this.admitJob(JSON.stringify({ workflow }), idempotencyKey);
1144
+ if (inputs) this.requireJobInputs();
1145
+ const body = inputs ? `{"workflow":${JSON.stringify(workflow)},"inputs":${inputs.json}}` : JSON.stringify({ workflow });
1146
+ const job2 = await this.admitJob(body, idempotencyKey);
852
1147
  return this.httpRun(job2.id, 0, job2);
853
1148
  }
854
1149
  const captured = await this.captureSnapshot(workflow);
@@ -924,6 +1219,7 @@ var HttpTransport = class {
924
1219
  return Object.freeze([...object.workflows]);
925
1220
  }
926
1221
  async workflow(name) {
1222
+ refuseLegacyContainedName(name);
927
1223
  await this.ensureServerIdentity();
928
1224
  const object = await this.json(`/v1/workflows/${workflowPath(name)}`, {
929
1225
  method: "GET",
@@ -935,6 +1231,7 @@ var HttpTransport = class {
935
1231
  return object;
936
1232
  }
937
1233
  async schedule(workflow, options) {
1234
+ refuseLegacyContainedName(workflow);
938
1235
  await this.ensureScheduleCapability();
939
1236
  const path3 = `/v1/schedules/${encodeURIComponent(options.id)}`;
940
1237
  const headers = new Headers({ "Content-Type": "application/json" });
@@ -1026,6 +1323,7 @@ var HttpTransport = class {
1026
1323
  const outputs = event ? eventOutputs(event) : durable?.outputs;
1027
1324
  const error = event ? eventError(event) : durable?.settlement?.error ?? durable?.error;
1028
1325
  const settlement = event ? eventSettlement(event, this.kind) : durable?.settlement;
1326
+ const evidence = event ? eventEvidence(event, this.kind) : durable?.evidence;
1029
1327
  resolveDone({
1030
1328
  id,
1031
1329
  status: source.status,
@@ -1035,7 +1333,8 @@ var HttpTransport = class {
1035
1333
  ...outputs ? { outputs } : {},
1036
1334
  ...receipt ? { receipt } : {},
1037
1335
  ...error ? { error } : {},
1038
- ...settlement ? { settlement } : {}
1336
+ ...settlement ? { settlement } : {},
1337
+ ...evidence ? { evidence } : {}
1039
1338
  });
1040
1339
  };
1041
1340
  if (attachedState && isObservationEnded(attachedState)) settle(attachedState);
@@ -1234,13 +1533,15 @@ var HttpTransport = class {
1234
1533
  if (!event) throw new NikaProtocolError(this.kind, "SSE data was not an object");
1235
1534
  const allowed = /* @__PURE__ */ new Set([
1236
1535
  "sequence",
1536
+ "at",
1237
1537
  "kind",
1238
1538
  "status",
1239
1539
  "code",
1240
1540
  "message",
1241
1541
  "outputs",
1242
1542
  "receipt",
1243
- "settlement"
1543
+ "settlement",
1544
+ "evidence"
1244
1545
  ]);
1245
1546
  if (Object.keys(event).some((key) => !allowed.has(key))) {
1246
1547
  throw new NikaProtocolError(this.kind, "SSE data contained fields outside the public projection");
@@ -1262,6 +1563,10 @@ var HttpTransport = class {
1262
1563
  throw new NikaProtocolError(this.kind, `SSE data.${field} was not a string`);
1263
1564
  }
1264
1565
  }
1566
+ if (event.at !== void 0 && (typeof event.at !== "string" || !isCanonicalTimestamp(event.at))) {
1567
+ throw new NikaProtocolError(this.kind, "SSE data.at was not an RFC 3339 timestamp");
1568
+ }
1569
+ if (event.evidence !== void 0) journalEvidence(event.evidence, this.kind, "SSE data.evidence");
1265
1570
  if (event.outputs !== void 0 && !machineObject(event.outputs)) {
1266
1571
  throw new NikaProtocolError(this.kind, "SSE data.outputs was not an object");
1267
1572
  }
@@ -1511,6 +1816,21 @@ var HttpTransport = class {
1511
1816
  );
1512
1817
  }
1513
1818
  }
1819
+ /**
1820
+ * The resident must advertise named-input admission before a map is sent
1821
+ * (nika#1642). A resident from before the envelope answered 202 to an extra
1822
+ * `inputs` field and applied nothing, so an accepted POST negotiates nothing:
1823
+ * only the capability `/health` advertised does. Called after
1824
+ * `ensureServerIdentity()`.
1825
+ */
1826
+ requireJobInputs() {
1827
+ const identity = this.remoteIdentity;
1828
+ if (identity?.supportedCapabilities.includes(JOB_INPUTS)) return;
1829
+ throw this.gap(
1830
+ JOB_INPUTS,
1831
+ `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`
1832
+ );
1833
+ }
1514
1834
  async captureSnapshot(workflow, signal, teach = false) {
1515
1835
  const identity = await this.ensureLocalEngine();
1516
1836
  const captured = await captureEngine(
@@ -1784,6 +2104,11 @@ var HttpTransport = class {
1784
2104
  }
1785
2105
  }
1786
2106
  };
2107
+ function refuseLegacyContainedName(workflow) {
2108
+ if (isRetiredByNameAttempt(workflow)) {
2109
+ throw new NikaConfigurationError(legacyWorkflowRenameMessage(workflow));
2110
+ }
2111
+ }
1787
2112
  function workflowPath(name) {
1788
2113
  const segments = name.split("/");
1789
2114
  if (name.startsWith("/") || name.includes("\\") || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
@@ -1791,15 +2116,6 @@ function workflowPath(name) {
1791
2116
  }
1792
2117
  return segments.map(encodeURIComponent).join("/");
1793
2118
  }
1794
- function isContainedWorkflowName(value) {
1795
- if (typeof value !== "string" || !value.endsWith(".nika.yaml")) return false;
1796
- try {
1797
- workflowPath(value);
1798
- return true;
1799
- } catch {
1800
- return false;
1801
- }
1802
- }
1803
2119
  function scheduleBody(workflow, options) {
1804
2120
  return {
1805
2121
  workflow,
@@ -1906,7 +2222,8 @@ function durableJob(value, expectedId, transport) {
1906
2222
  "outputs",
1907
2223
  "receipt",
1908
2224
  "error",
1909
- "settlement"
2225
+ "settlement",
2226
+ "evidence"
1910
2227
  ]);
1911
2228
  if (Object.keys(value).some((key) => !allowed.has(key))) {
1912
2229
  throw new NikaProtocolError(transport, "Durable job response contained unknown fields");
@@ -1930,6 +2247,7 @@ function durableJob(value, expectedId, transport) {
1930
2247
  const outputs = value.outputs === void 0 ? void 0 : machineObject(value.outputs);
1931
2248
  const receipt = value.receipt === void 0 ? void 0 : machineObject(value.receipt);
1932
2249
  const settlement = value.settlement === void 0 ? void 0 : readSettlement(value.settlement, transport, value.status);
2250
+ const evidence = value.evidence === void 0 ? void 0 : journalEvidence(value.evidence, transport, "Durable job response evidence");
1933
2251
  if (value.outputs !== void 0 && !outputs) {
1934
2252
  throw new NikaProtocolError(transport, "Durable job response outputs were malformed");
1935
2253
  }
@@ -1953,9 +2271,24 @@ function durableJob(value, expectedId, transport) {
1953
2271
  ...outputs ? { outputs } : {},
1954
2272
  ...receipt ? { receipt: Object.freeze(receipt) } : {},
1955
2273
  ...error ? { error } : {},
1956
- ...settlement ? { settlement } : {}
2274
+ ...settlement ? { settlement } : {},
2275
+ ...evidence ? { evidence } : {}
1957
2276
  };
1958
2277
  }
2278
+ var JOURNAL_EVIDENCE_REASONS = /* @__PURE__ */ new Set(["write_failed", "record_refused"]);
2279
+ function journalEvidence(value, transport, where) {
2280
+ const evidence = machineObject(value);
2281
+ if (!evidence || Object.keys(evidence).length !== 2 || evidence.status !== "mirror_lost" || typeof evidence.reason !== "string" || !JOURNAL_EVIDENCE_REASONS.has(evidence.reason)) {
2282
+ throw new NikaProtocolError(transport, `${where} was not the resident's journal evidence`);
2283
+ }
2284
+ return Object.freeze({
2285
+ status: "mirror_lost",
2286
+ reason: evidence.reason
2287
+ });
2288
+ }
2289
+ function eventEvidence(event, transport) {
2290
+ return event.evidence === void 0 ? void 0 : journalEvidence(event.evidence, transport, "SSE data.evidence");
2291
+ }
1959
2292
  function assertReceiptIdentity(receipt, expectedJobId, transport, expectedExecutionId, expectedTraceId, expectedSnapshotDigest) {
1960
2293
  const allowed = /* @__PURE__ */ new Set([
1961
2294
  "job_id",
@@ -2083,7 +2416,104 @@ function isCanonicalTimestamp(value) {
2083
2416
 
2084
2417
  // src/lib/native-process-transport.ts
2085
2418
  var import_node_child_process2 = require("child_process");
2086
- var import_node_crypto3 = require("crypto");
2419
+ var import_node_crypto2 = require("crypto");
2420
+
2421
+ // src/lib/legacy-run-refusals.ts
2422
+ var REFUSAL_EXITS = /* @__PURE__ */ new Set([2, 3]);
2423
+ var ENGINE_PREFIX = /^nika(?: [a-z-]+)?:\s*/;
2424
+ function legacyTeachingLine(line) {
2425
+ const machineCode = leadingCode(line);
2426
+ return machineCode ? { machineCode, message: line } : void 0;
2427
+ }
2428
+ async function legacyPrettyReport(opening, lines, limit, transport) {
2429
+ if (!opening.startsWith("{")) return void 0;
2430
+ let text = opening;
2431
+ let bytes = Buffer.byteLength(opening);
2432
+ for (; ; ) {
2433
+ const next = await lines.next();
2434
+ if (next.done) break;
2435
+ text += `
2436
+ ${next.value}`;
2437
+ bytes += Buffer.byteLength(next.value) + 1;
2438
+ if (bytes > limit) {
2439
+ throw new NikaProtocolError(transport, `Native pre-run report exceeded ${limit} bytes`);
2440
+ }
2441
+ }
2442
+ let value;
2443
+ try {
2444
+ value = JSON.parse(text);
2445
+ } catch {
2446
+ return { text };
2447
+ }
2448
+ const frame = machineObject(value);
2449
+ return frame ? { frame, text } : { text };
2450
+ }
2451
+ function legacyStderrRefusal(stderr, exitCode) {
2452
+ if (!REFUSAL_EXITS.has(exitCode)) return void 0;
2453
+ for (const raw of stderr.split("\n")) {
2454
+ const line = raw.trim().replace(ENGINE_PREFIX, "");
2455
+ const machineCode = leadingCode(line);
2456
+ if (machineCode) return { machineCode, message: line };
2457
+ }
2458
+ return void 0;
2459
+ }
2460
+ function leadingCode(text) {
2461
+ const code = /^NIKA-[A-Z0-9_-]+/.exec(text)?.[0].replace(/-+$/, "");
2462
+ return code && /\d/.test(code) ? code : void 0;
2463
+ }
2464
+
2465
+ // src/lib/run-refusal.ts
2466
+ var RUN_REFUSED = "run_refused";
2467
+ var UNEXPLAINED = "The engine refused this workflow before admission";
2468
+ function isRunEvent(frame) {
2469
+ return typeof frame.kind === "string";
2470
+ }
2471
+ function preRunRefusal(frame, transport) {
2472
+ if (isRunEvent(frame)) return void 0;
2473
+ if (frame.clean === false && Array.isArray(frame.findings)) {
2474
+ return checkRefusal(frame.findings, transport);
2475
+ }
2476
+ const error = machineObject(frame.error);
2477
+ if (!error || typeof error.message !== "string" || error.message.length === 0) {
2478
+ return void 0;
2479
+ }
2480
+ const machineCode = typeof error.code === "string" && error.code.length > 0 ? error.code : void 0;
2481
+ return { ...machineCode ? { machineCode } : {}, message: error.message };
2482
+ }
2483
+ function runRefusalError(transport, refusal, exitCode) {
2484
+ return new NikaOperationError(
2485
+ "run",
2486
+ transport,
2487
+ refusal.machineCode ?? RUN_REFUSED,
2488
+ refusal.message,
2489
+ {
2490
+ status: exitCode,
2491
+ ...refusal.findings ? { findings: refusal.findings } : {},
2492
+ ...refusal.machineCode ? { machineCode: refusal.machineCode } : {}
2493
+ }
2494
+ );
2495
+ }
2496
+ function checkRefusal(values, transport) {
2497
+ const findings = [];
2498
+ for (const value of values) {
2499
+ const finding = machineObject(value);
2500
+ if (!finding) {
2501
+ throw new NikaProtocolError(transport, "Pre-run refusal findings were malformed");
2502
+ }
2503
+ findings.push(finding);
2504
+ }
2505
+ const named = findings.find((finding) => typeof finding.code === "string" && finding.code) ?? findings[0];
2506
+ const machineCode = typeof named?.code === "string" && named.code ? named.code : void 0;
2507
+ const said = typeof named?.message === "string" && named.message ? named.message : void 0;
2508
+ const others = findings.length > 1 ? ` (+${findings.length - 1} more findings)` : "";
2509
+ return {
2510
+ ...machineCode ? { machineCode } : {},
2511
+ message: `${[machineCode, said ?? UNEXPLAINED].filter(Boolean).join(" \xB7 ")}${others}`,
2512
+ findings
2513
+ };
2514
+ }
2515
+
2516
+ // src/lib/native-process-transport.ts
2087
2517
  var NativeProcessTransport = class {
2088
2518
  constructor(options) {
2089
2519
  this.options = options;
@@ -2108,7 +2538,8 @@ var NativeProcessTransport = class {
2108
2538
  return { ...report, exitCode: captured.exitCode };
2109
2539
  }
2110
2540
  async startRun(workflow, options) {
2111
- await this.ensureReady();
2541
+ const inputs = literalInputs(options);
2542
+ const identity = await this.ensureReady();
2112
2543
  if (options.idempotencyKey !== void 0) {
2113
2544
  throw new NikaCompatibilityError(
2114
2545
  "idempotencyKey",
@@ -2116,13 +2547,26 @@ var NativeProcessTransport = class {
2116
2547
  "idempotencyKey is an HTTP admission option"
2117
2548
  );
2118
2549
  }
2119
- const args = ["run", workflow, "--json", ...runFlags(options)];
2120
- const child = (0, import_node_child_process2.spawn)(this.options.engine.bin, args, {
2121
- cwd: this.options.cwd,
2122
- stdio: ["ignore", "pipe", "pipe"],
2123
- shell: false
2124
- });
2125
- return this.processRun((0, import_node_crypto3.randomUUID)(), child);
2550
+ if (inputs && !identity.supportedCapabilities.includes(INPUTS_LITERAL)) {
2551
+ throw new NikaCompatibilityError(
2552
+ INPUTS_LITERAL,
2553
+ this.kind,
2554
+ `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`
2555
+ );
2556
+ }
2557
+ const args = ["run", workflow, "--json", ...runFlags(options, inputs !== void 0)];
2558
+ const spawnOptions = { cwd: this.options.cwd, shell: false };
2559
+ const child = inputs ? (0, import_node_child_process2.spawn)(this.options.engine.bin, args, { ...spawnOptions, stdio: ["pipe", "pipe", "pipe"] }) : (0, import_node_child_process2.spawn)(this.options.engine.bin, args, { ...spawnOptions, stdio: ["ignore", "pipe", "pipe"] });
2560
+ if (inputs && child.stdin) writeLiteralInputs(child.stdin, inputs.json);
2561
+ const engine = this.nativeProcess(child);
2562
+ let first;
2563
+ try {
2564
+ first = await this.admission(engine);
2565
+ } catch (cause) {
2566
+ await engine.stop();
2567
+ throw cause;
2568
+ }
2569
+ return this.processRun((0, import_node_crypto2.randomUUID)(), engine, first);
2126
2570
  }
2127
2571
  async attachRun(_id, _options) {
2128
2572
  throw new NikaCompatibilityError(
@@ -2205,19 +2649,8 @@ RECEIPT MISMATCH: ${mismatch}
2205
2649
  `)
2206
2650
  };
2207
2651
  }
2208
- processRun(id, child) {
2652
+ nativeProcess(child) {
2209
2653
  let settled = false;
2210
- let lastEvent;
2211
- let receipt;
2212
- let outputs;
2213
- let machineError;
2214
- let settlement;
2215
- let streamError;
2216
- let refusal;
2217
- let resolveDrained;
2218
- const drained = new Promise((resolve) => {
2219
- resolveDrained = resolve;
2220
- });
2221
2654
  let stderr = "";
2222
2655
  child.stderr.setEncoding("utf8");
2223
2656
  child.stderr.on("data", (chunk) => {
@@ -2225,94 +2658,154 @@ RECEIPT MISMATCH: ${mismatch}
2225
2658
  });
2226
2659
  const closed = new Promise((resolve, reject) => {
2227
2660
  child.once("error", (cause) => {
2228
- streamError = new NikaTransportError(
2661
+ reject(new NikaTransportError(
2229
2662
  this.kind,
2230
2663
  `Cannot spawn ${this.options.engine.bin}: ${cause.message}`,
2231
2664
  { cause }
2232
- );
2233
- reject(streamError);
2665
+ ));
2234
2666
  });
2235
2667
  child.once("close", (code, signal) => {
2236
2668
  settled = true;
2237
- if (code === null && signal && !streamError) resolve(130);
2238
- else resolve(code ?? 3);
2669
+ resolve(code === null && signal ? 130 : code ?? 3);
2239
2670
  });
2240
2671
  });
2241
2672
  closed.catch(() => {
2242
2673
  });
2674
+ const lines = machineLines(child.stdout, this.options.machineBufferBytes, this.kind);
2675
+ return {
2676
+ child,
2677
+ lines,
2678
+ closed,
2679
+ settled: () => settled,
2680
+ diagnostics: () => stderr,
2681
+ stop: async () => {
2682
+ void lines.return(void 0).catch(() => {
2683
+ });
2684
+ child.stdin?.destroy();
2685
+ if (settled) return;
2686
+ child.kill("SIGTERM");
2687
+ if (await endsWithin(closed, STOP_GRACE_MILLISECONDS)) return;
2688
+ child.kill("SIGKILL");
2689
+ await endsWithin(closed, STOP_GRACE_MILLISECONDS);
2690
+ }
2691
+ };
2692
+ }
2693
+ /**
2694
+ * The admission law (issue #121): resolve with the run's first event, or
2695
+ * reject before any Run exists. The first machine frame decides. A run
2696
+ * event is the engine's own admission evidence; an object without a `kind`
2697
+ * is its one pre-run refusal object; anything else proves neither and stays
2698
+ * a protocol fault. The SDK reads which one the engine wrote and judges
2699
+ * nothing itself.
2700
+ */
2701
+ async admission(engine) {
2702
+ const { lines } = engine;
2243
2703
  const kind = this.kind;
2244
- const machineBufferBytes = this.options.machineBufferBytes;
2245
- const lineEvent = (line) => {
2246
- const refused = engineRefusal(line);
2247
- if (refused) {
2248
- refusal ??= refused;
2249
- return void 0;
2704
+ const opening = await lines.next();
2705
+ if (opening.done) {
2706
+ const exitCode2 = await engine.closed;
2707
+ const taught = legacyStderrRefusal(engine.diagnostics(), exitCode2);
2708
+ if (taught) throw runRefusalError(kind, taught, exitCode2);
2709
+ throw new NikaProtocolError(kind, quoted(
2710
+ `Engine exited without a machine frame (exit ${exitCode2})`,
2711
+ engine.diagnostics()
2712
+ ));
2713
+ }
2714
+ const line = opening.value;
2715
+ let frame;
2716
+ let unreadable;
2717
+ try {
2718
+ frame = parseMachineObject(line, kind);
2719
+ } catch (cause) {
2720
+ unreadable = cause;
2721
+ }
2722
+ let text = line;
2723
+ let refusal;
2724
+ if (frame) {
2725
+ if (isRunEvent(frame)) return frame;
2726
+ refusal = preRunRefusal(frame, kind);
2727
+ } else {
2728
+ refusal = legacyTeachingLine(line);
2729
+ if (!refusal) {
2730
+ const report = await legacyPrettyReport(
2731
+ line,
2732
+ lines,
2733
+ this.options.machineBufferBytes,
2734
+ kind
2735
+ );
2736
+ text = report?.text ?? line;
2737
+ if (!report?.frame) throw quotedProtocolError(unreadable, text, kind);
2738
+ refusal = preRunRefusal(report.frame, kind);
2250
2739
  }
2251
- return machineLine(line, kind);
2740
+ }
2741
+ if (!refusal) {
2742
+ throw new NikaProtocolError(kind, quoted(
2743
+ "The first machine frame was neither a run event nor a pre-run refusal object",
2744
+ text
2745
+ ));
2746
+ }
2747
+ const extra = await lines.next();
2748
+ if (!extra.done) {
2749
+ throw new NikaProtocolError(kind, quoted(
2750
+ "Engine wrote more machine output after its pre-run refusal",
2751
+ extra.value
2752
+ ));
2753
+ }
2754
+ const exitCode = await engine.closed;
2755
+ if (exitCode === 0) {
2756
+ throw new NikaProtocolError(kind, quoted(
2757
+ "Engine wrote a pre-run refusal but exited 0",
2758
+ refusal.message
2759
+ ));
2760
+ }
2761
+ throw runRefusalError(kind, refusal, exitCode);
2762
+ }
2763
+ processRun(id, engine, first) {
2764
+ const { child, lines, closed } = engine;
2765
+ let lastEvent;
2766
+ let receipt;
2767
+ let outputs;
2768
+ let machineError;
2769
+ let settlement;
2770
+ let streamError;
2771
+ let resolveDrained;
2772
+ const drained = new Promise((resolve) => {
2773
+ resolveDrained = resolve;
2774
+ });
2775
+ const kind = this.kind;
2776
+ const observe = (event) => {
2777
+ lastEvent = event;
2778
+ receipt = eventReceipt(event) ?? receipt;
2779
+ outputs = eventOutputs(event) ?? outputs;
2780
+ const currentSettlement = eventSettlement(event, kind);
2781
+ machineError = currentSettlement ? eventError(event) : eventError(event) ?? machineError;
2782
+ settlement = currentSettlement ?? settlement;
2783
+ return event;
2252
2784
  };
2253
2785
  const events = {
2254
2786
  [Symbol.asyncIterator]: async function* () {
2255
- let buffer = "";
2256
- child.stdout.setEncoding("utf8");
2257
2787
  try {
2258
- for await (const chunk of child.stdout) {
2259
- buffer += String(chunk);
2260
- let newline;
2261
- while ((newline = buffer.indexOf("\n")) >= 0) {
2262
- const line = buffer.slice(0, newline).trim();
2263
- buffer = buffer.slice(newline + 1);
2264
- if (!line) continue;
2265
- const event = lineEvent(line);
2266
- if (!event) continue;
2267
- lastEvent = event;
2268
- receipt = eventReceipt(event) ?? receipt;
2269
- outputs = eventOutputs(event) ?? outputs;
2270
- const currentSettlement = eventSettlement(event, kind);
2271
- machineError = currentSettlement ? eventError(event) : eventError(event) ?? machineError;
2272
- settlement = currentSettlement ?? settlement;
2273
- yield event;
2274
- }
2275
- if (Buffer.byteLength(buffer) > machineBufferBytes) {
2276
- throw new NikaProtocolError(
2277
- kind,
2278
- `Native machine line exceeded ${machineBufferBytes} bytes`
2279
- );
2280
- }
2281
- }
2282
- const tail = buffer.trim();
2283
- if (tail) {
2284
- const event = lineEvent(tail);
2285
- if (event) {
2286
- lastEvent = event;
2287
- receipt = eventReceipt(event) ?? receipt;
2288
- outputs = eventOutputs(event) ?? outputs;
2289
- const currentSettlement = eventSettlement(event, kind);
2290
- machineError = currentSettlement ? eventError(event) : eventError(event) ?? machineError;
2291
- settlement = currentSettlement ?? settlement;
2292
- yield event;
2293
- }
2788
+ yield observe(first);
2789
+ for (; ; ) {
2790
+ const next = await lines.next();
2791
+ if (next.done) break;
2792
+ yield observe(machineLine(next.value, kind));
2294
2793
  }
2295
2794
  } catch (cause) {
2296
2795
  streamError = cause instanceof Error ? cause : new Error(String(cause));
2297
- if (!settled) child.kill("SIGTERM");
2796
+ if (!engine.settled()) child.kill("SIGTERM");
2298
2797
  throw streamError;
2299
2798
  } finally {
2799
+ void lines.return(void 0).catch(() => {
2800
+ });
2300
2801
  resolveDrained();
2301
2802
  }
2302
2803
  }
2303
2804
  };
2304
2805
  const done = Promise.all([closed, drained]).then(([exitCode]) => {
2305
2806
  if (streamError) throw streamError;
2306
- if (refusal) {
2307
- throw new NikaOperationError(
2308
- "run",
2309
- this.kind,
2310
- refusal.code,
2311
- refusal.line,
2312
- { status: exitCode }
2313
- );
2314
- }
2315
2807
  const status = eventStatus(lastEvent) ?? statusForExit(exitCode, lastEvent?.kind);
2808
+ const stderr = engine.diagnostics();
2316
2809
  return {
2317
2810
  id,
2318
2811
  status,
@@ -2336,12 +2829,12 @@ RECEIPT MISMATCH: ${mismatch}
2336
2829
  throw new NikaCompatibilityError(
2337
2830
  "runStatus",
2338
2831
  this.kind,
2339
- "A direct native process has no independent durable status authority; await run.done"
2832
+ "A direct native process has no independent durable status authority; await run.result()"
2340
2833
  );
2341
2834
  },
2342
2835
  cancel: () => {
2343
2836
  cancelPromise ??= Promise.resolve(
2344
- settled ? {
2837
+ engine.settled() ? {
2345
2838
  runId: id,
2346
2839
  accepted: false,
2347
2840
  status: "already_settled",
@@ -2355,11 +2848,7 @@ RECEIPT MISMATCH: ${mismatch}
2355
2848
  );
2356
2849
  return cancelPromise;
2357
2850
  },
2358
- cleanup: async () => {
2359
- if (!settled) child.kill("SIGTERM");
2360
- await closed.catch(() => {
2361
- });
2362
- }
2851
+ cleanup: () => engine.stop()
2363
2852
  };
2364
2853
  }
2365
2854
  capture(args, signal) {
@@ -2403,14 +2892,56 @@ RECEIPT MISMATCH: ${mismatch}
2403
2892
  });
2404
2893
  });
2405
2894
  }
2895
+ /** The verified engine's identity is kept: its capabilities gate what a run may ask. */
2406
2896
  ensureReady() {
2407
- this.ready ??= verifyNikaEngine(this.options.engine).then(() => void 0);
2897
+ this.ready ??= verifyNikaEngine(this.options.engine);
2408
2898
  return this.ready;
2409
2899
  }
2410
2900
  };
2411
- var ENGINE_PREFIX = /^nika:\s*/;
2412
- var REFUSAL_CODE = /^(NIKA-[A-Z0-9-]+)\b/;
2901
+ var INPUTS_LITERAL = "inputsLiteral";
2902
+ function writeLiteralInputs(stdin, json) {
2903
+ stdin.on("error", () => {
2904
+ });
2905
+ stdin.end(json);
2906
+ }
2907
+ var ENGINE_PREFIX2 = /^nika:\s*/;
2413
2908
  var DIAGNOSTIC_EXCERPT_LIMIT = 240;
2909
+ var STOP_GRACE_MILLISECONDS = 2e3;
2910
+ function endsWithin(settling, milliseconds) {
2911
+ return new Promise((resolve) => {
2912
+ const timer = setTimeout(() => resolve(false), milliseconds);
2913
+ const ended = () => {
2914
+ clearTimeout(timer);
2915
+ resolve(true);
2916
+ };
2917
+ settling.then(ended, ended);
2918
+ });
2919
+ }
2920
+ async function* machineLines(stdout, limit, kind) {
2921
+ const bounded = (raw) => {
2922
+ if (Buffer.byteLength(raw) > limit) {
2923
+ throw new NikaProtocolError(
2924
+ kind,
2925
+ `Native machine line exceeded ${limit} bytes; machineBufferBytes bounds one machine frame`
2926
+ );
2927
+ }
2928
+ return raw.trim();
2929
+ };
2930
+ let buffer = "";
2931
+ stdout.setEncoding("utf8");
2932
+ for await (const chunk of stdout) {
2933
+ buffer += String(chunk);
2934
+ let newline;
2935
+ while ((newline = buffer.indexOf("\n")) >= 0) {
2936
+ const line = bounded(buffer.slice(0, newline));
2937
+ buffer = buffer.slice(newline + 1);
2938
+ if (line) yield line;
2939
+ }
2940
+ bounded(buffer);
2941
+ }
2942
+ const tail = bounded(buffer);
2943
+ if (tail) yield tail;
2944
+ }
2414
2945
  function reportObject(text) {
2415
2946
  const trimmed = text.trim();
2416
2947
  if (!trimmed) return void 0;
@@ -2423,32 +2954,35 @@ function reportObject(text) {
2423
2954
  return machineObject(value);
2424
2955
  }
2425
2956
  function stripEnginePrefix(text) {
2426
- return text.trim().replace(ENGINE_PREFIX, "");
2957
+ return text.trim().replace(ENGINE_PREFIX2, "");
2427
2958
  }
2428
2959
  function diagnosticExcerpt(text, limit = DIAGNOSTIC_EXCERPT_LIMIT) {
2429
2960
  const single = stripEnginePrefix(text).replace(/\s+/g, " ").trim();
2430
2961
  if (!single) return void 0;
2431
2962
  return single.length > limit ? `${single.slice(0, limit)}\u2026` : single;
2432
2963
  }
2433
- function engineRefusal(line) {
2434
- const code = REFUSAL_CODE.exec(line)?.[1];
2435
- return code ? { code, line } : void 0;
2436
- }
2437
2964
  function machineLine(line, kind) {
2438
2965
  try {
2439
2966
  return parseMachineObject(line, kind);
2440
2967
  } catch (cause) {
2441
- const verdict = cause instanceof Error ? cause.message : "Engine machine output was unreadable";
2442
- const excerpt = diagnosticExcerpt(line);
2443
- throw new NikaProtocolError(
2444
- kind,
2445
- excerpt ? `${verdict}: ${excerpt}` : verdict,
2446
- { cause: cause instanceof Error ? cause : void 0 }
2447
- );
2968
+ throw quotedProtocolError(cause, line, kind);
2448
2969
  }
2449
2970
  }
2450
- function runFlags(options) {
2971
+ function quotedProtocolError(cause, text, kind) {
2972
+ const verdict = cause instanceof Error ? cause.message : "Engine machine output was unreadable";
2973
+ return new NikaProtocolError(
2974
+ kind,
2975
+ quoted(verdict, text),
2976
+ { cause: cause instanceof Error ? cause : void 0 }
2977
+ );
2978
+ }
2979
+ function quoted(verdict, text) {
2980
+ const excerpt = diagnosticExcerpt(text);
2981
+ return excerpt ? `${verdict}: ${excerpt}` : verdict;
2982
+ }
2983
+ function runFlags(options, literal) {
2451
2984
  const flags = [];
2985
+ if (literal) flags.push("--inputs-json", "-");
2452
2986
  for (const [key, value] of Object.entries(options.vars ?? {})) {
2453
2987
  flags.push("--var", `${key}=${String(value)}`);
2454
2988
  }
@@ -2493,18 +3027,87 @@ function receiptMismatch(receipt, manifest) {
2493
3027
  return void 0;
2494
3028
  }
2495
3029
 
3030
+ // src/lib/run-events.ts
3031
+ var STARTED = /* @__PURE__ */ new Set(["workflow_started", "execution.started"]);
3032
+ var TASKS = /* @__PURE__ */ new Map([
3033
+ ["task_scheduled", "task.scheduled"],
3034
+ ["task_started", "task.started"],
3035
+ ["task_completed", "task.completed"],
3036
+ ["task_failed", "task.failed"]
3037
+ ]);
3038
+ var SETTLEMENT = /* @__PURE__ */ new Set(["run_settled", "execution.settled"]);
3039
+ var SETTLEMENT_WORDS = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
3040
+ var SETTLED = /* @__PURE__ */ new Map([
3041
+ ["run_settled", SETTLEMENT_WORDS],
3042
+ ["execution.settled", SETTLEMENT_WORDS],
3043
+ ["execution.cancelled", /* @__PURE__ */ new Set(["cancelled"])],
3044
+ ["execution.refused", /* @__PURE__ */ new Set(["failed"])]
3045
+ ]);
3046
+ var INTERRUPTED = /* @__PURE__ */ new Set([
3047
+ "workflow_interrupted",
3048
+ "execution.interrupted",
3049
+ "interrupted"
3050
+ ]);
3051
+ function semanticRunEvent(raw, transport) {
3052
+ const stated = eventStatus(raw);
3053
+ const kind = semanticKind(raw.kind, stated);
3054
+ const named = kind !== "engine.event";
3055
+ const status = named ? stated : void 0;
3056
+ const task = kind.startsWith("task.") ? eventTask(raw) : void 0;
3057
+ const failed = kind === "task.failed" || kind === "run.settled" && status === "failed";
3058
+ const error = failed ? eventError(raw) : void 0;
3059
+ const sequence = transport === "http" && Number.isSafeInteger(raw.sequence) ? raw.sequence : void 0;
3060
+ return Object.freeze({
3061
+ kind,
3062
+ transport,
3063
+ ...status !== void 0 ? { status } : {},
3064
+ ...sequence !== void 0 ? { sequence } : {},
3065
+ ...task !== void 0 ? { task } : {},
3066
+ ...error !== void 0 ? { error } : {},
3067
+ raw
3068
+ });
3069
+ }
3070
+ function semanticKind(kind, status) {
3071
+ if (typeof kind !== "string") return "engine.event";
3072
+ if (STARTED.has(kind)) return "run.started";
3073
+ const task = TASKS.get(kind);
3074
+ if (task !== void 0) return task;
3075
+ if (kind === "run_sealed") return "run.sealed";
3076
+ if (INTERRUPTED.has(kind)) return status === "interrupted" ? "run.interrupted" : "engine.event";
3077
+ if (status === void 0) return "engine.event";
3078
+ if (status === "paused") return SETTLEMENT.has(kind) ? "run.waiting" : "engine.event";
3079
+ return SETTLED.get(kind)?.has(status) === true ? "run.settled" : "engine.event";
3080
+ }
3081
+ function eventTask(raw) {
3082
+ if (typeof raw.task === "string") return raw.task;
3083
+ const fields = Array.isArray(raw.fields) ? raw.fields : [];
3084
+ for (const field of fields) {
3085
+ const row = machineObject(field);
3086
+ if (row?.key === "task" && typeof row.value === "string") return row.value;
3087
+ }
3088
+ return void 0;
3089
+ }
3090
+
2496
3091
  // src/lib/run-session.ts
2497
3092
  var RunSession = class {
2498
- constructor(source, eventBufferSize) {
3093
+ constructor(source, eventBufferSize, transport) {
2499
3094
  this.source = source;
2500
3095
  this.eventBufferSize = eventBufferSize;
3096
+ this.transport = transport;
2501
3097
  this.done = new Promise((resolve, reject) => {
2502
3098
  this.resolveDone = resolve;
2503
3099
  this.rejectDone = reject;
2504
3100
  });
2505
3101
  this.done.catch(() => {
2506
3102
  });
2507
- this.run = Object.freeze({ id: source.id, done: this.done });
3103
+ this.run = Object.freeze({
3104
+ id: source.id,
3105
+ events: (options) => this.semanticEvents(options),
3106
+ result: () => this.done,
3107
+ status: () => this.status(),
3108
+ cancel: () => this.cancel(),
3109
+ done: this.done
3110
+ });
2508
3111
  void this.observeSettlement();
2509
3112
  void this.pump();
2510
3113
  }
@@ -2516,7 +3119,30 @@ var RunSession = class {
2516
3119
  rejectDone;
2517
3120
  terminal = false;
2518
3121
  cancelPromise;
2519
- historyOverflowed = false;
3122
+ /** Every frame the pump delivered; `history` holds at most the last `eventBufferSize`. */
3123
+ observed = 0;
3124
+ /**
3125
+ * The lifecycle vocabulary over the same bounded view: one history, one
3126
+ * queue, and a pure per-frame projection at the edge. No second event bus.
3127
+ */
3128
+ semanticEvents(options = {}) {
3129
+ const view = this.events(options);
3130
+ const transport = this.transport;
3131
+ const lifecycle = {
3132
+ [Symbol.asyncIterator]: () => lifecycle,
3133
+ next: async () => {
3134
+ const step = await view.next();
3135
+ if (step.done) return { value: void 0, done: true };
3136
+ return { value: semanticRunEvent(step.value, transport), done: false };
3137
+ },
3138
+ return: async () => {
3139
+ await view.return?.();
3140
+ return { value: void 0, done: true };
3141
+ }
3142
+ };
3143
+ return lifecycle;
3144
+ }
3145
+ /** The protocol frames, exactly as the transport delivered them. */
2520
3146
  events(options = {}) {
2521
3147
  const requested = options.bufferSize ?? this.eventBufferSize;
2522
3148
  if (!Number.isInteger(requested) || requested < 1 || requested > this.eventBufferSize) {
@@ -2524,8 +3150,11 @@ var RunSession = class {
2524
3150
  `events bufferSize must be an integer from 1 to ${this.eventBufferSize}`
2525
3151
  );
2526
3152
  }
2527
- if (this.historyOverflowed || this.history.length > requested) {
2528
- throw new NikaEventBufferOverflowError(this.source.id, requested);
3153
+ if (this.observed > requested) {
3154
+ throw new NikaEventBufferOverflowError(this.source.id, requested, {
3155
+ observed: this.observed,
3156
+ retained: this.history.length
3157
+ });
2529
3158
  }
2530
3159
  const subscription = new EventSubscription(
2531
3160
  this.source.id,
@@ -2573,11 +3202,9 @@ var RunSession = class {
2573
3202
  async pump() {
2574
3203
  try {
2575
3204
  for await (const event of this.source.events) {
3205
+ this.observed += 1;
2576
3206
  this.history.push(event);
2577
- if (this.history.length > this.eventBufferSize) {
2578
- this.history.shift();
2579
- this.historyOverflowed = true;
2580
- }
3207
+ if (this.history.length > this.eventBufferSize) this.history.shift();
2581
3208
  for (const subscriber of [...this.subscribers]) subscriber.push(event);
2582
3209
  }
2583
3210
  this.resolveDone(await this.source.done);
@@ -2669,6 +3296,11 @@ var EventSubscription = class {
2669
3296
  }
2670
3297
  };
2671
3298
 
3299
+ // src/results.ts
3300
+ function isNikaRunSucceeded(result) {
3301
+ return result.status === "succeeded";
3302
+ }
3303
+
2672
3304
  // src/events.ts
2673
3305
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
2674
3306
  "succeeded",
@@ -2687,7 +3319,7 @@ function isNikaRunSealedEvent(event) {
2687
3319
  }
2688
3320
 
2689
3321
  // src/index.ts
2690
- var DEFAULT_EVENT_BUFFER_SIZE = 256;
3322
+ var DEFAULT_EVENT_BUFFER_SIZE = 4096;
2691
3323
  var DEFAULT_MACHINE_BUFFER_BYTES = 64 * 1024;
2692
3324
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
2693
3325
  var Nika = class {
@@ -2742,25 +3374,30 @@ var Nika = class {
2742
3374
  return this.transport.check(workflowName(workflow), options);
2743
3375
  }
2744
3376
  /**
3377
+ * Resolves with the run's handle once the engine admitted it, and rejects
3378
+ * without one when the engine refused it. The handle owns the lifecycle:
3379
+ * `run.events()`, `run.result()`, `run.status()`, `run.cancel()`.
3380
+ *
2745
3381
  * `Outputs` is the caller's projection of the engine-emitted outputs map;
2746
3382
  * the SDK transports outputs without validating their shape.
2747
3383
  */
2748
3384
  async run(workflow, options = {}) {
2749
3385
  const source = await this.transport.startRun(workflowName(workflow), options);
2750
- const session = new RunSession(source, this.eventBufferSize);
2751
- this.sessions.set(session.run, session);
2752
- return session.run;
3386
+ return this.own(source);
2753
3387
  }
2754
- /** Reattach this client process to an already-admitted durable HTTP job. */
3388
+ /**
3389
+ * The one recovery door: reattach this client process to an
3390
+ * already-admitted durable HTTP job and get a full `NikaRun` back. Pass the
3391
+ * last `event.sequence` you fully processed as `lastEventId`. A native
3392
+ * process is process-bound and refuses with a typed compatibility error.
3393
+ */
2755
3394
  async attachRun(id, options = {}) {
2756
3395
  const lastEventId = options.lastEventId ?? 0;
2757
3396
  if (!Number.isSafeInteger(lastEventId) || lastEventId < 0) {
2758
3397
  throw new RangeError("lastEventId must be a non-negative safe integer");
2759
3398
  }
2760
3399
  const source = await this.transport.attachRun(jobId(id), { lastEventId });
2761
- const session = new RunSession(source, this.eventBufferSize);
2762
- this.sessions.set(session.run, session);
2763
- return session.run;
3400
+ return this.own(source);
2764
3401
  }
2765
3402
  /** List contained workflow names from a resident HTTP authority. */
2766
3403
  listWorkflows() {
@@ -2770,13 +3407,34 @@ var Nika = class {
2770
3407
  workflow(name) {
2771
3408
  return this.transport.workflow(workflowName(name));
2772
3409
  }
3410
+ /**
3411
+ * The run's events in the protocol vocabulary of its transport
3412
+ * (`workflow_*` · `task_*` · `run_*` natively, `execution.*` over HTTP).
3413
+ *
3414
+ * @deprecated Use `run.events()`: one lifecycle vocabulary on both
3415
+ * transports, with this same frame kept on `event.raw`. This wrapper is a
3416
+ * compatibility door for one release train, counted from publication: it
3417
+ * ships unchanged in the first published train that carries `run.events()`,
3418
+ * and the earliest train that may remove it is the one after, as announced
3419
+ * in that train's release notes. It accepts only a run this client created.
3420
+ */
2773
3421
  events(run, options = {}) {
2774
3422
  return this.session(run).events(options);
2775
3423
  }
3424
+ /**
3425
+ * @deprecated Use `run.cancel()`; both return the one memoized request.
3426
+ * Kept through the same compatibility window as `events(run)`. It accepts
3427
+ * only a run this client created.
3428
+ */
2776
3429
  cancel(run) {
2777
3430
  return this.session(run).cancel();
2778
3431
  }
2779
- /** Read the current durable status without waiting for terminal settlement. */
3432
+ /**
3433
+ * Read the current durable status without waiting for terminal settlement.
3434
+ *
3435
+ * @deprecated Use `run.status()`. Kept through the same compatibility
3436
+ * window as `events(run)`. It accepts only a run this client created.
3437
+ */
2780
3438
  status(run) {
2781
3439
  return this.session(run).status();
2782
3440
  }
@@ -2793,6 +3451,12 @@ var Nika = class {
2793
3451
  }
2794
3452
  return this.transport.traceVerify(receipt, options);
2795
3453
  }
3454
+ /** One session per admitted run, owned by this client and by no registry. */
3455
+ own(source) {
3456
+ const session = new RunSession(source, this.eventBufferSize, this.transportKind);
3457
+ this.sessions.set(session.run, session);
3458
+ return session.run;
3459
+ }
2796
3460
  session(run) {
2797
3461
  const session = this.sessions.get(run);
2798
3462
  if (!session) throw new NikaRunOwnershipError();
@@ -2885,5 +3549,6 @@ function jobId(value) {
2885
3549
  NikaTransportError,
2886
3550
  isNikaRunSealedEvent,
2887
3551
  isNikaRunSettledEvent,
3552
+ isNikaRunSucceeded,
2888
3553
  isNikaTerminalEvent
2889
3554
  });