@orkestrel/study 0.0.2 → 0.0.3

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.
@@ -4,13 +4,15 @@ let _orkestrel_contract = require("@orkestrel/contract");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_path = require("node:path");
6
6
  let node_fs = require("node:fs");
7
+ let node_fs_promises = require("node:fs/promises");
7
8
  let node_module = require("node:module");
8
9
  let node_url = require("node:url");
9
- let node_fs_promises = require("node:fs/promises");
10
10
  let node_child_process = require("node:child_process");
11
11
  let node_timers_promises = require("node:timers/promises");
12
12
  let _orkestrel_tool = require("@orkestrel/tool");
13
13
  let _orkestrel_budget = require("@orkestrel/budget");
14
+ let _orkestrel_abort = require("@orkestrel/abort");
15
+ let _orkestrel_timeout = require("@orkestrel/timeout");
14
16
  //#region src/server/helpers.ts
15
17
  /** Reports whether a caught value is a Node error carrying the given system code.
16
18
  * @remarks Requires an `Error` from this realm. Structured cloning, such as a worker's `postMessage`,
@@ -93,6 +95,40 @@ var STUDY_RESERVED_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
93
95
  /** Discloses the limits of neutral identifiers when public text remains visible. */
94
96
  var STUDY_PACKET_DISCLOSURE = "Identifiers omit treatment and model names. Input and product text may still reveal design details.";
95
97
  //#endregion
98
+ //#region src/server/ollama/validators.ts
99
+ /** Checks whether a value declares independent positive metadata allowances.
100
+ * @param value - The untrusted metadata declaration.
101
+ * @returns True if the declaration has positive safe calls and bytes and finite duration; false otherwise.
102
+ */
103
+ function isStudyOllamaMetadata(value) {
104
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && typeof value.calls === "number" && Number.isSafeInteger(value.calls) && value.calls > 0 && typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes > 0 && typeof value.duration === "number" && Number.isFinite(value.duration) && value.duration > 0);
105
+ }
106
+ /** Checks whether a value pins a served model name and SHA-256 digest.
107
+ * @param value - The untrusted model pin.
108
+ * @returns True if the value declares a nonempty name and lowercase SHA-256 digest; false otherwise.
109
+ */
110
+ function isStudyOllamaModel(value) {
111
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && typeof value.name === "string" && value.name.length > 0 && typeof value.digest === "string" && /^[a-f0-9]{64}$/.test(value.digest));
112
+ }
113
+ /** Checks whether a value declares a complete finite model-generation policy.
114
+ * @param value - The untrusted role policy.
115
+ * @returns True if every policy member and output allowance is valid; false otherwise.
116
+ */
117
+ function isStudyOllamaPolicy(value) {
118
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && typeof value.model === "string" && value.model.length > 0 && typeof value.think === "boolean" && typeof value.strict === "boolean" && typeof value.length === "boolean" && (0, _orkestrel_contract.isRecord)(value.options) && Object.values(value.options).every((entry) => typeof entry === "number" && Number.isFinite(entry)) && typeof value.options.num_predict === "number" && Number.isSafeInteger(value.options.num_predict) && value.options.num_predict > 0);
119
+ }
120
+ /** Checks whether a value declares exclusive exact or capture admission with a valid receipt.
121
+ * @param value - The untrusted ordered step.
122
+ * @returns True if identity, policy, admission shape and exact receipt are valid; false otherwise.
123
+ */
124
+ function isStudyOllamaStep(value) {
125
+ return (0, _orkestrel_contract.holds)(() => {
126
+ if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.ordinal !== "number" || !Number.isSafeInteger(value.ordinal) || value.ordinal <= 0 || typeof value.goal !== "string" || !_src_core.STUDY_NAME.test(value.goal) || value.role !== "agent" && value.role !== "summary" && value.role !== "judge" || value.path !== (value.role === "judge" ? "/api/generate" : "/api/chat") || !isStudyOllamaPolicy(value.policy) || value.role === "judge" && value.policy.strict) return false;
127
+ if (value.admission === "capture") return typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes > 0 && !Object.hasOwn(value, "body") && !Object.hasOwn(value, "input");
128
+ return value.admission === "exact" && typeof value.body === "string" && value.body.length > 0 && typeof value.input === "string" && value.input === hashStudyBytes(value.body) && !Object.hasOwn(value, "bytes");
129
+ });
130
+ }
131
+ //#endregion
96
132
  //#region src/server/stores/validators.ts
97
133
  /**
98
134
  * Checks whether a trial name is portable on all supported hosts.
@@ -126,7 +162,7 @@ function isStudyCustodyManifest(value) {
126
162
  "schedule",
127
163
  "bounds",
128
164
  "host"
129
- ], ["qualification"]) || !isStudyCustodyName(value.id) || !(0, _src_core.isStudyTimestamp)(value.created) || !(0, _src_core.isStudyDefinition)(value.definition) || !(0, _orkestrel_contract.isArray)(value.scenarios) || !value.scenarios.every(_src_core.isStudyScenario) || !(0, _src_core.isStudyBounds)(value.bounds) || !(0, _orkestrel_contract.isRecord)(value.identity) || !(0, _src_core.matchesStudyKeys)(value.identity, [
165
+ ], ["qualification", "record"]) || !isStudyCustodyName(value.id) || !(0, _src_core.isStudyTimestamp)(value.created) || !(0, _src_core.isStudyDefinition)(value.definition) || !(0, _orkestrel_contract.isArray)(value.scenarios) || !value.scenarios.every(_src_core.isStudyScenario) || !(0, _src_core.isStudyBounds)(value.bounds) || value.record !== void 0 && !(0, _src_core.isStudyRecord)(value.record) || !(0, _orkestrel_contract.isRecord)(value.identity) || !(0, _src_core.matchesStudyKeys)(value.identity, [
130
166
  "agent",
131
167
  "subject",
132
168
  "instrument",
@@ -269,7 +305,7 @@ function isStudyCustodyFile(value) {
269
305
  "path",
270
306
  "bytes",
271
307
  "digest"
272
- ]) && typeof value.path === "string" && (value.path === "manifest.json" || /^trials\/[a-z0-9]+(?:-[a-z0-9]+)*\/(?:evidence.jsonl|attempt.json)$/.test(value.path)) && value.path.split("/").every((name) => !STUDY_RESERVED_NAME.test(name)) && typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes >= 0 && (0, _src_core.isStudyDigest)(value.digest);
308
+ ]) && typeof value.path === "string" && (value.path === "manifest.json" || /^trials\/[a-z0-9]+(?:-[a-z0-9]+)*\/(?:evidence.jsonl|attempt.json|exchanges.jsonl)$/.test(value.path)) && value.path.split("/").every((name) => !STUDY_RESERVED_NAME.test(name)) && typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes >= 0 && (0, _src_core.isStudyDigest)(value.digest);
273
309
  });
274
310
  }
275
311
  /**
@@ -290,6 +326,106 @@ function isStudyCustodySeal(value) {
290
326
  ]) && isStudyCustodyName(value.result) && (0, _src_core.isStudyDigest)(value.id) && (0, _orkestrel_contract.isArray)(value.files) && value.files.length > 0 && value.files.every(isStudyCustodyFile) && new Set(value.files.map((entry) => entry.path)).size === value.files.length;
291
327
  });
292
328
  }
329
+ /** Checks whether encoded body bytes have their exact canonical base64 and SHA-256 identity.
330
+ * @param value - The candidate byte envelope.
331
+ * @returns True if decoded length and digest match; false otherwise.
332
+ */
333
+ function isStudyExchangeBytes(value) {
334
+ return (0, _orkestrel_contract.holds)(() => {
335
+ if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.body !== "string" || typeof value.size !== "number" || !Number.isSafeInteger(value.size) || value.size < 0 || !(0, _src_core.isStudyDigest)(value.digest)) return false;
336
+ const bytes = Buffer.from(value.body, "base64");
337
+ return bytes.length === value.size && bytes.toString("base64") === value.body && hashStudyBytes(bytes) === value.digest;
338
+ });
339
+ }
340
+ /** Checks whether an exchange carries an admissible ordered HTTP identity.
341
+ * @param value - The candidate identity.
342
+ * @returns True if identity and stage fields are valid; false otherwise.
343
+ */
344
+ function isStudyExchangeIdentity(value) {
345
+ return (0, _orkestrel_contract.holds)(() => {
346
+ if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.ordinal !== "number" || !Number.isSafeInteger(value.ordinal) || value.ordinal <= 0 || !(0, _src_core.isStudyName)(value.goal) || !(0, _src_core.isStudyRole)(value.role) || value.method !== "GET" && value.method !== "POST") return false;
347
+ if (value.stage === "execution") return (value.role === "agent" || value.role === "summary" || value.role === "judge") && value.method === "POST" && value.path === (value.role === "judge" ? "/api/generate" : "/api/chat") && typeof value.step === "number" && Number.isSafeInteger(value.step) && value.step > 0;
348
+ if (Object.hasOwn(value, "step")) return false;
349
+ if (value.stage === "maintenance") return value.role === "maintenance" && value.method === "POST" && value.path === "/api/generate";
350
+ return (value.stage === "preflight" || value.stage === "postflight") && value.role === "metadata" && (value.path === "/api/version" || value.path === "/api/tags" ? value.method === "GET" : value.path === "/api/show" && value.method === "POST");
351
+ });
352
+ }
353
+ /** Checks whether a retained reply describes bounded successful application bytes and chunks.
354
+ * @param value - The candidate reply.
355
+ * @returns True if status, selected headers and chunk lengths are valid; false otherwise.
356
+ */
357
+ function isStudyExchangeReply(value) {
358
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && isStudyExchangeBytes(value) && "status" in value && typeof value.status === "number" && Number.isSafeInteger(value.status) && value.status >= 200 && value.status <= 599 && "headers" in value && (0, _orkestrel_contract.isArray)(value.headers) && value.headers.every((header) => (0, _orkestrel_contract.isArray)(header) && header.length === 2 && (header[0] === "content-type" || header[0] === "content-encoding") && typeof header[1] === "string" && !/[\r\n]/.test(header[1])) && "chunks" in value && (0, _orkestrel_contract.isArray)(value.chunks) && value.chunks.every((size) => typeof size === "number" && Number.isSafeInteger(size) && size > 0) && value.chunks.reduce((total, size) => total + (typeof size === "number" ? size : 0), 0) === value.size);
359
+ }
360
+ /** Checks whether the opening row declares a finite ordered procedure and serving identities.
361
+ * @param value - The candidate declaration.
362
+ * @returns True if all declared populations are valid; false otherwise.
363
+ */
364
+ function isStudyExchangeDeclaration(value) {
365
+ return (0, _orkestrel_contract.holds)(() => {
366
+ if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _src_core.matchesStudyKeys)(value, [
367
+ "phase",
368
+ "trial",
369
+ "version",
370
+ "models",
371
+ "metadata",
372
+ "procedure",
373
+ "maintenance"
374
+ ]) || value.phase !== "declaration" || !isStudyCustodyName(value.trial) || !(0, _orkestrel_contract.isNonEmptyString)(value.version) || !(0, _orkestrel_contract.isRecord)(value.models) || Object.keys(value.models).length === 0 || !Object.values(value.models).every(isStudyOllamaModel) || !isStudyOllamaMetadata(value.metadata) || !(0, _orkestrel_contract.isArray)(value.procedure) || value.procedure.length === 0 || !value.procedure.every(isStudyOllamaStep) || !(0, _orkestrel_contract.isArray)(value.maintenance) || !value.maintenance.every(isStudyOllamaModel)) return false;
375
+ const models = Object.values(value.models).filter(isStudyOllamaModel);
376
+ return value.procedure.every((step, index) => step.ordinal === index + 1 && models.some((model) => model.name === step.policy.model)) && value.maintenance.every((model, index, entries) => entries.findIndex((entry) => entry.name === model.name) === index && models.some((entry) => entry.name === model.name && entry.digest === model.digest)) && models.every((model) => models.every((other) => model.name !== other.name || model.digest === other.digest));
377
+ });
378
+ }
379
+ /** Checks whether a sidecar row has exactly its declared fields and verified byte identities.
380
+ * @param value - The candidate row.
381
+ * @returns True if the row belongs to the closed sidecar grammar; false otherwise.
382
+ */
383
+ function isStudyExchangeRow(value) {
384
+ return (0, _orkestrel_contract.holds)(() => {
385
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
386
+ if (value.phase === "declaration") return isStudyExchangeDeclaration(value);
387
+ if (value.phase === "complete") return (0, _src_core.matchesStudyKeys)(value, ["phase", "count"]) && typeof value.count === "number" && Number.isSafeInteger(value.count) && value.count > 0;
388
+ const identity = [
389
+ "phase",
390
+ "ordinal",
391
+ "goal",
392
+ "role",
393
+ "path",
394
+ "method",
395
+ "stage"
396
+ ];
397
+ if (!isStudyExchangeIdentity(value)) return false;
398
+ if (value.phase === "attempt") return (0, _src_core.matchesStudyKeys)(value, [
399
+ ...identity,
400
+ "body",
401
+ "size",
402
+ "digest"
403
+ ], ["step"]) && isStudyExchangeBytes(value);
404
+ if (value.phase === "response") return (0, _src_core.matchesStudyKeys)(value, [
405
+ ...identity,
406
+ "body",
407
+ "size",
408
+ "digest",
409
+ "status",
410
+ "headers",
411
+ "chunks"
412
+ ], ["step"]) && isStudyExchangeReply(value) && value.status < 300;
413
+ return value.phase === "failure" && (0, _src_core.matchesStudyKeys)(value, [...identity, "reason"], ["step", "response"]) && typeof value.reason === "string" && (value.response === void 0 || isStudyExchangeReply(value.response));
414
+ });
415
+ }
416
+ /** Checks whether a history has verified row shapes before its complete relational validation.
417
+ * @param value - The candidate history.
418
+ * @returns True if its envelopes and rows have valid types; false otherwise.
419
+ */
420
+ function isStudyExchangeHistory(value) {
421
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.matchesStudyKeys)(value, [
422
+ "seal",
423
+ "file",
424
+ "declaration",
425
+ "exchanges",
426
+ "completion"
427
+ ]) && (0, _src_core.isStudyDigest)(value.seal) && isStudyCustodyFile(value.file) && isStudyExchangeDeclaration(value.declaration) && (0, _orkestrel_contract.isArray)(value.exchanges) && value.exchanges.every((pair) => (0, _orkestrel_contract.isRecord)(pair) && (0, _src_core.matchesStudyKeys)(pair, ["attempt", "response"]) && isStudyExchangeRow(pair.attempt) && pair.attempt.phase === "attempt" && isStudyExchangeRow(pair.response) && pair.response.phase === "response") && isStudyExchangeRow(value.completion) && value.completion.phase === "complete");
428
+ }
293
429
  //#endregion
294
430
  //#region src/server/adapters/validators.ts
295
431
  /** Checks whether a factory returned the complete consumer adapter contract.
@@ -419,28 +555,249 @@ var ARTIFACT_VISITS = 32768;
419
555
  var ARTIFACT_DEPTH = 64;
420
556
  /** Bounds selected package roots before traversal begins. */
421
557
  var ARTIFACT_ROOTS = 128;
558
+ /** Marks artifact errors across module formats and installations. */
559
+ var ARTIFACT_ERROR_BRAND = Symbol.for("@orkestrel/study.ArtifactError");
560
+ /** Lists the artifact refusal codes. */
561
+ var ARTIFACT_ERROR_CODES = Object.freeze([
562
+ "ARTIFACT",
563
+ "TRAVERSAL",
564
+ "REDIRECT",
565
+ "ALIAS",
566
+ "NAME",
567
+ "DUPLICATE",
568
+ "OCCUPIED",
569
+ "OVERLAP",
570
+ "STALE",
571
+ "VOLUME",
572
+ "DRIFT",
573
+ "LIMIT"
574
+ ]);
422
575
  //#endregion
423
576
  //#region src/server/artifacts/errors.ts
424
577
  /** Reports an invalid or drifted artifact inventory. */
425
578
  var ArtifactError = class extends Error {
426
- code = "ARTIFACT";
579
+ code;
427
580
  /** Records the refused artifact boundary.
428
581
  * @param message - The specific refusal.
582
+ * @param code - The refused boundary. Default: ARTIFACT.
429
583
  */
430
- constructor(message) {
584
+ constructor(message, code = "ARTIFACT") {
431
585
  super(message);
586
+ _orkestrel_contract.INTRINSICS.define(this, ARTIFACT_ERROR_BRAND, { value: this });
432
587
  this.name = "ArtifactError";
588
+ this.code = code;
433
589
  }
434
590
  };
435
591
  /** Narrows a caught value to an artifact refusal.
592
+ * @remarks
593
+ * Recognizes subclasses and other module copies by an own identity brand and valid members.
594
+ * The brand is a recognition marker. A transparent proxy, a look-alike without the brand,
595
+ * throwing accessors, and revoked proxies return false.
436
596
  * @param value - The caught value.
437
597
  * @returns True if the value is an artifact error; false otherwise.
438
598
  */
439
599
  function isArtifactError(value) {
440
- return (0, _orkestrel_contract.isInstance)(value, ArtifactError);
600
+ return (0, _orkestrel_contract.holds)(() => {
601
+ if (!(value instanceof _orkestrel_contract.INTRINSICS.error) || _orkestrel_contract.INTRINSICS.prototype(value) === _orkestrel_contract.INTRINSICS.error.prototype) return false;
602
+ if (_orkestrel_contract.INTRINSICS.describe(value, ARTIFACT_ERROR_BRAND)?.value !== value) return false;
603
+ const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
604
+ const stack = _orkestrel_contract.INTRINSICS.reflect.read(value, "stack");
605
+ return ARTIFACT_ERROR_CODES.some((candidate) => candidate === code) && (0, _orkestrel_contract.isString)(_orkestrel_contract.INTRINSICS.reflect.read(value, "name")) && (0, _orkestrel_contract.isString)(_orkestrel_contract.INTRINSICS.reflect.read(value, "message")) && (stack === void 0 || (0, _orkestrel_contract.isString)(stack));
606
+ });
607
+ }
608
+ //#endregion
609
+ //#region src/server/artifacts/validators.ts
610
+ /** Checks whether a key names portable, non-credential members within a closure.
611
+ * @param value - The untrusted slash-separated key.
612
+ * @returns True if every segment is safe and contained; false otherwise.
613
+ */
614
+ function isArtifactKey(value) {
615
+ return typeof value === "string" && value !== "" && !/[\\<>:"|?*]/u.test(value) && !Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) && value.split("/").every((name) => name !== "" && name !== "." && name !== ".." && !/[. ]$/.test(name) && !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name) && !matchesArtifactCredential(name));
616
+ }
617
+ /** Checks the bounded loader selections retained in a publication.
618
+ * @param value - The decoded selections.
619
+ * @returns True if required selections and optional groups have their declared shapes; false otherwise.
620
+ */
621
+ function isArtifactLoaderOptions(value) {
622
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.entries) && (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.roots) && value.entries.length <= 8192 && value.roots.length <= 128 && (value.source === void 0 || (0, _orkestrel_contract.isRecord)(value.source) && (0, _orkestrel_contract.isString)(value.source.root) && (0, _orkestrel_contract.isString)(value.source.entry)) && (value.instrument === void 0 || (0, _orkestrel_contract.isString)(value.instrument)) && (value.excluded === void 0 || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.excluded)) && (value.assets === void 0 || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.assets) && value.assets.length <= 8192));
623
+ }
624
+ /** Checks the exact file identity and provenance of a closure member.
625
+ * @param value - The decoded member.
626
+ * @returns True if the member has a safe key, source, digest and bounded size; false otherwise.
627
+ */
628
+ function isArtifactMember(value) {
629
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isString)(value.source) && isArtifactKey(value.key) && (0, _src_core.isStudyDigest)(value.digest) && typeof value.size === "number" && Number.isSafeInteger(value.size) && value.size >= 0);
630
+ }
631
+ /** Checks a bounded materialization record before any path is used.
632
+ * @param value - The decoded record.
633
+ * @returns True if its selections and member records have the declared shapes; false otherwise.
634
+ */
635
+ function isArtifactClosure(value) {
636
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isString)(value.destination) && (0, _src_core.isStudyDigest)(value.digest) && isArtifactLoaderOptions(value.options) && (0, _orkestrel_contract.isArray)(value.members) && value.members.length > 0 && value.members.length <= 8192 && value.members.every(isArtifactMember));
441
637
  }
442
638
  //#endregion
443
639
  //#region src/server/artifacts/helpers.ts
640
+ /** Checks whether a portable segment identifies credentials.
641
+ * @param value - One untrusted path segment.
642
+ * @returns True if the segment has a credential filename; false otherwise.
643
+ */
644
+ function matchesArtifactCredential(value) {
645
+ return /^(?:\.env(?:\..*)?|\.npmrc|auth\.json|id_(?:rsa|dsa|ecdsa|ed25519))$/i.test(value) || /\.(?:key|pem|p12|pfx)$/i.test(value) || /(?:^|[._-])token(?:$|[._-])/i.test(value);
646
+ }
647
+ /** Resolves a portable member key without traversing outside its destination.
648
+ * @param destination - The canonical absolute destination.
649
+ * @param key - The slash-separated member key.
650
+ * @returns The contained host path.
651
+ * @throws ArtifactError - Thrown for traversal or an unsafe name.
652
+ */
653
+ function resolveArtifactMember(destination, key) {
654
+ if ((0, node_path.isAbsolute)(key) || key.split("/").some((name) => name === ".." || name === ".") || /[\\:]/.test(key)) throw new ArtifactError("Artifact member traverses its boundary", "TRAVERSAL");
655
+ if (!isArtifactKey(key)) throw new ArtifactError("Artifact member name is unsafe", "NAME");
656
+ const path = (0, node_path.resolve)(destination, ...key.split("/"));
657
+ if (!containsArtifact((0, node_path.resolve)(destination), path)) throw new ArtifactError("Artifact member traverses its boundary", "TRAVERSAL");
658
+ return path;
659
+ }
660
+ /** Checks every existing physical ancestor before using a canonical absolute path.
661
+ * @param path - The existing absolute file or directory.
662
+ * @returns Its physical filesystem metadata.
663
+ * @throws ArtifactError - Thrown for traversal, unsafe names, aliases or redirects before reading bytes.
664
+ */
665
+ function readArtifactPhysical(path) {
666
+ if (!(0, node_path.isAbsolute)(path) || (0, node_path.resolve)(path) !== path) throw new ArtifactError("Artifact path must be canonical and absolute", "TRAVERSAL");
667
+ const origin = (0, node_path.parse)(path).root;
668
+ const names = (0, node_path.relative)(origin, path).split(node_path.sep).filter(Boolean);
669
+ if (names.some((name) => !isArtifactKey(name))) throw new ArtifactError("Artifact physical path has an unsafe name", "NAME");
670
+ for (const [index] of names.entries()) {
671
+ const candidate = (0, node_path.join)(origin, ...names.slice(0, index + 1));
672
+ if ((0, node_fs.lstatSync)(candidate, { bigint: true }).isSymbolicLink()) throw new ArtifactError("Artifact path is redirected", "REDIRECT");
673
+ const physical = (0, node_fs.realpathSync)(candidate);
674
+ if (physical !== candidate) throw new ArtifactError("Artifact path has a physical alias or redirect", physical.toLowerCase() === candidate.toLowerCase() ? "ALIAS" : "REDIRECT");
675
+ }
676
+ return (0, node_fs.lstatSync)(path, { bigint: true });
677
+ }
678
+ /** Finds the common physical parent that preserves every selected path's relative resolution.
679
+ * @param paths - Existing absolute members or selected roots on one volume.
680
+ * @returns Their common ancestor directory.
681
+ * @throws ArtifactError - Thrown when selections span volumes or contain no files.
682
+ */
683
+ function resolveArtifactBase(paths) {
684
+ const first = paths[0];
685
+ if (first === void 0) throw new ArtifactError("Artifact population is empty");
686
+ if (paths.some((path) => (0, node_path.parse)(path).root !== (0, node_path.parse)(first).root)) throw new ArtifactError("Artifact sources require one volume", "VOLUME");
687
+ const origin = (0, node_path.parse)(first).root;
688
+ const names = (0, node_path.relative)(origin, (0, node_path.dirname)(first)).split(node_path.sep).filter(Boolean);
689
+ const base = Array.from({ length: names.length + 1 }, (_entry, index) => (0, node_path.join)(origin, ...names.slice(0, index))).reverse().find((parent) => paths.every((path) => parent === path || containsArtifact(parent, path)));
690
+ if (base === void 0) throw new ArtifactError("Artifact sources have no common parent", "VOLUME");
691
+ return base;
692
+ }
693
+ /** Relocates an original path through a closure's retained physical forest.
694
+ * @param closure - The verified closure record.
695
+ * @param path - An original member or ancestor within its forest.
696
+ * @returns The corresponding destination path.
697
+ * @throws ArtifactError - Thrown when provenance or containment is inconsistent.
698
+ */
699
+ function relocateArtifactPath(closure, path) {
700
+ const first = closure.members[0];
701
+ if (first === void 0) throw new ArtifactError("Artifact population is empty");
702
+ const origin = (0, node_path.resolve)(first.source, ...first.key.split("/").map(() => ".."));
703
+ if (path !== origin && !containsArtifact(origin, path)) throw new ArtifactError("Artifact relocation traverses its forest", "TRAVERSAL");
704
+ if (closure.members.some((member) => resolveArtifactMember(origin, member.key) !== member.source)) throw new ArtifactError("Artifact member provenance is inconsistent", "TRAVERSAL");
705
+ return path === origin ? closure.destination : resolveArtifactMember(closure.destination, (0, node_path.relative)(origin, path).split(node_path.sep).join("/"));
706
+ }
707
+ /** Computes a destination-independent digest of members and relative package identities.
708
+ * @param members - Complete copied membership, with source provenance excluded from identity.
709
+ * @param packages - Package identities with relocated roots.
710
+ * @param destination - The common destination directory.
711
+ * @returns The portable SHA-256 identity.
712
+ */
713
+ function computeArtifactClosureDigest(members, packages, destination) {
714
+ return hashStudyBytes(encodeStudyJSON({
715
+ members: members.map(({ key, digest, size }) => ({
716
+ key,
717
+ digest,
718
+ size
719
+ })).toSorted((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0),
720
+ packages: packages.map((entry) => ({
721
+ ...entry,
722
+ root: (0, node_path.relative)(destination, entry.root).split(node_path.sep).join("/")
723
+ })).toSorted((left, right) => left.root < right.root ? -1 : left.root > right.root ? 1 : 0)
724
+ }));
725
+ }
726
+ /** Verifies complete copied membership, path safety, exact bytes, and portable identity.
727
+ * @param closure - The materialization record to verify independently.
728
+ * @param published - If true, permits the root publication marker; if false, refuses every extra file. Default: false.
729
+ * @returns Nothing when the entire forest matches.
730
+ * @throws ArtifactError - Thrown for malformed records, changed membership, bytes, or physical paths.
731
+ */
732
+ function verifyArtifactClosure(closure, published = false) {
733
+ if (!isArtifactClosure(closure)) throw new ArtifactError("Artifact closure record is invalid", "NAME");
734
+ if (!readArtifactPhysical(closure.destination).isDirectory()) throw new ArtifactError("Artifact closure requires a directory", "DRIFT");
735
+ const keys = new Set(closure.members.map((member) => member.key));
736
+ if (keys.size !== closure.members.length) throw new ArtifactError("Artifact member key is duplicated", "DUPLICATE");
737
+ if (new Set(closure.members.map((member) => member.key.toLowerCase())).size !== closure.members.length) throw new ArtifactError("Artifact member keys have a case alias", "ALIAS");
738
+ const directories = [{
739
+ path: closure.destination,
740
+ depth: 0
741
+ }];
742
+ const visited = [];
743
+ const files = [];
744
+ for (const directory of directories) {
745
+ if (directory.depth > 64) throw new ArtifactError("Artifact directory depth limit exceeded", "LIMIT");
746
+ const handle = (0, node_fs.opendirSync)(directory.path);
747
+ try {
748
+ for (const entry of iterateArtifactEntries(handle)) {
749
+ const path = (0, node_path.join)(directory.path, entry.name);
750
+ visited.push(path);
751
+ if (visited.length > 32768) throw new ArtifactError("Artifact traversal limit exceeded", "LIMIT");
752
+ const stat = readArtifactPhysical(path);
753
+ if (stat.isDirectory()) {
754
+ if (!closure.members.some((member) => containsArtifact(path, resolveArtifactMember(closure.destination, member.key)))) throw new ArtifactError("Artifact closure membership drifted", "DRIFT");
755
+ directories.push({
756
+ path,
757
+ depth: directory.depth + 1
758
+ });
759
+ continue;
760
+ }
761
+ if (!stat.isFile()) throw new ArtifactError("Artifact closure contains a non-file", "DRIFT");
762
+ if (published && path === (0, node_path.join)(closure.destination, "closure.json")) continue;
763
+ const key = (0, node_path.relative)(closure.destination, path).split(node_path.sep).join("/");
764
+ if (!keys.has(key)) throw new ArtifactError("Artifact closure membership drifted", "DRIFT");
765
+ files.push({
766
+ path,
767
+ identity: `${stat.dev}:${stat.ino}`,
768
+ size: Number(stat.size)
769
+ });
770
+ if (files.length > 8192) throw new ArtifactError("Artifact closure exceeds its capture allowance", "LIMIT");
771
+ }
772
+ } finally {
773
+ handle.closeSync();
774
+ }
775
+ }
776
+ if (files.length !== closure.members.length) throw new ArtifactError("Artifact closure membership drifted", "DRIFT");
777
+ if (files.reduce((sum, file) => sum + file.size, 0) > 134217728) throw new ArtifactError("Artifact closure exceeds its capture allowance", "LIMIT");
778
+ if (new Set(files.map((file) => file.identity)).size !== files.length) throw new ArtifactError("Artifact members have a physical alias", "ALIAS");
779
+ for (const member of closure.members) {
780
+ const file = readArtifact(resolveArtifactMember(closure.destination, member.key));
781
+ if (file.digest !== member.digest || file.size !== member.size) throw new ArtifactError("Artifact closure bytes drifted", "DRIFT");
782
+ }
783
+ if ([
784
+ ...closure.options.entries,
785
+ ...closure.options.assets ?? [],
786
+ ...closure.options.excluded ?? [],
787
+ ...closure.options.source === void 0 ? [] : [closure.options.source.entry]
788
+ ].some((path) => !files.some((file) => file.path === path)) || closure.options.roots.some((root) => root !== closure.destination && !containsArtifact(closure.destination, root)) || closure.options.source !== void 0 && closure.options.source.root !== closure.destination && !containsArtifact(closure.destination, closure.options.source.root)) throw new ArtifactError("Artifact closure selection traverses its population", "TRAVERSAL");
789
+ const packages = closure.options.roots.map((root) => {
790
+ const manifest = readArtifact((0, node_path.join)(root, "package.json"));
791
+ const value = (0, _orkestrel_contract.parseJSON)(Buffer.from(manifest.source).toString("utf8"));
792
+ if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.name !== "string" || typeof value.version !== "string") throw new ArtifactError("Artifact closure manifest is invalid", "DRIFT");
793
+ return {
794
+ root,
795
+ name: value.name,
796
+ version: value.version
797
+ };
798
+ });
799
+ if (computeArtifactClosureDigest(closure.members, packages, closure.destination) !== closure.digest) throw new ArtifactError("Artifact closure identity drifted", "DRIFT");
800
+ }
444
801
  /** Fills a buffer from an open descriptor, one positioned read at a time.
445
802
  * @param descriptor - The open file descriptor to read.
446
803
  * @param target - The buffer to fill.
@@ -471,6 +828,7 @@ function* iterateArtifactEntries(handle) {
471
828
  */
