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