@yaag/runtime 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/runtime",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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";
@@ -72,8 +73,14 @@ export async function publishRunCheckpoint(
72
73
  };
73
74
  }
74
75
  // A requested checkpoint silently going missing is worse than rejecting an
75
- // otherwise successful Run (ADR-0021).
76
- return { kind: "displaced", error };
76
+ // otherwise successful Run (ADR-0021). The two facts stay readable: the
77
+ // program completed, and the Cassette did not land.
78
+ return {
79
+ kind: "displaced",
80
+ error: new Error(`run completed but its Cassette could not be written: ${String(error)}`, {
81
+ cause: error,
82
+ }),
83
+ };
77
84
  }
78
85
  }
79
86
 
@@ -82,15 +89,40 @@ function keepsArtifact(options: RunCheckpointOptions): boolean {
82
89
  }
83
90
 
84
91
  /**
85
- * Creates the checkpoint directory a Run owns. An explicit `--record` path keeps
86
- * today's rule that its directory must already exist.
92
+ * Creates the directory the Run's artifact goes into. A `--record` path names
93
+ * where the artifact goes, so yaag creates the chain that path implies rather
94
+ * than refusing. ADR-0021 makes `--record` fix the destination path only, and a
95
+ * path yaag can make is a destination yaag makes. The checkpoint directory yaag owns stays
96
+ * private; a user-named `--record` directory takes the process umask, because it
97
+ * lives in the user's own tree.
87
98
  */
88
99
  export async function ensureCheckpointDirectory(
89
100
  record: string | undefined,
90
101
  destination: string,
91
102
  ): Promise<void> {
92
- if (record !== undefined) return;
93
- await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
103
+ const directory = dirname(destination);
104
+ if (record === undefined) {
105
+ await mkdir(directory, { recursive: true, mode: 0o700 });
106
+ return;
107
+ }
108
+ await mkdir(directory, { recursive: true });
109
+ }
110
+
111
+ /**
112
+ * Makes the directory chain of the `--record` path at Run start, and rejects the
113
+ * Run before the first spawn when that chain cannot exist. It does not prove the
114
+ * destination writable: an existing directory that refuses a write, or a name
115
+ * already taken, still fails later, at publication.
116
+ */
117
+ export async function prepareRecordDestination(record: string): Promise<void> {
118
+ try {
119
+ await ensureCheckpointDirectory(record, record);
120
+ } catch (error) {
121
+ throw new YaagError(
122
+ "RECORD_PATH_UNUSABLE",
123
+ `cannot create the Cassette directory ${dirname(record)}: ${String(error)}`,
124
+ );
125
+ }
94
126
  }
95
127
 
96
128
  /** 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 ?? (() => {});