472
829
  function readArtifact(path, maximum = ARTIFACT_FILE_BYTES) {
473
830
  if (!Number.isSafeInteger(maximum) || maximum < 0 || !(0, node_path.isAbsolute)(path) || (0, node_path.resolve)(path) !== path || (0, node_fs.realpathSync)(path) !== path || !(0, node_fs.lstatSync)(path).isFile()) throw new ArtifactError(`Artifact requires a canonical regular file and byte budget: ${path}`);
831
+ readArtifactPhysical(path);
474
832
  const descriptor = (0, node_fs.openSync)(path, "r");
475
833
  try {
476
834
  const before = (0, node_fs.fstatSync)(descriptor);
@@ -601,7 +959,7 @@ var Resolution = class {
601
959
  //#region src/server/artifacts/ArtifactLoader.ts
602
960
  /** Captures built ESM and a separate strip-only consumer adapter in a fresh worker.
603
961
  * @remarks
604
- * Loads trusted code, not a hostile-code sandbox. Native process APIs, eval and external
962
+ * Loads trusted code without sandboxing untrusted code. Native process APIs, eval and external
605
963
  * services are not replayed. One inventory may load per process; terminate it afterwards.
606
964
  */
607
965
  var ArtifactLoader = class ArtifactLoader {
@@ -610,6 +968,7 @@ var ArtifactLoader = class ArtifactLoader {
610
968
  #inventory;
611
969
  #sources = [];
612
970
  #token;
971
+ #references = /* @__PURE__ */ new Map();
613
972
  /** Captures selections without reading or executing selected modules.
614
973
  * @param options - Canonical built selections and optional consumer source selection.
615
974
  */
@@ -619,8 +978,12 @@ var ArtifactLoader = class ArtifactLoader {
619
978
  ...options,
620
979
  entries: [...options.entries],
621
980
  roots: [...options.roots],
622
- ...options.source === void 0 ? {} : { source: { ...options.source } },
623
- ...options.excluded === void 0 ? {} : { excluded: [...options.excluded] }
981
+ ...options.source === void 0 ? {} : { source: {
982
+ root: options.source.root,
983
+ entry: options.source.entry
984
+ } },
985
+ ...options.excluded === void 0 ? {} : { excluded: [...options.excluded] },
986
+ ...options.assets === void 0 ? {} : { assets: [...options.assets] }
624
987
  };
625
988
  }
626
989
  /** Reads and identifies admitted files without evaluating selected modules.
@@ -680,9 +1043,14 @@ var ArtifactLoader = class ArtifactLoader {
680
1043
  path,
681
1044
  digest,
682
1045
  size
683
- })))
1046
+ }))),
1047
+ ...this.#options.assets === void 0 ? {} : { assets: Object.freeze([...this.#options.assets].sort()) }
684
1048
  };
685
1049
  this.#sources = sources;
1050
+ this.#references = new Map(sources.map((file) => {
1051
+ const stat = readArtifactPhysical(file.path);
1052
+ return [file.path, `${stat.dev}:${stat.ino}:${stat.mtimeNs}:${stat.ctimeNs}`];
1053
+ }));
686
1054
  this.#inventory = Object.freeze({
687
1055
  digest: hashStudyBytes(JSON.stringify(evidence)),
688
1056
  ...evidence
@@ -712,7 +1080,7 @@ var ArtifactLoader = class ArtifactLoader {
712
1080
  const token = (0, node_crypto.randomUUID)();
713
1081
  const resolution = new Resolution({
714
1082
  token,
715
- files: this.#sources,
1083
+ files: this.#sources.filter((file) => this.#options.roots.some((root) => file.path === (0, node_path.join)(root, "package.json") || containsArtifact((0, node_path.join)(root, "dist"), file.path)) || this.#options.source !== void 0 && containsArtifact(this.#options.source.root, file.path)),
716
1084
  ...inventory.source === void 0 ? {} : { source: inventory.source },
717
1085
  ...inventory.instrument === void 0 ? {} : { instrument: inventory.instrument },
718
1086
  ...this.#options.excluded === void 0 ? {} : { excluded: this.#options.excluded }
@@ -730,6 +1098,123 @@ var ArtifactLoader = class ArtifactLoader {
730
1098
  this.verify();
731
1099
  return namespace;
732
1100
  }
1101
+ /** Copies the inspected physical forest into an exclusive absent destination.
1102
+ * @param destination - The canonical absent absolute directory under an existing physical parent.
1103
+ * @returns The independently verified copied membership and relocated selections.
1104
+ * @throws ArtifactError - Thrown at the refused path, stale-source, volume, or drift boundary.
1105
+ */
1106
+ async materialize(destination) {
1107
+ if (new Set(this.#options.roots).size !== this.#options.roots.length) throw new ArtifactError("Artifact package root is duplicated", "DUPLICATE");
1108
+ if (!(0, node_path.isAbsolute)(destination) || (0, node_path.resolve)(destination) !== destination) throw new ArtifactError("Artifact destination must be canonical and absolute", "TRAVERSAL");
1109
+ const boundaries = [
1110
+ ...this.#options.roots,
1111
+ ...this.#options.source === void 0 ? [] : [this.#options.source.root],
1112
+ ...this.#options.assets ?? []
1113
+ ];
1114
+ if (boundaries.some((path) => (0, node_path.parse)(path).root !== (0, node_path.parse)(destination).root)) throw new ArtifactError("Artifact closure requires one volume", "VOLUME");
1115
+ if (boundaries.some((path) => path === destination || containsArtifact(path, destination) || containsArtifact(destination, path))) throw new ArtifactError("Artifact destination overlaps a source", "OVERLAP");
1116
+ const parent = readArtifactPhysical((0, node_path.dirname)(destination));
1117
+ resolveArtifactMember((0, node_path.dirname)(destination), (0, node_path.relative)((0, node_path.dirname)(destination), destination));
1118
+ if (!parent.isDirectory()) throw new ArtifactError("Artifact destination parent must be a directory", "REDIRECT");
1119
+ try {
1120
+ (0, node_fs.lstatSync)(destination);
1121
+ throw new ArtifactError("Artifact destination must be absent", "OCCUPIED");
1122
+ } catch (error) {
1123
+ if (!matchesSystemError(error, "ENOENT")) throw error;
1124
+ }
1125
+ const inspected = this.#inventory !== void 0;
1126
+ try {
1127
+ for (const path of boundaries) readArtifactPhysical(path);
1128
+ this.inspect();
1129
+ for (const file of this.#sources) {
1130
+ const stat = readArtifactPhysical(file.path);
1131
+ if (stat.dev !== parent.dev) throw new ArtifactError("Artifact closure requires one volume", "VOLUME");
1132
+ if (`${stat.dev}:${stat.ino}:${stat.mtimeNs}:${stat.ctimeNs}` !== this.#references.get(file.path)) throw new ArtifactError("Artifact source reference is stale", "STALE");
1133
+ }
1134
+ } catch (error) {
1135
+ if (inspected && !(isArtifactError(error) && [
1136
+ "REDIRECT",
1137
+ "ALIAS",
1138
+ "NAME",
1139
+ "VOLUME"
1140
+ ].includes(error.code))) throw new ArtifactError("Artifact source inspection is stale", "STALE");
1141
+ throw error;
1142
+ }
1143
+ const base = resolveArtifactBase([
1144
+ ...this.#sources.map((file) => file.path),
1145
+ ...this.#options.roots,
1146
+ ...this.#options.source === void 0 ? [] : [this.#options.source.root]
1147
+ ]);
1148
+ const members = Object.freeze(this.#sources.map((file) => Object.freeze({
1149
+ source: file.path,
1150
+ key: (0, node_path.relative)(base, file.path).split(node_path.sep).join("/"),
1151
+ digest: file.digest,
1152
+ size: file.size
1153
+ })));
1154
+ if (new Set(members.map((member) => member.key)).size !== members.length) throw new ArtifactError("Artifact member key is duplicated", "DUPLICATE");
1155
+ if (new Set(members.map((member) => member.key.toLowerCase())).size !== members.length) throw new ArtifactError("Artifact member keys have a case alias", "ALIAS");
1156
+ if (members.some((member) => member.key.split("/").length - 1 > 64)) throw new ArtifactError("Artifact closure directory depth limit exceeded", "LIMIT");
1157
+ if (new Set(members.flatMap((member) => member.key.split("/").slice(0, -1).map((_name, index, names) => names.slice(0, index + 1).join("/")))).size + members.length > 32768) throw new ArtifactError("Artifact closure traversal limit exceeded", "LIMIT");
1158
+ for (const member of members) resolveArtifactMember(destination, member.key);
1159
+ const identities = this.#sources.map((file) => {
1160
+ const stat = readArtifactPhysical(file.path);
1161
+ return `${stat.dev}:${stat.ino}`;
1162
+ });
1163
+ if (new Set(identities).size !== identities.length) throw new ArtifactError("Artifact members have a physical alias", "ALIAS");
1164
+ const relocation = {
1165
+ destination,
1166
+ digest: "",
1167
+ options: this.#options,
1168
+ members
1169
+ };
1170
+ const options = Object.freeze({
1171
+ entries: Object.freeze(this.#options.entries.map((path) => relocateArtifactPath(relocation, path))),
1172
+ roots: Object.freeze(this.#options.roots.map((path) => relocateArtifactPath(relocation, path))),
1173
+ ...this.#options.source === void 0 ? {} : { source: Object.freeze({
1174
+ root: relocateArtifactPath(relocation, this.#options.source.root),
1175
+ entry: relocateArtifactPath(relocation, this.#options.source.entry)
1176
+ }) },
1177
+ ...this.#options.instrument === void 0 ? {} : { instrument: relocateArtifactPath(relocation, this.#options.instrument) },
1178
+ ...this.#options.excluded === void 0 ? {} : { excluded: Object.freeze(this.#options.excluded.map((path) => relocateArtifactPath(relocation, path))) },
1179
+ ...this.#options.assets === void 0 ? {} : { assets: Object.freeze(this.#options.assets.map((path) => relocateArtifactPath(relocation, path))) }
1180
+ });
1181
+ const packages = this.inspect().packages.map((entry) => ({
1182
+ ...entry,
1183
+ root: relocateArtifactPath(relocation, entry.root)
1184
+ }));
1185
+ const closure = Object.freeze({
1186
+ destination,
1187
+ digest: computeArtifactClosureDigest(members, packages, destination),
1188
+ options,
1189
+ members
1190
+ });
1191
+ (0, node_fs.mkdirSync)(destination);
1192
+ for (const file of this.#sources) {
1193
+ const target = relocateArtifactPath(closure, file.path);
1194
+ this.#directory(closure.destination, (0, node_path.dirname)(target));
1195
+ await (0, node_fs_promises.writeFile)(target, file.source, { flag: "wx" });
1196
+ }
1197
+ try {
1198
+ this.verify();
1199
+ } catch {
1200
+ throw new ArtifactError("Artifact source drifted during copying", "DRIFT");
1201
+ }
1202
+ verifyArtifactClosure(closure);
1203
+ return closure;
1204
+ }
1205
+ #directory(destination, target) {
1206
+ const names = (0, node_path.relative)(destination, target).split(node_path.sep).filter(Boolean);
1207
+ readArtifactPhysical(destination);
1208
+ for (const [index] of names.entries()) {
1209
+ const path = (0, node_path.join)(destination, ...names.slice(0, index + 1));
1210
+ try {
1211
+ (0, node_fs.mkdirSync)(path);
1212
+ } catch (error) {
1213
+ if (!matchesSystemError(error, "EEXIST")) throw error;
1214
+ }
1215
+ if (!readArtifactPhysical(path).isDirectory()) throw new ArtifactError("Artifact destination ancestor is redirected", "REDIRECT");
1216
+ }
1217
+ }
733
1218
  #capture() {
734
1219
  const sources = [];
735
1220
  const tally = this.#options.roots.reduce((counted, root) => {
@@ -742,10 +1227,20 @@ var ArtifactLoader = class ArtifactLoader {
742
1227
  const source = this.#options.source;
743
1228
  if (source !== void 0) {
744
1229
  if (this.#options.roots.some((root) => (0, node_path.join)(root, "dist") === source.root || containsArtifact((0, node_path.join)(root, "dist"), source.root) || containsArtifact(source.root, (0, node_path.join)(root, "dist")))) throw new ArtifactError("Artifact source and built inventories must not overlap");
745
- this.#walk(source.root, sources, true, 0, tally);
746
- }
1230
+ const counted = this.#walk(source.root, sources, true, 0, tally);
1231
+ this.#assets(sources, counted);
1232
+ } else this.#assets(sources, tally);
747
1233
  return sources;
748
1234
  }
1235
+ #assets(sources, tally) {
1236
+ const assets = this.#options.assets ?? [];
1237
+ if (assets.length > 8192) throw new ArtifactError("Artifact asset count limit exceeded", "LIMIT");
1238
+ if (new Set(assets).size !== assets.length) throw new ArtifactError("Artifact asset key is duplicated", "DUPLICATE");
1239
+ assets.reduce((counted, path) => {
1240
+ readArtifactPhysical(path);
1241
+ return sources.some((file) => file.path === path) ? counted : this.#append(path, sources, counted);
1242
+ }, tally);
1243
+ }
749
1244
  #append(path, sources, tally) {
750
1245
  if (sources.length >= 8192) throw new ArtifactError("Artifact file count limit exceeded");
751
1246
  const file = readArtifact(path, ARTIFACT_BYTES - tally.bytes);
@@ -767,7 +1262,7 @@ var ArtifactLoader = class ArtifactLoader {
767
1262
  });
768
1263
  if (visited.visits > 32768) throw new ArtifactError("Artifact traversal limit exceeded");
769
1264
  const path = (0, node_path.join)(directory, entry.name);
770
- if (entry.isSymbolicLink()) throw new ArtifactError(`Artifact links are unsupported: ${path}`);
1265
+ if (entry.isSymbolicLink()) throw new ArtifactError(`Artifact links are unsupported: ${path}`, "REDIRECT");
771
1266
  if (entry.isDirectory()) return this.#walk(path, sources, source, depth + 1, visited);
