@relayflows/sdk 2.0.31 → 2.0.33

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.
Files changed (95) hide show
  1. package/dist/authored-preflight.d.ts +4 -0
  2. package/dist/authored-preflight.d.ts.map +1 -0
  3. package/dist/authored-preflight.js +50 -0
  4. package/dist/authored-preflight.js.map +1 -0
  5. package/dist/authored-worker-step.d.ts.map +1 -1
  6. package/dist/authored-worker-step.js +3 -2
  7. package/dist/authored-worker-step.js.map +1 -1
  8. package/dist/cli/check.d.ts +10 -2
  9. package/dist/cli/check.d.ts.map +1 -1
  10. package/dist/cli/check.js +16 -137
  11. package/dist/cli/check.js.map +1 -1
  12. package/dist/cli/cli-probe.d.ts +20 -0
  13. package/dist/cli/cli-probe.d.ts.map +1 -0
  14. package/dist/cli/cli-probe.js +166 -0
  15. package/dist/cli/cli-probe.js.map +1 -0
  16. package/dist/cli/cloud-mirror-session.d.ts +97 -0
  17. package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
  18. package/dist/cli/cloud-mirror-session.js +264 -0
  19. package/dist/cli/cloud-mirror-session.js.map +1 -0
  20. package/dist/cli-commands.d.ts +6 -0
  21. package/dist/cli-commands.d.ts.map +1 -1
  22. package/dist/cli-commands.js +1 -0
  23. package/dist/cli-commands.js.map +1 -1
  24. package/dist/cli.d.ts +7 -0
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +84 -18
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloud-deploy.d.ts +1 -1
  29. package/dist/cloud-deploy.d.ts.map +1 -1
  30. package/dist/cloud-deploy.js +24 -8
  31. package/dist/cloud-deploy.js.map +1 -1
  32. package/dist/cloud-mirror-ledger.d.ts +40 -0
  33. package/dist/cloud-mirror-ledger.d.ts.map +1 -0
  34. package/dist/cloud-mirror-ledger.js +142 -0
  35. package/dist/cloud-mirror-ledger.js.map +1 -0
  36. package/dist/cloud-mirror-step.d.ts +180 -0
  37. package/dist/cloud-mirror-step.d.ts.map +1 -0
  38. package/dist/cloud-mirror-step.js +523 -0
  39. package/dist/cloud-mirror-step.js.map +1 -0
  40. package/dist/cloud-mirror-transport.d.ts +94 -0
  41. package/dist/cloud-mirror-transport.d.ts.map +1 -0
  42. package/dist/cloud-mirror-transport.js +163 -0
  43. package/dist/cloud-mirror-transport.js.map +1 -0
  44. package/dist/cloud-mirror.d.ts +132 -0
  45. package/dist/cloud-mirror.d.ts.map +1 -0
  46. package/dist/cloud-mirror.js +556 -0
  47. package/dist/cloud-mirror.js.map +1 -0
  48. package/dist/cloud-transcript-codex.d.ts.map +1 -1
  49. package/dist/cloud-transcript-codex.js +90 -4
  50. package/dist/cloud-transcript-codex.js.map +1 -1
  51. package/dist/cloud-transcript-types.d.ts +19 -1
  52. package/dist/cloud-transcript-types.d.ts.map +1 -1
  53. package/dist/cloud-transcript-types.js.map +1 -1
  54. package/dist/cloud-transcript.d.ts +1 -1
  55. package/dist/cloud-transcript.d.ts.map +1 -1
  56. package/dist/cloud-transcript.js +10 -0
  57. package/dist/cloud-transcript.js.map +1 -1
  58. package/dist/helper-reference.d.ts.map +1 -1
  59. package/dist/helper-reference.js +134 -6
  60. package/dist/helper-reference.js.map +1 -1
  61. package/dist/hosted-extension-runtime.js +1 -1
  62. package/dist/hosted-extension-sandbox.js +1 -1
  63. package/dist/package-version.d.ts +10 -0
  64. package/dist/package-version.d.ts.map +1 -0
  65. package/dist/package-version.js +22 -0
  66. package/dist/package-version.js.map +1 -0
  67. package/dist/preflight.d.ts +14 -0
  68. package/dist/preflight.d.ts.map +1 -1
  69. package/dist/preflight.js +18 -3
  70. package/dist/preflight.js.map +1 -1
  71. package/dist/relay-cli.d.ts.map +1 -1
  72. package/dist/relay-cli.js +1 -21
  73. package/dist/relay-cli.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/authored-preflight.ts +46 -0
  76. package/src/authored-worker-step.ts +4 -3
  77. package/src/cli/check.ts +25 -163
  78. package/src/cli/cli-probe.ts +194 -0
  79. package/src/cli/cloud-mirror-session.ts +329 -0
  80. package/src/cli-commands.ts +1 -0
  81. package/src/cli.ts +94 -19
  82. package/src/cloud-deploy.ts +26 -8
  83. package/src/cloud-mirror-ledger.ts +161 -0
  84. package/src/cloud-mirror-step.ts +650 -0
  85. package/src/cloud-mirror-transport.ts +261 -0
  86. package/src/cloud-mirror.ts +633 -0
  87. package/src/cloud-transcript-codex.ts +96 -4
  88. package/src/cloud-transcript-types.ts +18 -1
  89. package/src/cloud-transcript.ts +9 -1
  90. package/src/helper-reference.ts +131 -6
  91. package/src/hosted-extension-runtime.ts +1 -1
  92. package/src/hosted-extension-sandbox.ts +1 -1
  93. package/src/package-version.ts +23 -0
  94. package/src/preflight.ts +31 -7
  95. package/src/relay-cli.ts +1 -24
