@yaag/runtime 0.8.1 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/runtime",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -30,6 +30,7 @@ export { programIdentityWarning } from "./resume-identity.ts";
30
30
  export { resumeTransport } from "./resume-transport.ts";
31
31
  export {
32
32
  type ProgramIdentity,
33
+ prepareRecordDestination,
33
34
  publishRunCheckpoint,
34
35
  type RunCheckpointResult,
35
36
  resolveRunIdentity,
@@ -1,5 +1,6 @@
1
1
  import { mkdir, unlink } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
+ import { YaagError } from "../errors.ts";
3
4
  import type { RunOutcome } from "../events.ts";
4
5
  import type { CassetteCollector, CassetteRun } from "./cassette.ts";
5
6
  import { publishCassette } from "./cassette-publish.ts";
@@ -29,7 +30,8 @@ export interface RunCheckpointOptions {
29
30
  * What one publication attempt did. `published` covers both a written artifact
30
31
  * and a Run that keeps none. `displaced` carries the error that must become the
31
32
  * Run's outcome. `lost` reports a Checkpoint the Run asked for and did not get,
32
- * while a primary program error keeps the outcome.
33
+ * while the Run keeps its own outcome — a completed result, or a primary
34
+ * program error (ADR-0021 amendment).
33
35
  */
34
36
  export type RunCheckpointResult =
35
37
  | { readonly kind: "published" }
@@ -65,15 +67,24 @@ export async function publishRunCheckpoint(
65
67
  writeRecordingDiagnostic(error);
66
68
  return { kind: "lost", message: String(error) };
67
69
  }
68
- if (options.outcome === "stopped") {
69
- return {
70
- kind: "displaced",
71
- error: new Error(`run stopped but is not restorable: ${String(error)}`, { cause: error }),
72
- };
70
+ // A completed Run keeps its outcome and its result: the work is done, and
71
+ // no restorability promise is broken. The lost Cassette rides
72
+ // run_end.checkpointLost and the stderr diagnostic instead of displacing
73
+ // the outcome (ADR-0021 amendment).
74
+ if (options.outcome === "completed") {
75
+ const message = `run completed but its Cassette could not be written: ${String(error)}`;
76
+ writeRecordingDiagnostic(message);
77
+ return { kind: "lost", message };
73
78
  }
74
- // A requested checkpoint silently going missing is worse than rejecting an
75
- // otherwise successful Run (ADR-0021).
76
- return { kind: "displaced", error };
79
+ // A pausing or stopped Run promises restorability, and a Checkpoint that
80
+ // did not land makes that acknowledgement a lie, so the failure takes the
81
+ // outcome (ADR-0021: a flush failure is a hard failure there).
82
+ return {
83
+ kind: "displaced",
84
+ error: new Error(`run ${options.outcome} but is not restorable: ${String(error)}`, {
85
+ cause: error,
86
+ }),
87
+ };
77
88
  }
78
89
  }
79
90
 
@@ -82,15 +93,40 @@ function keepsArtifact(options: RunCheckpointOptions): boolean {
82
93
  }
83
94
 
84
95
  /**
85
- * Creates the checkpoint directory a Run owns. An explicit `--record` path keeps
86
- * today's rule that its directory must already exist.
96
+ * Creates the directory the Run's artifact goes into. A `--record` path names
97
+ * where the artifact goes, so yaag creates the chain that path implies rather
98
+ * than refusing. ADR-0021 makes `--record` fix the destination path only, and a
99
+ * path yaag can make is a destination yaag makes. The checkpoint directory yaag owns stays
100
+ * private; a user-named `--record` directory takes the process umask, because it
101
+ * lives in the user's own tree.
87
102
  */
88
103
  export async function ensureCheckpointDirectory(
89
104
  record: string | undefined,
90
105
  destination: string,
91
106
  ): Promise<void> {
92
- if (record !== undefined) return;
93
- await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
107
+ const directory = dirname(destination);
108
+ if (record === undefined) {
109
+ await mkdir(directory, { recursive: true, mode: 0o700 });
110
+ return;
111
+ }
112
+ await mkdir(directory, { recursive: true });
113
+ }
114
+
115
+ /**
116
+ * Makes the directory chain of the `--record` path at Run start, and rejects the
117
+ * Run before the first spawn when that chain cannot exist. It does not prove the
118
+ * destination writable: an existing directory that refuses a write, or a name
119
+ * already taken, still fails later, at publication.
120
+ */
121
+ export async function prepareRecordDestination(record: string): Promise<void> {
122
+ try {
123
+ await ensureCheckpointDirectory(record, record);
124
+ } catch (error) {
125
+ throw new YaagError(
126
+ "RECORD_PATH_UNUSABLE",
127
+ `cannot create the Cassette directory ${dirname(record)}: ${String(error)}`,
128
+ );
129
+ }
94
130
  }
95
131
 
96
132
  /** Which program a Run executes: a file on disk, or inline source text (ADR-0033). */
package/src/errors.ts CHANGED
@@ -48,6 +48,7 @@ export type YaagErrorCode =
48
48
  | "ARGS_INVALID" // arguments failed schema validation before the Run started
49
49
  | "CONFIG_INVALID" // a config layer is missing, unparsable, or violates the schema
50
50
  | "OPTIONS_CONFLICT" // incompatible Run options were supplied
51
+ | "RECORD_PATH_UNUSABLE" // the --record directory could not be created at Run start
51
52
  | "REPLAY_DIVERGED" // replayed program differed from its Cassette
52
53
  | "RESUME_REFUSED" // resume metadata is absent or the recorded tree moved
53
54
  | "RUN_CLOSED" // spawn was requested after the Run began settling
package/src/run/run.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  interruptedResumeWarning,
15
15
  loadCassette,
16
16
  type ProgramIdentity,
17
+ prepareRecordDestination,
17
18
  programIdentityWarning,
18
19
  publishRunCheckpoint,
19
20
  type RunCheckpointResult,
@@ -133,6 +134,9 @@ export async function executeRun<Args, Result>(
133
134
  await publishRunCheckpoint({ outcome: "failed", ...checkpoint });
134
135
  throw error;
135
136
  }
137
+ // The directory of the --record path is made before any Agent starts, so a Run
138
+ // whose Cassette can never land does not do its work first and fail last.
139
+ if (options.record !== undefined) await prepareRecordDestination(options.record);
136
140
 
137
141
  let summary: RunSummary = initialSummary();
138
142
  const sink: StampedEventSink = options.events ?? (() => {});