772
1267
  return (source ? [
773
1268
  ".ts",
@@ -1018,7 +1513,7 @@ async function writeStudyBytes(path, bytes) {
1018
1513
  */
1019
1514
  async function scanStudyExecution(directory, manifest) {
1020
1515
  await checkStudyDirectory(directory);
1021
- const files = await scanStudyTree(directory, "", new Set(manifest.schedule.map((trial) => trial.id)), manifest.bounds.series.bytes);
1516
+ const files = await scanStudyTree(directory, "", new Set(manifest.schedule.map((trial) => trial.id)), manifest.bounds.series.bytes, manifest.record !== void 0);
1022
1517
  return Object.freeze([...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
1023
1518
  }
1024
1519
  /**
@@ -1030,7 +1525,7 @@ async function scanStudyExecution(directory, manifest) {
1030
1525
  * @returns The admitted files in read order: this directory's files, then each admitted subdirectory's, last listed first
1031
1526
  * @throws StudyError - Thrown when the tree holds a link, an unknown directory or file, or a file beyond the remaining allowance
1032
1527
  */
1033
- async function scanStudyTree(directory, prefix, known, allowance) {
1528
+ async function scanStudyTree(directory, prefix, known, allowance, recording = false) {
1034
1529
  const files = [];
1035
1530
  const directories = [];
1036
1531
  for (const entry of await (0, node_fs_promises.readdir)((0, node_path.join)(directory, ...prefix.split("/").filter(Boolean)), { withFileTypes: true })) {
@@ -1042,7 +1537,7 @@ async function scanStudyTree(directory, prefix, known, allowance) {
1042
1537
  continue;
1043
1538
  }
1044
1539
  const names = path.split("/");
1045
- if (!entry.isFile() || !(path === "manifest.json" || names.length === 3 && names[0] === "trials" && known.has(names[1] ?? "") && (names[2] === "attempt.json" || names[2] === "evidence.jsonl"))) throw new _src_core.StudyError("CUSTODY", "Execution contains an unknown file");
1540
+ if (!entry.isFile() || !(path === "manifest.json" || names.length === 3 && names[0] === "trials" && known.has(names[1] ?? "") && (names[2] === "attempt.json" || names[2] === "evidence.jsonl" || recording && names[2] === "exchanges.jsonl"))) throw new _src_core.StudyError("CUSTODY", "Execution contains an unknown file");
1046
1541
  const bytes = await readStudyBytes((0, node_path.join)(directory, ...names), files.reduce((left, file) => left - file.bytes, allowance));
1047
1542
  files.push(Object.freeze({
1048
1543
  path,
@@ -1054,7 +1549,7 @@ async function scanStudyTree(directory, prefix, known, allowance) {
1054
1549
  const steps = [];
1055
1550
  for (const child of [...directories].reverse()) {
1056
1551
  const before = steps.at(-1)?.left ?? remaining;
1057
- const found = await scanStudyTree(directory, child, known, before);
1552
+ const found = await scanStudyTree(directory, child, known, before, recording);
1058
1553
  steps.push({
1059
1554
  found,
1060
1555
  left: found.reduce((left, file) => left - file.bytes, before)
@@ -1192,7 +1687,12 @@ async function readStudyResult(directory) {
1192
1687
  const sealBytes = await readStudyBytes((0, node_path.join)(absolute, "seal.json"), STUDY_CUSTODY_BYTES);
1193
1688
  if (files.reduce((total, file) => total + file.bytes, sealBytes.byteLength) > manifest.bounds.series.bytes) throw new _src_core.StudyError("CUSTODY", "Seal and execution exceed the series allowance");
1194
1689
  const seal = decodeStudyJSON(sealBytes);
1195
- if (!isStudyCustodySeal(seal) || seal.result !== manifest.id || seal.id !== computeStudySealId(manifest.id, files) || (0, _orkestrel_contract.canonicalStringify)(seal.files) !== (0, _orkestrel_contract.canonicalStringify)(files) || attempts.length !== manifest.schedule.length || files.length !== 1 + manifest.schedule.length * 2) throw new _src_core.StudyError("CUSTODY", "Execution seal does not match the complete byte inventory");
1690
+ if (!isStudyCustodySeal(seal) || seal.result !== manifest.id || seal.id !== computeStudySealId(manifest.id, files) || (0, _orkestrel_contract.canonicalStringify)(seal.files) !== (0, _orkestrel_contract.canonicalStringify)(files) || attempts.length !== manifest.schedule.length || files.length !== 1 + manifest.schedule.length * (manifest.record === void 0 ? 2 : 3)) throw new _src_core.StudyError("CUSTODY", "Execution seal does not match the complete byte inventory");
1691
+ if (manifest.record !== void 0) for (const trial of manifest.schedule) {
1692
+ const file = files.find((entry) => entry.path === `trials/${trial.id}/exchanges.jsonl`);
1693
+ if (file === void 0) throw new _src_core.StudyError("CUSTODY", "Recorded trial is missing its exchange sidecar");
1694
+ await readStudyTrialExchanges(absolute, manifest, trial.id, file);
1695
+ }
1196
1696
  return Object.freeze({
1197
1697
  status: "sealed",
1198
1698
  directory: absolute,
@@ -1201,6 +1701,228 @@ async function readStudyResult(directory) {
1201
1701
  seal
1202
1702
  });
1203
1703
  }
1704
+ /** Projects the declaration onto its complete ordered HTTP identity population.
1705
+ * @param declaration - The validated opening row.
1706
+ * @returns Maintenance, preflight, model and postflight identities in declaration order.
1707
+ */
1708
+ function buildStudyExchangeSequence(declaration) {
1709
+ const first = declaration.procedure[0];
1710
+ const last = declaration.procedure.at(-1);
1711
+ if (first === void 0 || last === void 0) throw new _src_core.StudyError("CUSTODY", "Exchange procedure is empty");
1712
+ const models = Object.values(declaration.models).filter((model, index, entries) => entries.findIndex((entry) => entry.name === model.name) === index);
1713
+ const maintenance = declaration.maintenance.map((_model, index) => ({
1714
+ ordinal: index + 1,
1715
+ goal: first.goal,
1716
+ role: "maintenance",
1717
+ path: "/api/generate",
1718
+ method: "POST",
1719
+ stage: "maintenance"
1720
+ }));
1721
+ const preflight = [
1722
+ "/api/version",
1723
+ "/api/tags",
1724
+ ...models.map(() => "/api/show")
1725
+ ].map((path, index) => ({
1726
+ ordinal: maintenance.length + index + 1,
1727
+ goal: first.goal,
1728
+ role: "metadata",
1729
+ path: path === "/api/version" ? "/api/version" : path === "/api/tags" ? "/api/tags" : "/api/show",
1730
+ method: path === "/api/show" ? "POST" : "GET",
1731
+ stage: "preflight"
1732
+ }));
1733
+ const execution = declaration.procedure.map((step, index) => ({
1734
+ ordinal: maintenance.length + preflight.length + index + 1,
1735
+ goal: step.goal,
1736
+ role: step.role,
1737
+ path: step.path,
1738
+ method: "POST",
1739
+ stage: "execution",
1740
+ step: step.ordinal
1741
+ }));
1742
+ const postflight = preflight.map((identity, index) => ({
1743
+ ...identity,
1744
+ ordinal: maintenance.length + preflight.length + execution.length + index + 1,
1745
+ goal: last.goal,
1746
+ stage: "postflight"
1747
+ }));
1748
+ return [
1749
+ ...maintenance,
1750
+ ...preflight,
1751
+ ...execution,
1752
+ ...postflight
1753
+ ];
1754
+ }
1755
+ /** Projects only the repeated identity fields of a retained HTTP exchange.
1756
+ * @param identity - The row whose body and lifecycle fields are omitted.
1757
+ * @returns The identity repeated by its attempt and terminal.
1758
+ */
1759
+ function projectStudyExchangeIdentity(identity) {
1760
+ return {
1761
+ ordinal: identity.ordinal,
1762
+ goal: identity.goal,
1763
+ role: identity.role,
1764
+ path: identity.path,
1765
+ method: identity.method,
1766
+ stage: identity.stage,
1767
+ ...identity.step === void 0 ? {} : { step: identity.step }
1768
+ };
1769
+ }
1770
+ /** Validates the exact declaration order and request bytes of an admitted exchange.
1771
+ * @param declaration - The complete opening procedure.
1772
+ * @param request - The admitted request to join.
1773
+ * @returns Nothing after identity, exact-body or capture-length validation.
1774
+ */
1775
+ function validateStudyExchangeRequest(declaration, request) {
1776
+ const expected = buildStudyExchangeSequence(declaration)[request.ordinal - 1];
1777
+ if (expected === void 0 || (0, _orkestrel_contract.canonicalStringify)(expected) !== (0, _orkestrel_contract.canonicalStringify)(projectStudyExchangeIdentity(request))) throw new _src_core.StudyError("CUSTODY", "Exchange differs from its declared order");
1778
+ const bytes = Buffer.from(request.body, "base64");
1779
+ if (request.stage === "execution") {
1780
+ const step = declaration.procedure.find((entry) => entry.ordinal === request.step);
1781
+ if (step === void 0 || (step.admission === "exact" ? !bytes.equals(Buffer.from(step.body)) || request.digest !== step.input : bytes.length !== step.bytes)) throw new _src_core.StudyError("CUSTODY", "Exchange request differs from its declared admission");
1782
+ return;
1783
+ }
1784
+ if (request.stage === "maintenance") {
1785
+ const model = declaration.maintenance[request.ordinal - 1];
1786
+ const body = (0, _orkestrel_contract.parseJSON)(bytes.toString("utf8"));
1787
+ if (model === void 0 || !(0, _orkestrel_contract.isRecord)(body) || body.model !== model.name || body.keep_alive !== 0) throw new _src_core.StudyError("CUSTODY", "Maintenance request differs from its declaration");
1788
+ return;
1789
+ }
1790
+ const models = Object.values(declaration.models).filter((model, index, entries) => entries.findIndex((entry) => entry.name === model.name) === index);
1791
+ const population = models.length + 2;
1792
+ const model = models[request.ordinal - declaration.maintenance.length - (request.stage === "postflight" ? population + declaration.procedure.length : 0) - 1 - 2];
1793
+ const body = request.path === "/api/show" && model !== void 0 ? JSON.stringify({ model: model.name }) : "";
1794
+ if (!bytes.equals(Buffer.from(body))) throw new _src_core.StudyError("CUSTODY", "Metadata request differs from its declaration");
1795
+ }
1796
+ /** Validates a serialized journal prefix or its complete successful population.
1797
+ * @param rows - The owned rows in physical append order.
1798
+ * @param complete - If true, requires successful completion; if false, admits an unfinished prefix.
1799
+ * @returns Nothing when every row joins its preceding attempt and declared sequence.
1800
+ */
1801
+ function validateStudyExchangeRows(rows, complete = true) {
1802
+ const declaration = rows[0];
1803
+ if (declaration?.phase !== "declaration" || !rows.every(isStudyExchangeRow)) throw new _src_core.StudyError("CUSTODY", "Exchange declaration or row is invalid");
1804
+ const attempts = rows.filter((row) => row.phase === "attempt");
1805
+ for (const [index, row] of rows.entries()) {
1806
+ if (index === 0) continue;
1807
+ const previous = rows[index - 1];
1808
+ if (row.phase === "attempt") {
1809
+ if (previous?.phase !== "declaration" && previous?.phase !== "response") throw new _src_core.StudyError("CUSTODY", "Exchange attempt overlaps an unfinished attempt");
1810
+ if (row.ordinal !== rows.slice(0, index).filter((entry) => entry.phase === "attempt").length + 1) throw new _src_core.StudyError("CUSTODY", "Exchange ordinals are not contiguous");
1811
+ validateStudyExchangeRequest(declaration, row);
1812
+ } else if (row.phase === "response" || row.phase === "failure") {
1813
+ if (previous?.phase !== "attempt" || (0, _orkestrel_contract.canonicalStringify)(projectStudyExchangeIdentity(previous)) !== (0, _orkestrel_contract.canonicalStringify)(projectStudyExchangeIdentity(row))) throw new _src_core.StudyError("CUSTODY", "Exchange terminal has no matching attempt");
1814
+ } else if (row.phase === "complete") {
1815
+ if (index !== rows.length - 1 || previous?.phase !== "response" || row.count !== attempts.length || row.count !== buildStudyExchangeSequence(declaration).length || rows.some((entry) => entry.phase === "failure")) throw new _src_core.StudyError("CUSTODY", "Exchange completion is partial or failed");
1816
+ } else throw new _src_core.StudyError("CUSTODY", "Exchange declaration repeats");
1817
+ }
1818
+ if (complete && rows.at(-1)?.phase !== "complete") throw new _src_core.StudyError("CUSTODY", "Exchange history has no successful completion");
1819
+ }
1820
+ /** Reads strict complete LF-framed exchange rows with verified bodies and declaration joins.
1821
+ * @param bytes - The bounded retained sidecar bytes.
1822
+ * @param complete - If true, requires completion; if false, admits a verified unfinished prefix.
1823
+ * @returns Owned rows in physical order.
1824
+ */
1825
+ function readStudyExchangeRows(bytes, complete = true) {
1826
+ try {
1827
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1828
+ if (!text.endsWith("\n")) throw new _src_core.StudyError("CUSTODY", "Exchange sidecar ends with a partial record");
1829
+ const rows = text.split(/\r\n|\n/).slice(0, -1).map((line) => {
1830
+ const row = (0, _orkestrel_contract.parseJSON)(line);
1831
+ if (!isStudyExchangeRow(row)) throw new _src_core.StudyError("CUSTODY", "Exchange sidecar contains an invalid row");
1832
+ return row;
1833
+ });
1834
+ validateStudyExchangeRows(rows, complete);
1835
+ return rows;
1836
+ } catch (error) {
1837
+ throw new _src_core.StudyError("CUSTODY", error instanceof Error ? error.message : "Invalid exchange bytes");
1838
+ }
1839
+ }
1840
+ /** Validates complete history envelopes again before a replay may perform dependent work.
1841
+ * @param history - The strict-reader result, treated as untrusted at this boundary.
1842
+ * @returns Nothing after complete row, file and declaration verification.
1843
+ */
1844
+ function validateStudyExchangeHistory(history) {
1845
+ if (!isStudyExchangeHistory(history) || history.file.path !== `trials/${history.declaration.trial}/exchanges.jsonl`) throw new _src_core.StudyError("CUSTODY", "Replay requires a verified sealed exchange history");
1846
+ const rows = [
1847
+ history.declaration,
1848
+ ...history.exchanges.flatMap((pair) => [pair.attempt, pair.response]),
1849
+ history.completion
1850
+ ];
1851
+ validateStudyExchangeRows(rows);
1852
+ const bytes = Buffer.from(rows.map((row) => JSON.stringify(row) + "\n").join(""));
1853
+ if (history.file.bytes !== bytes.length || history.file.digest !== hashStudyBytes(bytes)) throw new _src_core.StudyError("CUSTODY", "Replay history differs from its retained file identity");
1854
+ }
1855
+ /** Joins recorded exchanges to exactly one dispatch and wire evidence identity per ordinal.
1856
+ * @param rows - The complete successful sidecar rows.
1857
+ * @param evidence - The validated trial evidence rows.
1858
+ * @returns Nothing when all retained exchange identities have exact evidence counterparts.
1859
+ */
1860
+ function validateStudyExchangeEvidence(rows, evidence) {
1861
+ const attempts = rows.filter((row) => row.phase === "attempt");
1862
+ for (const event of ["dispatch", "wire"]) {
1863
+ const joined = evidence.filter((row) => row.event === event && (0, _orkestrel_contract.isRecord)(row.value) && row.value.ordinal !== void 0);
1864
+ if (joined.length !== attempts.length || !joined.every((row, index) => {
1865
+ const request = attempts[index];
1866
+ return request !== void 0 && (0, _orkestrel_contract.isRecord)(row.value) && row.value.ordinal === request.ordinal && row.goal === request.goal && row.value.role === request.role && row.value.path === request.path;
1867
+ })) throw new _src_core.StudyError("CUSTODY", "Exchange sidecar and evidence ordinals do not join exactly");
1868
+ }
1869
+ }
1870
+ /** Reads and joins a complete trial sidecar before or after sealing.
1871
+ * @param directory - The result directory.
1872
+ * @param manifest - The verified manifest declaring recording and bounds.
1873
+ * @param trial - The exact scheduled trial id.
1874
+ * @param file - The inventory identity read for this sidecar.
1875
+ * @returns Complete rows after byte, declaration and evidence joins.
1876
+ */
1877
+ async function readStudyTrialExchanges(directory, manifest, trial, file) {
1878
+ if (manifest.record === void 0 || file.path !== `trials/${trial}/exchanges.jsonl`) throw new _src_core.StudyError("CUSTODY", "Sidecar has no recording declaration");
1879
+ const bytes = await readStudyBytes((0, node_path.join)(directory, "execution", "trials", trial, "exchanges.jsonl"), manifest.record.bytes);
1880
+ if (bytes.length !== file.bytes || hashStudyBytes(bytes) !== file.digest) throw new _src_core.StudyError("CUSTODY", "Exchange sidecar changed during reading");
1881
+ const rows = readStudyExchangeRows(bytes);
1882
+ const declaration = rows[0];
1883
+ const scheduled = manifest.schedule.find((entry) => entry.id === trial);
1884
+ const scenario = manifest.scenarios.find((entry) => entry.id === scheduled?.scenario);
1885
+ if (declaration?.phase !== "declaration" || declaration.trial !== trial || scenario === void 0 || !declaration.procedure.every((step) => scenario.goals.some((goal) => goal.id === step.goal))) throw new _src_core.StudyError("CUSTODY", "Exchange declaration differs from its trial");
1886
+ const evidenceBytes = await readStudyBytes((0, node_path.join)(directory, "execution", "trials", trial, "evidence.jsonl"), manifest.bounds.trial.bytes);
1887
+ validateStudyEvidenceBytes(evidenceBytes, manifest, trial);
1888
+ validateStudyExchangeEvidence(rows, new TextDecoder("utf-8", { fatal: true }).decode(evidenceBytes).split(/\r\n|\n/).slice(0, -1).map((line) => {
1889
+ const row = (0, _orkestrel_contract.parseJSON)(line);
1890
+ if (!isStudyCustodyEvidence(row)) throw new _src_core.StudyError("CUSTODY", "Invalid exchange evidence");
1891
+ return row;
1892
+ }));
1893
+ return rows;
1894
+ }
1895
+ /** Reads a sealed trial's exact complete exchange history after reverifying its entire result.
1896
+ * @param result - The sealed execution selected for replay.
1897
+ * @param trial - The scheduled trial id.
1898
+ * @returns Complete ordered request and response pairs with their verified file and seal identities.
1899
+ */
1900
+ async function readStudyExchanges(result, trial) {
1901
+ const reading = await readStudyResult(result.directory);
1902
+ if (reading.status !== "sealed" || reading.seal.id !== result.seal.id) throw new _src_core.StudyError("CUSTODY", "Replay result is unsealed or changed");
1903
+ const file = reading.seal.files.find((entry) => entry.path === `trials/${trial}/exchanges.jsonl`);
1904
+ if (file === void 0) throw new _src_core.StudyError("CUSTODY", "Recorded trial is missing its exchange sidecar");
1905
+ const rows = await readStudyTrialExchanges(reading.directory, reading.manifest, trial, file);
1906
+ const declaration = rows[0];
1907
+ const completion = rows.at(-1);
1908
+ if (declaration?.phase !== "declaration" || completion?.phase !== "complete") throw new _src_core.StudyError("CUSTODY", "Exchange boundaries are missing");
1909
+ const exchanges = rows.flatMap((row, index) => {
1910
+ const response = rows[index + 1];
1911
+ return row.phase === "attempt" && response?.phase === "response" ? [{
1912
+ attempt: row,
1913
+ response
1914
+ }] : [];
1915
+ });
1916
+ const history = {
1917
+ seal: reading.seal.id,
1918
+ file,
1919
+ declaration,
1920
+ exchanges,
1921
+ completion
1922
+ };
1923
+ validateStudyExchangeHistory(history);
1924
+ return history;
1925
+ }
1204
1926
  //#endregion
1205
1927
  //#region src/server/stores/StudyEvidenceSink.ts
1206
1928
  /** Retains admitted evidence in order and closes admission before terminal records. */
@@ -1288,6 +2010,93 @@ var StudyEvidenceSink = class {
1288
2010
  }
1289
2011
  };
1290
2012
  //#endregion
2013
+ //#region src/server/stores/StudyExchangeSink.ts
2014
+ /** Retains ordered exchange rows under a parent-reserved serialized byte allowance. */
2015
+ var StudyExchangeSink = class {
2016
+ #options;
2017
+ #writes = /* @__PURE__ */ new Set();
2018
+ #rows = [];
2019
+ #bytes = 0;
2020
+ #fault;
2021
+ #closing;
2022
+ /** Owns the descriptor and restores a verified prefix without rewriting it.
2023
+ * @param options - The exclusive descriptor, byte bound and optional retained rows.
2024
+ */
2025
+ constructor(options) {
2026
+ if (!Number.isSafeInteger(options.record.bytes) || options.record.bytes <= 0) throw new _src_core.StudyError("CONFIG", "Invalid exchange byte allowance");
2027
+ this.#options = {
2028
+ ...options,
2029
+ record: { ...options.record }
2030
+ };
2031
+ if (options.retained !== void 0) {
2032
+ const rows = structuredClone(options.retained);
2033
+ validateStudyExchangeRows(rows, false);
2034
+ this.#rows = rows;
2035
+ this.#bytes = rows.reduce((total, row) => total + Buffer.byteLength(JSON.stringify(row) + "\n"), 0);
2036
+ if (this.#bytes > this.#options.record.bytes) throw new _src_core.StudyError("CAPACITY", "Retained exchange prefix exceeds its allowance");
2037
+ }
2038
+ }
2039
+ /** Holds the next contiguous one-based HTTP ordinal, including the retained prefix. */
2040
+ get next() {
2041
+ return this.#rows.filter((row) => row.phase === "attempt").length + 1;
2042
+ }
2043
+ /** Appends one owned row after validating order and reserving its complete serialized size.
2044
+ * @param row - The next declaration, attempt, terminal or completion.
2045
+ * @returns Completion after synchronized retention; callers await each append before proceeding.
2046
+ */
2047
+ append(row) {
2048
+ try {
2049
+ if (this.#closing !== void 0 || this.#writes.size !== 0) throw new _src_core.StudyError("CUSTODY", "Exchange append requires drained open admission");
2050
+ const text = JSON.stringify(row) + "\n";
2051
+ const owned = (0, _orkestrel_contract.parseJSON)(text);
2052
+ if (!isStudyExchangeRow(owned)) throw new _src_core.StudyError("CUSTODY", "Invalid exchange row");
2053
+ if (this.#fault !== void 0 && owned.phase !== "failure") throw this.#fault.error;
2054
+ const rows = [...this.#rows, owned];
2055
+ validateStudyExchangeRows(rows, false);
2056
+ const bytes = Buffer.from(text);
2057
+ const reservation = owned.phase === "attempt" ? Buffer.byteLength(JSON.stringify({
2058
+ ...owned,
2059
+ phase: "failure",
2060
+ reason: "Exchange failed",
2061
+ body: void 0,
2062
+ digest: void 0,
2063
+ size: void 0
2064
+ }) + "\n") + 1024 : 0;
2065
+ if (this.#bytes + bytes.length + reservation > this.#options.record.bytes) throw new _src_core.StudyError("CAPACITY", "Serialized exchange bytes exceed the trial allowance");
2066
+ this.#rows = rows;
2067
+ this.#bytes += bytes.length;
2068
+ const pending = this.#write(bytes);
2069
+ this.#writes.add(pending);
2070
+ pending.then(() => this.#writes.delete(pending), () => this.#writes.delete(pending));
2071
+ return pending;
2072
+ } catch (error) {
2073
+ this.#fault ??= { error };
2074
+ return Promise.reject(error);
2075
+ }
2076
+ }
2077
+ /** Closes admission, drains accepted writes and synchronizes the owned descriptor.
2078
+ * @returns Completion after descriptor release, with a retained refusal propagated.
2079
+ */
2080
+ close() {
2081
+ this.#closing ??= Promise.allSettled([...this.#writes]).then(async () => {
2082
+ await this.#options.file.sync();
2083
+ if (this.#fault !== void 0) throw this.#fault.error;
2084
+ }).finally(async () => {
2085
+ await this.#options.file.close();
2086
+ });
2087
+ return this.#closing;
2088
+ }
2089
+ async #write(bytes) {
2090
+ try {
2091
+ await this.#options.file.writeFile(bytes);
2092
+ await this.#options.file.sync();
2093
+ } catch (error) {
2094
+ this.#fault ??= { error };
2095
+ throw error;
2096
+ }
2097
+ }
2098
+ };
2099
+ //#endregion
1291
2100
  //#region src/server/stores/StudyResultStore.ts
1292
2101
  /** Owns append-only execution bytes and seals only a complete terminal population. */
1293
2102
  var StudyResultStore = class {
@@ -1295,6 +2104,7 @@ var StudyResultStore = class {
1295
2104
  #manifest;
1296
2105
  #trials = /* @__PURE__ */ new Map();
1297
2106
  #starting = /* @__PURE__ */ new Set();
2107
+ #exchanges = /* @__PURE__ */ new Map();
1298
2108
  #phase = "new";
1299
2109
  #bytes = 0;
1300
2110
  /**
@@ -1332,6 +2142,21 @@ var StudyResultStore = class {
1332
2142
  throw error;
1333
2143
  }
1334
2144
  }
2145
+ /** Reserves a trial's entire exchange allowance before any worker starts.
2146
+ * @param trial - The exact scheduled trial whose manifest declares recording.
2147
+ * @returns Its exclusive sidecar destination, without creating the trial directory.
2148
+ */
2149
+ async exchanges(trial) {
2150
+ const scheduled = this.#manifest.schedule.find((entry) => entry.id === trial.id);
2151
+ if (this.#phase !== "ready" || !isStudyCustodyTrial(trial) || scheduled === void 0 || this.#manifest.record === void 0 || (0, _orkestrel_contract.canonicalStringify)(scheduled) !== (0, _orkestrel_contract.canonicalStringify)(trial) || this.#exchanges.has(trial.id) || this.#trials.has(trial.id) || this.#starting.has(trial.id)) throw new _src_core.StudyError("CUSTODY", "Trial sidecar cannot be reserved");
2152
+ this.#reserve(this.#manifest.record.bytes);
2153
+ const destination = Object.freeze({
2154
+ path: (0, node_path.join)(this.#directory, "execution", "trials", scheduled.id, "exchanges.jsonl"),
2155
+ record: Object.freeze({ ...this.#manifest.record })
2156
+ });
2157
+ this.#exchanges.set(scheduled.id, destination);
2158
+ return destination;
2159
+ }
1335
2160
  /**
1336
2161
  * Exclusively creates the evidence sink for an exact scheduled trial.
1337
2162
  * @param trial - The scheduled trajectory
@@ -1342,7 +2167,7 @@ var StudyResultStore = class {
1342
2167
  if (this.#phase !== "ready" || !isStudyCustodyTrial(trial) || this.#trials.has(trial.id) || this.#starting.has(trial.id)) throw new _src_core.StudyError("CUSTODY", "Trial cannot start in the current state");
1343
2168
  const scheduled = this.#manifest.schedule.find((entry) => entry.id === trial.id);
1344
2169
  const scenario = this.#manifest.scenarios.find((entry) => entry.id === trial.scenario);
1345
- if (scheduled === void 0 || scenario === void 0 || (0, _orkestrel_contract.canonicalStringify)(scheduled) !== (0, _orkestrel_contract.canonicalStringify)(trial)) throw new _src_core.StudyError("CUSTODY", "Trial differs from the execution schedule");
2170
+ if (scheduled === void 0 || scenario === void 0 || this.#manifest.record !== void 0 && !this.#exchanges.has(trial.id) || (0, _orkestrel_contract.canonicalStringify)(scheduled) !== (0, _orkestrel_contract.canonicalStringify)(trial)) throw new _src_core.StudyError("CUSTODY", "Trial differs from the execution schedule");
1346
2171
  this.#starting.add(scheduled.id);
1347
2172
  try {
1348
2173
  const directory = (0, node_path.join)(this.#directory, "execution", "trials", scheduled.id);
@@ -1404,8 +2229,15 @@ var StudyResultStore = class {
1404
2229
  const reading = await readStudyResult(this.#directory);
1405
2230
  if (reading.status !== "interrupted" || reading.manifest === void 0 || reading.attempts.length !== this.#manifest.schedule.length || (0, _orkestrel_contract.canonicalStringify)(reading.manifest) !== (0, _orkestrel_contract.canonicalStringify)(this.#manifest)) throw new _src_core.StudyError("CUSTODY", "Execution changed before sealing");
1406
2231
  const files = await scanStudyExecution((0, node_path.join)(this.#directory, "execution"), this.#manifest);
1407
- if (files.length !== 1 + this.#manifest.schedule.length * 2) throw new _src_core.StudyError("CUSTODY", "Execution inventory is incomplete");
1408
- for (const trial of this.#manifest.schedule) validateStudyEvidenceBytes(await readStudyBytes((0, node_path.join)(this.#directory, "execution", "trials", trial.id, "evidence.jsonl"), this.#manifest.bounds.trial.bytes), this.#manifest, trial.id);
2232
+ if (files.length !== 1 + this.#manifest.schedule.length * (this.#manifest.record === void 0 ? 2 : 3)) throw new _src_core.StudyError("CUSTODY", "Execution inventory is incomplete");
2233
+ for (const trial of this.#manifest.schedule) {
2234
+ if (this.#manifest.record !== void 0) {
2235
+ const file = files.find((entry) => entry.path === `trials/${trial.id}/exchanges.jsonl`);
2236
+ if (file === void 0 || !this.#exchanges.has(trial.id)) throw new _src_core.StudyError("CUSTODY", "Recorded trial has no reserved complete sidecar");
2237
+ await readStudyTrialExchanges(this.#directory, this.#manifest, trial.id, file);
2238
+ }
2239
+ validateStudyEvidenceBytes(await readStudyBytes((0, node_path.join)(this.#directory, "execution", "trials", trial.id, "evidence.jsonl"), this.#manifest.bounds.trial.bytes), this.#manifest, trial.id);
2240
+ }
1409
2241
  const seal = Object.freeze({
1410
2242
  result: this.#manifest.id,
1411
2243
  id: computeStudySealId(this.#manifest.id, files),
@@ -1602,79 +2434,187 @@ var STUDY_COMMAND_DURATION = 864e5;
1602
2434
  /** Names the trusted command's independent offline predicate. */
1603
2435
  var STUDY_COMMAND_GRADER = "consumer-adapter";
1604
2436
  /** Defines the complete public CLI reference shared by help and grammar diagnostics. */
1605
- var STUDY_COMMAND_HELP = `study check <study> [--profile <name>]
1606
- study execute <study> [--profile <name>] [--out <path>] [--expected <sha256>]
2437
+ var STUDY_COMMAND_HELP = `study check <study> [--profile <name>] [--closure <directory>]
2438
+ study freeze <study> --profile <name> --to <directory>
2439
+ study execute <study> [--profile <name>] [--out <path>] [--expected <sha256>] [--closure <directory>] [--replay <result> --prefix <JSON trial counts> --offline <true|false>]
1607
2440
  study grade <result> --rubric <path>
1608
2441
  study label <result> --grading <sha256> --labels <path>
1609
2442
  study report <result> --grading <sha256>
1610
2443
  study --help
1611
2444
  study --version
1612
2445
 
1613
- Profiles default to local. Flags require values and may appear only once.
2446
+ Profiles default to local; freeze requires --profile. Flags require values and may appear only once.
1614
2447
  Results default to tmp/studies/<result>; scratch also uses tmp/studies.
1615
2448
  A result is its id under tmp/studies, a root-relative path, or an absolute path.
1616
2449
  Relative paths, including --out, must stay inside the consumer working directory.
1617
2450
  Output directories must be new; existing results are never overwritten.
1618
2451
  --expected pins the fingerprint returned by check. --grading selects an exact grading.
1619
2452
  --rubric selects independent criteria. --labels imports caller-supplied semantic labels.
2453
+ --closure selects retained artifacts. Replay requires explicit prefix counts and offline behavior.
1620
2454
  Success writes one JSON document; next contains suggested argv, never executed automatically.
1621
- check, grade, label and report have a fixed allowance of 60 seconds.
2455
+ check, freeze, grade, label and report have a fixed allowance of 60 seconds.
1622
2456
  execute checks first, then uses the declared series duration plus 15 seconds for cleanup.
1623
2457
  Exit codes: 0 success, 64 invalid input, 124 deadline, 130 interruption, 1 operational failure.
1624
2458
  `;
1625
2459
  //#endregion
1626
- //#region src/server/commands/helpers.ts
1627
- /** Reads one declaration file and refuses a missing or linked path with its root-relative path.
1628
- * @param root - The consumer root used to describe a missing or linked declaration.
1629
- * @param path - The already resolved and contained declaration path.
1630
- * @returns The exact declaration bytes under the custody metadata allowance.
1631
- * @throws StudyError - Thrown with code `CONFIG` when the path is missing or linked; other custody refusals retain their codes.
1632
- * @example Read declaration bytes
1633
- * ```ts
1634
- * import { readStudyDeclarationBytes } from '@orkestrel/study/server'
1635
- *
1636
- * declare const root: string
1637
- * declare const path: string
1638
- * await readStudyDeclarationBytes(root, path)
1639
- * ```
2460
+ //#region src/server/processes/validators.ts
2461
+ /** Checks whether a worker destination has an absolute sidecar path and finite reservation.
2462
+ * @param value - The decoded destination.
2463
+ * @returns True if its path, allowance and optional retained identity are valid; false otherwise.
1640
2464
  */
1641
- async function readStudyDeclarationBytes(root, path) {
1642
- try {
1643
- const absolute = (0, node_path.resolve)(path);
1644
- const origin = (0, node_path.parse)(absolute).root;
1645
- const names = (0, node_path.relative)(origin, absolute).split(node_path.sep).filter(Boolean);
1646
- for (const [index] of names.entries()) {
1647
- const candidate = (0, node_path.join)(origin, ...names.slice(0, index + 1));
1648
- const stat = await (0, node_fs_promises.lstat)(candidate);
1649
- if (stat.isSymbolicLink()) throw new _src_core.StudyError("CONFIG", `Declaration path is linked: ${(0, node_path.relative)(root, candidate).split(node_path.sep).join("/")}`);
1650
- if (!stat.isDirectory()) break;
1651
- }
1652
- return await readStudyBytes(path, STUDY_CUSTODY_BYTES);
1653
- } catch (error) {
1654
- if (matchesSystemError(error, "ENOENT")) throw new _src_core.StudyError("CONFIG", `Declaration is missing: ${(0, node_path.relative)(root, path).split(node_path.sep).join("/")}`);
1655
- throw error;
1656
- }
2465
+ function isStudyExchangeDestination(value) {
2466
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.matchesStudyKeys)(value, ["path", "record"], ["retained"]) && typeof value.path === "string" && (0, node_path.isAbsolute)(value.path) && (0, node_path.basename)(value.path) === "exchanges.jsonl" && (0, _src_core.isStudyRecord)(value.record) && (value.retained === void 0 || isStudyCustodyFile(value.retained)));
1657
2467
  }
1658
- /** Reads a study declaration, its driver's scenarios, and its rubric as the command resolves them.
1659
- * @param root - The consumer root containing the studies directory.
1660
- * @param study - The selected study name.
1661
- * @param signal - Optional cancellation checked between scenario reads.
1662
- * @returns The parsed declaration, driver bundle, ordered scenarios, fixtures, and selected rubric.
1663
- * @throws StudyError - Thrown when declarations are missing, invalid, or incompatible.
1664
- * @example Read a study declaration
1665
- * ```ts
1666
- * import type { StudyDeclaration } from '@orkestrel/study/server'
1667
- * import { readStudyDeclaration } from '@orkestrel/study/server'
1668
- *
1669
- * declare const root: string
1670
- * const declaration: StudyDeclaration = await readStudyDeclaration(root, 'assessment')
1671
- * ```
2468
+ /** Checks whether replay explicitly selects a sealed directory and per-trial prefix counts.
2469
+ * @param value - The decoded replay selection.
2470
+ * @returns True if result, offline and every prefix are declared; false otherwise.
1672
2471
  */
1673
- async function readStudyDeclaration(root, study, signal) {
1674
- const directory = resolveStudyPath((0, node_path.join)(root, "studies"), study);
1675
- const definition = (0, _src_core.parseStudyDefinition)(decodeStudyJSON(await readStudyDeclarationBytes(root, resolveStudyPath(directory, "study.json"))));
1676
- if (definition.name !== study) throw new _src_core.StudyError("CONFIG", "Study name differs from its selected directory");
1677
- const bundle = resolveStudyPath((0, node_path.join)(root, "studies"), definition.driver);
2472
+ function isStudyReplaySelection(value) {
2473
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.matchesStudyKeys)(value, [
2474
+ "result",
2475
+ "prefix",
2476
+ "offline"
2477
+ ]) && typeof value.result === "string" && value.result.length > 0 && typeof value.offline === "boolean" && (0, _orkestrel_contract.isRecord)(value.prefix) && Object.keys(value.prefix).length > 0 && Object.entries(value.prefix).every(([trial, count]) => isStudyCustodyName(trial) && typeof count === "number" && Number.isSafeInteger(count) && count > 0));
2478
+ }
2479
+ /** Checks the complete transport envelope before domain handling.
2480
+ * @param value - Native IPC decoded value.
2481
+ * @returns True if the envelope is bounded in shape and JSON-compatible; false otherwise.
2482
+ */
2483
+ function isStudyFrame(value) {
2484
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).length === 3 && Object.hasOwn(value, "id") && Object.hasOwn(value, "operation") && Object.hasOwn(value, "value") && typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id >= 0 && typeof value.operation === "string" && [
2485
+ "open",
2486
+ "goal",
2487
+ "close",
2488
+ "admit",
2489
+ "finish",
2490
+ "fail",
2491
+ "evidence",
2492
+ "reply",
2493
+ "error",
2494
+ "abort"
2495
+ ].includes(value.operation) && (0, _orkestrel_contract.isJSONValue)(value.value));
2496
+ }
2497
+ /** Checks operational failure attribution arriving over IPC.
2498
+ * @param value - Decoded failure value.
2499
+ * @returns True if the failure carries a known cause, an optional role, and an optional stop that satisfies `isStudyStop` on a `capacity` or `deadline` cause; false otherwise.
2500
+ */
2501
+ function isStudyProcessFailure(value) {
2502
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && typeof value.message === "string" && (0, _src_core.isStudyCause)(value.cause) && (value.role === void 0 || (0, _src_core.isStudyRole)(value.role)) && (value.stop === void 0 || (value.cause === "capacity" || value.cause === "deadline") && (0, _src_core.isStudyStop)(value.stop)));
2503
+ }
2504
+ /** Checks a request's declared role and finite cost ceilings.
2505
+ * @param value - Decoded charge.
2506
+ * @returns True if all charge leaves are valid; false otherwise.
2507
+ */
2508
+ function isStudyCharge(value) {
2509
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyRole)(value.role) && typeof value.tokens === "number" && Number.isSafeInteger(value.tokens) && value.tokens >= 0 && typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes >= 0);
2510
+ }
2511
+ /** Checks append-only public evidence.
2512
+ * @param value - Decoded evidence.
2513
+ * @returns True if event, goal and payload are valid; false otherwise.
2514
+ */
2515
+ function isStudyEvidence(value) {
2516
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyName)(value.event) && (0, _src_core.isStudyName)(value.goal) && (0, _orkestrel_contract.isJSONValue)(value.value));
2517
+ }
2518
+ /** Checks a fresh trial's declared identifiers.
2519
+ * @param value - Decoded trial.
2520
+ * @returns True if every identifier and repetition is valid; false otherwise.
2521
+ */
2522
+ function isStudyTrial(value) {
2523
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyName)(value.id) && (0, _src_core.isStudyName)(value.scenario) && (0, _src_core.isStudyName)(value.arm) && typeof value.repetition === "number" && Number.isSafeInteger(value.repetition) && value.repetition >= 0);
2524
+ }
2525
+ /** Checks the exact digest fields carried by the execution declaration.
2526
+ * @param value - Decoded identity.
2527
+ * @returns True if all required identities are digests; false otherwise.
2528
+ */
2529
+ function isStudyIdentity(value) {
2530
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyDigest)(value.agent) && (0, _src_core.isStudyDigest)(value.subject) && (value.provider === void 0 || (0, _src_core.isStudyDigest)(value.provider)) && (0, _src_core.isStudyDigest)(value.instrument) && (0, _src_core.isStudyDigest)(value.fixtures) && (0, _src_core.isStudyDigest)(value.profile));
2531
+ }
2532
+ //#endregion
2533
+ //#region src/server/commands/validators.ts
2534
+ /** Checks a closure publication before using its command inputs.
2535
+ * @param value - The decoded closure.json value.
2536
+ * @returns True if relocation, selectors, and both identities are declared; false otherwise.
2537
+ */
2538
+ function isStudyClosure(value) {
2539
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && isArtifactClosure(value.closure) && (0, _orkestrel_contract.isString)(value.root) && (0, _orkestrel_contract.isString)(value.entry) && (0, _src_core.isStudyName)(value.target) && (0, _src_core.isStudyName)(value.profile) && isStudyIdentity(value.source) && isStudyIdentity(value.identity) && (0, _src_core.isStudyDigest)(value.fingerprint));
2540
+ }
2541
+ //#endregion
2542
+ //#region src/server/commands/helpers.ts
2543
+ /** Reads and independently verifies an exclusively published closure.
2544
+ * @param directory - The canonical closure destination.
2545
+ * @returns The verified publication and its copied physical forest.
2546
+ * @throws StudyError - Thrown when publication or its command inputs are invalid.
2547
+ */
2548
+ function readStudyClosure(directory) {
2549
+ const marker = readArtifact((0, node_path.join)(directory, "closure.json"), STUDY_CUSTODY_BYTES);
2550
+ const value = decodeStudyJSON(marker.source);
2551
+ if (!isStudyClosure(value) || value.closure.destination !== directory) throw new _src_core.StudyError("CONFIG", "Study closure publication is invalid");
2552
+ verifyArtifactClosure(value.closure, true);
2553
+ if (value.root !== directory && !containsArtifact(directory, value.root) || !value.closure.members.some((member) => (0, node_path.resolve)(directory, ...member.key.split("/")) === value.entry)) throw new _src_core.StudyError("CONFIG", "Study closure command inputs leave the copied forest");
2554
+ if (readArtifact((0, node_path.join)(directory, "closure.json"), 16777216).digest !== marker.digest) throw new _src_core.StudyError("FAULT", "Study closure publication changed while reading");
2555
+ return value;
2556
+ }
2557
+ /** Resolves a profile path through a retained physical relocation when selected.
2558
+ * @param root - The active consumer root.
2559
+ * @param path - The profile's absolute or consumer-relative selection.
2560
+ * @param closure - The optional verified physical forest.
2561
+ * @returns The active canonical selection.
2562
+ */
2563
+ function resolveStudyClosurePath(root, path, closure) {
2564
+ const absolute = (0, node_path.resolve)(root, path);
2565
+ return closure === void 0 || absolute === closure.destination || containsArtifact(closure.destination, absolute) ? absolute : relocateArtifactPath(closure, absolute);
2566
+ }
2567
+ /** Reads one declaration file and refuses a missing or linked path with its root-relative path.
2568
+ * @param root - The consumer root used to describe a missing or linked declaration.
2569
+ * @param path - The already resolved and contained declaration path.
2570
+ * @returns The exact declaration bytes under the custody metadata allowance.
2571
+ * @throws StudyError - Thrown with code `CONFIG` when the path is missing or linked; other custody refusals retain their codes.
2572
+ * @example Read declaration bytes
2573
+ * ```ts
2574
+ * import { readStudyDeclarationBytes } from '@orkestrel/study/server'
2575
+ *
2576
+ * declare const root: string
2577
+ * declare const path: string
2578
+ * await readStudyDeclarationBytes(root, path)
2579
+ * ```
2580
+ */
2581
+ async function readStudyDeclarationBytes(root, path) {
2582
+ try {
2583
+ const absolute = (0, node_path.resolve)(path);
2584
+ const origin = (0, node_path.parse)(absolute).root;
2585
+ const names = (0, node_path.relative)(origin, absolute).split(node_path.sep).filter(Boolean);
2586
+ for (const [index] of names.entries()) {
2587
+ const candidate = (0, node_path.join)(origin, ...names.slice(0, index + 1));
2588
+ const stat = await (0, node_fs_promises.lstat)(candidate);
2589
+ if (stat.isSymbolicLink()) throw new _src_core.StudyError("CONFIG", `Declaration path is linked: ${(0, node_path.relative)(root, candidate).split(node_path.sep).join("/")}`);
2590
+ if (!stat.isDirectory()) break;
2591
+ }
2592
+ return await readStudyBytes(path, STUDY_CUSTODY_BYTES);
2593
+ } catch (error) {
2594
+ if (matchesSystemError(error, "ENOENT")) throw new _src_core.StudyError("CONFIG", `Declaration is missing: ${(0, node_path.relative)(root, path).split(node_path.sep).join("/")}`);
2595
+ throw error;
2596
+ }
2597
+ }
2598
+ /** Reads a study declaration, its driver's scenarios, and its rubric as the command resolves them.
2599
+ * @param root - The consumer root containing the studies directory.
2600
+ * @param study - The selected study name.
2601
+ * @param signal - Optional cancellation checked between scenario reads.
2602
+ * @returns The parsed declaration, driver bundle, ordered scenarios, fixtures, and selected rubric.
2603
+ * @throws StudyError - Thrown when declarations are missing, invalid, or incompatible.
2604
+ * @example Read a study declaration
2605
+ * ```ts
2606
+ * import type { StudyDeclaration } from '@orkestrel/study/server'
2607
+ * import { readStudyDeclaration } from '@orkestrel/study/server'
2608
+ *
2609
+ * declare const root: string
2610
+ * const declaration: StudyDeclaration = await readStudyDeclaration(root, 'assessment')
2611
+ * ```
2612
+ */
2613
+ async function readStudyDeclaration(root, study, signal) {
2614
+ const directory = resolveStudyPath((0, node_path.join)(root, "studies"), study);
2615
+ const definition = (0, _src_core.parseStudyDefinition)(decodeStudyJSON(await readStudyDeclarationBytes(root, resolveStudyPath(directory, "study.json"))));
2616
+ if (definition.name !== study) throw new _src_core.StudyError("CONFIG", "Study name differs from its selected directory");
2617
+ const bundle = resolveStudyPath((0, node_path.join)(root, "studies"), definition.driver);
1678
2618
  const steps = [];
1679
2619
  for (const id of definition.scenarios) {
1680
2620
  signal?.throwIfAborted();
@@ -1721,22 +2661,25 @@ function resolveStudyPath(directory, path) {
1721
2661
  * Resolves the execution profile's caller-selected built artifact paths.
1722
2662
  * @param root - The repository root
1723
2663
  * @param profile - The validated source profile
2664
+ * @param closure - The optional verified physical forest used to relocate executable selections
1724
2665
  * @returns The reproducible resolved execution profile
1725
2666
  * @example
1726
2667
  * ```ts
1727
2668
  * resolveStudyProfile(root, profile)
1728
2669
  * ```
1729
2670
  */
1730
- function resolveStudyProfile(root, profile) {
2671
+ function resolveStudyProfile(root, profile, closure) {
2672
+ const daemon = profile.provider?.daemon;
2673
+ if (daemon !== void 0 && (!(0, node_path.isAbsolute)(daemon.executable) || (0, node_fs.realpathSync)(daemon.executable) !== daemon.executable)) throw new _src_core.StudyError("CONFIG", "Daemon executable must name its canonical absolute path");
1731
2674
  return {
1732
2675
  ...profile,
1733
2676
  qualification: profile.qualification === void 0 ? void 0 : (0, node_path.resolve)(root, profile.qualification),
1734
- agent: (0, node_path.resolve)(root, profile.agent),
1735
- subject: (0, node_path.resolve)(root, profile.subject),
1736
- roots: profile.roots.map((path) => (0, node_path.resolve)(root, path)),
2677
+ agent: resolveStudyClosurePath(root, profile.agent, closure),
2678
+ subject: resolveStudyClosurePath(root, profile.subject, closure),
2679
+ roots: profile.roots.map((path) => resolveStudyClosurePath(root, path, closure)),
1737
2680
  provider: profile.provider === void 0 ? void 0 : {
1738
2681
  ...profile.provider,
1739
- entry: (0, node_path.resolve)(root, profile.provider.entry)
2682
+ entry: resolveStudyClosurePath(root, profile.provider.entry, closure)
1740
2683
  }
1741
2684
  };
1742
2685
  }
@@ -1998,20 +2941,25 @@ function computeStudyReport(result, rubric, grading) {
1998
2941
  function parseStudyArguments(arguments_) {
1999
2942
  const command = arguments_[0];
2000
2943
  const target = arguments_[1];
2001
- const usage = "study check <study> [--profile <name>]\nstudy execute <study> [--profile <name>] [--out <path>] [--expected <sha256>]\nstudy grade <result> --rubric <path>\nstudy label <result> --grading <sha256> --labels <path>\nstudy report <result> --grading <sha256>\nstudy --help\nstudy --version\n\nProfiles default to local. Flags require values and may appear only once.\nResults default to tmp/studies/<result>; scratch also uses tmp/studies.\nA result is its id under tmp/studies, a root-relative path, or an absolute path.\nRelative paths, including --out, must stay inside the consumer working directory.\nOutput directories must be new; existing results are never overwritten.\n--expected pins the fingerprint returned by check. --grading selects an exact grading.\n--rubric selects independent criteria. --labels imports caller-supplied semantic labels.\nSuccess writes one JSON document; next contains suggested argv, never executed automatically.\ncheck, grade, label and report have a fixed allowance of 60 seconds.\nexecute checks first, then uses the declared series duration plus 15 seconds for cleanup.\nExit codes: 0 success, 64 invalid input, 124 deadline, 130 interruption, 1 operational failure.\n".split("\n").find((line) => line.startsWith("study " + command + " ")) ?? "Expected study check, execute, grade, label, or report; use study --help.";
2944
+ const usage = "study check <study> [--profile <name>] [--closure <directory>]\nstudy freeze <study> --profile <name> --to <directory>\nstudy execute <study> [--profile <name>] [--out <path>] [--expected <sha256>] [--closure <directory>] [--replay <result> --prefix <JSON trial counts> --offline <true|false>]\nstudy grade <result> --rubric <path>\nstudy label <result> --grading <sha256> --labels <path>\nstudy report <result> --grading <sha256>\nstudy --help\nstudy --version\n\nProfiles default to local; freeze requires --profile. Flags require values and may appear only once.\nResults default to tmp/studies/<result>; scratch also uses tmp/studies.\nA result is its id under tmp/studies, a root-relative path, or an absolute path.\nRelative paths, including --out, must stay inside the consumer working directory.\nOutput directories must be new; existing results are never overwritten.\n--expected pins the fingerprint returned by check. --grading selects an exact grading.\n--rubric selects independent criteria. --labels imports caller-supplied semantic labels.\n--closure selects retained artifacts. Replay requires explicit prefix counts and offline behavior.\nSuccess writes one JSON document; next contains suggested argv, never executed automatically.\ncheck, freeze, grade, label and report have a fixed allowance of 60 seconds.\nexecute checks first, then uses the declared series duration plus 15 seconds for cleanup.\nExit codes: 0 success, 64 invalid input, 124 deadline, 130 interruption, 1 operational failure.\n".split("\n").find((line) => line.startsWith("study " + command + " ")) ?? "Expected study check, freeze, execute, grade, label, or report; use study --help.";
2002
2945
  if (![
2003
2946
  "check",
2947
+ "freeze",
2004
2948
  "execute",
2005
2949
  "grade",
2006
2950
  "label",
2007
2951
  "report"
2008
2952
  ].includes(command ?? "")) throw new _src_core.StudyError("CONFIG", "Unknown or missing command " + JSON.stringify(command ?? "<command>") + ". " + usage);
2009
- if (target === void 0 || target === "" || target.startsWith("-")) throw new _src_core.StudyError("CONFIG", "Missing <" + (command === "check" || command === "execute" ? "study" : "result") + "> before " + JSON.stringify(target ?? "<end>") + ". " + usage);
2010
- const allowed = command === "check" ? ["--profile"] : command === "execute" ? [
2953
+ if (target === void 0 || target === "" || target.startsWith("-")) throw new _src_core.StudyError("CONFIG", "Missing <" + (command === "check" || command === "execute" || command === "freeze" ? "study" : "result") + "> before " + JSON.stringify(target ?? "<end>") + ". " + usage);
2954
+ const allowed = command === "check" ? ["--profile", "--closure"] : command === "execute" ? [
2011
2955
  "--profile",
2012
2956
  "--out",
2013
- "--expected"
2014
- ] : command === "grade" ? ["--rubric"] : command === "label" ? ["--grading", "--labels"] : ["--grading"];
2957
+ "--expected",
2958
+ "--closure",
2959
+ "--replay",
2960
+ "--prefix",
2961
+ "--offline"
2962
+ ] : command === "freeze" ? ["--profile", "--to"] : command === "grade" ? ["--rubric"] : command === "label" ? ["--grading", "--labels"] : ["--grading"];
2015
2963
  const tokens = arguments_.slice(2);
2016
2964
  const pairs = [...tokens.entries()].flatMap(([index, flag]) => index % 2 === 0 ? [{
2017
2965
  flag,
@@ -2024,24 +2972,50 @@ function parseStudyArguments(arguments_) {
2024
2972
  if (value === void 0 || value === "" || value.startsWith("-")) throw new _src_core.StudyError("CONFIG", flag + " requires a value before " + JSON.stringify(value ?? "<end>") + ". " + usage);
2025
2973
  }
2026
2974
  const flags = new Map(pairs.flatMap(({ flag, value }) => flag === void 0 || value === void 0 ? [] : [[flag, value]]));
2027
- if (command === "check" || command === "execute") {
2975
+ if (command === "check" || command === "execute" || command === "freeze") {
2028
2976
  if (!isStudyCustodyName(target)) throw new _src_core.StudyError("CONFIG", "Invalid study token " + JSON.stringify(target) + "; use a portable study name. " + usage);
2029
2977
  const profile = flags.get("--profile") ?? "local";
2030
2978
  const out = flags.get("--out");
2031
2979
  const expected = flags.get("--expected");
2032
2980
  if (!isStudyCustodyName(profile)) throw new _src_core.StudyError("CONFIG", "--profile " + JSON.stringify(profile) + " must be a portable name. " + usage);
2981
+ if (command === "freeze") {
2982
+ if (!flags.has("--profile")) throw new _src_core.StudyError("CONFIG", "Missing --profile. " + usage);
2983
+ const destination = flags.get("--to");
2984
+ if (destination === void 0) throw new _src_core.StudyError("CONFIG", "Missing --to. " + usage);
2985
+ if (!(0, node_path.isAbsolute)(destination)) resolveStudyPath((0, node_path.resolve)("."), destination);
2986
+ return {
2987
+ command,
2988
+ target,
2989
+ profile,
2990
+ destination
2991
+ };
2992
+ }
2033
2993
  if (out !== void 0 && !(0, node_path.isAbsolute)(out)) try {
2034
2994
  resolveStudyPath((0, node_path.resolve)("."), out);
2035
2995
  } catch {
2036
2996
  throw new _src_core.StudyError("CONFIG", "--out " + JSON.stringify(out) + " must be a portable contained path and cannot escape the consumer root. " + usage);
2037
2997
  }
2038
2998
  if (expected !== void 0 && !(0, _src_core.isStudyDigest)(expected)) throw new _src_core.StudyError("CONFIG", "--expected " + JSON.stringify(expected) + " must be a SHA-256 fingerprint. " + usage);
2999
+ const closure = flags.get("--closure");
3000
+ if (closure !== void 0 && !(0, node_path.isAbsolute)(closure)) resolveStudyPath((0, node_path.resolve)("."), closure);
3001
+ const result = flags.get("--replay");
3002
+ const prefix = flags.get("--prefix");
3003
+ const offline = flags.get("--offline");
3004
+ if (result === void 0 && (prefix !== void 0 || offline !== void 0)) throw new _src_core.StudyError("CONFIG", "--prefix and --offline require --replay. " + usage);
3005
+ const replay = result === void 0 ? void 0 : (0, _orkestrel_contract.parseJSON)(prefix ?? "");
3006
+ if (result !== void 0 && (!(0, _orkestrel_contract.isRecord)(replay) || Object.keys(replay).length === 0 || !Object.entries(replay).every(([trial, count]) => isStudyCustodyName(trial) && typeof count === "number" && Number.isSafeInteger(count) && count > 0) || offline !== "true" && offline !== "false")) throw new _src_core.StudyError("CONFIG", "--replay requires --prefix trial counts and --offline true or false. " + usage);
2039
3007
  return {
2040
3008
  command,
2041
3009
  target,
2042
3010
  profile,
2043
3011
  out,
2044
- expected
3012
+ expected,
3013
+ ...closure === void 0 ? {} : { closure },
3014
+ ...result === void 0 || !(0, _orkestrel_contract.isRecord)(replay) ? {} : { replay: {
3015
+ result,
3016
+ prefix: Object.fromEntries(Object.entries(replay).map(([trial, count]) => [trial, Number(count)])),
3017
+ offline: offline === "true"
3018
+ } }
2045
3019
  };
2046
3020
  }
2047
3021
  if (command === "grade") {
@@ -2092,340 +3066,736 @@ var STUDY_COMMAND_OPERATION = "study-command";
2092
3066
  /** Caps pure domain work independently of its event loop. */
2093
3067
  var STUDY_TASK_DURATION = 6e4;
2094
3068
  //#endregion
2095
- //#region src/server/processes/helpers.ts
2096
- /** Projects optional domain fields into the JSON wire representation.
2097
- * @param value - An owned domain value with optional undefined fields.
2098
- * @returns A JSON-compatible snapshot.
2099
- * @example Project a wire value
2100
- * ```ts
2101
- * import { valueToWire } from '@orkestrel/study/server'
2102
- *
2103
- * valueToWire({ state: 'ready', failure: undefined }) // { state: 'ready' }
2104
- * ```
2105
- */
2106
- function valueToWire(value) {
2107
- const parsed = JSON.parse(JSON.stringify(value));
2108
- if (!(0, _orkestrel_contract.isJSONValue)(parsed)) throw new _src_core.StudyError("FAULT", "Invalid study wire value");
2109
- return parsed;
2110
- }
2111
- /** Creates attributable protocol infrastructure failure.
2112
- * @param message - The refused operation or broken boundary.
2113
- * @returns A domain failure that never awards task credit.
3069
+ //#region src/server/ollama/helpers.ts
3070
+ /**
3071
+ * Declares the pinned serving identities the Ollama transport verifies, keyed by the label their inventory evidence carries.
3072
+ * @param provider - Declared serving identities
3073
+ * @returns The model pin, plus the judge pin when the provider declares both the judge name and its digest
2114
3074
  */
2115
- function createStudyProcessFailure(message) {
3075
+ function buildStudyOllamaModels(provider) {
2116
3076
  return {
2117
- cause: "infrastructure",
2118
- message,
2119
- role: void 0,
2120
- stop: void 0
3077
+ model: {
3078
+ name: provider.model,
3079
+ digest: provider.digest.model
3080
+ },
3081
+ ...provider.judge === void 0 || provider.digest.judge === void 0 ? {} : { judge: {
3082
+ name: provider.judge,
3083
+ digest: provider.digest.judge
3084
+ } }
2121
3085
  };
2122
3086
  }
2123
- /** Binds the canonical fully resolved execution profile carried over IPC.
2124
- * @param profile - The resolved execution settings, with optional absent leaves.
2125
- * @returns The SHA256 digest of the canonical JSON wire representation.
3087
+ /**
3088
+ * Builds the provider options one role's declared wire policy determines.
3089
+ * @remarks
3090
+ * The policy's `options` object keeps its identity and key order. A strict policy adds `truncate`
3091
+ * and `shift` as `false`; any other policy omits both keys. The `length` allowance is read by the
3092
+ * transport and projects nothing.
3093
+ * @param policy - The role's declared wire policy
3094
+ * @returns The model, think flag, options object, and the strict native fields when declared
2126
3095
  */
2127
- function computeStudyProfileDigest(profile) {
2128
- const encoded = (0, _orkestrel_contract.canonicalStringify)(valueToWire(profile));
2129
- if (encoded === void 0) throw new _src_core.StudyError("CONFIG", "Study profile is not JSON-compatible");
2130
- return hashStudyBytes(encoded);
3096
+ function buildStudyOllamaOptions(policy) {
3097
+ return {
3098
+ model: policy.model,
3099
+ options: policy.options,
3100
+ think: policy.think,
3101
+ ...policy.strict ? {
3102
+ truncate: false,
3103
+ shift: false
3104
+ } : {}
3105
+ };
2131
3106
  }
2132
- /** Collects the executable entries a profile selects, each canonical path once.
2133
- * @param profile - The resolved execution profile.
2134
- * @returns The agent, subject, and provider entries without repeating a shared entry.
2135
- * @remarks The declared roles stay separate in the profile; only capture and loading deduplicate.
3107
+ /**
3108
+ * Validates declared role policies against the pinned inventory before any dispatch.
3109
+ * @param roles - The declared wire policy of each model role
3110
+ * @param models - The pinned served identities
3111
+ * @returns Nothing when every policy names a pinned model and a positive output allowance
3112
+ * @throws StudyError - Thrown with code `CONFIG` when a policy names an unpinned model, declares no positive safe-integer `num_predict`, or makes the raw judge wire strict
2136
3113
  */
2137
- function collectStudyEntries(profile) {
2138
- return [.../* @__PURE__ */ new Set([
2139
- profile.agent,
2140
- profile.subject,
2141
- ...profile.provider === void 0 ? [] : [profile.provider.entry]
2142
- ])];
3114
+ function validateStudyOllamaRoles(roles, models) {
3115
+ const names = Object.values(models).map((model) => model.name);
3116
+ for (const [role, policy] of Object.entries(roles)) {
3117
+ if (policy === void 0) continue;
3118
+ if (!names.includes(policy.model)) throw new _src_core.StudyError("CONFIG", "Ollama role policy names a model the inventory does not pin");
3119
+ const output = policy.options.num_predict;
3120
+ if (output === void 0 || !Number.isSafeInteger(output) || output <= 0) throw new _src_core.StudyError("CONFIG", "Ollama role policy declares no positive num_predict allowance");
3121
+ if (role === "judge" && policy.strict) throw new _src_core.StudyError("CONFIG", "Ollama strict fields apply only to chat roles");
3122
+ }
2143
3123
  }
2144
- /** Computes the instrument identity binding trusted runtime bytes to the adapter inventory.
2145
- * @param entry - The SHA-256 identity of the trusted runtime entry bytes.
2146
- * @param inventory - The captured adapter inventory digest.
2147
- * @returns The identity that validation, grading, and trial children must share.
3124
+ /** Encodes exact application bytes with their decoded size and SHA-256 identity.
3125
+ * @param bytes - The request or response application bytes.
3126
+ * @returns Canonical base64 and the independent byte receipt.
2148
3127
  */
2149
- function computeStudyInstrumentDigest(entry, inventory) {
2150
- return hashStudyBytes(JSON.stringify([entry, inventory]));
3128
+ function buildStudyOllamaBytes(bytes) {
3129
+ return {
3130
+ body: Buffer.from(bytes).toString("base64"),
3131
+ digest: hashStudyBytes(bytes),
3132
+ size: bytes.byteLength
3133
+ };
2151
3134
  }
2152
- /** Resolves the actual host package from its trusted executable entry.
2153
- * @param entry - The runtime's own executable, never a consumer-supplied module.
2154
- * @returns The canonical study package root.
3135
+ /** Validates independent metadata bounds and the complete optional recording group.
3136
+ * @param options - The owned transport configuration.
3137
+ * @returns Nothing when declarations are executable.
3138
+ * @throws StudyError - Thrown with CONFIG for invalid bounds, groups, identities or ordered policies.
2155
3139
  */
2156
- function resolveStudyInstrument(entry) {
2157
- const root = collectStudyAncestors((0, node_path.dirname)((0, node_fs.realpathSync)(entry))).find((directory) => {
2158
- const manifest = (0, node_path.join)(directory, "package.json");
2159
- if (!(0, node_fs.existsSync)(manifest)) return false;
2160
- const value = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
2161
- return (0, _orkestrel_contract.isRecord)(value) && value.name === "@orkestrel/study";
2162
- });
2163
- if (root === void 0) throw new _src_core.StudyError("CONFIG", "Cannot locate the study runtime package");
2164
- return root;
3140
+ function validateStudyOllamaRecording(options) {
3141
+ if (!isStudyOllamaMetadata(options.metadata)) throw new _src_core.StudyError("CONFIG", "Invalid independent metadata budget");
3142
+ if (!Object.values(options.models).every(isStudyOllamaModel)) throw new _src_core.StudyError("CONFIG", "Invalid pinned model identity");
3143
+ const declared = [
3144
+ options.version,
3145
+ options.procedure,
3146
+ options.exchanges
3147
+ ].filter((value) => value !== void 0).length;
3148
+ if (declared === 0) return;
3149
+ if (declared !== 3 || typeof options.version !== "string" || options.version.length === 0 || !Array.isArray(options.procedure) || options.exchanges === void 0 || !Number.isSafeInteger(options.exchanges.next) || options.exchanges.next < 1) throw new _src_core.StudyError("CONFIG", "Recording requires version, procedure and an exchange sink");
3150
+ for (const [index, step] of options.procedure.entries()) {
3151
+ if (!isStudyOllamaStep(step) || step.ordinal !== index + 1 || (0, _orkestrel_contract.canonicalStringify)(step.policy) !== (0, _orkestrel_contract.canonicalStringify)(options.roles[step.role])) throw new _src_core.StudyError("CONFIG", "Invalid ordered model-step declaration");
3152
+ if (step.admission === "exact") readStudyOllamaRequest(options, step.role, new URL(step.path, options.url), {
3153
+ method: "POST",
3154
+ body: step.body
3155
+ });
3156
+ }
2165
3157
  }
2166
- /** Reads the version of the study package that owns an executable entry.
2167
- * @param entry - The installed runtime's executable entry.
2168
- * @returns The non-empty version declared by the owning study package.
2169
- * @throws StudyError - Thrown when no study package owns the entry or its version is absent or empty.
2170
- * @example Read the installed version
2171
- * ```ts
2172
- * import { readStudyVersion } from '@orkestrel/study/server'
2173
- *
2174
- * declare const entry: string
2175
- * readStudyVersion(entry)
2176
- * ```
3158
+ /** Admits one model request against its next exact or capture declaration.
3159
+ * @param step - The next model step, absent after the procedure ends.
3160
+ * @param goal - The bound goal identity.
3161
+ * @param request - The independently validated request policy and byte length.
3162
+ * @param body - The owned encoded request body.
3163
+ * @returns Nothing when identity, policy and bytes match the declaration.
3164
+ * @throws StudyError - Thrown with FAULT before sending any mismatched request.
2177
3165
  */
2178
- function readStudyVersion(entry) {
2179
- const value = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(resolveStudyInstrument(entry), "package.json"), "utf8"));
2180
- if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.version !== "string" || value.version.length === 0) throw new _src_core.StudyError("CONFIG", "Study runtime package declares no version");
2181
- return value.version;
3166
+ function validateStudyOllamaStep(step, goal, request, body) {
3167
+ if (step === void 0 || step.goal !== goal || step.role !== request.role || step.path !== request.path || typeof body !== "string" || (step.admission === "exact" ? !Buffer.from(step.body).equals(Buffer.from(body)) : step.bytes !== request.bytes)) throw new _src_core.StudyError("FAULT", "Request differs from the declared model step");
2182
3168
  }
2183
- /** Collects a directory and each of its ancestors up to the filesystem root, nearest first.
2184
- * @param directory - The absolute directory to start from.
2185
- * @returns The directory followed by every parent directory, ending at the root.
3169
+ /**
3170
+ * Reads exact outbound Ollama facts and refuses a request outside its declared policy.
3171
+ * @remarks
3172
+ * The refusal happens before admission, so a refused request is never accounted.
3173
+ * @param options - The admitted origin, pinned inventory, and declared role policies
3174
+ * @param role - The responsible request role
3175
+ * @param input - The requested URL
3176
+ * @param init - The native fetch settings
3177
+ * @returns The validated dispatch facts
3178
+ * @throws StudyError - Thrown with code `CONFIG` when the endpoint, model, options, flags, or strict fields differ from the declaration, or the role declares no policy
2186
3179
  */
2187
- function collectStudyAncestors(directory) {
2188
- const parent = (0, node_path.dirname)(directory);
2189
- return parent === directory ? [directory] : [directory, ...collectStudyAncestors(parent)];
3180
+ function readStudyOllamaRequest(options, role, input, init) {
3181
+ if (input instanceof Request) throw new _src_core.StudyError("CONFIG", "Ollama transport requires explicit URL requests");
3182
+ const url = new URL(input);
3183
+ const path = role === "metadata" ? init?.method === "POST" ? "/api/show" : url.pathname === "/api/version" ? "/api/version" : "/api/tags" : role === "judge" ? "/api/generate" : "/api/chat";
3184
+ if (url.origin !== new URL(options.url).origin || url.pathname !== path || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "") throw new _src_core.StudyError("CONFIG", "Ollama transport endpoint differs from the admitted origin and path");
3185
+ if (role === "metadata") {
3186
+ if (path === "/api/show") {
3187
+ if (typeof init?.body !== "string") throw new _src_core.StudyError("CONFIG", "Invalid model metadata request");
3188
+ const body = JSON.parse(init.body);
3189
+ if (!(0, _orkestrel_contract.isRecord)(body) || Object.keys(body).length !== 1 || typeof body.model !== "string" || !Object.values(options.models).some((model) => model.name === body.model)) throw new _src_core.StudyError("CONFIG", "Invalid model metadata identity");
3190
+ return {
3191
+ role,
3192
+ path,
3193
+ model: body.model,
3194
+ output: 0,
3195
+ bytes: new TextEncoder().encode(init.body).byteLength,
3196
+ options: {},
3197
+ strict: false,
3198
+ length: false
3199
+ };
3200
+ }
3201
+ if (init?.method !== "GET" || init.body !== void 0) throw new _src_core.StudyError("CONFIG", "Invalid inventory request");
3202
+ return {
3203
+ role,
3204
+ path,
3205
+ model: void 0,
3206
+ output: 0,
3207
+ bytes: 0,
3208
+ options: {},
3209
+ strict: false,
3210
+ length: false
3211
+ };
3212
+ }
3213
+ if (init?.method !== "POST" || typeof init.body !== "string") throw new _src_core.StudyError("CONFIG", "Ollama transport requires an encoded JSON POST");
3214
+ const policy = role === "tool" || role === "maintenance" ? void 0 : options.roles[role];
3215
+ if (policy === void 0) throw new _src_core.StudyError("CONFIG", "Ollama transport role has no declared wire policy");
3216
+ const output = policy.options.num_predict;
3217
+ if (output === void 0 || !Number.isSafeInteger(output) || output <= 0) throw new _src_core.StudyError("CONFIG", "Ollama role policy declares no positive num_predict allowance");
3218
+ const body = JSON.parse(init.body);
3219
+ if (!(0, _orkestrel_contract.isRecord)(body) || body.model !== policy.model || !(0, _orkestrel_contract.isRecord)(body.options) || (0, _orkestrel_contract.canonicalStringify)(body.options) !== (0, _orkestrel_contract.canonicalStringify)(policy.options) || body.stream !== (role !== "judge")) throw new _src_core.StudyError("CONFIG", "Ollama transport model or effective options differ from the declared policy");
3220
+ if (role === "judge" ? body.raw !== true || body.logprobs !== true : typeof body.think !== "boolean" || body.think === true && !policy.think) throw new _src_core.StudyError("CONFIG", "Ollama transport flags differ from the declared policy");
3221
+ const strict = policy.strict && role !== "judge";
3222
+ if (strict && (body.truncate !== false || body.shift !== false)) throw new _src_core.StudyError("CONFIG", "Ollama transport must disable native truncation and shifting");
3223
+ return {
3224
+ role,
3225
+ path,
3226
+ model: policy.model,
3227
+ output,
3228
+ bytes: new TextEncoder().encode(init.body).byteLength,
3229
+ options: { ...policy.options },
3230
+ strict,
3231
+ length: policy.length
3232
+ };
2190
3233
  }
2191
- /** Refuses a profile that omits the actual host runtime's declared dependency bytes.
2192
- * @param instrument - The canonical package owning the trusted runtime entry.
2193
- * @param roots - Explicitly admitted package roots, including the instrument.
3234
+ /**
3235
+ * Reads the declared model context ceiling without claiming daemon allocation.
3236
+ * @param value - The parsed Ollama show response
3237
+ * @param context - The requested total context
3238
+ * @returns The unambiguous model context ceiling
3239
+ * @throws StudyError - Thrown with code `FAULT` when context metadata is missing, or `CAPACITY` when it is ambiguous or below the requested context
2194
3240
  */
2195
- function validateStudyRuntime(instrument, roots) {
2196
- const pending = [instrument];
2197
- const visited = /* @__PURE__ */ new Set();
2198
- while (pending.length > 0) {
2199
- const root = pending.pop();
2200
- if (root === void 0 || visited.has(root)) continue;
2201
- visited.add(root);
2202
- if (!roots.includes(root)) throw new _src_core.StudyError("CONFIG", `Profile roots omit a runtime dependency: ${root}`);
2203
- const manifest = (0, node_path.join)(root, "package.json");
2204
- const value = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
2205
- if (!(0, _orkestrel_contract.isRecord)(value) || value.dependencies === void 0) continue;
2206
- if (!(0, _orkestrel_contract.isRecord)(value.dependencies)) throw new _src_core.StudyError("CONFIG", "Runtime dependency declarations are invalid");
2207
- const resolver = (0, node_module.createRequire)(manifest);
2208
- for (const dependency of Object.keys(value.dependencies)) pending.push((0, node_path.dirname)((0, node_fs.realpathSync)(resolver.resolve(`${dependency}/package.json`))));
3241
+ function readStudyOllamaSupport(value, context) {
3242
+ if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(value.model_info) || typeof value.model_info["general.architecture"] !== "string") throw new _src_core.StudyError("FAULT", "Missing model context metadata");
3243
+ const architecture = value.model_info["general.architecture"];
3244
+ const keys = Object.keys(value.model_info).filter((key) => key.endsWith(".context_length"));
3245
+ const ceiling = value.model_info[`${architecture}.context_length`];
3246
+ if (keys.length !== 1 || typeof ceiling !== "number" || !Number.isSafeInteger(ceiling) || ceiling < context) {
3247
+ const message = "Model context metadata does not support the requested context";
3248
+ throw new _src_core.StudyError("CAPACITY", message, {
3249
+ cause: "capacity",
3250
+ message,
3251
+ role: void 0,
3252
+ stop: {
3253
+ layer: "model",
3254
+ id: "context_length",
3255
+ max: keys.length === 1 && Number.isSafeInteger(ceiling) ? (0, _src_core.parseStudyReading)(ceiling) : void 0,
3256
+ consumed: context
3257
+ }
3258
+ });
2209
3259
  }
3260
+ return ceiling;
2210
3261
  }
2211
- /** Releases inherited output handles a bounded time after native exit.
2212
- * @param child - A newly spawned child, subscribed before native exit.
2213
- * @remarks A descendant that outlives an exited root is outside discoverable-tree cleanup; this only stops the parent from waiting on its pipes.
3262
+ /**
3263
+ * Reads a native Ollama error member as the attributable failure it names, without usage.
3264
+ * @remarks
3265
+ * No token usage is read from the member.
3266
+ * The failure is `CAPACITY` when the member, a string or an object, names the exact structured
3267
+ * type `exceed_context_size_error` with a finite positive integer `n_prompt_tokens` above `n_ctx`,
3268
+ * a `code` of 400 if supplied, and a status of 400 if supplied. Every other member, a malformed
3269
+ * one included, is `FAULT` with its readable reason.
3270
+ * @param value - The parsed HTTP error body or wire row
3271
+ * @param role - The responsibility whose request was dispatched; the failure retains it
3272
+ * @param status - The HTTP status of the carrying response; undefined for a row of a stream that answered 200
3273
+ * @returns Undefined when the value carries no `error` member; otherwise the attributable failure
2214
3274
  */
2215
- function drainStudyChild(child) {
2216
- const closed = new AbortController();
2217
- child.once("exit", () => {
2218
- (0, node_timers_promises.setTimeout)(STUDY_DRAIN_DURATION, void 0, { signal: closed.signal }).then(() => {
2219
- child.stdout?.destroy();
2220
- child.stderr?.destroy();
2221
- }, () => void 0);
3275
+ function readStudyOllamaFailure(value, role, status) {
3276
+ if (!(0, _orkestrel_contract.isRecord)(value) || !("error" in value)) return void 0;
3277
+ const field = value.error;
3278
+ const decoded = typeof field === "string" ? (0, _orkestrel_contract.parseJSON)(field) : field;
3279
+ const structured = (0, _orkestrel_contract.isRecord)(decoded) ? (0, _orkestrel_contract.isRecord)(decoded.error) ? decoded.error : decoded : void 0;
3280
+ const native = structured?.message;
3281
+ const text = typeof native === "string" && native.trim() !== "" ? native : typeof field === "string" ? field : "";
3282
+ const label = structured?.type;
3283
+ const type = typeof label === "string" && label !== "" ? label : void 0;
3284
+ const origin = status === void 0 ? "Ollama error" : `Ollama HTTP ${status} error`;
3285
+ const prompt = structured?.n_prompt_tokens;
3286
+ const limit = structured?.n_ctx;
3287
+ const capacity = type === "exceed_context_size_error" && (0, _orkestrel_contract.isInteger)(prompt) && prompt > 0 && (0, _orkestrel_contract.isInteger)(limit) && limit > 0 && prompt > limit && (structured?.code === void 0 || structured?.code === 400) && (status === void 0 || status === 400);
3288
+ const reason = text.trim() === "" && type === void 0 ? `Malformed ${origin}` : `${origin}${type === void 0 ? "" : ` (${type})`}${text.trim() === "" ? "" : `: ${text}`}`;
3289
+ return new _src_core.StudyError(capacity ? "CAPACITY" : "FAULT", reason, {
3290
+ cause: capacity ? "capacity" : "infrastructure",
3291
+ message: reason,
3292
+ role,
3293
+ stop: capacity ? {
3294
+ layer: "model",
3295
+ id: "num_ctx",
3296
+ max: (0, _src_core.parseStudyReading)(limit),
3297
+ consumed: (0, _src_core.parseStudyReading)(prompt)
3298
+ } : void 0
2222
3299
  });
2223
- child.once("close", () => closed.abort());
2224
3300
  }
2225
- /** Terminates the recorded native process tree under a finite cleanup allowance.
2226
- * @param child - The exclusively owned child process.
3301
+ /**
3302
+ * Reads one Ollama response row without inventing absent usage.
3303
+ * @param value - The parsed wire row
3304
+ * @param request - The admitted dispatch
3305
+ * @returns The terminal facts, or undefined for a nonterminal row
3306
+ * @throws StudyError - Thrown with the recognized provider failure when the row carries an `error` member, before any other row check; `FAULT` for a malformed row, model, message, finish reason, or usage
2227
3307
  */
2228
- function terminateStudyChild(child) {
2229
- if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return;
2230
- try {
2231
- if (process.platform === "win32") (0, node_child_process.execFileSync)("taskkill", [
2232
- "/PID",
2233
- String(child.pid),
2234
- "/T",
2235
- "/F"
2236
- ], {
2237
- windowsHide: true,
2238
- timeout: 2e3,
2239
- stdio: "ignore"
2240
- });
2241
- else {
2242
- const rows = (0, node_child_process.execFileSync)("ps", ["-eo", "pid=,ppid="], {
2243
- timeout: 2e3,
2244
- encoding: "utf8",
2245
- maxBuffer: 1048576
2246
- }).trim().split(/\r\n|\n/).map((row) => row.trim().split(/\s+/).map(Number));
2247
- const descendants = /* @__PURE__ */ new Set([child.pid]);
2248
- rows.forEach(() => {
2249
- for (const [pid, parent] of rows) if (pid !== void 0 && parent !== void 0 && descendants.has(parent)) descendants.add(pid);
2250
- });
2251
- for (const pid of [...descendants].reverse()) try {
2252
- process.kill(pid, "SIGKILL");
2253
- } catch (error) {
2254
- if (!matchesSystemError(error, "ESRCH")) throw error;
2255
- }
2256
- }
2257
- } catch (error) {
2258
- try {
2259
- process.kill(child.pid, 0);
2260
- } catch (status) {
2261
- if (matchesSystemError(status, "ESRCH")) return;
2262
- throw status;
2263
- }
2264
- if (child.exitCode === null && child.signalCode === null) {
2265
- child.kill("SIGKILL");
2266
- throw error;
3308
+ function readStudyOllamaTerminal(value, request) {
3309
+ const failure = readStudyOllamaFailure(value, request.role);
3310
+ if (failure !== void 0) throw failure;
3311
+ if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.done !== "boolean") throw new _src_core.StudyError("FAULT", "Malformed Ollama response row");
3312
+ if (value.model !== request.model) throw new _src_core.StudyError("FAULT", "Ollama response model differs from the dispatched model");
3313
+ if (request.role !== "judge") {
3314
+ if (!(0, _orkestrel_contract.isRecord)(value.message) || typeof value.message.content !== "string" || value.message.thinking !== void 0 && typeof value.message.thinking !== "string") throw new _src_core.StudyError("FAULT", "Malformed Ollama message");
3315
+ if (value.message.tool_calls !== void 0) {
3316
+ if (!(0, _orkestrel_contract.isArray)(value.message.tool_calls)) throw new _src_core.StudyError("FAULT", "Malformed Ollama tool calls");
3317
+ for (const call of value.message.tool_calls) if (!(0, _orkestrel_contract.isRecord)(call) || !(0, _orkestrel_contract.isRecord)(call.function) || typeof call.function.name !== "string" || !(0, _orkestrel_contract.isRecord)(call.function.arguments)) throw new _src_core.StudyError("FAULT", "Malformed Ollama tool call");
2267
3318
  }
2268
3319
  }
3320
+ if (!value.done) return void 0;
3321
+ if (typeof value.done_reason !== "string" || value.done_reason.length === 0) throw new _src_core.StudyError("FAULT", "Missing Ollama finish reason");
3322
+ for (const key of ["prompt_eval_count", "eval_count"]) if (value[key] !== void 0 && (typeof value[key] !== "number" || !Number.isSafeInteger(value[key]) || value[key] < 0)) throw new _src_core.StudyError("FAULT", "Malformed Ollama usage", {
3323
+ cause: "infrastructure",
3324
+ message: "Malformed Ollama usage",
3325
+ role: request.role,
3326
+ stop: void 0
3327
+ });
3328
+ return {
3329
+ prompt: typeof value.prompt_eval_count === "number" ? value.prompt_eval_count : void 0,
3330
+ completion: typeof value.eval_count === "number" ? value.eval_count : void 0,
3331
+ reason: value.done_reason
3332
+ };
2269
3333
  }
2270
- //#endregion
2271
- //#region src/server/processes/StudyArtifactLoader.ts
2272
- /** Binds consumer instrumentation separately from selected executable identities. */
2273
- var StudyArtifactLoader = class {
2274
- #options;
2275
- #loader;
2276
- #instrument;
2277
- #identity;
2278
- #entry;
2279
- /** Owns resolved built and consumer source selections.
2280
- * @param options - The trusted host, adapter, and profile.
2281
- */
2282
- constructor(options) {
2283
- this.#options = structuredClone(options);
2284
- if (![
2285
- options.entry,
2286
- options.root,
2287
- options.adapter,
2288
- ...collectStudyEntries(options.profile),
2289
- ...options.profile.roots
2290
- ].every(node_path.isAbsolute)) throw new _src_core.StudyError("CONFIG", "Study artifact paths must be absolute");
2291
- }
2292
- /** Inventories bytes without importing selected code.
2293
- * @returns Bound executable and instrumentation identities.
2294
- */
2295
- async inspect() {
2296
- if (this.#identity !== void 0) {
2297
- this.verify();
2298
- return this.#identity;
2299
- }
2300
- const profile = this.#options.profile;
2301
- const instrument = resolveStudyInstrument(this.#options.entry);
2302
- const roots = [.../* @__PURE__ */ new Set([...profile.roots, instrument])];
2303
- validateStudyRuntime(instrument, roots);
2304
- const source = {
2305
- root: (0, node_path.dirname)(this.#options.adapter),
2306
- entry: this.#options.adapter
2307
- };
2308
- this.#instrument = new ArtifactLoader({
2309
- entries: [],
2310
- roots,
2311
- source,
2312
- instrument
2313
- });
2314
- const entry = hashStudyBytes(await readStudyBytes(this.#options.entry, STUDY_CUSTODY_BYTES));
2315
- this.#entry = entry;
2316
- this.#loader = new ArtifactLoader({
2317
- entries: collectStudyEntries(profile),
2318
- roots,
2319
- source,
2320
- instrument
3334
+ /**
3335
+ * Checks terminal policy after the actual wire cost has been retained.
3336
+ * @param terminal - The valid terminal facts
3337
+ * @param room - The reserved context and safety margin
3338
+ * @param request - The admitted dispatch and its declared finish policy; every refusal retains its role
3339
+ * @returns Nothing when the completion satisfies the declared policy
3340
+ * @throws StudyError - Thrown with code `CAPACITY` when the actual prompt exceeds its room, generation exceeds its allowance, or an undeclared `length` finish or capped generation occurs; `FAULT` for another unsupported finish reason
3341
+ */
3342
+ function validateStudyOllamaCompletion(terminal, room, request) {
3343
+ const { prompt, completion, reason } = terminal;
3344
+ const { role } = request;
3345
+ const capped = {
3346
+ layer: "model",
3347
+ id: "num_predict",
3348
+ max: request.output,
3349
+ consumed: completion
3350
+ };
3351
+ const ceiling = room.context - request.output - room.safety;
3352
+ if (prompt !== void 0 && prompt > ceiling) {
3353
+ const message = "Actual prompt exceeded reserved context capacity";
3354
+ throw new _src_core.StudyError("CAPACITY", message, {
3355
+ cause: "capacity",
3356
+ message,
3357
+ role,
3358
+ stop: {
3359
+ layer: "window",
3360
+ id: "Context room",
3361
+ max: ceiling,
3362
+ consumed: prompt
3363
+ }
2321
3364
  });
2322
- const inventory = this.#loader.inspect();
2323
- this.#identity = Object.freeze({
2324
- agent: inventory.digest,
2325
- subject: inventory.digest,
2326
- provider: profile.provider === void 0 ? void 0 : inventory.digest,
2327
- instrument: computeStudyInstrumentDigest(entry, this.#instrument.inspect().digest)
3365
+ }
3366
+ if (completion !== void 0 && completion > request.output) {
3367
+ const message = "Actual generation exceeded the output allowance";
3368
+ throw new _src_core.StudyError("CAPACITY", message, {
3369
+ cause: "capacity",
3370
+ message,
3371
+ role,
3372
+ stop: capped
2328
3373
  });
2329
- this.verify();
2330
- return this.#identity;
2331
3374
  }
2332
- /** Loads the adapter from the trial inventory.
2333
- * @returns The complete consumer adapter.
2334
- */
2335
- async adapter() {
2336
- await this.inspect();
2337
- if (this.#loader === void 0) throw new _src_core.StudyError("CONFIG", "Artifact inspection is required");
2338
- return readStudyAdapter(await this.#loader.load(this.#options.adapter));
3375
+ if (!request.length && (reason === "length" || completion !== void 0 && completion >= request.output)) {
3376
+ const message = "Substantive generation reached its output cap";
3377
+ throw new _src_core.StudyError("CAPACITY", message, {
3378
+ cause: "capacity",
3379
+ message,
3380
+ role,
3381
+ stop: capped
3382
+ });
2339
3383
  }
2340
- /** Loads admitted built namespaces inside the trial child.
2341
- * @returns The agent, subject and optional provider; a shared entry loads once and keeps each role.
2342
- */
2343
- async load() {
2344
- await this.inspect();
2345
- if (this.#loader === void 0) throw new _src_core.StudyError("CONFIG", "Artifact inspection is required");
2346
- const profile = this.#options.profile;
2347
- const loaded = [];
2348
- for (const entry of collectStudyEntries(profile)) loaded.push([entry, await this.#loader.load(entry)]);
2349
- const namespaces = new Map(loaded);
2350
- this.verify();
2351
- const provider = profile.provider;
2352
- return Object.freeze({
2353
- agent: namespaces.get(profile.agent),
2354
- subject: namespaces.get(profile.subject),
2355
- provider: provider === void 0 ? void 0 : namespaces.get(provider.entry)
3384
+ if (reason !== "stop" && !(request.length && reason === "length")) {
3385
+ const message = "Unsupported Ollama finish reason";
3386
+ throw new _src_core.StudyError("FAULT", message, {
3387
+ cause: "infrastructure",
3388
+ message,
3389
+ role,
3390
+ stop: void 0
2356
3391
  });
2357
3392
  }
2358
- /** Refuses instrument and inventory drift after inspection. */
2359
- verify() {
2360
- if (this.#loader === void 0 || this.#instrument === void 0) throw new _src_core.StudyError("CONFIG", "Artifact inspection is required");
2361
- if (hashStudyBytes((0, node_fs.readFileSync)(this.#options.entry)) !== this.#entry) throw new _src_core.StudyError("FAULT", "Study instrument bytes drifted");
2362
- this.#loader.verify();
2363
- this.#instrument.verify();
3393
+ }
3394
+ /**
3395
+ * Verifies that each pinned name resolves to one inventory entry with its pinned digest.
3396
+ * @param value - The parsed inventory response
3397
+ * @param models - The pinned served identities
3398
+ * @returns Nothing for a matching inventory
3399
+ * @throws StudyError - Thrown with code `FAULT` when the inventory is unsupported or a pinned identity does not match
3400
+ */
3401
+ function validateStudyOllamaInventory(value, models) {
3402
+ if (!(0, _orkestrel_contract.isObject)(value) || !("models" in value) || !(0, _orkestrel_contract.isArray)(value.models)) throw new _src_core.StudyError("FAULT", "Unsupported Ollama inventory");
3403
+ const served = value.models;
3404
+ for (const { name, digest } of Object.values(models)) {
3405
+ const matches = served.filter((model) => (0, _orkestrel_contract.isRecord)(model) && model.name === name);
3406
+ const model = matches[0];
3407
+ if (matches.length !== 1 || !(0, _orkestrel_contract.isRecord)(model) || typeof model.digest !== "string" || model.digest.replace(/^sha256:/, "") !== digest.replace(/^sha256:/, "")) throw new _src_core.StudyError("FAULT", "Serving inventory does not match the required model digest");
2364
3408
  }
2365
- };
3409
+ }
3410
+ /**
3411
+ * Reads the pinned serving digest of an admitted request's model.
3412
+ * @param models - The pinned served identities
3413
+ * @param request - The admitted dispatch
3414
+ * @returns The pinned digest, or undefined for an inventory request or an unpinned model
3415
+ */
3416
+ function readStudyOllamaDigest(models, request) {
3417
+ if (request.model === void 0) return void 0;
3418
+ return Object.values(models).find((model) => model.name === request.model)?.digest;
3419
+ }
3420
+ /**
3421
+ * Reads daemon timing from a terminal row without letting an observation fail the request.
3422
+ * @param extract - The admitted extractor
3423
+ * @param record - The validated terminal row
3424
+ * @returns The raw timing, or undefined when absent or the extractor throws
3425
+ */
3426
+ function readStudyOllamaTiming(extract, record) {
3427
+ try {
3428
+ return extract(record);
3429
+ } catch {
3430
+ return;
3431
+ }
3432
+ }
3433
+ /**
3434
+ * Builds the JSON evidence form of a timing observation with raw nanoseconds and counts.
3435
+ * @param timing - The extracted timing
3436
+ * @returns Null when no terminal timing was observed; otherwise every unreported member as null
3437
+ */
3438
+ function buildStudyOllamaTiming(timing) {
3439
+ if (timing === void 0) return null;
3440
+ return {
3441
+ durations: {
3442
+ total: timing.durations.total ?? null,
3443
+ load: timing.durations.load ?? null,
3444
+ prompt: timing.durations.prompt ?? null,
3445
+ decode: timing.durations.decode ?? null
3446
+ },
3447
+ counts: {
3448
+ prompt: timing.counts.prompt ?? null,
3449
+ decode: timing.counts.decode ?? null,
3450
+ cached: timing.counts.cached ?? null
3451
+ },
3452
+ invalid: [...timing.invalid]
3453
+ };
3454
+ }
3455
+ /**
3456
+ * Builds a reading of the raw prompt text an encoded request body carries, as sent.
3457
+ * @param body - The encoded request body
3458
+ * @returns The prompt's UTF-16 code units and UTF-8 bytes, labeled with its encoding, or undefined when there is no string prompt; these are readings of the actual text, not token allocations
3459
+ */
3460
+ function buildStudyOllamaRaw(body) {
3461
+ if (typeof body !== "string") return void 0;
3462
+ const parsed = (0, _orkestrel_contract.attempt)(() => JSON.parse(body));
3463
+ if (!parsed.success || !(0, _orkestrel_contract.isRecord)(parsed.value) || typeof parsed.value.prompt !== "string") return void 0;
3464
+ return {
3465
+ encoding: "utf-8",
3466
+ units: parsed.value.prompt.length,
3467
+ bytes: new TextEncoder().encode(parsed.value.prompt).byteLength
3468
+ };
3469
+ }
2366
3470
  //#endregion
2367
- //#region src/server/processes/validators.ts
2368
- /** Checks the complete transport envelope before domain handling.
2369
- * @param value - Native IPC decoded value.
2370
- * @returns True if the envelope is bounded in shape and JSON-compatible; false otherwise.
3471
+ //#region src/server/processes/helpers.ts
3472
+ /** Reads the executing profile's explicit recording declarations before a worker starts.
3473
+ * @param profile - The validated executing profile.
3474
+ * @param trial - The scheduled trial identity.
3475
+ * @returns A validated opening row, with no inferred metadata or replay allowance.
2371
3476
  */
2372
- function isStudyFrame(value) {
2373
- return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).length === 3 && Object.hasOwn(value, "id") && Object.hasOwn(value, "operation") && Object.hasOwn(value, "value") && typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id >= 0 && typeof value.operation === "string" && [
2374
- "open",
2375
- "goal",
2376
- "close",
2377
- "admit",
2378
- "finish",
2379
- "fail",
2380
- "evidence",
2381
- "reply",
2382
- "error",
2383
- "abort"
2384
- ].includes(value.operation) && (0, _orkestrel_contract.isJSONValue)(value.value));
3477
+ function readStudyRecordingDeclaration(profile, trial) {
3478
+ if (profile.provider === void 0 || profile.settings === void 0) throw new _src_core.StudyError("CONFIG", "Recording requires provider and explicit wire declarations");
3479
+ const declaration = {
3480
+ phase: "declaration",
3481
+ trial: trial.id,
3482
+ version: profile.settings.version,
3483
+ metadata: profile.settings.metadata,
3484
+ procedure: profile.settings.procedure,
3485
+ models: buildStudyOllamaModels(profile.provider),
3486
+ maintenance: []
3487
+ };
3488
+ if (!isStudyExchangeDeclaration(declaration)) throw new _src_core.StudyError("CONFIG", "Recording requires valid version, metadata and ordered procedure settings");
3489
+ return declaration;
2385
3490
  }
2386
- /** Checks operational failure attribution arriving over IPC.
2387
- * @param value - Decoded failure value.
2388
- * @returns True if the failure carries a known cause, an optional role, and an optional stop that satisfies `isStudyStop` on a `capacity` or `deadline` cause; false otherwise.
3491
+ /** Validates parent preparation output without permitting a destination or allowance substitution.
3492
+ * @param original - The parent-reserved destination, or undefined without recording.
3493
+ * @param prepared - The callback's retained destination.
3494
+ * @returns The validated destination for the worker.
2389
3495
  */
2390
- function isStudyProcessFailure(value) {
2391
- return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && typeof value.message === "string" && (0, _src_core.isStudyCause)(value.cause) && (value.role === void 0 || (0, _src_core.isStudyRole)(value.role)) && (value.stop === void 0 || (value.cause === "capacity" || value.cause === "deadline") && (0, _src_core.isStudyStop)(value.stop)));
3496
+ function readStudyPreparedDestination(original, prepared) {
3497
+ if (original === void 0 ? prepared !== void 0 : !isStudyExchangeDestination(prepared) || prepared.path !== original.path || (0, _orkestrel_contract.canonicalStringify)(prepared.record) !== (0, _orkestrel_contract.canonicalStringify)(original.record)) throw new _src_core.StudyError("CUSTODY", "Trial preparation changed its reserved destination");
3498
+ return prepared;
2392
3499
  }
2393
- /** Checks a request's declared role and finite cost ceilings.
2394
- * @param value - Decoded charge.
2395
- * @returns True if all charge leaves are valid; false otherwise.
3500
+ /** Rechecks the sealed source and this trial's prefix under the executing profile declaration.
3501
+ * @param selection - The explicit sealed source and offline behavior.
3502
+ * @param profile - The executing profile, whose bounds remain authoritative.
3503
+ * @param trial - The trial whose history must be present.
3504
+ * @returns Complete verified history before any dependent sender or worker starts.
2396
3505
  */
2397
- function isStudyCharge(value) {
2398
- return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyRole)(value.role) && typeof value.tokens === "number" && Number.isSafeInteger(value.tokens) && value.tokens >= 0 && typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes >= 0);
3506
+ async function readStudyReplayHistory(selection, profile, trial) {
3507
+ if (!isStudyReplaySelection(selection)) throw new _src_core.StudyError("CONFIG", "Invalid replay selection");
3508
+ const result = await readStudyResult(selection.result);
3509
+ if (result.status !== "sealed") throw new _src_core.StudyError("CUSTODY", "Replay requires a sealed result");
3510
+ const history = await readStudyExchanges(result, trial.id);
3511
+ const prefix = selection.prefix[trial.id];
3512
+ if (prefix === void 0 || prefix > history.exchanges.length || selection.offline && prefix !== history.exchanges.length) throw new _src_core.StudyError("CONFIG", "Replay prefix does not cover its declared trial");
3513
+ const { metadata: _metadata, maintenance: _maintenance, ...current } = readStudyRecordingDeclaration(profile, trial);
3514
+ const { metadata: _recordedMetadata, maintenance: _recordedMaintenance, ...recorded } = history.declaration;
3515
+ if ((0, _orkestrel_contract.canonicalStringify)(current) !== (0, _orkestrel_contract.canonicalStringify)(recorded)) throw new _src_core.StudyError("CONFIG", "Replay declaration differs from recorded execution");
3516
+ return history;
2399
3517
  }
2400
- /** Checks append-only public evidence.
2401
- * @param value - Decoded evidence.
2402
- * @returns True if event, goal and payload are valid; false otherwise.
3518
+ /** Projects optional domain fields into the JSON wire representation.
3519
+ * @param value - An owned domain value with optional undefined fields.
3520
+ * @returns A JSON-compatible snapshot.
3521
+ * @example Project a wire value
3522
+ * ```ts
3523
+ * import { valueToWire } from '@orkestrel/study/server'
3524
+ *
3525
+ * valueToWire({ state: 'ready', failure: undefined }) // { state: 'ready' }
3526
+ * ```
2403
3527
  */
2404
- function isStudyEvidence(value) {
2405
- return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyName)(value.event) && (0, _src_core.isStudyName)(value.goal) && (0, _orkestrel_contract.isJSONValue)(value.value));
3528
+ function valueToWire(value) {
3529
+ const parsed = JSON.parse(JSON.stringify(value));
3530
+ if (!(0, _orkestrel_contract.isJSONValue)(parsed)) throw new _src_core.StudyError("FAULT", "Invalid study wire value");
3531
+ return parsed;
2406
3532
  }
2407
- /** Checks a fresh trial's declared identifiers.
2408
- * @param value - Decoded trial.
2409
- * @returns True if every identifier and repetition is valid; false otherwise.
3533
+ /** Creates attributable protocol infrastructure failure.
3534
+ * @param message - The refused operation or broken boundary.
3535
+ * @returns A domain failure that never awards task credit.
2410
3536
  */
2411
- function isStudyTrial(value) {
2412
- return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyName)(value.id) && (0, _src_core.isStudyName)(value.scenario) && (0, _src_core.isStudyName)(value.arm) && typeof value.repetition === "number" && Number.isSafeInteger(value.repetition) && value.repetition >= 0);
3537
+ function createStudyProcessFailure(message) {
3538
+ return {
3539
+ cause: "infrastructure",
3540
+ message,
3541
+ role: void 0,
3542
+ stop: void 0
3543
+ };
2413
3544
  }
2414
- /** Checks the exact digest fields carried by the execution declaration.
2415
- * @param value - Decoded identity.
2416
- * @returns True if all required identities are digests; false otherwise.
3545
+ /** Binds the canonical fully resolved execution profile carried over IPC.
3546
+ * @param profile - The resolved execution settings, with optional absent leaves.
3547
+ * @returns The SHA256 digest of the canonical JSON wire representation.
2417
3548
  */
2418
- function isStudyIdentity(value) {
2419
- return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyDigest)(value.agent) && (0, _src_core.isStudyDigest)(value.subject) && (value.provider === void 0 || (0, _src_core.isStudyDigest)(value.provider)) && (0, _src_core.isStudyDigest)(value.instrument) && (0, _src_core.isStudyDigest)(value.fixtures) && (0, _src_core.isStudyDigest)(value.profile));
3549
+ function computeStudyProfileDigest(profile) {
3550
+ const encoded = (0, _orkestrel_contract.canonicalStringify)(valueToWire(profile));
3551
+ if (encoded === void 0) throw new _src_core.StudyError("CONFIG", "Study profile is not JSON-compatible");
3552
+ return hashStudyBytes(encoded);
2420
3553
  }
2421
- //#endregion
2422
- //#region src/server/processes/StudyChannel.ts
2423
- /** Correlates bounded IPC work and rejects every outstanding request on transport failure. */
2424
- var StudyChannel = class {
2425
- #options;
2426
- #control = new _src_core.StudyControl();
2427
- #pending = /* @__PURE__ */ new Map();
2428
- #next = 0;
3554
+ /** Collects the executable entries a profile selects, each canonical path once.
3555
+ * @param profile - The resolved execution profile.
3556
+ * @returns The agent, subject, and provider entries without repeating a shared entry.
3557
+ * @remarks The declared roles stay separate in the profile; only capture and loading deduplicate.
3558
+ */
3559
+ function collectStudyEntries(profile) {
3560
+ return [.../* @__PURE__ */ new Set([
3561
+ profile.agent,
3562
+ profile.subject,
3563
+ ...profile.provider === void 0 ? [] : [profile.provider.entry]
3564
+ ])];
3565
+ }
3566
+ /** Computes the instrument identity binding trusted runtime bytes to the adapter inventory.
3567
+ * @param entry - The SHA-256 identity of the trusted runtime entry bytes.
3568
+ * @param inventory - The captured adapter inventory digest.
3569
+ * @returns The identity that validation, grading, and trial children must share.
3570
+ */
3571
+ function computeStudyInstrumentDigest(entry, inventory) {
3572
+ return hashStudyBytes(JSON.stringify([entry, inventory]));
3573
+ }
3574
+ /** Resolves the actual host package from its trusted executable entry.
3575
+ * @param entry - The runtime's own executable, never a consumer-supplied module.
3576
+ * @returns The canonical study package root.
3577
+ */
3578
+ function resolveStudyInstrument(entry) {
3579
+ const root = collectStudyAncestors((0, node_path.dirname)((0, node_fs.realpathSync)(entry))).find((directory) => {
3580
+ const manifest = (0, node_path.join)(directory, "package.json");
3581
+ if (!(0, node_fs.existsSync)(manifest)) return false;
3582
+ const value = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
3583
+ return (0, _orkestrel_contract.isRecord)(value) && value.name === "@orkestrel/study";
3584
+ });
3585
+ if (root === void 0) throw new _src_core.StudyError("CONFIG", "Cannot locate the study runtime package");
3586
+ return root;
3587
+ }
3588
+ /** Reads the version of the study package that owns an executable entry.
3589
+ * @param entry - The installed runtime's executable entry.
3590
+ * @returns The non-empty version declared by the owning study package.
3591
+ * @throws StudyError - Thrown when no study package owns the entry or its version is absent or empty.
3592
+ * @example Read the installed version
3593
+ * ```ts
3594
+ * import { readStudyVersion } from '@orkestrel/study/server'
3595
+ *
3596
+ * declare const entry: string
3597
+ * readStudyVersion(entry)
3598
+ * ```
3599
+ */
3600
+ function readStudyVersion(entry) {
3601
+ const value = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(resolveStudyInstrument(entry), "package.json"), "utf8"));
3602
+ if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.version !== "string" || value.version.length === 0) throw new _src_core.StudyError("CONFIG", "Study runtime package declares no version");
3603
+ return value.version;
3604
+ }
3605
+ /** Collects a directory and each of its ancestors up to the filesystem root, nearest first.
3606
+ * @param directory - The absolute directory to start from.
3607
+ * @returns The directory followed by every parent directory, ending at the root.
3608
+ */
3609
+ function collectStudyAncestors(directory) {
3610
+ const parent = (0, node_path.dirname)(directory);
3611
+ return parent === directory ? [directory] : [directory, ...collectStudyAncestors(parent)];
3612
+ }
3613
+ /** Refuses a profile that omits the actual host runtime's declared dependency bytes.
3614
+ * @param instrument - The canonical package owning the trusted runtime entry.
3615
+ * @param roots - Explicitly admitted package roots, including the instrument.
3616
+ */
3617
+ function validateStudyRuntime(instrument, roots) {
3618
+ const pending = [instrument];
3619
+ const visited = /* @__PURE__ */ new Set();
3620
+ while (pending.length > 0) {
3621
+ const root = pending.pop();
3622
+ if (root === void 0 || visited.has(root)) continue;
3623
+ visited.add(root);
3624
+ if (!roots.includes(root)) throw new _src_core.StudyError("CONFIG", `Profile roots omit a runtime dependency: ${root}`);
3625
+ const manifest = (0, node_path.join)(root, "package.json");
3626
+ const value = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
3627
+ if (!(0, _orkestrel_contract.isRecord)(value) || value.dependencies === void 0) continue;
3628
+ if (!(0, _orkestrel_contract.isRecord)(value.dependencies)) throw new _src_core.StudyError("CONFIG", "Runtime dependency declarations are invalid");
3629
+ const resolver = (0, node_module.createRequire)(manifest);
3630
+ for (const dependency of Object.keys(value.dependencies)) pending.push((0, node_path.dirname)((0, node_fs.realpathSync)(resolver.resolve(`${dependency}/package.json`))));
3631
+ }
3632
+ }
3633
+ /** Releases inherited output handles a bounded time after native exit.
3634
+ * @param child - A newly spawned child, subscribed before native exit.
3635
+ * @remarks A descendant that outlives an exited root is outside discoverable-tree cleanup; this only stops the parent from waiting on its pipes.
3636
+ */
3637
+ function drainStudyChild(child) {
3638
+ const closed = new AbortController();
3639
+ child.once("exit", () => {
3640
+ (0, node_timers_promises.setTimeout)(STUDY_DRAIN_DURATION, void 0, { signal: closed.signal }).then(() => {
3641
+ child.stdout?.destroy();
3642
+ child.stderr?.destroy();
3643
+ }, () => void 0);
3644
+ });
3645
+ child.once("close", () => closed.abort());
3646
+ }
3647
+ /** Terminates the recorded native process tree under a finite cleanup allowance.
3648
+ * @param child - The exclusively owned child process.
3649
+ */
3650
+ function terminateStudyChild(child) {
3651
+ if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return;
3652
+ try {
3653
+ if (process.platform === "win32") (0, node_child_process.execFileSync)("taskkill", [
3654
+ "/PID",
3655
+ String(child.pid),
3656
+ "/T",
3657
+ "/F"
3658
+ ], {
3659
+ windowsHide: true,
3660
+ timeout: 2e3,
3661
+ stdio: "ignore"
3662
+ });
3663
+ else {
3664
+ const rows = (0, node_child_process.execFileSync)("ps", ["-eo", "pid=,ppid="], {
3665
+ timeout: 2e3,
3666
+ encoding: "utf8",
3667
+ maxBuffer: 1048576
3668
+ }).trim().split(/\r\n|\n/).map((row) => row.trim().split(/\s+/).map(Number));
3669
+ const descendants = /* @__PURE__ */ new Set([child.pid]);
3670
+ rows.forEach(() => {
3671
+ for (const [pid, parent] of rows) if (pid !== void 0 && parent !== void 0 && descendants.has(parent)) descendants.add(pid);
3672
+ });
3673
+ for (const pid of [...descendants].reverse()) try {
3674
+ process.kill(pid, "SIGKILL");
3675
+ } catch (error) {
3676
+ if (!matchesSystemError(error, "ESRCH")) throw error;
3677
+ }
3678
+ }
3679
+ } catch (error) {
3680
+ try {
3681
+ process.kill(child.pid, 0);
3682
+ } catch (status) {
3683
+ if (matchesSystemError(status, "ESRCH")) return;
3684
+ throw status;
3685
+ }
3686
+ if (child.exitCode === null && child.signalCode === null) {
3687
+ child.kill("SIGKILL");
3688
+ throw error;
3689
+ }
3690
+ }
3691
+ }
3692
+ //#endregion
3693
+ //#region src/server/processes/StudyArtifactLoader.ts
3694
+ /** Binds consumer instrumentation separately from selected executable identities. */
3695
+ var StudyArtifactLoader = class {
3696
+ #options;
3697
+ #loader;
3698
+ #instrument;
3699
+ #identity;
3700
+ #entry;
3701
+ /** Owns resolved built and consumer source selections.
3702
+ * @param options - The trusted host, adapter, and profile.
3703
+ */
3704
+ constructor(options) {
3705
+ this.#options = structuredClone(options);
3706
+ if (![
3707
+ options.entry,
3708
+ options.root,
3709
+ options.adapter,
3710
+ ...collectStudyEntries(options.profile),
3711
+ ...options.profile.roots
3712
+ ].every(node_path.isAbsolute)) throw new _src_core.StudyError("CONFIG", "Study artifact paths must be absolute");
3713
+ }
3714
+ /** Inventories bytes without importing selected code.
3715
+ * @returns Bound executable and instrumentation identities.
3716
+ */
3717
+ async inspect() {
3718
+ const closure = this.#options.closure;
3719
+ if (closure !== void 0) verifyArtifactClosure(closure.closure, true);
3720
+ if (this.#identity !== void 0) {
3721
+ this.verify();
3722
+ return this.#identity;
3723
+ }
3724
+ const profile = this.#options.profile;
3725
+ const instrument = resolveStudyInstrument(this.#options.entry);
3726
+ const roots = [.../* @__PURE__ */ new Set([...profile.roots, instrument])];
3727
+ validateStudyRuntime(instrument, roots);
3728
+ const source = {
3729
+ root: (0, node_path.dirname)(this.#options.adapter),
3730
+ entry: this.#options.adapter
3731
+ };
3732
+ this.#instrument = new ArtifactLoader({
3733
+ entries: [],
3734
+ roots,
3735
+ source,
3736
+ instrument
3737
+ });
3738
+ const entry = hashStudyBytes(await readStudyBytes(this.#options.entry, STUDY_CUSTODY_BYTES));
3739
+ this.#entry = entry;
3740
+ this.#loader = new ArtifactLoader(closure === void 0 ? {
3741
+ entries: collectStudyEntries(profile),
3742
+ roots,
3743
+ source,
3744
+ instrument
3745
+ } : closure.closure.options);
3746
+ const inventory = this.#loader.inspect();
3747
+ this.#identity = Object.freeze({
3748
+ agent: inventory.digest,
3749
+ subject: inventory.digest,
3750
+ provider: profile.provider === void 0 ? void 0 : inventory.digest,
3751
+ instrument: computeStudyInstrumentDigest(entry, this.#instrument.inspect().digest)
3752
+ });
3753
+ this.verify();
3754
+ return this.#identity;
3755
+ }
3756
+ /** Loads the adapter from the trial inventory.
3757
+ * @returns The complete consumer adapter.
3758
+ */
3759
+ async adapter() {
3760
+ await this.inspect();
3761
+ if (this.#loader === void 0) throw new _src_core.StudyError("CONFIG", "Artifact inspection is required");
3762
+ return readStudyAdapter(await this.#loader.load(this.#options.adapter));
3763
+ }
3764
+ /** Loads admitted built namespaces inside the trial child.
3765
+ * @returns The agent, subject and optional provider; a shared entry loads once and keeps each role.
3766
+ */
3767
+ async load() {
3768
+ await this.inspect();
3769
+ if (this.#loader === void 0) throw new _src_core.StudyError("CONFIG", "Artifact inspection is required");
3770
+ const profile = this.#options.profile;
3771
+ const loaded = [];
3772
+ for (const entry of collectStudyEntries(profile)) loaded.push([entry, await this.#loader.load(entry)]);
3773
+ const namespaces = new Map(loaded);
3774
+ this.verify();
3775
+ const provider = profile.provider;
3776
+ return Object.freeze({
3777
+ agent: namespaces.get(profile.agent),
3778
+ subject: namespaces.get(profile.subject),
3779
+ provider: provider === void 0 ? void 0 : namespaces.get(provider.entry)
3780
+ });
3781
+ }
3782
+ /** Refuses instrument and inventory drift after inspection. */
3783
+ verify() {
3784
+ if (this.#loader === void 0 || this.#instrument === void 0) throw new _src_core.StudyError("CONFIG", "Artifact inspection is required");
3785
+ if (hashStudyBytes((0, node_fs.readFileSync)(this.#options.entry)) !== this.#entry) throw new _src_core.StudyError("FAULT", "Study instrument bytes drifted");
3786
+ this.#loader.verify();
3787
+ this.#instrument.verify();
3788
+ if (this.#options.closure !== void 0) verifyArtifactClosure(this.#options.closure.closure, true);
3789
+ }
3790
+ };
3791
+ //#endregion
3792
+ //#region src/server/processes/StudyChannel.ts
3793
+ /** Correlates bounded IPC work and rejects every outstanding request on transport failure. */
3794
+ var StudyChannel = class {
3795
+ #options;
3796
+ #control = new _src_core.StudyControl();
3797
+ #pending = /* @__PURE__ */ new Map();
3798
+ #next = 0;
2429
3799
  #received = 0;
2430
3800
  #working = /* @__PURE__ */ new Set();
2431
3801
  #message;
@@ -2621,6 +3991,8 @@ var StudyProcessSession = class {
2621
3991
  this.#options.root,
2622
3992
  this.#options.directory
2623
3993
  ].every(node_path.isAbsolute)) throw new _src_core.StudyError("CONFIG", "Study process paths must be absolute");
3994
+ const destination = this.#options.exchanges?.[this.#options.trial.id];
3995
+ if (this.#options.profile.record === void 0 ? destination !== void 0 : !isStudyExchangeDestination(destination) || destination.record.bytes !== this.#options.profile.record.bytes) throw new _src_core.StudyError("CONFIG", "Session recording differs from its reserved allowance");
2624
3996
  const directory = await (0, node_fs_promises.mkdtemp)((0, node_path.join)(this.#options.directory, "trial-"));
2625
3997
  if (this.#control.failure !== void 0) throw (0, _src_core.failureToStudyError)(this.#control.failure);
2626
3998
  const child = (0, node_child_process.fork)(this.#options.entry, [STUDY_WORKER_OPERATION], {
@@ -2827,11 +4199,16 @@ var StudyProcessSession = class {
2827
4199
  /** Opens one fresh native child for each scheduled trial. */
2828
4200
  var StudyProcessDriver = class {
2829
4201
  #options;
4202
+ #prepare;
2830
4203
  /** Owns immutable resolved launch settings.
2831
4204
  * @param options - Built entry, admitted identities and exclusive scratch parent.
2832
4205
  */
2833
4206
  constructor(options) {
2834
- this.#options = structuredClone(options);
4207
+ const { prepare, ...configuration } = options;
4208
+ if (prepare !== void 0 && typeof prepare !== "function") throw new _src_core.StudyError("CONFIG", "Trial preparation must be callable");
4209
+ if (options.replay?.offline === true && prepare !== void 0) throw new _src_core.StudyError("CONFIG", "Offline replay cannot prepare external resources");
4210
+ this.#options = structuredClone(configuration);
4211
+ this.#prepare = prepare;
2835
4212
  }
2836
4213
  /** Opens isolated child state and waits for its verified readiness.
2837
4214
  * @param trial - The public scheduled trial.
@@ -2839,10 +4216,19 @@ var StudyProcessDriver = class {
2839
4216
  * @returns A ready process-backed session.
2840
4217
  */
2841
4218
  async open(trial, signal) {
4219
+ signal.throwIfAborted();
4220
+ const owned = structuredClone(trial);
4221
+ const destination = this.#options.exchanges?.[owned.id];
4222
+ if (this.#options.profile.record !== void 0 && (!isStudyExchangeDestination(destination) || destination.record.bytes !== this.#options.profile.record.bytes)) throw new _src_core.StudyError("CONFIG", "Recorded trial requires its reserved destination");
4223
+ if (this.#options.profile.record === void 0 && destination !== void 0) throw new _src_core.StudyError("CONFIG", "Unrecorded trial cannot receive a sidecar");
4224
+ if (this.#options.replay !== void 0) await readStudyReplayHistory(this.#options.replay, this.#options.profile, owned);
4225
+ const prepared = this.#prepare === void 0 ? destination : readStudyPreparedDestination(destination, await this.#prepare(structuredClone(owned), signal, structuredClone(destination)));
4226
+ signal.throwIfAborted();
2842
4227
  const session = new StudyProcessSession({
2843
4228
  ...this.#options,
2844
- trial,
2845
- signal
4229
+ trial: owned,
4230
+ signal,
4231
+ ...prepared === void 0 ? {} : { exchanges: { [owned.id]: prepared } }
2846
4232
  });
2847
4233
  try {
2848
4234
  await session.start();
@@ -2854,15 +4240,261 @@ var StudyProcessDriver = class {
2854
4240
  }
2855
4241
  };
2856
4242
  //#endregion
2857
- //#region src/server/processes/StudyTaskChild.ts
2858
- /** Supervises one forked adapter child until it closes with one verified response. */
2859
- var StudyTaskChild = class {
4243
+ //#region src/server/commands/StudyDaemonTrial.ts
4244
+ /** Records one trial's daemon maintenance before handing its verified prefix to a worker.
4245
+ * @example
4246
+ * ```ts
4247
+ * const trial = new StudyDaemonTrial(options)
4248
+ * await trial.execute(daemon)
4249
+ * ```
4250
+ */
4251
+ var StudyDaemonTrial = class {
2860
4252
  #options;
2861
- #duration;
2862
- #signal;
2863
- #failure;
2864
- #result;
2865
- #logs = 0;
4253
+ #pending = /* @__PURE__ */ new Map();
4254
+ #sink;
4255
+ /** Owns the declaration and reserved destination for one preparation.
4256
+ * @param options - The immutable trial declaration, destination, and cancellation.
4257
+ */
4258
+ constructor(options) {
4259
+ this.#options = {
4260
+ ...options,
4261
+ declaration: structuredClone(options.declaration),
4262
+ destination: structuredClone(options.destination)
4263
+ };
4264
+ }
4265
+ /** Unloads declared models, drains the journal, and returns its exact retained identity.
4266
+ * @remarks The daemon's unload sender must call this instance's fetch method. Startup metadata must forward without recording.
4267
+ * @param daemon - The verified execution-owned daemon.
4268
+ * @returns The original destination with its closed maintenance prefix identity.
4269
+ */
4270
+ async execute(daemon) {
4271
+ if (this.#sink !== void 0) throw new _src_core.StudyError("FAULT", "Daemon preparation cannot repeat");
4272
+ const file = await (0, node_fs_promises.open)(this.#options.destination.path, "wx");
4273
+ this.#sink = new StudyExchangeSink({
4274
+ file,
4275
+ record: this.#options.destination.record
4276
+ });
4277
+ try {
4278
+ await this.#sink.append(this.#options.declaration);
4279
+ await daemon.unload();
4280
+ await this.#finish();
4281
+ } catch (error) {
4282
+ await this.#finish(error);
4283
+ throw error;
4284
+ } finally {
4285
+ await this.#sink.close();
4286
+ }
4287
+ const bytes = await readStudyBytes(this.#options.destination.path, this.#options.destination.record.bytes);
4288
+ return {
4289
+ ...this.#options.destination,
4290
+ retained: {
4291
+ path: `trials/${this.#options.declaration.trial}/exchanges.jsonl`,
4292
+ bytes: bytes.length,
4293
+ digest: hashStudyBytes(bytes)
4294
+ }
4295
+ };
4296
+ }
4297
+ /** Retains each request before sending and preserves the sender's native response chunks.
4298
+ * @param input - The daemon's exact HTTP input.
4299
+ * @param init - The daemon's method, body, redirect policy, and cancellation.
4300
+ * @returns An unread response carrying the same body bytes for daemon validation.
4301
+ */
4302
+ async fetch(input, init) {
4303
+ const sink = this.#sink;
4304
+ if (sink === void 0) throw new _src_core.StudyError("FAULT", "Daemon journal is not open");
4305
+ await this.#finish();
4306
+ const request = new Request(input, {
4307
+ ...init,
4308
+ signal: AbortSignal.any([this.#options.signal, ...init?.signal === void 0 || init.signal === null ? [] : [init.signal]])
4309
+ });
4310
+ const first = this.#options.declaration.procedure[0];
4311
+ if (first === void 0 || new URL(request.url).pathname !== "/api/generate" || request.method !== "POST") throw new _src_core.StudyError("FAULT", "Unexpected maintenance request");
4312
+ const identity = {
4313
+ ordinal: sink.next,
4314
+ goal: first.goal,
4315
+ role: "maintenance",
4316
+ path: "/api/generate",
4317
+ method: "POST",
4318
+ stage: "maintenance"
4319
+ };
4320
+ await sink.append({
4321
+ ...identity,
4322
+ phase: "attempt",
4323
+ ...buildStudyOllamaBytes(Buffer.from(await request.clone().arrayBuffer()))
4324
+ });
4325
+ const chunks = [];
4326
+ try {
4327
+ const response = await globalThis.fetch(request);
4328
+ const headers = [...response.headers].filter(([name]) => name === "content-type" || name === "content-encoding");
4329
+ const reader = response.body?.getReader();
4330
+ try {
4331
+ if (reader !== void 0) for (;;) {
4332
+ const step = await reader.read();
4333
+ if (step.done) break;
4334
+ if (chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0) + step.value.byteLength > this.#options.declaration.metadata.bytes) throw new _src_core.StudyError("CAPACITY", "Maintenance reply exceeds its declared byte allowance");
4335
+ if (step.value.byteLength > 0) chunks.push(Buffer.from(step.value));
4336
+ }
4337
+ } finally {
4338
+ await reader?.cancel().catch(() => void 0);
4339
+ reader?.releaseLock();
4340
+ }
4341
+ const bytes = Buffer.concat(chunks);
4342
+ this.#pending.set(identity, {
4343
+ status: response.status,
4344
+ headers,
4345
+ chunks: chunks.map((chunk) => chunk.byteLength),
4346
+ ...buildStudyOllamaBytes(bytes)
4347
+ });
4348
+ return new Response(response.body === null ? null : bytes, {
4349
+ status: response.status,
4350
+ statusText: response.statusText,
4351
+ headers: response.headers
4352
+ });
4353
+ } catch (error) {
4354
+ await sink.append({
4355
+ ...identity,
4356
+ phase: "failure",
4357
+ reason: error instanceof Error ? error.message : "Maintenance request failed"
4358
+ });
4359
+ throw error;
4360
+ }
4361
+ }
4362
+ async #finish(error) {
4363
+ for (const [identity, reply] of this.#pending) try {
4364
+ await this.#sink?.append(error === void 0 ? {
4365
+ ...identity,
4366
+ ...reply,
4367
+ phase: "response"
4368
+ } : {
4369
+ ...identity,
4370
+ phase: "failure",
4371
+ reason: error instanceof Error ? error.message : "Maintenance refused",
4372
+ response: reply
4373
+ });
4374
+ } finally {
4375
+ this.#pending.delete(identity);
4376
+ }
4377
+ }
4378
+ };
4379
+ //#endregion
4380
+ //#region src/server/commands/StudyDaemonSession.ts
4381
+ /** Owns the declared daemon and its serial trial preparation through permanent cleanup.
4382
+ * @example
4383
+ * ```ts
4384
+ * const session = new StudyDaemonSession(options)
4385
+ * try { await session.start() } finally { await session.stop() }
4386
+ * ```
4387
+ */
4388
+ var StudyDaemonSession = class {
4389
+ #options;
4390
+ #daemon;
4391
+ #stopped = false;
4392
+ #trials = /* @__PURE__ */ new Set();
4393
+ /** Captures resolved declarations without loading the daemon runtime.
4394
+ * @param options - The execution profile, scenarios, workspace, and cancellation.
4395
+ */
4396
+ constructor(options) {
4397
+ const provider = options.profile.provider;
4398
+ const daemon = provider?.daemon;
4399
+ if (provider === void 0 || daemon === void 0) throw new _src_core.StudyError("CONFIG", "Execution has no declared daemon");
4400
+ if (options.profile.record !== void 0 && options.profile.settings?.version !== daemon.version) throw new _src_core.StudyError("CONFIG", "Daemon and recording versions differ");
4401
+ this.#options = {
4402
+ ...options,
4403
+ profile: structuredClone(options.profile),
4404
+ scenarios: structuredClone(options.scenarios)
4405
+ };
4406
+ }
4407
+ /** Starts the daemon and returns its verified execution provenance.
4408
+ * @returns The executable, endpoint, owned pid, and model receipt.
4409
+ */
4410
+ async start() {
4411
+ this.#options.signal.throwIfAborted();
4412
+ const { createOllamaDaemon } = await import("@orkestrel/ollama/server");
4413
+ if (this.#stopped) throw new _src_core.StudyError("FAULT", "Daemon session has stopped");
4414
+ const provider = this.#options.profile.provider;
4415
+ const daemon = provider?.daemon;
4416
+ if (provider === void 0 || daemon === void 0) throw new _src_core.StudyError("CONFIG", "Execution has no declared daemon");
4417
+ this.#daemon ??= createOllamaDaemon({
4418
+ executable: {
4419
+ path: daemon.executable,
4420
+ digest: daemon.digest,
4421
+ version: daemon.version
4422
+ },
4423
+ url: provider.url,
4424
+ models: daemon.models,
4425
+ workspace: this.#options.workspace,
4426
+ ...daemon.environment === void 0 ? {} : { environment: daemon.environment },
4427
+ signal: this.#options.signal,
4428
+ readiness: this.#options.profile.bounds.request.duration,
4429
+ metadata: {
4430
+ timeout: this.#options.profile.bounds.request.duration,
4431
+ bytes: this.#options.profile.bounds.request.bytes
4432
+ },
4433
+ fetch: (input, init) => this.#send(input, init)
4434
+ });
4435
+ return this.#daemon.start();
4436
+ }
4437
+ /** Unloads the declared population before opening one trial worker.
4438
+ * @param trial - The scheduled trial.
4439
+ * @param signal - The trial's cancellation.
4440
+ * @param destination - The optional reserved recording destination.
4441
+ * @returns The unchanged destination with its verified maintenance prefix when recording.
4442
+ */
4443
+ async prepare(trial, signal, destination) {
4444
+ signal.throwIfAborted();
4445
+ const daemon = this.#daemon;
4446
+ if (daemon === void 0) throw new _src_core.StudyError("FAULT", "Daemon startup is required");
4447
+ if (this.#trials.size !== 0) throw new _src_core.StudyError("FAULT", "Daemon preparations cannot overlap");
4448
+ if (destination === void 0) {
4449
+ if (this.#options.profile.record !== void 0) throw new _src_core.StudyError("CUSTODY", "Daemon maintenance requires its reserved sidecar");
4450
+ await daemon.unload();
4451
+ return;
4452
+ }
4453
+ const declaration = readStudyRecordingDeclaration(this.#options.profile, trial);
4454
+ const models = this.#options.profile.provider?.daemon?.models;
4455
+ const first = this.#options.scenarios.find((scenario) => scenario.id === trial.scenario)?.goals[0];
4456
+ if (models === void 0 || first?.id !== declaration.procedure[0]?.goal) throw new _src_core.StudyError("CONFIG", "Maintenance must join the first declared goal");
4457
+ const preparation = new StudyDaemonTrial({
4458
+ destination,
4459
+ declaration: {
4460
+ ...declaration,
4461
+ maintenance: models
4462
+ },
4463
+ signal
4464
+ });
4465
+ this.#trials.add(preparation);
4466
+ try {
4467
+ return await preparation.execute(daemon);
4468
+ } finally {
4469
+ this.#trials.delete(preparation);
4470
+ }
4471
+ }
4472
+ /** Stops and joins the owned tree and refuses an unconfirmed cleanup.
4473
+ * @returns Completion after daemon and emitter teardown.
4474
+ */
4475
+ async stop() {
4476
+ this.#stopped = true;
4477
+ const daemon = this.#daemon;
4478
+ if (daemon === void 0) return;
4479
+ const confirmed = await daemon.stop();
4480
+ await daemon.destroy();
4481
+ if (!confirmed) throw new _src_core.StudyError("FAULT", "Daemon cleanup was not confirmed");
4482
+ }
4483
+ #send(input, init) {
4484
+ for (const trial of this.#trials) return trial.fetch(input, init);
4485
+ return globalThis.fetch(input, init);
4486
+ }
4487
+ };
4488
+ //#endregion
4489
+ //#region src/server/processes/StudyTaskChild.ts
4490
+ /** Supervises one forked adapter child until it closes with one verified response. */
4491
+ var StudyTaskChild = class {
4492
+ #options;
4493
+ #duration;
4494
+ #signal;
4495
+ #failure;
4496
+ #result;
4497
+ #logs = 0;
2866
4498
  #executed = false;
2867
4499
  /** Binds one adapter evaluation to its owned route, validated deadline and caller cancellation.
2868
4500
  * @param options - The owning task's snapshotted entry, root and selection.
@@ -3020,32 +4652,109 @@ var StudyCommand = class {
3020
4652
  signal.throwIfAborted();
3021
4653
  if (request.command === "check") return this.#check(await this.#prepare(request, signal));
3022
4654
  if (request.command === "execute") return this.#execution(request, signal);
4655
+ if (request.command === "freeze") return this.#freeze(request, signal);
3023
4656
  if (request.command === "grade") return this.#grade(request, signal);
3024
4657
  if (request.command === "label") return this.#label(request, signal);
3025
4658
  if (request.command === "report") return this.#report(request, signal);
3026
4659
  throw new _src_core.StudyError("CONFIG", "Unsupported study operation");
3027
4660
  }
3028
- async #prepare(request, signal) {
3029
- const { bundle, definition, scenarios, fixtures, rubric } = await readStudyDeclaration(this.#root, request.target, signal);
3030
- const profileBytes = await readStudyDeclarationBytes(this.#root, resolveStudyPath(bundle, `profiles/${request.profile}.json`));
4661
+ async #freeze(request, signal) {
4662
+ const selection = {
4663
+ command: "check",
4664
+ target: request.target,
4665
+ profile: request.profile,
4666
+ out: void 0,
4667
+ expected: void 0
4668
+ };
4669
+ const original = await this.#prepare(selection, signal);
4670
+ const instrument = resolveStudyInstrument(this.#entry);
4671
+ const directory = resolveStudyPath((0, node_path.join)(this.#root, "studies"), request.target);
4672
+ const assets = [.../* @__PURE__ */ new Set([
4673
+ this.#entry,
4674
+ (0, node_path.join)(directory, "study.json"),
4675
+ resolveStudyPath(directory, original.definition.rubric),
4676
+ resolveStudyPath(original.bundle, `profiles/${request.profile}.json`),
4677
+ ...original.definition.scenarios.map((id) => resolveStudyPath(original.bundle, `scenarios/${id}.json`))
4678
+ ])];
4679
+ const loader = new ArtifactLoader({
4680
+ entries: [.../* @__PURE__ */ new Set([
4681
+ original.profile.agent,
4682
+ original.profile.subject,
4683
+ ...original.profile.provider === void 0 ? [] : [original.profile.provider.entry]
4684
+ ])],
4685
+ roots: [.../* @__PURE__ */ new Set([...original.profile.roots, instrument])],
4686
+ source: {
4687
+ root: original.bundle,
4688
+ entry: (0, node_path.join)(original.bundle, "index.ts")
4689
+ },
4690
+ instrument,
4691
+ assets
4692
+ });
4693
+ loader.inspect();
4694
+ signal.throwIfAborted();
4695
+ const closure = await loader.materialize(request.destination);
4696
+ const candidate = {
4697
+ closure,
4698
+ root: relocateArtifactPath(closure, this.#root),
4699
+ entry: relocateArtifactPath(closure, this.#entry),
4700
+ target: request.target,
4701
+ profile: request.profile,
4702
+ source: original.identity,
4703
+ identity: original.identity,
4704
+ fingerprint: computeStudyCommandFingerprint(original.identity, original.profile.bounds, original.definition)
4705
+ };
4706
+ const checked = await this.#prepare(selection, signal, candidate);
4707
+ if (checked.source !== original.source || checked.identity.fixtures !== original.identity.fixtures || hashStudyBytes(encodeStudyJSON(checked.definition)) !== hashStudyBytes(encodeStudyJSON(original.definition)) || hashStudyBytes(encodeStudyJSON(checked.rubric)) !== hashStudyBytes(encodeStudyJSON(original.rubric))) throw new _src_core.StudyError("FAULT", "Study declarations changed during freezing");
4708
+ const publication = {
4709
+ ...candidate,
4710
+ identity: checked.identity,
4711
+ fingerprint: computeStudyCommandFingerprint(checked.identity, checked.profile.bounds, checked.definition)
4712
+ };
4713
+ original.artifacts.verify();
4714
+ loader.verify();
4715
+ checked.artifacts.verify();
4716
+ verifyArtifactClosure(closure);
4717
+ signal.throwIfAborted();
4718
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(closure.destination, "closure.json"), encodeStudyJSON(publication), { flag: "wx" });
4719
+ return valueToWire({
4720
+ command: "freeze",
4721
+ path: closure.destination,
4722
+ digest: closure.digest,
4723
+ fingerprint: publication.fingerprint
4724
+ });
4725
+ }
4726
+ #closure(request) {
4727
+ if (request.closure === void 0) throw new _src_core.StudyError("CONFIG", "Study closure selection is absent");
4728
+ const closure = readStudyClosure((0, node_path.resolve)(this.#root, request.closure));
4729
+ if (closure.target !== request.target || closure.profile !== request.profile) throw new _src_core.StudyError("CONFIG", "Study closure selectors differ from the checked selection");
4730
+ return closure;
4731
+ }
4732
+ async #prepare(request, signal, retained) {
4733
+ const closure = retained ?? (request.closure === void 0 ? void 0 : this.#closure(request));
4734
+ const root = closure?.root ?? this.#root;
4735
+ const entry = closure?.entry ?? this.#entry;
4736
+ const { bundle, definition, scenarios, fixtures, rubric } = await readStudyDeclaration(root, request.target, signal);
4737
+ const profileBytes = await readStudyDeclarationBytes(root, resolveStudyPath(bundle, `profiles/${request.profile}.json`));
3031
4738
  const source = (0, _src_core.parseStudyProfile)(decodeStudyJSON(profileBytes));
3032
4739
  if (source.name !== request.profile) throw new _src_core.StudyError("CONFIG", "Profile name differs from its selected file");
3033
4740
  if (source.bounds.series.duration > 864e5) throw new _src_core.StudyError("CONFIG", "Series duration exceeds the 24-hour host ceiling");
3034
- const profile = resolveStudyProfile(this.#root, source);
4741
+ const profile = resolveStudyProfile(root, source, closure?.closure);
3035
4742
  (0, _src_core.validateStudyInputs)(definition, scenarios, rubric, profile);
3036
4743
  signal.throwIfAborted();
3037
4744
  const artifacts = new StudyArtifactLoader({
3038
- root: this.#root,
3039
- entry: this.#entry,
4745
+ root,
4746
+ entry,
3040
4747
  profile,
3041
- adapter: (0, node_path.join)(bundle, "index.ts")
4748
+ adapter: (0, node_path.join)(bundle, "index.ts"),
4749
+ ...closure === void 0 ? {} : { closure }
3042
4750
  });
3043
4751
  const selected = await artifacts.inspect();
3044
4752
  const validation = await new StudyTask({
3045
- root: this.#root,
3046
- entry: this.#entry,
4753
+ root,
4754
+ entry,
3047
4755
  adapter: (0, node_path.join)(bundle, "index.ts"),
3048
- roots: profile.roots
4756
+ roots: profile.roots,
4757
+ ...closure === void 0 ? {} : { closure }
3049
4758
  }).execute({
3050
4759
  operation: "validate",
3051
4760
  input: {
@@ -3057,7 +4766,7 @@ var StudyCommand = class {
3057
4766
  }, signal);
3058
4767
  if (validation.value !== true || validation.instrument !== selected.instrument) throw new _src_core.StudyError("FAULT", "Adapter validation identity differs");
3059
4768
  artifacts.verify();
3060
- return {
4769
+ const preparation = {
3061
4770
  bundle,
3062
4771
  definition,
3063
4772
  scenarios: Object.freeze(scenarios),
@@ -3069,8 +4778,11 @@ var StudyCommand = class {
3069
4778
  profile: computeStudyProfileDigest(profile)
3070
4779
  },
3071
4780
  artifacts,
3072
- source: hashStudyBytes(profileBytes)
4781
+ source: hashStudyBytes(profileBytes),
4782
+ ...closure === void 0 ? {} : { closure }
3073
4783
  };
4784
+ if (request.closure !== void 0 && closure !== void 0 && (computeStudyCommandFingerprint(preparation.identity, profile.bounds, definition) !== closure.fingerprint || hashStudyBytes(encodeStudyJSON(preparation.identity)) !== hashStudyBytes(encodeStudyJSON(closure.identity)))) throw new _src_core.StudyError("FAULT", `Study closure checked identity differs: ${Object.entries(preparation.identity).filter(([name, digest]) => Object.entries(closure.identity).some(([key, expected]) => key === name && digest !== expected)).map(([name]) => name).join(", ")}`);
4785
+ return preparation;
3074
4786
  }
3075
4787
  #check(preparation) {
3076
4788
  const fingerprint = computeStudyCommandFingerprint(preparation.identity, preparation.profile.bounds, preparation.definition);
@@ -3083,6 +4795,10 @@ var StudyCommand = class {
3083
4795
  bounds: preparation.profile.bounds,
3084
4796
  identity: preparation.identity,
3085
4797
  provenance: { profile: preparation.source },
4798
+ ...preparation.closure === void 0 ? {} : { closure: {
4799
+ digest: preparation.closure.closure.digest,
4800
+ source: preparation.closure.source
4801
+ } },
3086
4802
  fingerprint,
3087
4803
  next: [
3088
4804
  "execute",
@@ -3090,12 +4806,14 @@ var StudyCommand = class {
3090
4806
  "--profile",
3091
4807
  preparation.profile.name,
3092
4808
  "--expected",
3093
- fingerprint
4809
+ fingerprint,
4810
+ ...preparation.closure === void 0 ? [] : ["--closure", preparation.closure.closure.destination]
3094
4811
  ],
3095
4812
  schedule: (0, _src_core.buildStudySchedule)(preparation.definition, preparation.scenarios)
3096
4813
  });
3097
4814
  }
3098
4815
  async #execution(request, signal) {
4816
+ const replay = request.replay === void 0 ? void 0 : await this.#replay(request, signal);
3099
4817
  const preparation = await this.#prepare(request, signal);
3100
4818
  const fingerprint = computeStudyCommandFingerprint(preparation.identity, preparation.profile.bounds, preparation.definition);
3101
4819
  if (request.expected !== void 0 && request.expected !== fingerprint) throw new _src_core.StudyError("CONFIG", "Validated execution changed after the supervisor check");
@@ -3106,61 +4824,80 @@ var StudyCommand = class {
3106
4824
  const parent = await this.#temporary();
3107
4825
  signal.throwIfAborted();
3108
4826
  const directory = request.out === void 0 ? (0, node_path.join)(parent, id) : this.#file(request.out);
3109
- const manifest = {
3110
- id,
3111
- created: (/* @__PURE__ */ new Date()).toISOString(),
3112
- definition: preparation.definition,
3113
- scenarios: preparation.scenarios,
3114
- identity: preparation.identity,
3115
- schedule: (0, _src_core.buildStudySchedule)(preparation.definition, preparation.scenarios),
3116
- bounds: preparation.profile.bounds,
3117
- qualification,
3118
- host: {
3119
- node: process.version,
3120
- platform: process.platform,
3121
- architecture: process.arch,
3122
- profile: preparation.source,
3123
- roots: JSON.stringify(preparation.profile.roots)
3124
- }
3125
- };
3126
- const store = new StudyResultStore({
3127
- directory,
3128
- manifest
3129
- });
3130
- await store.create();
3131
4827
  const scratch = await (0, node_fs_promises.mkdtemp)((0, node_path.join)(parent, "scratch-"));
3132
4828
  try {
3133
- const driver = new StudyProcessDriver({
3134
- root: this.#root,
3135
- entry: this.#entry,
4829
+ const daemon = preparation.profile.provider?.daemon === void 0 || replay?.offline === true ? void 0 : new StudyDaemonSession({
3136
4830
  profile: preparation.profile,
3137
- identity: preparation.identity,
3138
- driver: preparation.definition.driver,
3139
- adapter: (0, node_path.join)(preparation.bundle, "index.ts"),
3140
- directory: scratch
3141
- });
3142
- const attempts = await new _src_core.StudySeries({
3143
- manifest,
3144
- driver,
3145
- store,
4831
+ scenarios: preparation.scenarios,
4832
+ workspace: scratch,
3146
4833
  signal
3147
- }).execute();
3148
- preparation.artifacts.verify();
3149
- const seal = await store.seal();
3150
- return valueToWire({
3151
- command: "execute",
3152
- status: attempts.some((attempt) => attempt.failure !== void 0) ? "operational-failure" : "completed",
3153
- result: id,
3154
- path: directory,
3155
- seal: seal.id,
3156
- fingerprint,
3157
- next: [
3158
- "grade",
3159
- directory,
3160
- "--rubric",
3161
- resolveStudyPath(resolveStudyPath((0, node_path.join)(this.#root, "studies"), preparation.definition.name), preparation.definition.rubric)
3162
- ]
3163
4834
  });
4835
+ try {
4836
+ const startup = await daemon?.start();
4837
+ const manifest = {
4838
+ id,
4839
+ created: (/* @__PURE__ */ new Date()).toISOString(),
4840
+ definition: preparation.definition,
4841
+ scenarios: preparation.scenarios,
4842
+ identity: preparation.identity,
4843
+ schedule: (0, _src_core.buildStudySchedule)(preparation.definition, preparation.scenarios),
4844
+ bounds: preparation.profile.bounds,
4845
+ ...preparation.profile.record === void 0 ? {} : { record: preparation.profile.record },
4846
+ qualification,
4847
+ host: {
4848
+ node: process.version,
4849
+ platform: process.platform,
4850
+ architecture: process.arch,
4851
+ profile: preparation.source,
4852
+ roots: JSON.stringify(preparation.profile.roots),
4853
+ ...startup === void 0 ? {} : { daemon: JSON.stringify(startup) }
4854
+ }
4855
+ };
4856
+ const store = new StudyResultStore({
4857
+ directory,
4858
+ manifest
4859
+ });
4860
+ await store.create();
4861
+ const destinations = preparation.profile.record === void 0 ? void 0 : Object.fromEntries(await Promise.all(manifest.schedule.map(async (trial) => [trial.id, await store.exchanges(trial)])));
4862
+ const driver = new StudyProcessDriver({
4863
+ root: preparation.closure?.root ?? this.#root,
4864
+ entry: preparation.closure?.entry ?? this.#entry,
4865
+ profile: preparation.profile,
4866
+ identity: preparation.identity,
4867
+ driver: preparation.definition.driver,
4868
+ adapter: (0, node_path.join)(preparation.bundle, "index.ts"),
4869
+ directory: scratch,
4870
+ ...preparation.closure === void 0 ? {} : { closure: preparation.closure },
4871
+ ...destinations === void 0 ? {} : { exchanges: destinations },
4872
+ ...replay === void 0 ? {} : { replay },
4873
+ ...daemon === void 0 ? {} : { prepare: daemon.prepare.bind(daemon) }
4874
+ });
4875
+ const attempts = await new _src_core.StudySeries({
4876
+ manifest,
4877
+ driver,
4878
+ store,
4879
+ signal
4880
+ }).execute();
4881
+ await daemon?.stop();
4882
+ preparation.artifacts.verify();
4883
+ const seal = await store.seal();
4884
+ return valueToWire({
4885
+ command: "execute",
4886
+ status: attempts.some((attempt) => attempt.failure !== void 0) ? "operational-failure" : "completed",
4887
+ result: id,
4888
+ path: directory,
4889
+ seal: seal.id,
4890
+ fingerprint,
4891
+ next: [
4892
+ "grade",
4893
+ directory,
4894
+ "--rubric",
4895
+ resolveStudyPath(resolveStudyPath((0, node_path.join)(this.#root, "studies"), preparation.definition.name), preparation.definition.rubric)
4896
+ ]
4897
+ });
4898
+ } finally {
4899
+ await daemon?.stop();
4900
+ }
3164
4901
  } finally {
3165
4902
  await (0, node_fs_promises.rm)(scratch, {
3166
4903
  recursive: true,
@@ -3169,6 +4906,44 @@ var StudyCommand = class {
3169
4906
  });
3170
4907
  }
3171
4908
  }
4909
+ async #replay(request, signal) {
4910
+ if (!isStudyReplaySelection(request.replay)) throw new _src_core.StudyError("CONFIG", "Replay requires an explicit sealed source, prefixes and offline behavior");
4911
+ const selection = {
4912
+ ...structuredClone(request.replay),
4913
+ result: this.#file(request.replay.result)
4914
+ };
4915
+ const source = await readStudyResult(selection.result);
4916
+ if (source.status !== "sealed") throw new _src_core.StudyError("CUSTODY", "Replay source is not sealed");
4917
+ const closure = request.closure === void 0 ? void 0 : this.#closure(request);
4918
+ const root = closure?.root ?? this.#root;
4919
+ const entry = closure?.entry ?? this.#entry;
4920
+ const { bundle, definition, scenarios, fixtures } = await readStudyDeclaration(root, request.target, signal);
4921
+ const bytes = await readStudyDeclarationBytes(root, resolveStudyPath(bundle, `profiles/${request.profile}.json`));
4922
+ const profile = resolveStudyProfile(root, (0, _src_core.parseStudyProfile)(decodeStudyJSON(bytes)), closure?.closure);
4923
+ const schedule = (0, _src_core.buildStudySchedule)(definition, scenarios);
4924
+ if ((0, _orkestrel_contract.canonicalStringify)(definition) !== (0, _orkestrel_contract.canonicalStringify)(source.manifest.definition) || (0, _orkestrel_contract.canonicalStringify)(scenarios) !== (0, _orkestrel_contract.canonicalStringify)(source.manifest.scenarios) || (0, _orkestrel_contract.canonicalStringify)(schedule) !== (0, _orkestrel_contract.canonicalStringify)(source.manifest.schedule) || computeStudyFixtureDigest(definition, fixtures) !== source.manifest.identity.fixtures || Object.keys(selection.prefix).length !== schedule.length || !schedule.every((trial) => Object.hasOwn(selection.prefix, trial.id))) throw new _src_core.StudyError("CONFIG", "Replay declaration or scheduled membership changed");
4925
+ for (const trial of schedule) {
4926
+ signal.throwIfAborted();
4927
+ await readStudyReplayHistory(selection, profile, trial);
4928
+ }
4929
+ const artifacts = new StudyArtifactLoader({
4930
+ root,
4931
+ entry,
4932
+ profile,
4933
+ adapter: (0, node_path.join)(bundle, "index.ts"),
4934
+ ...closure === void 0 ? {} : { closure }
4935
+ });
4936
+ const selected = await artifacts.inspect();
4937
+ const identity = {
4938
+ ...selected,
4939
+ fixtures: computeStudyFixtureDigest(definition, fixtures),
4940
+ profile: computeStudyProfileDigest(profile)
4941
+ };
4942
+ if (selected.agent !== source.manifest.identity.agent || selected.subject !== source.manifest.identity.subject || selected.instrument !== source.manifest.identity.instrument || selected.provider !== source.manifest.identity.provider) throw new _src_core.StudyError("CONFIG", "Replay artifact declaration changed");
4943
+ if (request.expected !== void 0 && request.expected !== computeStudyCommandFingerprint(identity, profile.bounds, definition)) throw new _src_core.StudyError("CONFIG", "Replay execution bounds or declaration changed after check");
4944
+ artifacts.verify();
4945
+ return selection;
4946
+ }
3172
4947
  async #grade(request, signal) {
3173
4948
  const result = await this.#result(request.target);
3174
4949
  const bytes = await readStudyBytes(this.#file(request.rubric), STUDY_CUSTODY_BYTES);
@@ -3488,6 +5263,138 @@ var StudyRemoteMeter = class {
3488
5263
  }
3489
5264
  };
3490
5265
  //#endregion
5266
+ //#region src/server/ollama/StudyOllamaReplay.ts
5267
+ /** Answers declared HTTP prefixes from complete ordered recorded application bytes. */
5268
+ var StudyOllamaReplay = class {
5269
+ #options;
5270
+ #readers = /* @__PURE__ */ new Set();
5271
+ #pending = /* @__PURE__ */ new Set();
5272
+ #started = performance.now();
5273
+ #cursor;
5274
+ #bytes = 0;
5275
+ #calls = 0;
5276
+ #fault;
5277
+ #closed = false;
5278
+ #procedure;
5279
+ /** Owns and revalidates the complete selected history before any sender can be reached.
5280
+ * @param options - Verified history, explicit prefix, executing origin and independent bounds.
5281
+ */
5282
+ constructor(options) {
5283
+ const { signal, fetch, ...configuration } = options;
5284
+ const owned = structuredClone(configuration);
5285
+ validateStudyExchangeHistory(owned.history);
5286
+ const url = new URL(owned.url);
5287
+ if (!["http:", "https:"].includes(url.protocol) || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.pathname !== "/" || !Number.isSafeInteger(owned.prefix) || owned.prefix <= 0 || owned.prefix > owned.history.exchanges.length || fetch === void 0 && owned.prefix !== owned.history.exchanges.length || fetch !== void 0 && typeof fetch !== "function" || !isStudyOllamaMetadata(owned.limit)) throw new _src_core.StudyError("CONFIG", "Replay requires an explicit valid origin, prefix and current bounds");
5288
+ if (owned.prefix < owned.history.declaration.maintenance.length) throw new _src_core.StudyError("CONFIG", "Replay prefix omits recorded maintenance");
5289
+ signal.throwIfAborted();
5290
+ this.#options = {
5291
+ ...owned,
5292
+ signal,
5293
+ ...fetch === void 0 ? {} : { fetch }
5294
+ };
5295
+ this.#cursor = owned.history.declaration.maintenance.length;
5296
+ this.#procedure = owned.history.declaration.procedure.map((step) => {
5297
+ const pair = owned.history.exchanges.find((entry) => entry.attempt.step === step.ordinal && entry.attempt.ordinal <= owned.prefix);
5298
+ if (pair === void 0) return step;
5299
+ const { admission: _admission, ...identity } = step;
5300
+ const { bytes: _bytes, ...exact } = identity;
5301
+ return {
5302
+ ...exact,
5303
+ admission: "exact",
5304
+ body: new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(pair.attempt.body, "base64")),
5305
+ input: pair.attempt.digest
5306
+ };
5307
+ });
5308
+ }
5309
+ /** Holds exact recorded bodies for model requests inside the declared prefix. */
5310
+ get procedure() {
5311
+ return structuredClone(this.#procedure);
5312
+ }
5313
+ /** Supplies recorded native responses, forwarding only after the exact declared prefix.
5314
+ * @param role - The validated Study responsibility.
5315
+ * @param input - The executing origin and exact route.
5316
+ * @param init - The method, encoded body and bounded signal.
5317
+ * @returns The recorded response or the explicit live collaborator's response.
5318
+ */
5319
+ fetch = (role, input, init) => {
5320
+ if (this.#closed || this.#fault !== void 0 || this.#pending.size !== 0 || this.#readers.size !== 0) {
5321
+ const error = this.#fault?.error ?? new _src_core.StudyError("FAULT", "Replay request is closed, concurrent or follows an unread response");
5322
+ this.#fault ??= { error };
5323
+ return Promise.reject(error);
5324
+ }
5325
+ const pending = this.#fetch(role, input, init).catch((error) => {
5326
+ this.#fault ??= { error };
5327
+ throw error;
5328
+ });
5329
+ this.#pending.add(pending);
5330
+ pending.then(() => this.#pending.delete(pending), () => this.#pending.delete(pending));
5331
+ return pending;
5332
+ };
5333
+ /** Refuses a mismatch, an unread response or an incompletely consumed prefix. */
5334
+ check() {
5335
+ if (this.#fault !== void 0) throw this.#fault.error;
5336
+ this.#options.signal.throwIfAborted();
5337
+ if (this.#cursor < this.#options.prefix || this.#readers.size !== 0 || this.#pending.size !== 0) throw new _src_core.StudyError("FAULT", "Replay prefix is incomplete or still being read");
5338
+ }
5339
+ /** Closes admission and cancels retained replay responses.
5340
+ * @returns Completion after pending sends and owned readers settle.
5341
+ */
5342
+ async close() {
5343
+ this.#closed = true;
5344
+ await Promise.allSettled([...this.#readers].map((reader) => reader.cancel()));
5345
+ this.#readers.clear();
5346
+ await Promise.allSettled([...this.#pending]);
5347
+ }
5348
+ async #fetch(role, input, init) {
5349
+ this.#options.signal.throwIfAborted();
5350
+ init.signal?.throwIfAborted();
5351
+ if (this.#cursor >= this.#options.prefix) {
5352
+ if (this.#options.fetch === void 0) throw new _src_core.StudyError("FAULT", "Offline replay has no later sender");
5353
+ return this.#options.fetch(role, input, init);
5354
+ }
5355
+ const pair = this.#options.history.exchanges[this.#cursor];
5356
+ const url = new URL(input);
5357
+ const request = new Request(url, init);
5358
+ const body = Buffer.from(await request.arrayBuffer());
5359
+ if (pair === void 0 || url.origin !== new URL(this.#options.url).origin || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.pathname !== pair.attempt.path || request.method !== pair.attempt.method || role !== pair.attempt.role || !body.equals(Buffer.from(pair.attempt.body, "base64"))) throw new _src_core.StudyError("FAULT", "Replay request differs from the next recorded exchange");
5360
+ this.#calls += 1;
5361
+ this.#bytes += body.length + pair.response.size;
5362
+ if (this.#calls > this.#options.limit.calls || this.#bytes > this.#options.limit.bytes) throw new _src_core.StudyError("CAPACITY", "Replay exceeds the executing allowance");
5363
+ if (performance.now() - this.#started >= this.#options.limit.duration) throw new _src_core.StudyError("DEADLINE", "Replay exceeds the executing duration");
5364
+ this.#cursor += 1;
5365
+ const bytes = Buffer.from(pair.response.body, "base64");
5366
+ const source = pair.response.chunks.map((size, index, sizes) => {
5367
+ const offset = sizes.slice(0, index).reduce((total, length) => total + length, 0);
5368
+ return bytes.subarray(offset, offset + size);
5369
+ }).values();
5370
+ const stream = new ReadableStream({
5371
+ pull: (controller) => {
5372
+ if (this.#options.signal.aborted || init.signal?.aborted || this.#closed) {
5373
+ const error = this.#options.signal.reason ?? init.signal?.reason ?? new _src_core.StudyError("FAULT", "Replay response was closed");
5374
+ this.#fault ??= { error };
5375
+ this.#readers.delete(stream);
5376
+ controller.error(error);
5377
+ return;
5378
+ }
5379
+ const chunk = source.next();
5380
+ if (chunk.done) {
5381
+ controller.close();
5382
+ this.#readers.delete(stream);
5383
+ } else controller.enqueue(chunk.value);
5384
+ },
5385
+ cancel: () => {
5386
+ this.#readers.delete(stream);
5387
+ this.#fault ??= { error: new _src_core.StudyError("FAULT", "Replay response was abandoned") };
5388
+ }
5389
+ }, { highWaterMark: 0 });
5390
+ this.#readers.add(stream);
5391
+ return new Response(stream, {
5392
+ status: pair.response.status,
5393
+ headers: pair.response.headers.filter(([name]) => name !== "content-encoding").map(([name, value]) => [name, value])
5394
+ });
5395
+ }
5396
+ };
5397
+ //#endregion
3491
5398
  //#region src/server/processes/StudyWorker.ts
3492
5399
  /** Executes one verified trial behind parent-authorized admission and custody. */
3493
5400
  var StudyWorker = class {
@@ -3500,6 +5407,9 @@ var StudyWorker = class {
3500
5407
  #closing = false;
3501
5408
  #duration = 1e3;
3502
5409
  #executed = false;
5410
+ #exchanges;
5411
+ #replay;
5412
+ #maintenance = [];
3503
5413
  #evidence = /* @__PURE__ */ new Set();
3504
5414
  /** Serves the inherited IPC channel until its parent disconnects.
3505
5415
  * @returns Completion after channel and acquired session cleanup.
@@ -3521,6 +5431,7 @@ var StudyWorker = class {
3521
5431
  label: STUDY_PROCESS_LABEL
3522
5432
  }).wait(this.#session.close()).catch(() => void 0);
3523
5433
  this.#session = void 0;
5434
+ await Promise.allSettled([this.#exchanges?.close(), this.#replay?.close()]);
3524
5435
  this.#channel.close();
3525
5436
  this.#control.close();
3526
5437
  }
@@ -3532,7 +5443,7 @@ var StudyWorker = class {
3532
5443
  if (operation === "close") {
3533
5444
  if (this.#closing) throw new _src_core.StudyError("FAULT", "Worker cleanup repeated");
3534
5445
  this.#closing = true;
3535
- this.#control.fail({
5446
+ if (this.#meters.size !== 0 || this.#control.failure !== void 0) this.#control.fail({
3536
5447
  cause: "interrupted",
3537
5448
  message: "Worker cleanup requested",
3538
5449
  role: void 0,
@@ -3543,6 +5454,12 @@ var StudyWorker = class {
3543
5454
  label: STUDY_PROCESS_LABEL
3544
5455
  }).wait(this.#session.close());
3545
5456
  this.#session = void 0;
5457
+ try {
5458
+ this.#replay?.check();
5459
+ } finally {
5460
+ await this.#replay?.close();
5461
+ await this.#exchanges?.close();
5462
+ }
3546
5463
  this.#artifacts?.verify();
3547
5464
  return true;
3548
5465
  }
@@ -3557,11 +5474,58 @@ var StudyWorker = class {
3557
5474
  this.#opening = true;
3558
5475
  if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.entry !== "string" || typeof value.root !== "string" || typeof value.directory !== "string" || typeof value.driver !== "string" || typeof value.adapter !== "string" || !(0, _src_core.isStudyProfile)(value.profile) || !isStudyIdentity(value.identity) || !isStudyTrial(value.trial)) throw new _src_core.StudyError("CONFIG", "Worker opening declaration is invalid or driver is unsupported");
3559
5476
  this.#duration = value.profile.bounds.request.duration;
5477
+ if (value.closure !== void 0 && !isStudyClosure(value.closure)) throw new _src_core.StudyError("CONFIG", "Worker closure selection is malformed");
5478
+ if (value.replay !== void 0) {
5479
+ if (!isStudyReplaySelection(value.replay) || value.profile.provider === void 0) throw new _src_core.StudyError("CONFIG", "Worker replay selection is invalid");
5480
+ const history = await readStudyReplayHistory(value.replay, value.profile, value.trial);
5481
+ const prefix = value.replay.prefix[value.trial.id];
5482
+ if (prefix === void 0) throw new _src_core.StudyError("CONFIG", "Worker replay prefix is absent");
5483
+ this.#replay = new StudyOllamaReplay({
5484
+ history,
5485
+ prefix,
5486
+ url: value.profile.provider.url,
5487
+ limit: readStudyRecordingDeclaration(value.profile, value.trial).metadata,
5488
+ signal: this.#control.signal,
5489
+ ...value.replay.offline ? {} : { fetch: this.#forward.bind(this) }
5490
+ });
5491
+ }
5492
+ const destination = (0, _orkestrel_contract.isRecord)(value.exchanges) ? value.exchanges[value.trial.id] : void 0;
5493
+ if (value.profile.record === void 0 && destination !== void 0) throw new _src_core.StudyError("CONFIG", "Unrecorded worker received a sidecar");
5494
+ if (value.profile.record !== void 0) {
5495
+ if (!isStudyExchangeDestination(destination) || destination.record.bytes !== value.profile.record.bytes) throw new _src_core.StudyError("CONFIG", "Recorded worker has no matching reserved sidecar");
5496
+ const declaration = readStudyRecordingDeclaration(value.profile, value.trial);
5497
+ const retained = destination.retained === void 0 ? void 0 : await readStudyBytes(destination.path, destination.record.bytes);
5498
+ if (retained !== void 0 && (hashStudyBytes(retained) !== destination.retained?.digest || retained.length !== destination.retained.bytes || destination.retained.path !== `trials/${value.trial.id}/exchanges.jsonl`)) throw new _src_core.StudyError("CUSTODY", "Maintenance prefix changed before worker opening");
5499
+ const rows = retained === void 0 ? void 0 : readStudyExchangeRows(retained, false);
5500
+ if (rows !== void 0 && (rows[0]?.phase !== "declaration" || rows[0].trial !== value.trial.id || rows.some((row) => row.phase !== "declaration" && (row.phase !== "attempt" && row.phase !== "response" || row.stage !== "maintenance")) || rows.at(-1)?.phase !== "response")) throw new _src_core.StudyError("CUSTODY", "Retained prefix is not completed maintenance");
5501
+ if (rows !== void 0 && rows[0]?.phase === "declaration") {
5502
+ const { maintenance: _retainedMaintenance, ...opening } = rows[0];
5503
+ const { maintenance: _maintenance, ...expected } = declaration;
5504
+ if ((0, _orkestrel_contract.canonicalStringify)(opening) !== (0, _orkestrel_contract.canonicalStringify)(expected) || rows.filter((row) => row.phase === "response").length !== rows[0].maintenance.length) throw new _src_core.StudyError("CUSTODY", "Retained maintenance declaration differs from the executing profile");
5505
+ }
5506
+ const file = await (0, node_fs_promises.open)(destination.path, rows === void 0 ? "wx" : "a");
5507
+ try {
5508
+ this.#exchanges = new StudyExchangeSink({
5509
+ file,
5510
+ record: destination.record,
5511
+ ...rows === void 0 ? {} : { retained: rows }
5512
+ });
5513
+ } catch (error) {
5514
+ await file.close();
5515
+ throw error;
5516
+ }
5517
+ if (rows === void 0) await this.#exchanges.append({
5518
+ ...declaration,
5519
+ procedure: this.#replay?.procedure ?? declaration.procedure
5520
+ });
5521
+ else this.#maintenance = rows;
5522
+ }
3560
5523
  this.#artifacts = new StudyArtifactLoader({
3561
5524
  entry: value.entry,
3562
5525
  root: value.root,
3563
5526
  profile: value.profile,
3564
- adapter: value.adapter
5527
+ adapter: value.adapter,
5528
+ ...value.closure === void 0 ? {} : { closure: value.closure }
3565
5529
  });
3566
5530
  const identity = await this.#artifacts.inspect();
3567
5531
  if (identity.agent !== value.identity.agent || identity.subject !== value.identity.subject || identity.provider !== value.identity.provider || identity.instrument !== value.identity.instrument || computeStudyProfileDigest(value.profile) !== value.identity.profile) throw new _src_core.StudyError("FAULT", "Worker artifact identity differs from parent declaration");
@@ -3569,7 +5533,9 @@ var StudyWorker = class {
3569
5533
  const driver = (await this.#artifacts.adapter()).driver({
3570
5534
  ...modules,
3571
5535
  profile: value.profile,
3572
- directory: value.directory
5536
+ directory: value.directory,
5537
+ ...this.#exchanges === void 0 ? {} : { exchanges: this.#exchanges },
5538
+ ...this.#replay === void 0 ? {} : { replay: this.#replay }
3573
5539
  });
3574
5540
  this.#session = await driver.open(value.trial, this.#control.signal);
3575
5541
  if (this.#control.failure !== void 0) throw (0, _src_core.failureToStudyError)(this.#control.failure);
@@ -3586,6 +5552,16 @@ var StudyWorker = class {
3586
5552
  });
3587
5553
  this.#meters.add(meter);
3588
5554
  try {
5555
+ for (const row of this.#maintenance) if (row.phase === "attempt" || row.phase === "response") await this.#append(valueToWire({
5556
+ event: row.phase === "attempt" ? "dispatch" : "wire",
5557
+ goal: value.id,
5558
+ value: {
5559
+ ordinal: row.ordinal,
5560
+ role: row.role,
5561
+ path: row.path
5562
+ }
5563
+ }));
5564
+ this.#maintenance = [];
3589
5565
  const product = await this.#control.wait(this.#session.execute(value, {
3590
5566
  signal: this.#control.signal,
3591
5567
  meter,
@@ -3621,6 +5597,12 @@ var StudyWorker = class {
3621
5597
  meter.abort(value.id, value.failure);
3622
5598
  }
3623
5599
  }
5600
+ #forward(_role, input, init) {
5601
+ return globalThis.fetch(input, {
5602
+ ...init,
5603
+ signal: AbortSignal.any([this.#control.signal, ...init.signal === void 0 || init.signal === null ? [] : [init.signal]])
5604
+ });
5605
+ }
3624
5606
  };
3625
5607
  //#endregion
3626
5608
  //#region src/server/processes/StudyTaskWorker.ts
@@ -3643,6 +5625,9 @@ var StudyTaskWorker = class {
3643
5625
  }
3644
5626
  async #execute(value) {
3645
5627
  if (!(0, _orkestrel_contract.isRecord)(value) || Buffer.byteLength(JSON.stringify(value)) > 16777216 || typeof value.adapter !== "string" || typeof value.entry !== "string" || !(0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.roots) || !(0, _orkestrel_contract.isRecord)(value.input)) throw new _src_core.StudyError("CONFIG", "Adapter request is malformed");
5628
+ if (value.closure !== void 0 && !isStudyClosure(value.closure)) throw new _src_core.StudyError("CONFIG", "Adapter closure selection is malformed");
5629
+ const closure = value.closure;
5630
+ if (closure !== void 0) verifyArtifactClosure(closure.closure, true);
3646
5631
  const runtime = resolveStudyInstrument(value.entry);
3647
5632
  const roots = [.../* @__PURE__ */ new Set([...value.roots, runtime])];
3648
5633
  validateStudyRuntime(runtime, roots);
@@ -3662,6 +5647,7 @@ var StudyTaskWorker = class {
3662
5647
  if (profile === void 0 && value.operation !== "grade") throw new _src_core.StudyError("CONFIG", "Unknown adapter operation");
3663
5648
  const output = profile === void 0 ? this.#grade(adapter, value.input) : this.#validate(adapter, value.input, profile);
3664
5649
  loader.verify();
5650
+ if (closure !== void 0) verifyArtifactClosure(closure.closure, true);
3665
5651
  return valueToWire({
3666
5652
  instrument,
3667
5653
  value: output
@@ -3817,7 +5803,8 @@ var StudyLauncher = class {
3817
5803
  "check",
3818
5804
  request.target,
3819
5805
  "--profile",
3820
- request.profile
5806
+ request.profile,
5807
+ ...request.closure === void 0 ? [] : ["--closure", request.closure]
3821
5808
  ];
3822
5809
  const reading = await this.#launch(entry, check, STUDY_LAUNCH_DURATION, false);
3823
5810
  if (reading.code !== 0) {
@@ -3835,6 +5822,15 @@ var StudyLauncher = class {
3835
5822
  "--profile",
3836
5823
  request.profile,
3837
5824
  ...request.out === void 0 ? [] : ["--out", request.out],
5825
+ ...request.closure === void 0 ? [] : ["--closure", request.closure],
5826
+ ...request.replay === void 0 ? [] : [
5827
+ "--replay",
5828
+ request.replay.result,
5829
+ "--prefix",
5830
+ JSON.stringify(request.replay.prefix),
5831
+ "--offline",
5832
+ String(request.replay.offline)
5833
+ ],
3838
5834
  "--expected",
3839
5835
  value.fingerprint
3840
5836
  ];
@@ -4448,7 +6444,7 @@ var StudyAgentScriptGoal = class {
4448
6444
  return this.#plan;
4449
6445
  }
4450
6446
  /** Takes the next declared response and advances past it.
4451
- * @returns The response; undefined once the sequence is exhausted, which it then stays.
6447
+ * @returns The response; undefined after the sequence is exhausted, which it then stays.
4452
6448
  */
4453
6449
  take() {
4454
6450
  const response = this.#plan.responses[this.#cursor];
@@ -4585,433 +6581,77 @@ var StudyAgentScript = class {
4585
6581
  };
4586
6582
  bounded.throwIfAborted();
4587
6583
  ticket.finish({
4588
- role: "agent",
4589
- calls: 1,
4590
- tokens: void 0,
4591
- bytes,
4592
- duration: Math.ceil(performance.now() - started)
4593
- });
4594
- if (response.abort === true) {
4595
- await plan.context.evidence.append({
4596
- event: "workload",
4597
- goal: plan.goal,
4598
- value: { aborted: true }
4599
- });
4600
- plan.workload.abort(/* @__PURE__ */ new Error("Declared workload cancellation"));
4601
- }
4602
- return result;
4603
- } catch (error) {
4604
- if ((0, _orkestrel_budget.isBudgetError)(error)) this.#cut(ticket, bytes, started);
4605
- else (0, _src_core.failStudyTicket)(ticket, error, "agent");
4606
- throw error;
4607
- } finally {
4608
- if ((0, _orkestrel_budget.isBudgetError)(signal.reason)) this.#cut(ticket, bytes, started);
4609
- else (0, _src_core.failStudyTicket)(ticket, signal.reason ?? new _src_core.StudyError("FAULT", "Scripted response was abandoned before completion"), "agent");
4610
- }
4611
- } finally {
4612
- this.#serving.delete(response);
4613
- }
4614
- }
4615
- #cut(ticket, bytes, started) {
4616
- if (ticket.settled || ticket.signal.aborted) return;
4617
- ticket.finish({
4618
- role: "agent",
4619
- calls: 1,
4620
- tokens: void 0,
4621
- bytes,
4622
- duration: Math.ceil(performance.now() - started)
4623
- });
4624
- }
4625
- async #admit(plan, response) {
4626
- try {
4627
- return await plan.context.meter.admit({
4628
- role: "agent",
4629
- tokens: response.tokens ?? 1,
4630
- bytes: response.bytes ?? this.#bytes
4631
- });
4632
- } catch (error) {
4633
- await plan.context.evidence.append({
4634
- event: "admission",
4635
- goal: plan.goal,
4636
- value: {
4637
- role: "agent",
4638
- refused: true
4639
- }
4640
- });
4641
- throw error;
4642
- }
4643
- }
4644
- };
4645
- //#endregion
4646
- //#region src/server/ollama/helpers.ts
4647
- /**
4648
- * Declares the pinned serving identities the Ollama transport verifies, keyed by the label their inventory evidence carries.
4649
- * @param provider - Declared serving identities
4650
- * @returns The model pin, plus the judge pin when the provider declares both the judge name and its digest
4651
- */
4652
- function buildStudyOllamaModels(provider) {
4653
- return {
4654
- model: {
4655
- name: provider.model,
4656
- digest: provider.digest.model
4657
- },
4658
- ...provider.judge === void 0 || provider.digest.judge === void 0 ? {} : { judge: {
4659
- name: provider.judge,
4660
- digest: provider.digest.judge
4661
- } }
4662
- };
4663
- }
4664
- /**
4665
- * Builds the provider options one role's declared wire policy determines.
4666
- * @remarks
4667
- * The policy's `options` object keeps its identity and key order. A strict policy adds `truncate`
4668
- * and `shift` as `false`; any other policy omits both keys. The `length` allowance is read by the
4669
- * transport and projects nothing.
4670
- * @param policy - The role's declared wire policy
4671
- * @returns The model, think flag, options object, and the strict native fields when declared
4672
- */
4673
- function buildStudyOllamaOptions(policy) {
4674
- return {
4675
- model: policy.model,
4676
- options: policy.options,
4677
- think: policy.think,
4678
- ...policy.strict ? {
4679
- truncate: false,
4680
- shift: false
4681
- } : {}
4682
- };
4683
- }
4684
- /**
4685
- * Validates declared role policies against the pinned inventory before any dispatch.
4686
- * @param roles - The declared wire policy of each model role
4687
- * @param models - The pinned served identities
4688
- * @returns Nothing when every policy names a pinned model and a positive output allowance
4689
- * @throws StudyError - Thrown with code `CONFIG` when a policy names an unpinned model, declares no positive safe-integer `num_predict`, or makes the raw judge wire strict
4690
- */
4691
- function validateStudyOllamaRoles(roles, models) {
4692
- const names = Object.values(models).map((model) => model.name);
4693
- for (const [role, policy] of Object.entries(roles)) {
4694
- if (policy === void 0) continue;
4695
- if (!names.includes(policy.model)) throw new _src_core.StudyError("CONFIG", "Ollama role policy names a model the inventory does not pin");
4696
- const output = policy.options.num_predict;
4697
- if (output === void 0 || !Number.isSafeInteger(output) || output <= 0) throw new _src_core.StudyError("CONFIG", "Ollama role policy declares no positive num_predict allowance");
4698
- if (role === "judge" && policy.strict) throw new _src_core.StudyError("CONFIG", "Ollama strict fields apply only to chat roles");
4699
- }
4700
- }
4701
- /**
4702
- * Reads exact outbound Ollama facts and refuses a request outside its declared policy.
4703
- * @remarks
4704
- * The refusal happens before admission, so a refused request is never accounted.
4705
- * @param options - The admitted origin, pinned inventory, and declared role policies
4706
- * @param role - The responsible request role
4707
- * @param input - The requested URL
4708
- * @param init - The native fetch settings
4709
- * @returns The validated dispatch facts
4710
- * @throws StudyError - Thrown with code `CONFIG` when the endpoint, model, options, flags, or strict fields differ from the declaration, or the role declares no policy
4711
- */
4712
- function readStudyOllamaRequest(options, role, input, init) {
4713
- if (input instanceof Request) throw new _src_core.StudyError("CONFIG", "Ollama transport requires explicit URL requests");
4714
- const url = new URL(input);
4715
- const path = role === "metadata" ? init?.method === "POST" ? "/api/show" : "/api/tags" : role === "judge" ? "/api/generate" : "/api/chat";
4716
- if (url.origin !== new URL(options.url).origin || url.pathname !== path || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "") throw new _src_core.StudyError("CONFIG", "Ollama transport endpoint differs from the admitted origin and path");
4717
- if (role === "metadata") {
4718
- if (path === "/api/show") {
4719
- if (typeof init?.body !== "string") throw new _src_core.StudyError("CONFIG", "Invalid model metadata request");
4720
- const body = JSON.parse(init.body);
4721
- if (!(0, _orkestrel_contract.isRecord)(body) || Object.keys(body).length !== 1 || typeof body.model !== "string" || !Object.values(options.models).some((model) => model.name === body.model)) throw new _src_core.StudyError("CONFIG", "Invalid model metadata identity");
4722
- return {
4723
- role,
4724
- path,
4725
- model: body.model,
4726
- output: 0,
4727
- bytes: new TextEncoder().encode(init.body).byteLength,
4728
- options: {},
4729
- strict: false,
4730
- length: false
4731
- };
4732
- }
4733
- if (init?.method !== "GET" || init.body !== void 0) throw new _src_core.StudyError("CONFIG", "Invalid inventory request");
4734
- return {
4735
- role,
4736
- path,
4737
- model: void 0,
4738
- output: 0,
4739
- bytes: 0,
4740
- options: {},
4741
- strict: false,
4742
- length: false
4743
- };
4744
- }
4745
- if (init?.method !== "POST" || typeof init.body !== "string") throw new _src_core.StudyError("CONFIG", "Ollama transport requires an encoded JSON POST");
4746
- const policy = role === "tool" ? void 0 : options.roles[role];
4747
- if (policy === void 0) throw new _src_core.StudyError("CONFIG", "Ollama transport role has no declared wire policy");
4748
- const output = policy.options.num_predict;
4749
- if (output === void 0 || !Number.isSafeInteger(output) || output <= 0) throw new _src_core.StudyError("CONFIG", "Ollama role policy declares no positive num_predict allowance");
4750
- const body = JSON.parse(init.body);
4751
- if (!(0, _orkestrel_contract.isRecord)(body) || body.model !== policy.model || !(0, _orkestrel_contract.isRecord)(body.options) || (0, _orkestrel_contract.canonicalStringify)(body.options) !== (0, _orkestrel_contract.canonicalStringify)(policy.options) || body.stream !== (role !== "judge")) throw new _src_core.StudyError("CONFIG", "Ollama transport model or effective options differ from the declared policy");
4752
- if (role === "judge" ? body.raw !== true || body.logprobs !== true : typeof body.think !== "boolean" || body.think === true && !policy.think) throw new _src_core.StudyError("CONFIG", "Ollama transport flags differ from the declared policy");
4753
- const strict = policy.strict && role !== "judge";
4754
- if (strict && (body.truncate !== false || body.shift !== false)) throw new _src_core.StudyError("CONFIG", "Ollama transport must disable native truncation and shifting");
4755
- return {
4756
- role,
4757
- path,
4758
- model: policy.model,
4759
- output,
4760
- bytes: new TextEncoder().encode(init.body).byteLength,
4761
- options: { ...policy.options },
4762
- strict,
4763
- length: policy.length
4764
- };
4765
- }
4766
- /**
4767
- * Reads the declared model context ceiling without claiming daemon allocation.
4768
- * @param value - The parsed Ollama show response
4769
- * @param context - The requested total context
4770
- * @returns The unambiguous model context ceiling
4771
- * @throws StudyError - Thrown with code `FAULT` when context metadata is missing, or `CAPACITY` when it is ambiguous or below the requested context
4772
- */
4773
- function readStudyOllamaSupport(value, context) {
4774
- if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(value.model_info) || typeof value.model_info["general.architecture"] !== "string") throw new _src_core.StudyError("FAULT", "Missing model context metadata");
4775
- const architecture = value.model_info["general.architecture"];
4776
- const keys = Object.keys(value.model_info).filter((key) => key.endsWith(".context_length"));
4777
- const ceiling = value.model_info[`${architecture}.context_length`];
4778
- if (keys.length !== 1 || typeof ceiling !== "number" || !Number.isSafeInteger(ceiling) || ceiling < context) {
4779
- const message = "Model context metadata does not support the requested context";
4780
- throw new _src_core.StudyError("CAPACITY", message, {
4781
- cause: "capacity",
4782
- message,
4783
- role: void 0,
4784
- stop: {
4785
- layer: "model",
4786
- id: "context_length",
4787
- max: keys.length === 1 && Number.isSafeInteger(ceiling) ? (0, _src_core.parseStudyReading)(ceiling) : void 0,
4788
- consumed: context
4789
- }
4790
- });
4791
- }
4792
- return ceiling;
4793
- }
4794
- /**
4795
- * Reads a native Ollama error member as the attributable failure it names, without usage.
4796
- * @remarks
4797
- * No token usage is read from the member.
4798
- * The failure is `CAPACITY` when the member, a string or an object, names the exact structured
4799
- * type `exceed_context_size_error` with a finite positive integer `n_prompt_tokens` above `n_ctx`,
4800
- * a `code` of 400 if supplied, and a status of 400 if supplied. Every other member, a malformed
4801
- * one included, is `FAULT` with its readable reason.
4802
- * @param value - The parsed HTTP error body or wire row
4803
- * @param role - The responsibility whose request was dispatched; the failure retains it
4804
- * @param status - The HTTP status of the carrying response; undefined for a row of a stream that answered 200
4805
- * @returns Undefined when the value carries no `error` member; otherwise the attributable failure
4806
- */
4807
- function readStudyOllamaFailure(value, role, status) {
4808
- if (!(0, _orkestrel_contract.isRecord)(value) || !("error" in value)) return void 0;
4809
- const field = value.error;
4810
- const decoded = typeof field === "string" ? (0, _orkestrel_contract.parseJSON)(field) : field;
4811
- const structured = (0, _orkestrel_contract.isRecord)(decoded) ? (0, _orkestrel_contract.isRecord)(decoded.error) ? decoded.error : decoded : void 0;
4812
- const native = structured?.message;
4813
- const text = typeof native === "string" && native.trim() !== "" ? native : typeof field === "string" ? field : "";
4814
- const label = structured?.type;
4815
- const type = typeof label === "string" && label !== "" ? label : void 0;
4816
- const origin = status === void 0 ? "Ollama error" : `Ollama HTTP ${status} error`;
4817
- const prompt = structured?.n_prompt_tokens;
4818
- const limit = structured?.n_ctx;
4819
- const capacity = type === "exceed_context_size_error" && (0, _orkestrel_contract.isInteger)(prompt) && prompt > 0 && (0, _orkestrel_contract.isInteger)(limit) && limit > 0 && prompt > limit && (structured?.code === void 0 || structured?.code === 400) && (status === void 0 || status === 400);
4820
- const reason = text.trim() === "" && type === void 0 ? `Malformed ${origin}` : `${origin}${type === void 0 ? "" : ` (${type})`}${text.trim() === "" ? "" : `: ${text}`}`;
4821
- return new _src_core.StudyError(capacity ? "CAPACITY" : "FAULT", reason, {
4822
- cause: capacity ? "capacity" : "infrastructure",
4823
- message: reason,
4824
- role,
4825
- stop: capacity ? {
4826
- layer: "model",
4827
- id: "num_ctx",
4828
- max: (0, _src_core.parseStudyReading)(limit),
4829
- consumed: (0, _src_core.parseStudyReading)(prompt)
4830
- } : void 0
4831
- });
4832
- }
4833
- /**
4834
- * Reads one Ollama response row without inventing absent usage.
4835
- * @param value - The parsed wire row
4836
- * @param request - The admitted dispatch
4837
- * @returns The terminal facts, or undefined for a nonterminal row
4838
- * @throws StudyError - Thrown with the recognized provider failure when the row carries an `error` member, before any other row check; `FAULT` for a malformed row, model, message, finish reason, or usage
4839
- */
4840
- function readStudyOllamaTerminal(value, request) {
4841
- const failure = readStudyOllamaFailure(value, request.role);
4842
- if (failure !== void 0) throw failure;
4843
- if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.done !== "boolean") throw new _src_core.StudyError("FAULT", "Malformed Ollama response row");
4844
- if (value.model !== request.model) throw new _src_core.StudyError("FAULT", "Ollama response model differs from the dispatched model");
4845
- if (request.role !== "judge") {
4846
- if (!(0, _orkestrel_contract.isRecord)(value.message) || typeof value.message.content !== "string" || value.message.thinking !== void 0 && typeof value.message.thinking !== "string") throw new _src_core.StudyError("FAULT", "Malformed Ollama message");
4847
- if (value.message.tool_calls !== void 0) {
4848
- if (!(0, _orkestrel_contract.isArray)(value.message.tool_calls)) throw new _src_core.StudyError("FAULT", "Malformed Ollama tool calls");
4849
- for (const call of value.message.tool_calls) if (!(0, _orkestrel_contract.isRecord)(call) || !(0, _orkestrel_contract.isRecord)(call.function) || typeof call.function.name !== "string" || !(0, _orkestrel_contract.isRecord)(call.function.arguments)) throw new _src_core.StudyError("FAULT", "Malformed Ollama tool call");
4850
- }
4851
- }
4852
- if (!value.done) return void 0;
4853
- if (typeof value.done_reason !== "string" || value.done_reason.length === 0) throw new _src_core.StudyError("FAULT", "Missing Ollama finish reason");
4854
- for (const key of ["prompt_eval_count", "eval_count"]) if (value[key] !== void 0 && (typeof value[key] !== "number" || !Number.isSafeInteger(value[key]) || value[key] < 0)) throw new _src_core.StudyError("FAULT", "Malformed Ollama usage", {
4855
- cause: "infrastructure",
4856
- message: "Malformed Ollama usage",
4857
- role: request.role,
4858
- stop: void 0
4859
- });
4860
- return {
4861
- prompt: typeof value.prompt_eval_count === "number" ? value.prompt_eval_count : void 0,
4862
- completion: typeof value.eval_count === "number" ? value.eval_count : void 0,
4863
- reason: value.done_reason
4864
- };
4865
- }
4866
- /**
4867
- * Checks terminal policy after the actual wire cost has been retained.
4868
- * @param terminal - The valid terminal facts
4869
- * @param room - The reserved context and safety margin
4870
- * @param request - The admitted dispatch and its declared finish policy; every refusal retains its role
4871
- * @returns Nothing when the completion satisfies the declared policy
4872
- * @throws StudyError - Thrown with code `CAPACITY` when the actual prompt exceeds its room, generation exceeds its allowance, or an undeclared `length` finish or capped generation occurs; `FAULT` for another unsupported finish reason
4873
- */
4874
- function validateStudyOllamaCompletion(terminal, room, request) {
4875
- const { prompt, completion, reason } = terminal;
4876
- const { role } = request;
4877
- const capped = {
4878
- layer: "model",
4879
- id: "num_predict",
4880
- max: request.output,
4881
- consumed: completion
4882
- };
4883
- const ceiling = room.context - request.output - room.safety;
4884
- if (prompt !== void 0 && prompt > ceiling) {
4885
- const message = "Actual prompt exceeded reserved context capacity";
4886
- throw new _src_core.StudyError("CAPACITY", message, {
4887
- cause: "capacity",
4888
- message,
4889
- role,
4890
- stop: {
4891
- layer: "window",
4892
- id: "Context room",
4893
- max: ceiling,
4894
- consumed: prompt
6584
+ role: "agent",
6585
+ calls: 1,
6586
+ tokens: void 0,
6587
+ bytes,
6588
+ duration: Math.ceil(performance.now() - started)
6589
+ });
6590
+ if (response.abort === true) {
6591
+ await plan.context.evidence.append({
6592
+ event: "workload",
6593
+ goal: plan.goal,
6594
+ value: { aborted: true }
6595
+ });
6596
+ plan.workload.abort(/* @__PURE__ */ new Error("Declared workload cancellation"));
6597
+ }
6598
+ return result;
6599
+ } catch (error) {
6600
+ if ((0, _orkestrel_budget.isBudgetError)(error)) this.#cut(ticket, bytes, started);
6601
+ else (0, _src_core.failStudyTicket)(ticket, error, "agent");
6602
+ throw error;
6603
+ } finally {
6604
+ if ((0, _orkestrel_budget.isBudgetError)(signal.reason)) this.#cut(ticket, bytes, started);
6605
+ else (0, _src_core.failStudyTicket)(ticket, signal.reason ?? new _src_core.StudyError("FAULT", "Scripted response was abandoned before completion"), "agent");
4895
6606
  }
4896
- });
4897
- }
4898
- if (completion !== void 0 && completion > request.output) {
4899
- const message = "Actual generation exceeded the output allowance";
4900
- throw new _src_core.StudyError("CAPACITY", message, {
4901
- cause: "capacity",
4902
- message,
4903
- role,
4904
- stop: capped
4905
- });
4906
- }
4907
- if (!request.length && (reason === "length" || completion !== void 0 && completion >= request.output)) {
4908
- const message = "Substantive generation reached its output cap";
4909
- throw new _src_core.StudyError("CAPACITY", message, {
4910
- cause: "capacity",
4911
- message,
4912
- role,
4913
- stop: capped
4914
- });
6607
+ } finally {
6608
+ this.#serving.delete(response);
6609
+ }
4915
6610
  }
4916
- if (reason !== "stop" && !(request.length && reason === "length")) {
4917
- const message = "Unsupported Ollama finish reason";
4918
- throw new _src_core.StudyError("FAULT", message, {
4919
- cause: "infrastructure",
4920
- message,
4921
- role,
4922
- stop: void 0
6611
+ #cut(ticket, bytes, started) {
6612
+ if (ticket.settled || ticket.signal.aborted) return;
6613
+ ticket.finish({
6614
+ role: "agent",
6615
+ calls: 1,
6616
+ tokens: void 0,
6617
+ bytes,
6618
+ duration: Math.ceil(performance.now() - started)
4923
6619
  });
4924
6620
  }
4925
- }
4926
- /**
4927
- * Verifies that each pinned name resolves to one inventory entry with its pinned digest.
4928
- * @param value - The parsed inventory response
4929
- * @param models - The pinned served identities
4930
- * @returns Nothing for a matching inventory
4931
- * @throws StudyError - Thrown with code `FAULT` when the inventory is unsupported or a pinned identity does not match
4932
- */
4933
- function validateStudyOllamaInventory(value, models) {
4934
- if (!(0, _orkestrel_contract.isObject)(value) || !("models" in value) || !(0, _orkestrel_contract.isArray)(value.models)) throw new _src_core.StudyError("FAULT", "Unsupported Ollama inventory");
4935
- const served = value.models;
4936
- for (const { name, digest } of Object.values(models)) {
4937
- const matches = served.filter((model) => (0, _orkestrel_contract.isRecord)(model) && model.name === name);
4938
- const model = matches[0];
4939
- if (matches.length !== 1 || !(0, _orkestrel_contract.isRecord)(model) || typeof model.digest !== "string" || model.digest.replace(/^sha256:/, "") !== digest.replace(/^sha256:/, "")) throw new _src_core.StudyError("FAULT", "Serving inventory does not match the required model digest");
4940
- }
4941
- }
4942
- /**
4943
- * Reads the pinned serving digest of an admitted request's model.
4944
- * @param models - The pinned served identities
4945
- * @param request - The admitted dispatch
4946
- * @returns The pinned digest, or undefined for an inventory request or an unpinned model
4947
- */
4948
- function readStudyOllamaDigest(models, request) {
4949
- if (request.model === void 0) return void 0;
4950
- return Object.values(models).find((model) => model.name === request.model)?.digest;
4951
- }
4952
- /**
4953
- * Reads daemon timing from a terminal row without letting an observation fail the request.
4954
- * @param extract - The admitted extractor
4955
- * @param record - The validated terminal row
4956
- * @returns The raw timing, or undefined when absent or the extractor throws
4957
- */
4958
- function readStudyOllamaTiming(extract, record) {
4959
- try {
4960
- return extract(record);
4961
- } catch {
4962
- return;
6621
+ async #admit(plan, response) {
6622
+ try {
6623
+ return await plan.context.meter.admit({
6624
+ role: "agent",
6625
+ tokens: response.tokens ?? 1,
6626
+ bytes: response.bytes ?? this.#bytes
6627
+ });
6628
+ } catch (error) {
6629
+ await plan.context.evidence.append({
6630
+ event: "admission",
6631
+ goal: plan.goal,
6632
+ value: {
6633
+ role: "agent",
6634
+ refused: true
6635
+ }
6636
+ });
6637
+ throw error;
6638
+ }
4963
6639
  }
4964
- }
4965
- /**
4966
- * Builds the JSON evidence form of a timing observation with raw nanoseconds and counts.
4967
- * @param timing - The extracted timing
4968
- * @returns Null when no terminal timing was observed; otherwise every unreported member as null
4969
- */
4970
- function buildStudyOllamaTiming(timing) {
4971
- if (timing === void 0) return null;
4972
- return {
4973
- durations: {
4974
- total: timing.durations.total ?? null,
4975
- load: timing.durations.load ?? null,
4976
- prompt: timing.durations.prompt ?? null,
4977
- decode: timing.durations.decode ?? null
4978
- },
4979
- counts: {
4980
- prompt: timing.counts.prompt ?? null,
4981
- decode: timing.counts.decode ?? null,
4982
- cached: timing.counts.cached ?? null
4983
- },
4984
- invalid: [...timing.invalid]
4985
- };
4986
- }
4987
- /**
4988
- * Builds a reading of the raw prompt text an encoded request body carries, as sent.
4989
- * @param body - The encoded request body
4990
- * @returns The prompt's UTF-16 code units and UTF-8 bytes, labeled with its encoding, or undefined when there is no string prompt; these are readings of the actual text, not token allocations
4991
- */
4992
- function buildStudyOllamaRaw(body) {
4993
- if (typeof body !== "string") return void 0;
4994
- const parsed = (0, _orkestrel_contract.attempt)(() => JSON.parse(body));
4995
- if (!parsed.success || !(0, _orkestrel_contract.isRecord)(parsed.value) || typeof parsed.value.prompt !== "string") return void 0;
4996
- return {
4997
- encoding: "utf-8",
4998
- units: parsed.value.prompt.length,
4999
- bytes: new TextEncoder().encode(parsed.value.prompt).byteLength
5000
- };
5001
- }
6640
+ };
5002
6641
  //#endregion
5003
6642
  //#region src/server/ollama/cloners.ts
5004
6643
  /**
5005
6644
  * Builds a frozen owned copy of transport configuration that later caller mutation cannot reach.
5006
6645
  * @remarks
5007
6646
  * Every caller member is read once, outer fields before nested data. Every nested data object is
5008
- * copied and frozen; the caller's objects are never frozen. The cancellation signal and timing
5009
- * extractor keep their live identity.
6647
+ * copied and frozen; the caller's objects are never frozen. The cancellation signal, timing
6648
+ * extractor, and fetch collaborator keep their live identity.
5010
6649
  * @param options - The caller's transport configuration
5011
6650
  * @returns The owned configuration, not yet validated
5012
6651
  */
5013
6652
  function buildStudyOllamaTransportOptions(options) {
5014
- const { url, room, limit, models, roles, signal, observe, timing } = options;
6653
+ const { url, room, limit, models, roles, signal, observe, timing, fetch, metadata, version, procedure, exchanges } = options;
6654
+ if (Object.hasOwn(roles, "maintenance")) throw new _src_core.StudyError("CONFIG", "Maintenance cannot declare a generation policy");
5015
6655
  const owned = Object.fromEntries(Object.entries(models).map(([label, { name, digest }]) => [label, Object.freeze({
5016
6656
  name,
5017
6657
  digest
@@ -5048,7 +6688,23 @@ function buildStudyOllamaTransportOptions(options) {
5048
6688
  roles: Object.freeze(policies),
5049
6689
  signal,
5050
6690
  observe,
5051
- timing
6691
+ timing,
6692
+ metadata: Object.freeze({ ...metadata }),
6693
+ ...version === void 0 ? {} : { version },
6694
+ ...procedure === void 0 ? {} : { procedure: Object.freeze(procedure.map((step) => {
6695
+ const { policy, ...identity } = step;
6696
+ if (identity.admission === "exact" && Object.hasOwn(step, "bytes") || identity.admission === "capture" && (Object.hasOwn(step, "body") || Object.hasOwn(step, "input"))) throw new _src_core.StudyError("CONFIG", "Conflicting model-step admission declarations");
6697
+ const { options: wire, ...settings } = policy;
6698
+ return Object.freeze({
6699
+ ...identity,
6700
+ policy: Object.freeze({
6701
+ ...settings,
6702
+ options: Object.freeze({ ...wire })
6703
+ })
6704
+ });
6705
+ })) },
6706
+ ...exchanges === void 0 ? {} : { exchanges },
6707
+ ...fetch === void 0 ? {} : { fetch }
5052
6708
  });
5053
6709
  }
5054
6710
  /**
@@ -5082,6 +6738,10 @@ function buildStudyOllamaSnapshot(input, init, dispatch) {
5082
6738
  //#region src/server/ollama/StudyOllamaExchange.ts
5083
6739
  /** Tracks one owned HTTP response through its counted bytes, decoded rows, terminal and settlement. */
5084
6740
  var StudyOllamaExchange = class {
6741
+ slot;
6742
+ identity;
6743
+ headers;
6744
+ ceiling;
5085
6745
  activity;
5086
6746
  request;
5087
6747
  ticket;
@@ -5098,10 +6758,16 @@ var StudyOllamaExchange = class {
5098
6758
  #terminal;
5099
6759
  #timing;
5100
6760
  #settled = false;
6761
+ #chunks = [];
6762
+ #recorded = false;
5101
6763
  /** Binds the facts of one dispatched request and its open response.
5102
6764
  * @param wire - The request, accounting boundary, reader and identities; its byte count starts the tally.
5103
6765
  */
5104
6766
  constructor(wire) {
6767
+ this.slot = wire.slot;
6768
+ this.identity = wire.identity;
6769
+ this.headers = wire.headers;
6770
+ this.ceiling = wire.ceiling;
5105
6771
  this.activity = wire.activity;
5106
6772
  this.request = wire.request;
5107
6773
  this.ticket = wire.ticket;
@@ -5135,6 +6801,29 @@ var StudyOllamaExchange = class {
5135
6801
  get settled() {
5136
6802
  return this.#settled;
5137
6803
  }
6804
+ /** Reports whether a sidecar terminal has already been admitted. */
6805
+ get recorded() {
6806
+ return this.#recorded;
6807
+ }
6808
+ /** Holds the exact bounded bytes and native chunk lengths observed so far. */
6809
+ get reply() {
6810
+ return {
6811
+ status: this.status,
6812
+ headers: this.headers,
6813
+ ...buildStudyOllamaBytes(Buffer.concat(this.#chunks)),
6814
+ chunks: this.#chunks.map((chunk) => chunk.byteLength)
6815
+ };
6816
+ }
6817
+ /** Retains one bounded chunk before decoding changes its representation.
6818
+ * @param chunk - The native response bytes already counted under the ceiling.
6819
+ */
6820
+ retain(chunk) {
6821
+ if (this.identity !== void 0 && chunk.byteLength > 0) this.#chunks = [...this.#chunks, Buffer.from(chunk)];
6822
+ }
6823
+ /** Marks a sidecar terminal admitted so failure cleanup cannot append another. */
6824
+ record() {
6825
+ this.#recorded = true;
6826
+ }
5138
6827
  /** Adds a response chunk's bytes to the tally and refuses a tally beyond the allowance.
5139
6828
  * @param length - The chunk's byte length.
5140
6829
  * @param limit - The response byte allowance.
@@ -5223,7 +6912,15 @@ var StudyOllamaGoal = class {
5223
6912
  var StudyOllamaTransport = class {
5224
6913
  #options;
5225
6914
  #abort = new AbortController();
6915
+ #sends = /* @__PURE__ */ new Set();
5226
6916
  #wires = /* @__PURE__ */ new Set();
6917
+ #requests = /* @__PURE__ */ new Set();
6918
+ #verifications = /* @__PURE__ */ new Set();
6919
+ #metadataControl;
6920
+ #metadataScope;
6921
+ #metadataMeter;
6922
+ #passes = 0;
6923
+ #step = 0;
5227
6924
  #goal;
5228
6925
  #fault;
5229
6926
  #count = 0;
@@ -5233,8 +6930,31 @@ var StudyOllamaTransport = class {
5233
6930
  */
5234
6931
  constructor(options) {
5235
6932
  const owned = buildStudyOllamaTransportOptions(options);
6933
+ if (owned.fetch !== void 0 && !(0, _orkestrel_contract.isFunction)(owned.fetch)) throw new _src_core.StudyError("CONFIG", "Ollama fetch collaborator must be callable");
5236
6934
  validateStudyOllamaRoles(owned.roles, owned.models);
6935
+ validateStudyOllamaRecording(owned);
5237
6936
  this.#options = owned;
6937
+ this.#metadataControl = new _src_core.StudyControl(owned.signal);
6938
+ const duration = Math.min(_orkestrel_timeout.MAX_TIMEOUT_MS, Math.ceil(owned.metadata.duration));
6939
+ this.#metadataScope = new _src_core.StudyScope({
6940
+ limit: {
6941
+ ...owned.metadata,
6942
+ duration: _orkestrel_timeout.MAX_TIMEOUT_MS,
6943
+ tokens: 1
6944
+ },
6945
+ control: this.#metadataControl,
6946
+ label: "Metadata"
6947
+ });
6948
+ this.#metadataMeter = new _src_core.StudyMeter({
6949
+ request: {
6950
+ duration: Math.min(duration, owned.limit.duration),
6951
+ calls: 1,
6952
+ tokens: 1,
6953
+ bytes: owned.metadata.bytes
6954
+ },
6955
+ scopes: [this.#metadataScope],
6956
+ control: this.#metadataControl
6957
+ });
5238
6958
  }
5239
6959
  /** Binds subsequent wire requests to one live goal until `clear` or `close` ends it.
5240
6960
  * @param activity - The goal's accounting boundary.
@@ -5272,7 +6992,14 @@ var StudyOllamaTransport = class {
5272
6992
  * @throws StudyError - Thrown with code `FAULT` when no goal is bound; the transport retains this refusal as its failure, as it retains every verification failure.
5273
6993
  */
5274
6994
  async verify() {
6995
+ const verification = Symbol("verification");
5275
6996
  try {
6997
+ this.check();
6998
+ if (this.#requests.size !== 0 || this.#verifications.size !== 0) throw new _src_core.StudyError("FAULT", "Verification cannot overlap an admitted request");
6999
+ if (this.#goal === void 0) throw new _src_core.StudyError("FAULT", "Model dispatch outside an active goal");
7000
+ if (this.#options.procedure !== void 0 && (this.#passes > 1 || this.#passes === 1 && this.#step !== this.#options.procedure.length)) throw new _src_core.StudyError("FAULT", "Postflight requires the complete model procedure");
7001
+ this.#verifications.add(verification);
7002
+ if (this.#options.version !== void 0) await (await this.fetch("metadata", new URL("/api/version", this.#options.url), { method: "GET" })).text();
5276
7003
  validateStudyOllamaInventory(await (await this.fetch("metadata", new URL("/api/tags", this.#options.url), { method: "GET" })).json(), this.#options.models);
5277
7004
  const goal = this.#goal;
5278
7005
  if (goal === void 0) throw new _src_core.StudyError("FAULT", "Inventory has no goal attribution");
@@ -5299,9 +7026,20 @@ var StudyOllamaTransport = class {
5299
7026
  goal: activity.goal,
5300
7027
  value: Object.fromEntries(Object.entries(this.#options.models).map(([label, model]) => [label, model.digest]))
5301
7028
  });
7029
+ this.check();
7030
+ this.#passes += 1;
7031
+ if (this.#options.exchanges !== void 0 && this.#passes === 2) {
7032
+ if (this.#requests.size !== 0 || this.#sends.size !== 0 || this.#wires.size !== 0) throw new _src_core.StudyError("FAULT", "Cannot complete while an exchange is pending");
7033
+ await this.#options.exchanges.append({
7034
+ phase: "complete",
7035
+ count: this.#options.exchanges.next - 1
7036
+ });
7037
+ }
5302
7038
  } catch (error) {
5303
7039
  this.fail(error);
5304
7040
  throw error;
7041
+ } finally {
7042
+ this.#verifications.delete(verification);
5305
7043
  }
5306
7044
  }
5307
7045
  /** Dispatches exactly one admitted request with bounded streaming reads.
@@ -5318,52 +7056,22 @@ var StudyOllamaTransport = class {
5318
7056
  const goal = this.#goal;
5319
7057
  if (goal === void 0) throw new _src_core.StudyError("FAULT", "Model dispatch outside an active goal");
5320
7058
  const activity = goal.activity;
7059
+ const slot = Symbol("request");
5321
7060
  try {
7061
+ if (this.#requests.size !== 0) throw new _src_core.StudyError("FAULT", "A previous Ollama request is still in flight");
7062
+ if (this.#verifications.size !== 0 && role !== "metadata") throw new _src_core.StudyError("FAULT", "Model dispatch cannot overlap verification");
7063
+ this.#requests.add(slot);
5322
7064
  const snapshot = buildStudyOllamaSnapshot(input, init, original);
5323
7065
  const settings = snapshot.init;
5324
7066
  const request = readStudyOllamaRequest(this.#options, role, snapshot.url, settings);
5325
- const limit = this.#options.limit;
5326
- if (request.bytes >= limit.bytes) {
5327
- const message = "Encoded request consumes the response byte allowance";
5328
- throw new _src_core.StudyError("CAPACITY", message, {
5329
- cause: "capacity",
5330
- message,
5331
- role: void 0,
5332
- stop: {
5333
- layer: "scope",
5334
- id: "Wire bytes",
5335
- max: limit.bytes,
5336
- consumed: request.bytes
5337
- }
5338
- });
7067
+ if (this.#options.procedure !== void 0 && role !== "metadata") {
7068
+ if (this.#passes !== 1) throw new _src_core.StudyError("FAULT", "Model dispatch requires completed preflight and open execution");
7069
+ validateStudyOllamaStep(this.#options.procedure[this.#step], activity.goal, request, settings?.body);
5339
7070
  }
5340
- const ticket = await activity.context.meter.admit({
5341
- role,
5342
- tokens: role === "metadata" ? 0 : this.#options.room.context - this.#options.room.safety,
5343
- bytes: limit.bytes
5344
- });
5345
- const id = ++this.#count;
5346
- const started = performance.now();
5347
- const signal = AbortSignal.any([
5348
- this.#options.signal,
5349
- this.#abort.signal,
5350
- activity.context.signal,
5351
- activity.context.meter.signal,
5352
- ticket.signal,
5353
- AbortSignal.timeout(limit.duration),
5354
- ...settings?.signal === void 0 || settings.signal === null ? [] : [settings.signal]
5355
- ]);
5356
- const wire = await this.#send(role, goal, ticket, request, snapshot, signal, id, started);
5357
- this.#wires.add(wire);
5358
- if (wire.status < 200 || wire.status > 299) return await this.#refuse(wire);
5359
- return new Response(new ReadableStream({
5360
- pull: (controller) => this.#pull(wire, controller),
5361
- cancel: (reason) => (0, _orkestrel_budget.isBudgetError)(reason) ? this.#cut(wire, reason) : this.#failWire(wire, reason === void 0 ? new _src_core.StudyError("FAULT", "Ollama response cancelled before its end") : reason)
5362
- }), {
5363
- status: wire.status,
5364
- headers: { "content-type": role === "judge" || role === "metadata" ? "application/json" : "application/x-ndjson" }
5365
- });
7071
+ if (this.#options.exchanges !== void 0 && role === "metadata" && this.#verifications.size === 0) throw new _src_core.StudyError("FAULT", "Recorded metadata requires a verification pass");
7072
+ return await this.#request(slot, role, goal, request, snapshot);
5366
7073
  } catch (error) {
7074
+ this.#requests.delete(slot);
5367
7075
  this.fail(error);
5368
7076
  throw error;
5369
7077
  }
@@ -5373,8 +7081,87 @@ var StudyOllamaTransport = class {
5373
7081
  */
5374
7082
  async close() {
5375
7083
  this.#abort.abort(new _src_core.StudyError("FAULT", "Ollama transport closed"));
7084
+ await Promise.all([...this.#sends].map((sending) => sending.catch((error) => this.fail(error))));
5376
7085
  await Promise.all([...this.#wires].map((wire) => this.#failWire(wire, this.#abort.signal.reason)));
5377
7086
  this.#goal = void 0;
7087
+ this.#metadataMeter.close();
7088
+ this.#metadataScope.close();
7089
+ this.#metadataControl.close();
7090
+ }
7091
+ async #request(slot, role, goal, request, snapshot) {
7092
+ const activity = goal.activity;
7093
+ const settings = snapshot.init;
7094
+ const metadata = role === "metadata";
7095
+ const available = this.#options.metadata.bytes - this.#metadataMeter.usage.reduce((total, usage) => total + (usage.bytes ?? this.#options.metadata.bytes), 0);
7096
+ const remaining = this.#remaining;
7097
+ if (metadata && remaining <= 0) throw new _src_core.StudyError("DEADLINE", "Metadata duration allowance exhausted", {
7098
+ cause: "deadline",
7099
+ message: "Metadata duration allowance exhausted",
7100
+ role,
7101
+ stop: {
7102
+ layer: "scope",
7103
+ id: "Metadata duration",
7104
+ max: this.#options.metadata.duration,
7105
+ consumed: this.#options.metadata.duration - remaining
7106
+ }
7107
+ });
7108
+ const limit = {
7109
+ duration: metadata ? Math.min(this.#options.limit.duration, Math.ceil(remaining)) : this.#options.limit.duration,
7110
+ bytes: metadata ? Math.min(this.#options.limit.bytes, available) : this.#options.limit.bytes
7111
+ };
7112
+ if (request.bytes >= limit.bytes) {
7113
+ const message = "Encoded request consumes the response byte allowance";
7114
+ throw new _src_core.StudyError("CAPACITY", message, {
7115
+ cause: "capacity",
7116
+ message,
7117
+ role,
7118
+ stop: {
7119
+ layer: "scope",
7120
+ id: "Wire bytes",
7121
+ max: limit.bytes,
7122
+ consumed: request.bytes
7123
+ }
7124
+ });
7125
+ }
7126
+ const ticket = await (metadata ? this.#metadataMeter : activity.context.meter).admit({
7127
+ role,
7128
+ tokens: role === "metadata" ? 0 : this.#options.room.context - this.#options.room.safety,
7129
+ bytes: limit.bytes
7130
+ });
7131
+ const id = this.#options.exchanges?.next ?? ++this.#count;
7132
+ const started = performance.now();
7133
+ const signal = AbortSignal.any([
7134
+ this.#options.signal,
7135
+ this.#abort.signal,
7136
+ activity.context.signal,
7137
+ ...metadata ? [this.#metadataMeter.signal] : [activity.context.meter.signal],
7138
+ ticket.signal,
7139
+ AbortSignal.timeout(limit.duration),
7140
+ ...settings?.signal === void 0 || settings.signal === null ? [] : [settings.signal]
7141
+ ]);
7142
+ const identity = this.#options.exchanges === void 0 ? void 0 : {
7143
+ ordinal: id,
7144
+ goal: activity.goal,
7145
+ role,
7146
+ path: role === "metadata" ? request.path === "/api/version" ? "/api/version" : request.path === "/api/tags" ? "/api/tags" : "/api/show" : role === "judge" ? "/api/generate" : "/api/chat",
7147
+ method: settings?.method === "GET" ? "GET" : "POST",
7148
+ stage: role === "metadata" ? this.#passes === 0 ? "preflight" : "postflight" : "execution",
7149
+ ...role === "metadata" ? {} : { step: this.#step + 1 }
7150
+ };
7151
+ const sending = this.#send(role, goal, ticket, request, snapshot, signal, id, started, slot, identity, limit.bytes);
7152
+ this.#sends.add(sending);
7153
+ const wire = await sending.finally(() => this.#sends.delete(sending));
7154
+ if (wire.status < 200 || wire.status > 299) return await this.#refuse(wire);
7155
+ return new Response(new ReadableStream({
7156
+ pull: (controller) => this.#pull(wire, controller),
7157
+ cancel: (reason) => (0, _orkestrel_budget.isBudgetError)(reason) ? this.#cut(wire, reason) : this.#failWire(wire, reason === void 0 ? new _src_core.StudyError("FAULT", "Ollama response cancelled before its end") : reason)
7158
+ }), {
7159
+ status: wire.status,
7160
+ headers: { "content-type": role === "judge" || role === "metadata" ? "application/json" : "application/x-ndjson" }
7161
+ });
7162
+ }
7163
+ get #remaining() {
7164
+ return this.#options.metadata.duration - this.#metadataMeter.usage.reduce((total, usage) => total + usage.duration, 0);
5378
7165
  }
5379
7166
  async #refuse(wire) {
5380
7167
  const failure = await this.#diagnose(wire);
@@ -5387,10 +7174,11 @@ var StudyOllamaTransport = class {
5387
7174
  try {
5388
7175
  for (;;) {
5389
7176
  wire.signal.throwIfAborted();
5390
- const chunk = await wire.reader.read();
7177
+ const chunk = await this.#read(wire);
5391
7178
  wire.signal.throwIfAborted();
5392
7179
  if (chunk.done) break;
5393
- wire.count(chunk.value.byteLength, this.#options.limit.bytes);
7180
+ wire.count(chunk.value.byteLength, wire.ceiling);
7181
+ wire.retain(chunk.value);
5394
7182
  parts.push(decoder.decode(chunk.value, { stream: true }));
5395
7183
  }
5396
7184
  const body = (0, _orkestrel_contract.parseJSON)(parts.join("") + decoder.decode());
@@ -5408,10 +7196,11 @@ var StudyOllamaTransport = class {
5408
7196
  async #pull(wire, controller) {
5409
7197
  try {
5410
7198
  wire.signal.throwIfAborted();
5411
- const chunk = await wire.reader.read();
7199
+ const chunk = await this.#read(wire);
5412
7200
  wire.signal.throwIfAborted();
5413
7201
  if (!chunk.done) {
5414
- wire.count(chunk.value.byteLength, this.#options.limit.bytes);
7202
+ wire.count(chunk.value.byteLength, wire.ceiling);
7203
+ wire.retain(chunk.value);
5415
7204
  wire.decode(chunk.value);
5416
7205
  if (wire.request.role !== "metadata" && wire.request.role !== "judge") this.#rows(wire, false);
5417
7206
  controller.enqueue(chunk.value);
@@ -5421,19 +7210,48 @@ var StudyOllamaTransport = class {
5421
7210
  if (wire.request.role === "metadata") {
5422
7211
  const value = JSON.parse(wire.text);
5423
7212
  if (wire.request.path === "/api/tags") validateStudyOllamaInventory(value, this.#options.models);
5424
- else readStudyOllamaSupport(value, this.#options.room.context);
7213
+ else if (wire.request.path === "/api/version") {
7214
+ if (!(0, _orkestrel_contract.isRecord)(value) || value.version !== this.#options.version) throw new _src_core.StudyError("FAULT", "Serving version differs from the declared version");
7215
+ } else readStudyOllamaSupport(value, this.#options.room.context);
5425
7216
  } else if (wire.request.role === "judge") this.#observe(wire, JSON.parse(wire.text));
5426
7217
  else this.#rows(wire, true);
5427
7218
  if (wire.request.role !== "metadata" && wire.terminal === void 0) throw new _src_core.StudyError("FAULT", "Ollama response has no terminal row");
5428
7219
  await this.#finish(wire);
7220
+ if (wire.request.role === "metadata" && this.#remaining < 0) {
7221
+ const message = "Metadata duration allowance exhausted";
7222
+ throw new _src_core.StudyError("DEADLINE", message, {
7223
+ cause: "deadline",
7224
+ message,
7225
+ role: "metadata",
7226
+ stop: {
7227
+ layer: "scope",
7228
+ id: "Metadata duration",
7229
+ max: this.#options.metadata.duration,
7230
+ consumed: this.#options.metadata.duration - this.#remaining
7231
+ }
7232
+ });
7233
+ }
5429
7234
  if (wire.terminal !== void 0) await this.#judge(wire, wire.terminal);
7235
+ if (wire.identity !== void 0) {
7236
+ this.check();
7237
+ await this.#options.exchanges?.append({
7238
+ phase: "response",
7239
+ ...wire.identity,
7240
+ ...wire.reply
7241
+ });
7242
+ wire.record();
7243
+ if (wire.identity.step !== void 0) this.#step += 1;
7244
+ }
7245
+ this.#requests.delete(wire.slot);
5430
7246
  controller.close();
5431
7247
  } catch (error) {
5432
7248
  this.fail(error);
5433
7249
  try {
7250
+ if (wire.settled) await this.#recordFailure(wire, error);
5434
7251
  if ((0, _orkestrel_budget.isBudgetError)(error)) await this.#cut(wire, error);
5435
7252
  else await this.#failWire(wire, error);
5436
7253
  } finally {
7254
+ this.#requests.delete(wire.slot);
5437
7255
  controller.error(error);
5438
7256
  }
5439
7257
  }
@@ -5473,10 +7291,16 @@ var StudyOllamaTransport = class {
5473
7291
  #time(row) {
5474
7292
  return this.#options.observe && (0, _orkestrel_contract.isRecord)(row) ? readStudyOllamaTiming(this.#options.timing, row) : void 0;
5475
7293
  }
5476
- async #send(role, goal, ticket, request, snapshot, signal, id, started) {
7294
+ async #send(role, goal, ticket, request, snapshot, signal, id, started, slot, identity, ceiling) {
5477
7295
  const { url, init: settings, dispatch } = snapshot;
5478
7296
  try {
7297
+ this.check();
5479
7298
  signal.throwIfAborted();
7299
+ if (identity !== void 0) await this.#options.exchanges?.append({
7300
+ phase: "attempt",
7301
+ ...identity,
7302
+ ...buildStudyOllamaBytes(new TextEncoder().encode(typeof settings?.body === "string" ? settings.body : ""))
7303
+ });
5480
7304
  await goal.activity.context.evidence.append({
5481
7305
  event: "dispatch",
5482
7306
  goal: goal.activity.goal,
@@ -5486,18 +7310,28 @@ var StudyOllamaTransport = class {
5486
7310
  model: request.model ?? null,
5487
7311
  options: { ...request.options },
5488
7312
  bytes: request.bytes,
7313
+ ...identity === void 0 ? {} : { ordinal: identity.ordinal },
5489
7314
  ...this.#options.observe ? this.#dispatch(goal, request, id, settings, dispatch) : {}
5490
7315
  }
5491
7316
  });
5492
7317
  signal.throwIfAborted();
7318
+ this.check();
5493
7319
  const sent = performance.now();
5494
- const response = await globalThis.fetch(url, {
7320
+ const response = this.#options.fetch === void 0 ? await globalThis.fetch(url, {
5495
7321
  ...settings,
5496
7322
  signal,
5497
7323
  redirect: "error"
5498
- });
7324
+ }) : await this.#fetch(this.#options.fetch, role, url, settings, signal);
7325
+ if (signal.aborted) {
7326
+ response.body?.cancel(signal.reason).catch(() => void 0);
7327
+ signal.throwIfAborted();
7328
+ }
5499
7329
  if (response.body === null) throw new _src_core.StudyError("FAULT", `Ollama HTTP response refused: ${response.status}`);
5500
- return new StudyOllamaExchange({
7330
+ const wire = new StudyOllamaExchange({
7331
+ slot,
7332
+ identity,
7333
+ ceiling,
7334
+ headers: [...response.headers.entries()].filter(([name]) => name === "content-type" || name === "content-encoding"),
5501
7335
  activity: goal.activity,
5502
7336
  request,
5503
7337
  ticket,
@@ -5511,23 +7345,88 @@ var StudyOllamaTransport = class {
5511
7345
  dispatch,
5512
7346
  bytes: request.bytes
5513
7347
  });
7348
+ this.#wires.add(wire);
7349
+ return wire;
5514
7350
  } catch (error) {
5515
- if (!ticket.signal.aborted) {
5516
- if ((0, _orkestrel_budget.isBudgetError)(error)) ticket.finish({
5517
- role,
5518
- calls: 1,
5519
- tokens: void 0,
5520
- bytes: void 0,
5521
- duration: Math.ceil(performance.now() - started)
5522
- });
5523
- else ticket.fail({
5524
- ...(0, _src_core.readStudyFailure)(error),
5525
- role
5526
- });
7351
+ try {
7352
+ if (identity !== void 0 && this.#options.exchanges !== void 0 && this.#options.exchanges.next > identity.ordinal) {
7353
+ this.#fault ??= { error };
7354
+ await this.#options.exchanges.append({
7355
+ phase: "failure",
7356
+ ...identity,
7357
+ reason: (0, _src_core.readStudyFailure)(error).message.slice(0, 256)
7358
+ });
7359
+ }
7360
+ } finally {
7361
+ if (!ticket.settled && !ticket.signal.aborted) {
7362
+ if ((0, _orkestrel_budget.isBudgetError)(error)) ticket.finish({
7363
+ role,
7364
+ calls: 1,
7365
+ tokens: void 0,
7366
+ bytes: void 0,
7367
+ duration: Math.ceil(performance.now() - started)
7368
+ });
7369
+ else ticket.fail({
7370
+ ...(0, _src_core.readStudyFailure)(error),
7371
+ role
7372
+ });
7373
+ }
5527
7374
  }
5528
7375
  throw error;
5529
7376
  }
5530
7377
  }
7378
+ #read(wire) {
7379
+ const listeners = new _orkestrel_abort.Abort();
7380
+ return new Promise((resolve, reject) => {
7381
+ wire.signal.addEventListener("abort", () => reject(wire.signal.reason), {
7382
+ once: true,
7383
+ signal: listeners.signal
7384
+ });
7385
+ wire.signal.throwIfAborted();
7386
+ wire.reader.read().then(resolve, reject);
7387
+ }).finally(() => listeners.abort());
7388
+ }
7389
+ async #recordFailure(wire, error) {
7390
+ if (wire.identity === void 0 || wire.recorded) return;
7391
+ this.#fault ??= { error };
7392
+ const reason = (0, _src_core.readStudyFailure)(error).message;
7393
+ try {
7394
+ await this.#options.exchanges?.append({
7395
+ phase: "failure",
7396
+ ...wire.identity,
7397
+ reason: reason.slice(0, 256),
7398
+ response: wire.reply
7399
+ });
7400
+ } catch (failure) {
7401
+ if (!(0, _src_core.isStudyError)(failure) || failure.code !== "CAPACITY") throw failure;
7402
+ await this.#options.exchanges?.append({
7403
+ phase: "failure",
7404
+ ...wire.identity,
7405
+ reason: `The reply was not retained because it exceeded the sidecar allowance; ${reason.slice(0, 128)}`
7406
+ });
7407
+ }
7408
+ wire.record();
7409
+ }
7410
+ #fetch(fetch, role, url, settings, signal) {
7411
+ const listeners = new _orkestrel_abort.Abort();
7412
+ return new Promise((resolve, reject) => {
7413
+ signal.addEventListener("abort", () => reject(signal.reason), {
7414
+ once: true,
7415
+ signal: listeners.signal
7416
+ });
7417
+ signal.throwIfAborted();
7418
+ fetch(role, url, {
7419
+ ...settings,
7420
+ signal,
7421
+ redirect: "error"
7422
+ }).then((response) => {
7423
+ if (signal.aborted) {
7424
+ response.body?.cancel(signal.reason).catch(() => void 0);
7425
+ reject(signal.reason);
7426
+ } else resolve(response);
7427
+ }, reject);
7428
+ }).finally(() => listeners.abort());
7429
+ }
5531
7430
  #dispatch(goal, request, id, init, dispatch) {
5532
7431
  const previous = this.#previous;
5533
7432
  const model = request.role !== "metadata";
@@ -5575,6 +7474,7 @@ var StudyOllamaTransport = class {
5575
7474
  prompt: terminal?.prompt ?? null,
5576
7475
  completion: terminal?.completion ?? null,
5577
7476
  bytes: wire.bytes,
7477
+ ...wire.identity === void 0 ? {} : { ordinal: wire.identity.ordinal },
5578
7478
  ...observation
5579
7479
  }
5580
7480
  });
@@ -5598,6 +7498,7 @@ var StudyOllamaTransport = class {
5598
7498
  const observation = this.#options.observe ? this.#timeline(wire) : {};
5599
7499
  const stop = (0, _src_core.readStudyFailure)(error).stop;
5600
7500
  try {
7501
+ await this.#recordFailure(wire, error);
5601
7502
  await wire.activity.context.evidence.append({
5602
7503
  event: "cut",
5603
7504
  goal: wire.activity.goal,
@@ -5629,6 +7530,7 @@ var StudyOllamaTransport = class {
5629
7530
  });
5630
7531
  throw failure;
5631
7532
  } finally {
7533
+ this.#requests.delete(wire.slot);
5632
7534
  this.#wires.delete(wire);
5633
7535
  await wire.reader.cancel(error).catch(() => void 0);
5634
7536
  wire.reader.releaseLock();
@@ -5645,6 +7547,7 @@ var StudyOllamaTransport = class {
5645
7547
  ...failure,
5646
7548
  role: wire.request.role
5647
7549
  });
7550
+ await this.#recordFailure(wire, error);
5648
7551
  await wire.activity.context.evidence.append({
5649
7552
  event: "failure",
5650
7553
  goal: wire.activity.goal,
@@ -5658,6 +7561,7 @@ var StudyOllamaTransport = class {
5658
7561
  }
5659
7562
  });
5660
7563
  } finally {
7564
+ this.#requests.delete(wire.slot);
5661
7565
  this.#wires.delete(wire);
5662
7566
  await wire.reader.cancel(error).catch(() => void 0);
5663
7567
  wire.reader.releaseLock();
@@ -5667,6 +7571,8 @@ var StudyOllamaTransport = class {
5667
7571
  //#endregion
5668
7572
  exports.ARTIFACT_BYTES = ARTIFACT_BYTES;
5669
7573
  exports.ARTIFACT_DEPTH = ARTIFACT_DEPTH;
7574
+ exports.ARTIFACT_ERROR_BRAND = ARTIFACT_ERROR_BRAND;
7575
+ exports.ARTIFACT_ERROR_CODES = ARTIFACT_ERROR_CODES;
5670
7576
  exports.ARTIFACT_FILES = ARTIFACT_FILES;
5671
7577
  exports.ARTIFACT_FILE_BYTES = ARTIFACT_FILE_BYTES;
5672
7578
  exports.ARTIFACT_ROOTS = ARTIFACT_ROOTS;
@@ -5699,9 +7605,13 @@ exports.StudyArtifactLoader = StudyArtifactLoader;
5699
7605
  exports.StudyCLI = StudyCLI;
5700
7606
  exports.StudyChannel = StudyChannel;
5701
7607
  exports.StudyCommand = StudyCommand;
7608
+ exports.StudyDaemonSession = StudyDaemonSession;
7609
+ exports.StudyDaemonTrial = StudyDaemonTrial;
5702
7610
  exports.StudyEvidenceSink = StudyEvidenceSink;
7611
+ exports.StudyExchangeSink = StudyExchangeSink;
5703
7612
  exports.StudyGradingStore = StudyGradingStore;
5704
7613
  exports.StudyLauncher = StudyLauncher;
7614
+ exports.StudyOllamaReplay = StudyOllamaReplay;
5705
7615
  exports.StudyOllamaTransport = StudyOllamaTransport;
5706
7616
  exports.StudyProcessDriver = StudyProcessDriver;
5707
7617
  exports.StudyProcessGoal = StudyProcessGoal;
@@ -5716,6 +7626,8 @@ exports.buildStudyAgentReadings = buildStudyAgentReadings;
5716
7626
  exports.buildStudyAgentRevision = buildStudyAgentRevision;
5717
7627
  exports.buildStudyAgentToolDenial = buildStudyAgentToolDenial;
5718
7628
  exports.buildStudyAgentToolResult = buildStudyAgentToolResult;
7629
+ exports.buildStudyExchangeSequence = buildStudyExchangeSequence;
7630
+ exports.buildStudyOllamaBytes = buildStudyOllamaBytes;
5719
7631
  exports.buildStudyOllamaModels = buildStudyOllamaModels;
5720
7632
  exports.buildStudyOllamaOptions = buildStudyOllamaOptions;
5721
7633
  exports.buildStudyOllamaRaw = buildStudyOllamaRaw;
@@ -5729,6 +7641,7 @@ exports.checkStudyDirectory = checkStudyDirectory;
5729
7641
  exports.collectStudyAncestors = collectStudyAncestors;
5730
7642
  exports.collectStudyEntries = collectStudyEntries;
5731
7643
  exports.collectStudyEvidence = collectStudyEvidence;
7644
+ exports.computeArtifactClosureDigest = computeArtifactClosureDigest;
5732
7645
  exports.computeStudyAttributions = computeStudyAttributions;
5733
7646
  exports.computeStudyCommandFingerprint = computeStudyCommandFingerprint;
5734
7647
  exports.computeStudyFixtureDigest = computeStudyFixtureDigest;
@@ -5745,10 +7658,15 @@ exports.drainStudyChild = drainStudyChild;
5745
7658
  exports.encodeStudyJSON = encodeStudyJSON;
5746
7659
  exports.gradeStudyEvidence = gradeStudyEvidence;
5747
7660
  exports.hashStudyBytes = hashStudyBytes;
7661
+ exports.isArtifactClosure = isArtifactClosure;
5748
7662
  exports.isArtifactError = isArtifactError;
7663
+ exports.isArtifactKey = isArtifactKey;
7664
+ exports.isArtifactLoaderOptions = isArtifactLoaderOptions;
7665
+ exports.isArtifactMember = isArtifactMember;
5749
7666
  exports.isStudyAdapter = isStudyAdapter;
5750
7667
  exports.isStudyAgentResponse = isStudyAgentResponse;
5751
7668
  exports.isStudyCharge = isStudyCharge;
7669
+ exports.isStudyClosure = isStudyClosure;
5752
7670
  exports.isStudyCustodyAttempt = isStudyCustodyAttempt;
5753
7671
  exports.isStudyCustodyEvidence = isStudyCustodyEvidence;
5754
7672
  exports.isStudyCustodyFailure = isStudyCustodyFailure;
@@ -5759,22 +7677,40 @@ exports.isStudyCustodyObservation = isStudyCustodyObservation;
5759
7677
  exports.isStudyCustodySeal = isStudyCustodySeal;
5760
7678
  exports.isStudyCustodyTrial = isStudyCustodyTrial;
5761
7679
  exports.isStudyEvidence = isStudyEvidence;
7680
+ exports.isStudyExchangeBytes = isStudyExchangeBytes;
7681
+ exports.isStudyExchangeDeclaration = isStudyExchangeDeclaration;
7682
+ exports.isStudyExchangeDestination = isStudyExchangeDestination;
7683
+ exports.isStudyExchangeHistory = isStudyExchangeHistory;
7684
+ exports.isStudyExchangeIdentity = isStudyExchangeIdentity;
7685
+ exports.isStudyExchangeReply = isStudyExchangeReply;
7686
+ exports.isStudyExchangeRow = isStudyExchangeRow;
5762
7687
  exports.isStudyFrame = isStudyFrame;
5763
7688
  exports.isStudyIdentity = isStudyIdentity;
7689
+ exports.isStudyOllamaMetadata = isStudyOllamaMetadata;
7690
+ exports.isStudyOllamaModel = isStudyOllamaModel;
7691
+ exports.isStudyOllamaPolicy = isStudyOllamaPolicy;
7692
+ exports.isStudyOllamaStep = isStudyOllamaStep;
5764
7693
  exports.isStudyProcessFailure = isStudyProcessFailure;
7694
+ exports.isStudyReplaySelection = isStudyReplaySelection;
5765
7695
  exports.isStudyTrial = isStudyTrial;
5766
7696
  exports.iterateArtifactEntries = iterateArtifactEntries;
7697
+ exports.matchesArtifactCredential = matchesArtifactCredential;
5767
7698
  exports.matchesSystemError = matchesSystemError;
5768
7699
  exports.parseStudyArguments = parseStudyArguments;
7700
+ exports.projectStudyExchangeIdentity = projectStudyExchangeIdentity;
5769
7701
  exports.readArtifact = readArtifact;
5770
7702
  exports.readArtifactBytes = readArtifactBytes;
7703
+ exports.readArtifactPhysical = readArtifactPhysical;
5771
7704
  exports.readStudyAdapter = readStudyAdapter;
5772
7705
  exports.readStudyAgentMessages = readStudyAgentMessages;
5773
7706
  exports.readStudyAgentResult = readStudyAgentResult;
5774
7707
  exports.readStudyAgentToolCall = readStudyAgentToolCall;
5775
7708
  exports.readStudyBytes = readStudyBytes;
7709
+ exports.readStudyClosure = readStudyClosure;
5776
7710
  exports.readStudyDeclaration = readStudyDeclaration;
5777
7711
  exports.readStudyDeclarationBytes = readStudyDeclarationBytes;
7712
+ exports.readStudyExchangeRows = readStudyExchangeRows;
7713
+ exports.readStudyExchanges = readStudyExchanges;
5778
7714
  exports.readStudyHandle = readStudyHandle;
5779
7715
  exports.readStudyOllamaDigest = readStudyOllamaDigest;
5780
7716
  exports.readStudyOllamaFailure = readStudyOllamaFailure;
@@ -5782,9 +7718,17 @@ exports.readStudyOllamaRequest = readStudyOllamaRequest;
5782
7718
  exports.readStudyOllamaSupport = readStudyOllamaSupport;
5783
7719
  exports.readStudyOllamaTerminal = readStudyOllamaTerminal;
5784
7720
  exports.readStudyOllamaTiming = readStudyOllamaTiming;
7721
+ exports.readStudyPreparedDestination = readStudyPreparedDestination;
7722
+ exports.readStudyRecordingDeclaration = readStudyRecordingDeclaration;
7723
+ exports.readStudyReplayHistory = readStudyReplayHistory;
5785
7724
  exports.readStudyResult = readStudyResult;
7725
+ exports.readStudyTrialExchanges = readStudyTrialExchanges;
5786
7726
  exports.readStudyVersion = readStudyVersion;
7727
+ exports.relocateArtifactPath = relocateArtifactPath;
7728
+ exports.resolveArtifactBase = resolveArtifactBase;
7729
+ exports.resolveArtifactMember = resolveArtifactMember;
5787
7730
  exports.resolveStudyAgentContent = resolveStudyAgentContent;
7731
+ exports.resolveStudyClosurePath = resolveStudyClosurePath;
5788
7732
  exports.resolveStudyInstrument = resolveStudyInstrument;
5789
7733
  exports.resolveStudyPath = resolveStudyPath;
5790
7734
  exports.resolveStudyProfile = resolveStudyProfile;
@@ -5793,11 +7737,18 @@ exports.scanStudyTree = scanStudyTree;
5793
7737
  exports.terminateStudyChild = terminateStudyChild;
5794
7738
  exports.translateStudyPacketLabeling = translateStudyPacketLabeling;
5795
7739
  exports.validateStudyEvidenceBytes = validateStudyEvidenceBytes;
7740
+ exports.validateStudyExchangeEvidence = validateStudyExchangeEvidence;
7741
+ exports.validateStudyExchangeHistory = validateStudyExchangeHistory;
7742
+ exports.validateStudyExchangeRequest = validateStudyExchangeRequest;
7743
+ exports.validateStudyExchangeRows = validateStudyExchangeRows;
5796
7744
  exports.validateStudyOllamaCompletion = validateStudyOllamaCompletion;
5797
7745
  exports.validateStudyOllamaInventory = validateStudyOllamaInventory;
7746
+ exports.validateStudyOllamaRecording = validateStudyOllamaRecording;
5798
7747
  exports.validateStudyOllamaRoles = validateStudyOllamaRoles;
7748
+ exports.validateStudyOllamaStep = validateStudyOllamaStep;
5799
7749
  exports.validateStudyRuntime = validateStudyRuntime;
5800
7750
  exports.valueToWire = valueToWire;
7751
+ exports.verifyArtifactClosure = verifyArtifactClosure;
5801
7752
  exports.writeStudyBytes = writeStudyBytes;
5802
7753
 
5803
7754
  //# sourceMappingURL=index.cjs.map