@@ -0,0 +1,161 @@
1
+ // Which Cloud run mirrored which journal, remembered between invocations.
2
+ //
3
+ // A journal outlives the process that started it: `flows run` parks on an
4
+ // `f.human`, exits, and some time later a different `flows resume` picks the
5
+ // same journal up. Cloud models that second attempt as its own run row — its
6
+ // own hosted resume mints a fresh run id too — so a parked-then-resumed flow
7
+ // is two rows however it is driven. What made that read as two *unrelated*
8
+ // pieces of work was that the resuming process had no idea the first attempt
9
+ // existed: nothing on disk connected the journal to the Cloud run mirroring
10
+ // it, so the second registration could not name its predecessor.
11
+ //
12
+ // This is that connection, and deliberately nothing more.
13
+ //
14
+ // ## What is stored, and what is not
15
+ //
16
+ // The run id and the deployment that issued it. **Never the credential.** The
17
+ // run token can write this run's steps and read the workspace's runs; leaving
18
+ // one in a dotfile under a developer's home directory, for every run they
19
+ // have ever started, would be a worse trade than the feature is worth. A
20
+ // resume mints its own credential the same way the first attempt did — the
21
+ // registration request it already makes.
22
+ //
23
+ // So an entry is not a capability. Everything in it is already in the URL the
24
+ // first attempt printed to the terminal.
25
+ //
26
+ // ## Failure is not an error
27
+ //
28
+ // Every operation here is best-effort and silent. A missing entry means the
29
+ // resumed run registers without naming a predecessor, which is exactly what
30
+ // happened before this file existed; an unreadable or corrupt one means the
31
+ // same. A mirror is an observer, and bookkeeping for an observer must never
32
+ // be able to fail a run.
33
+
34
+ import { mkdir, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises';
35
+ import { join, resolve } from 'node:path';
36
+
37
+ /** Where the ledger lives, beside the journals it describes. */
38
+ export const LEDGER_DIRECTORY = 'cloud-runs';
39
+ /**
40
+ * How long an entry is worth keeping.
41
+ *
42
+ * A resume of a two-week-old parked run is a real thing; a resume of a run
43
+ * from last quarter is not the case this serves, and a ledger that only grows
44
+ * is a directory nobody ever looks at filling up forever.
45
+ */
46
+ export const LEDGER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
47
+ /** Entries kept. Past it, the oldest are dropped on the next write. */
48
+ export const LEDGER_MAX_ENTRIES = 500;
49
+
50
+ /** One journal's mirror, as the ledger records it. */
51
+ export interface LedgerEntry {
52
+ /** Cloud's run id for the attempt that mirrored this journal. */
53
+ cloudRunId: string;
54
+ /** The deployment that issued it; an entry from another one is not this one's. */
55
+ apiUrl: string;
56
+ }
57
+
58
+ const JOURNAL_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
59
+ const CLOUD_RUN_ID = /^[A-Za-z0-9_-]{1,128}$/u;
60
+
61
+ function entryPath(dataDir: string, journalRunId: string): string {
62
+ return join(resolve(dataDir), LEDGER_DIRECTORY, `${journalRunId}.json`);
63
+ }
64
+
65
+ /**
66
+ * Record that `journalRunId` is mirrored by `entry.cloudRunId`.
67
+ *
68
+ * Written with mode 0600 for the same reason the journals are private: it
69
+ * names runs in someone's workspace, and a shared machine should not publish
70
+ * that. It is not a secret — there is nothing here to steal — but it is
71
+ * nobody else's business either.
72
+ */
73
+ export async function recordMirroredRun(
74
+ dataDir: string,
75
+ journalRunId: string,
76
+ entry: LedgerEntry,
77
+ ): Promise<void> {
78
+ if (!JOURNAL_RUN_ID.test(journalRunId) || !CLOUD_RUN_ID.test(entry.cloudRunId)) return;
79
+ const directory = join(resolve(dataDir), LEDGER_DIRECTORY);
80
+ try {
81
+ await mkdir(directory, { recursive: true, mode: 0o700 });
82
+ await writeFile(
83
+ entryPath(dataDir, journalRunId),
84
+ JSON.stringify({ cloudRunId: entry.cloudRunId, apiUrl: entry.apiUrl }),
85
+ { mode: 0o600 },
86
+ );
87
+ } catch {
88
+ // A resume that cannot name its predecessor is the behaviour this file
89
+ // improves on, not a failure of the run.
90
+ return;
91
+ }
92
+ await prune(directory);
93
+ }
94
+
95
+ /**
96
+ * The Cloud run mirroring this journal, if one is recorded *for this
97
+ * deployment*.
98
+ *
99
+ * The deployment check is not bookkeeping. A developer who ran a flow against
100
+ * staging and resumes it against production must not have the resumed run
101
+ * claim to continue a run id that means something else there — or, worse,
102
+ * nothing at all, which Cloud would refuse and which would read as a bug in
103
+ * the resume rather than in the pairing.
104
+ */
105
+ export async function readMirroredRun(
106
+ dataDir: string,
107
+ journalRunId: string,
108
+ apiUrl: string,
109
+ ): Promise<string | undefined> {
110
+ if (!JOURNAL_RUN_ID.test(journalRunId)) return undefined;
111
+ let parsed: unknown;
112
+ try {
113
+ parsed = JSON.parse(await readFile(entryPath(dataDir, journalRunId), 'utf8'));
114
+ } catch {
115
+ return undefined;
116
+ }
117
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
118
+ const entry = parsed as Record<string, unknown>;
119
+ if (typeof entry['cloudRunId'] !== 'string' || !CLOUD_RUN_ID.test(entry['cloudRunId'])) return undefined;
120
+ if (entry['apiUrl'] !== apiUrl) return undefined;
121
+ return entry['cloudRunId'];
122
+ }
123
+
124
+ /** Drop entries past their age, then past the count. Never throws. */
125
+ async function prune(directory: string): Promise<void> {
126
+ try {
127
+ const names = (await readdir(directory)).filter(name => name.endsWith('.json'));
128
+ if (names.length <= LEDGER_MAX_ENTRIES) {
129
+ // Still age out: a data dir that never reaches the count cap would
130
+ // otherwise keep its first entry forever.
131
+ await dropOlderThan(directory, names, Date.now() - LEDGER_MAX_AGE_MS);
132
+ return;
133
+ }
134
+ const ages = await Promise.all(names.map(async name => {
135
+ try {
136
+ return { name, at: (await stat(join(directory, name))).mtimeMs };
137
+ } catch {
138
+ return { name, at: 0 };
139
+ }
140
+ }));
141
+ ages.sort((left, right) => right.at - left.at);
142
+ await Promise.all(ages.slice(LEDGER_MAX_ENTRIES).map(entry =>
143
+ unlink(join(directory, entry.name)).catch(() => undefined)));
144
+ await dropOlderThan(directory, ages.slice(0, LEDGER_MAX_ENTRIES).map(entry => entry.name),
145
+ Date.now() - LEDGER_MAX_AGE_MS);
146
+ } catch {
147
+ return;
148
+ }
149
+ }
150
+
151
+ async function dropOlderThan(directory: string, names: string[], floor: number): Promise<void> {
152
+ await Promise.all(names.map(async name => {
153
+ try {
154
+ if ((await stat(join(directory, name))).mtimeMs < floor) {
155
+ await unlink(join(directory, name));
156
+ }
157
+ } catch {
158
+ return;
159
+ }
160
+ }));
161
+ }