@elaraai/e3-core 0.0.2-beta.4 → 0.0.2-beta.41

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 (208) hide show
  1. package/README.md +25 -22
  2. package/dist/src/dataflow/api-compat.d.ts +90 -0
  3. package/dist/src/dataflow/api-compat.d.ts.map +1 -0
  4. package/dist/src/dataflow/api-compat.js +139 -0
  5. package/dist/src/dataflow/api-compat.js.map +1 -0
  6. package/dist/src/dataflow/index.d.ts +18 -0
  7. package/dist/src/dataflow/index.d.ts.map +1 -0
  8. package/dist/src/dataflow/index.js +23 -0
  9. package/dist/src/dataflow/index.js.map +1 -0
  10. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts +76 -0
  11. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts.map +1 -0
  12. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js +695 -0
  13. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js.map +1 -0
  14. package/dist/src/dataflow/orchestrator/index.d.ts +12 -0
  15. package/dist/src/dataflow/orchestrator/index.d.ts.map +1 -0
  16. package/dist/src/dataflow/orchestrator/index.js +12 -0
  17. package/dist/src/dataflow/orchestrator/index.js.map +1 -0
  18. package/dist/src/dataflow/orchestrator/interfaces.d.ts +163 -0
  19. package/dist/src/dataflow/orchestrator/interfaces.d.ts.map +1 -0
  20. package/dist/src/dataflow/orchestrator/interfaces.js +52 -0
  21. package/dist/src/dataflow/orchestrator/interfaces.js.map +1 -0
  22. package/dist/src/dataflow/state-store/FileStateStore.d.ts +67 -0
  23. package/dist/src/dataflow/state-store/FileStateStore.d.ts.map +1 -0
  24. package/dist/src/dataflow/state-store/FileStateStore.js +300 -0
  25. package/dist/src/dataflow/state-store/FileStateStore.js.map +1 -0
  26. package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts +42 -0
  27. package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts.map +1 -0
  28. package/dist/src/dataflow/state-store/InMemoryStateStore.js +229 -0
  29. package/dist/src/dataflow/state-store/InMemoryStateStore.js.map +1 -0
  30. package/dist/src/dataflow/state-store/index.d.ts +13 -0
  31. package/dist/src/dataflow/state-store/index.d.ts.map +1 -0
  32. package/dist/src/dataflow/state-store/index.js +13 -0
  33. package/dist/src/dataflow/state-store/index.js.map +1 -0
  34. package/dist/src/dataflow/state-store/interfaces.d.ts +159 -0
  35. package/dist/src/dataflow/state-store/interfaces.d.ts.map +1 -0
  36. package/dist/src/dataflow/state-store/interfaces.js +6 -0
  37. package/dist/src/dataflow/state-store/interfaces.js.map +1 -0
  38. package/dist/src/dataflow/steps.d.ts +222 -0
  39. package/dist/src/dataflow/steps.d.ts.map +1 -0
  40. package/dist/src/dataflow/steps.js +707 -0
  41. package/dist/src/dataflow/steps.js.map +1 -0
  42. package/dist/src/dataflow/types.d.ts +127 -0
  43. package/dist/src/dataflow/types.d.ts.map +1 -0
  44. package/dist/src/dataflow/types.js +7 -0
  45. package/dist/src/dataflow/types.js.map +1 -0
  46. package/dist/src/dataflow.d.ts +113 -38
  47. package/dist/src/dataflow.d.ts.map +1 -1
  48. package/dist/src/dataflow.js +269 -416
  49. package/dist/src/dataflow.js.map +1 -1
  50. package/dist/src/dataset-refs.d.ts +124 -0
  51. package/dist/src/dataset-refs.d.ts.map +1 -0
  52. package/dist/src/dataset-refs.js +319 -0
  53. package/dist/src/dataset-refs.js.map +1 -0
  54. package/dist/src/errors.d.ts +39 -9
  55. package/dist/src/errors.d.ts.map +1 -1
  56. package/dist/src/errors.js +51 -8
  57. package/dist/src/errors.js.map +1 -1
  58. package/dist/src/execution/LocalTaskRunner.d.ts +73 -0
  59. package/dist/src/execution/LocalTaskRunner.d.ts.map +1 -0
  60. package/dist/src/execution/LocalTaskRunner.js +399 -0
  61. package/dist/src/execution/LocalTaskRunner.js.map +1 -0
  62. package/dist/src/execution/MockTaskRunner.d.ts +49 -0
  63. package/dist/src/execution/MockTaskRunner.d.ts.map +1 -0
  64. package/dist/src/execution/MockTaskRunner.js +54 -0
  65. package/dist/src/execution/MockTaskRunner.js.map +1 -0
  66. package/dist/src/execution/index.d.ts +16 -0
  67. package/dist/src/execution/index.d.ts.map +1 -0
  68. package/dist/src/execution/index.js +8 -0
  69. package/dist/src/execution/index.js.map +1 -0
  70. package/dist/src/execution/interfaces.d.ts +246 -0
  71. package/dist/src/execution/interfaces.d.ts.map +1 -0
  72. package/dist/src/execution/interfaces.js +6 -0
  73. package/dist/src/execution/interfaces.js.map +1 -0
  74. package/dist/src/execution/processHelpers.d.ts +20 -0
  75. package/dist/src/execution/processHelpers.d.ts.map +1 -0
  76. package/dist/src/execution/processHelpers.js +62 -0
  77. package/dist/src/execution/processHelpers.js.map +1 -0
  78. package/dist/src/executions.d.ts +71 -104
  79. package/dist/src/executions.d.ts.map +1 -1
  80. package/dist/src/executions.js +110 -476
  81. package/dist/src/executions.js.map +1 -1
  82. package/dist/src/index.d.ts +19 -9
  83. package/dist/src/index.d.ts.map +1 -1
  84. package/dist/src/index.js +48 -18
  85. package/dist/src/index.js.map +1 -1
  86. package/dist/src/objects.d.ts +8 -51
  87. package/dist/src/objects.d.ts.map +1 -1
  88. package/dist/src/objects.js +13 -230
  89. package/dist/src/objects.js.map +1 -1
  90. package/dist/src/packages.d.ts +22 -14
  91. package/dist/src/packages.d.ts.map +1 -1
  92. package/dist/src/packages.js +134 -88
  93. package/dist/src/packages.js.map +1 -1
  94. package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts +35 -0
  95. package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts.map +1 -0
  96. package/dist/src/storage/in-memory/InMemoryRepoStore.js +107 -0
  97. package/dist/src/storage/in-memory/InMemoryRepoStore.js.map +1 -0
  98. package/dist/src/storage/in-memory/InMemoryStorage.d.ts +139 -0
  99. package/dist/src/storage/in-memory/InMemoryStorage.d.ts.map +1 -0
  100. package/dist/src/storage/in-memory/InMemoryStorage.js +439 -0
  101. package/dist/src/storage/in-memory/InMemoryStorage.js.map +1 -0
  102. package/dist/src/storage/in-memory/index.d.ts +12 -0
  103. package/dist/src/storage/in-memory/index.d.ts.map +1 -0
  104. package/dist/src/storage/in-memory/index.js +12 -0
  105. package/dist/src/storage/in-memory/index.js.map +1 -0
  106. package/dist/src/storage/index.d.ts +18 -0
  107. package/dist/src/storage/index.d.ts.map +1 -0
  108. package/dist/src/storage/index.js +10 -0
  109. package/dist/src/storage/index.js.map +1 -0
  110. package/dist/src/storage/interfaces.d.ts +581 -0
  111. package/dist/src/storage/interfaces.d.ts.map +1 -0
  112. package/dist/src/storage/interfaces.js +6 -0
  113. package/dist/src/storage/interfaces.js.map +1 -0
  114. package/dist/src/storage/local/LocalBackend.d.ts +56 -0
  115. package/dist/src/storage/local/LocalBackend.d.ts.map +1 -0
  116. package/dist/src/storage/local/LocalBackend.js +145 -0
  117. package/dist/src/storage/local/LocalBackend.js.map +1 -0
  118. package/dist/src/storage/local/LocalDatasetRefStore.d.ts +22 -0
  119. package/dist/src/storage/local/LocalDatasetRefStore.d.ts.map +1 -0
  120. package/dist/src/storage/local/LocalDatasetRefStore.js +118 -0
  121. package/dist/src/storage/local/LocalDatasetRefStore.js.map +1 -0
  122. package/dist/src/storage/local/LocalLockService.d.ts +111 -0
  123. package/dist/src/storage/local/LocalLockService.d.ts.map +1 -0
  124. package/dist/src/storage/local/LocalLockService.js +355 -0
  125. package/dist/src/storage/local/LocalLockService.js.map +1 -0
  126. package/dist/src/storage/local/LocalLogStore.d.ts +23 -0
  127. package/dist/src/storage/local/LocalLogStore.d.ts.map +1 -0
  128. package/dist/src/storage/local/LocalLogStore.js +66 -0
  129. package/dist/src/storage/local/LocalLogStore.js.map +1 -0
  130. package/dist/src/storage/local/LocalObjectStore.d.ts +55 -0
  131. package/dist/src/storage/local/LocalObjectStore.d.ts.map +1 -0
  132. package/dist/src/storage/local/LocalObjectStore.js +300 -0
  133. package/dist/src/storage/local/LocalObjectStore.js.map +1 -0
  134. package/dist/src/storage/local/LocalRefStore.d.ts +50 -0
  135. package/dist/src/storage/local/LocalRefStore.d.ts.map +1 -0
  136. package/dist/src/storage/local/LocalRefStore.js +337 -0
  137. package/dist/src/storage/local/LocalRefStore.js.map +1 -0
  138. package/dist/src/storage/local/LocalRepoStore.d.ts +55 -0
  139. package/dist/src/storage/local/LocalRepoStore.d.ts.map +1 -0
  140. package/dist/src/storage/local/LocalRepoStore.js +365 -0
  141. package/dist/src/storage/local/LocalRepoStore.js.map +1 -0
  142. package/dist/src/storage/local/gc.d.ts +92 -0
  143. package/dist/src/storage/local/gc.d.ts.map +1 -0
  144. package/dist/src/storage/local/gc.js +377 -0
  145. package/dist/src/storage/local/gc.js.map +1 -0
  146. package/dist/src/storage/local/index.d.ts +18 -0
  147. package/dist/src/storage/local/index.d.ts.map +1 -0
  148. package/dist/src/storage/local/index.js +18 -0
  149. package/dist/src/storage/local/index.js.map +1 -0
  150. package/dist/src/storage/local/localHelpers.d.ts +25 -0
  151. package/dist/src/storage/local/localHelpers.d.ts.map +1 -0
  152. package/dist/src/storage/local/localHelpers.js +69 -0
  153. package/dist/src/storage/local/localHelpers.js.map +1 -0
  154. package/dist/src/{repository.d.ts → storage/local/repository.d.ts} +8 -4
  155. package/dist/src/storage/local/repository.d.ts.map +1 -0
  156. package/dist/src/{repository.js → storage/local/repository.js} +31 -29
  157. package/dist/src/storage/local/repository.js.map +1 -0
  158. package/dist/src/tasks.d.ts +16 -10
  159. package/dist/src/tasks.d.ts.map +1 -1
  160. package/dist/src/tasks.js +35 -41
  161. package/dist/src/tasks.js.map +1 -1
  162. package/dist/src/test-helpers.d.ts +5 -4
  163. package/dist/src/test-helpers.d.ts.map +1 -1
  164. package/dist/src/test-helpers.js +9 -21
  165. package/dist/src/test-helpers.js.map +1 -1
  166. package/dist/src/transfer/InMemoryTransferBackend.d.ts +66 -0
  167. package/dist/src/transfer/InMemoryTransferBackend.d.ts.map +1 -0
  168. package/dist/src/transfer/InMemoryTransferBackend.js +166 -0
  169. package/dist/src/transfer/InMemoryTransferBackend.js.map +1 -0
  170. package/dist/src/transfer/index.d.ts +8 -0
  171. package/dist/src/transfer/index.d.ts.map +1 -0
  172. package/dist/src/transfer/index.js +9 -0
  173. package/dist/src/transfer/index.js.map +1 -0
  174. package/dist/src/transfer/interfaces.d.ts +103 -0
  175. package/dist/src/transfer/interfaces.d.ts.map +1 -0
  176. package/dist/src/transfer/interfaces.js +6 -0
  177. package/dist/src/transfer/interfaces.js.map +1 -0
  178. package/dist/src/transfer/types.d.ts +79 -0
  179. package/dist/src/transfer/types.d.ts.map +1 -0
  180. package/dist/src/transfer/types.js +58 -0
  181. package/dist/src/transfer/types.js.map +1 -0
  182. package/dist/src/trees.d.ts +147 -59
  183. package/dist/src/trees.d.ts.map +1 -1
  184. package/dist/src/trees.js +372 -419
  185. package/dist/src/trees.js.map +1 -1
  186. package/dist/src/uuid.d.ts +26 -0
  187. package/dist/src/uuid.d.ts.map +1 -0
  188. package/dist/src/uuid.js +80 -0
  189. package/dist/src/uuid.js.map +1 -0
  190. package/dist/src/workspaceStatus.d.ts +6 -4
  191. package/dist/src/workspaceStatus.d.ts.map +1 -1
  192. package/dist/src/workspaceStatus.js +43 -49
  193. package/dist/src/workspaceStatus.js.map +1 -1
  194. package/dist/src/workspaces.d.ts +35 -47
  195. package/dist/src/workspaces.d.ts.map +1 -1
  196. package/dist/src/workspaces.js +194 -156
  197. package/dist/src/workspaces.js.map +1 -1
  198. package/package.json +4 -4
  199. package/dist/src/gc.d.ts +0 -54
  200. package/dist/src/gc.d.ts.map +0 -1
  201. package/dist/src/gc.js +0 -233
  202. package/dist/src/gc.js.map +0 -1
  203. package/dist/src/repository.d.ts.map +0 -1
  204. package/dist/src/repository.js.map +0 -1
  205. package/dist/src/workspaceLock.d.ts +0 -67
  206. package/dist/src/workspaceLock.d.ts.map +0 -1
  207. package/dist/src/workspaceLock.js +0 -217
  208. package/dist/src/workspaceLock.js.map +0 -1
@@ -3,23 +3,18 @@
3
3
  * Licensed under BSL 1.1. See LICENSE for details.
4
4
  */
5
5
  /**
6
- * Task execution for e3 repositories.
6
+ * Generic task execution APIs for e3 repositories.
7
7
  *
8
- * Provides APIs for:
8
+ * Provides storage-agnostic APIs for:
9
9
  * - Computing execution identity (inputsHash)
10
10
  * - Querying execution status and results
11
11
  * - Reading execution logs
12
- * - Running tasks with caching
12
+ * - Evaluating command IR
13
+ *
14
+ * Note: Local process execution is in execution/LocalTaskRunner.ts
13
15
  */
14
- import * as fs from 'fs/promises';
15
- import * as path from 'path';
16
- import { spawn } from 'child_process';
17
- import { createWriteStream } from 'fs';
18
- import { tmpdir } from 'os';
19
- import { decodeBeast2For, encodeBeast2For, variant, EastIR, IRType } from '@elaraai/east';
20
- import { ExecutionStatusType, TaskObjectType, } from '@elaraai/e3-types';
21
- import { computeHash, objectRead, objectWrite } from './objects.js';
22
- import { ExecutionCorruptError, isNotFoundError } from './errors.js';
16
+ import { decodeBeast2For, EastIR, IRType } from '@elaraai/east';
17
+ import { computeHash } from './objects.js';
23
18
  // ============================================================================
24
19
  // Execution Identity
25
20
  // ============================================================================
@@ -36,166 +31,149 @@ export function inputsHash(inputHashes) {
36
31
  const data = inputHashes.join('\0');
37
32
  return computeHash(new TextEncoder().encode(data));
38
33
  }
34
+ // ============================================================================
35
+ // Execution Status
36
+ // ============================================================================
39
37
  /**
40
- * Get the filesystem path for an execution directory.
38
+ * Get execution status for a specific execution.
41
39
  *
42
- * @param repoPath - Path to .e3 repository
40
+ * @param storage - Storage backend
41
+ * @param repo - Repository identifier (for local storage, the path to e3 repository directory)
43
42
  * @param taskHash - Hash of the task object
44
43
  * @param inHash - Combined hash of input hashes
45
- * @returns Path to execution directory: executions/<taskHash>/<inputsHash>/
44
+ * @param executionId - Execution ID (UUIDv7)
45
+ * @returns ExecutionStatus or null if execution doesn't exist
46
+ * @throws {ExecutionCorruptError} If status file exists but cannot be decoded
46
47
  */
47
- export function executionPath(repoPath, taskHash, inHash) {
48
- return path.join(repoPath, 'executions', taskHash, inHash);
48
+ export async function executionGet(storage, repo, taskHash, inHash, executionId) {
49
+ return storage.refs.executionGet(repo, taskHash, inHash, executionId);
49
50
  }
50
- // ============================================================================
51
- // Execution Status
52
- // ============================================================================
53
51
  /**
54
- * Get execution status.
52
+ * Get the latest execution status (lexicographically greatest executionId).
55
53
  *
56
- * @param repoPath - Path to .e3 repository
54
+ * @param storage - Storage backend
55
+ * @param repo - Repository identifier (for local storage, the path to e3 repository directory)
57
56
  * @param taskHash - Hash of the task object
58
57
  * @param inHash - Combined hash of input hashes
59
- * @returns ExecutionStatus or null if execution doesn't exist
60
- * @throws {ExecutionCorruptError} If status file exists but cannot be decoded
58
+ * @returns ExecutionStatus or null if no executions exist
61
59
  */
62
- export async function executionGet(repoPath, taskHash, inHash) {
63
- const execDir = executionPath(repoPath, taskHash, inHash);
64
- const statusPath = path.join(execDir, 'status.beast2');
65
- let data;
66
- try {
67
- data = await fs.readFile(statusPath);
68
- }
69
- catch (err) {
70
- if (isNotFoundError(err)) {
71
- return null;
72
- }
73
- throw err;
74
- }
75
- try {
76
- const decoder = decodeBeast2For(ExecutionStatusType);
77
- return decoder(data);
78
- }
79
- catch (err) {
80
- throw new ExecutionCorruptError(taskHash, inHash, err instanceof Error ? err : new Error(String(err)));
81
- }
60
+ export async function executionGetLatest(storage, repo, taskHash, inHash) {
61
+ return storage.refs.executionGetLatest(repo, taskHash, inHash);
82
62
  }
83
63
  /**
84
- * Get output hash for a completed execution.
64
+ * Get the latest successful output hash for a completed execution.
65
+ * This is the primary cache lookup function.
85
66
  *
86
- * @param repoPath - Path to .e3 repository
67
+ * @param storage - Storage backend
68
+ * @param repo - Repository identifier (for local storage, the path to e3 repository directory)
87
69
  * @param taskHash - Hash of the task object
88
70
  * @param inHash - Combined hash of input hashes
89
- * @returns Output hash or null if not complete or failed
71
+ * @returns Output hash or null if no successful execution exists
90
72
  */
91
- export async function executionGetOutput(repoPath, taskHash, inHash) {
92
- const execDir = executionPath(repoPath, taskHash, inHash);
93
- const outputPath = path.join(execDir, 'output');
94
- try {
95
- const content = await fs.readFile(outputPath, 'utf-8');
96
- return content.trim();
97
- }
98
- catch (err) {
99
- if (isNotFoundError(err)) {
100
- return null;
101
- }
102
- throw err;
103
- }
73
+ export async function executionGetOutput(storage, repo, taskHash, inHash) {
74
+ return storage.refs.executionGetLatestOutput(repo, taskHash, inHash);
75
+ }
76
+ /**
77
+ * List all execution IDs for a (taskHash, inputsHash) pair.
78
+ *
79
+ * @param storage - Storage backend
80
+ * @param repo - Repository identifier (for local storage, the path to e3 repository directory)
81
+ * @param taskHash - Hash of the task object
82
+ * @param inHash - Combined hash of input hashes
83
+ * @returns Array of execution IDs (sorted lexicographically ascending)
84
+ */
85
+ export async function executionListIds(storage, repo, taskHash, inHash) {
86
+ return storage.refs.executionListIds(repo, taskHash, inHash);
104
87
  }
105
88
  /**
106
89
  * List all input hashes that have executions for a given task.
107
90
  *
108
- * @param repoPath - Path to .e3 repository
91
+ * @param storage - Storage backend
92
+ * @param repo - Repository identifier (for local storage, the path to e3 repository directory)
109
93
  * @param taskHash - Hash of the task object
110
94
  * @returns Array of input hashes
111
95
  */
112
- export async function executionListForTask(repoPath, taskHash) {
113
- const taskDir = path.join(repoPath, 'executions', taskHash);
114
- try {
115
- const entries = await fs.readdir(taskDir);
116
- // Filter to only valid hash directories (64 hex chars)
117
- return entries.filter((e) => /^[a-f0-9]{64}$/.test(e));
118
- }
119
- catch {
120
- return [];
121
- }
96
+ export async function executionListForTask(storage, repo, taskHash) {
97
+ return storage.refs.executionListForTask(repo, taskHash);
122
98
  }
123
99
  /**
124
100
  * List all executions in the repository.
125
101
  *
126
- * @param repoPath - Path to .e3 repository
102
+ * @param storage - Storage backend
103
+ * @param repo - Repository identifier (for local storage, the path to e3 repository directory)
127
104
  * @returns Array of { taskHash, inputsHash } objects
128
105
  */
129
- export async function executionList(repoPath) {
130
- const executionsDir = path.join(repoPath, 'executions');
131
- const result = [];
132
- try {
133
- const taskDirs = await fs.readdir(executionsDir);
134
- for (const taskHash of taskDirs) {
135
- if (!/^[a-f0-9]{64}$/.test(taskHash))
136
- continue;
137
- const taskDir = path.join(executionsDir, taskHash);
138
- const stat = await fs.stat(taskDir);
139
- if (!stat.isDirectory())
140
- continue;
141
- const inputsDirs = await fs.readdir(taskDir);
142
- for (const inputsHash of inputsDirs) {
143
- if (/^[a-f0-9]{64}$/.test(inputsHash)) {
144
- result.push({ taskHash, inputsHash });
145
- }
106
+ export async function executionList(storage, repo) {
107
+ return storage.refs.executionList(repo);
108
+ }
109
+ /**
110
+ * Find the execution reference for a task in a workspace.
111
+ *
112
+ * This looks up the task's current input hashes from the workspace state
113
+ * and finds the matching execution. If no execution exists for the current
114
+ * inputs, falls back to the most recent execution.
115
+ *
116
+ * @param storage - Storage backend
117
+ * @param repo - Repository identifier (for local storage, the path to e3 repository directory)
118
+ * @param ws - Workspace name
119
+ * @param taskName - Task name
120
+ * @returns Execution reference or null if no executions exist
121
+ */
122
+ export async function executionFindCurrent(storage, repo, ws, taskName) {
123
+ // Import here to avoid circular dependency
124
+ const { workspaceGetTaskHash, workspaceGetTask } = await import('./tasks.js');
125
+ const { workspaceGetDatasetHash } = await import('./trees.js');
126
+ const taskHash = await workspaceGetTaskHash(storage, repo, ws, taskName);
127
+ const task = await workspaceGetTask(storage, repo, ws, taskName);
128
+ // Get the current input hashes from the workspace
129
+ const currentInputHashes = [];
130
+ let allInputsAssigned = true;
131
+ for (const inputPath of task.inputs) {
132
+ const { refType, hash } = await workspaceGetDatasetHash(storage, repo, ws, inputPath);
133
+ if (refType !== 'value' || hash === null) {
134
+ allInputsAssigned = false;
135
+ break;
136
+ }
137
+ currentInputHashes.push(hash);
138
+ }
139
+ const executions = await executionListForTask(storage, repo, taskHash);
140
+ if (allInputsAssigned) {
141
+ const inHash = inputsHash(currentInputHashes);
142
+ if (executions.includes(inHash)) {
143
+ // Get the latest execution status to get the executionId
144
+ const status = await storage.refs.executionGetLatest(repo, taskHash, inHash);
145
+ if (status) {
146
+ // Extract executionId from the status (all variants have it)
147
+ const executionId = status.value.executionId;
148
+ return { taskHash, inputsHash: inHash, executionId, isCurrent: true };
146
149
  }
147
150
  }
148
151
  }
149
- catch {
150
- // Executions directory doesn't exist
152
+ // Fall back to most recent execution
153
+ if (executions.length > 0) {
154
+ const inHash = executions[0];
155
+ const status = await storage.refs.executionGetLatest(repo, taskHash, inHash);
156
+ if (status) {
157
+ const executionId = status.value.executionId;
158
+ return { taskHash, inputsHash: inHash, executionId, isCurrent: false };
159
+ }
151
160
  }
152
- return result;
161
+ return null;
153
162
  }
154
163
  /**
155
164
  * Read execution logs with pagination support.
156
165
  *
157
- * @param repoPath - Path to .e3 repository
166
+ * @param storage - Storage backend
167
+ * @param repo - Repository identifier (for local storage, the path to e3 repository directory)
158
168
  * @param taskHash - Hash of the task object
159
169
  * @param inHash - Combined hash of input hashes
170
+ * @param executionId - Execution ID (UUIDv7)
160
171
  * @param stream - Which log stream to read ('stdout' or 'stderr')
161
172
  * @param options - Pagination options
162
173
  * @returns Log chunk with data and metadata
163
174
  */
164
- export async function executionReadLog(repoPath, taskHash, inHash, stream, options = {}) {
165
- const execDir = executionPath(repoPath, taskHash, inHash);
166
- const logPath = path.join(execDir, `${stream}.txt`);
167
- const offset = options.offset ?? 0;
168
- const limit = options.limit ?? 65536; // 64KB default
169
- try {
170
- const stat = await fs.stat(logPath);
171
- const totalSize = stat.size;
172
- // Open file and read chunk
173
- const fd = await fs.open(logPath, 'r');
174
- try {
175
- const buffer = Buffer.alloc(Math.min(limit, totalSize - offset));
176
- const { bytesRead } = await fd.read(buffer, 0, buffer.length, offset);
177
- return {
178
- data: buffer.slice(0, bytesRead).toString('utf-8'),
179
- offset,
180
- size: bytesRead,
181
- totalSize,
182
- complete: offset + bytesRead >= totalSize,
183
- };
184
- }
185
- finally {
186
- await fd.close();
187
- }
188
- }
189
- catch {
190
- // Log file doesn't exist yet
191
- return {
192
- data: '',
193
- offset: 0,
194
- size: 0,
195
- totalSize: 0,
196
- complete: true,
197
- };
198
- }
175
+ export async function executionReadLog(storage, repo, taskHash, inHash, executionId, stream, options = {}) {
176
+ return storage.logs.read(repo, taskHash, inHash, executionId, stream, options);
199
177
  }
200
178
  // ============================================================================
201
179
  // Command IR Evaluation
@@ -205,14 +183,15 @@ export async function executionReadLog(repoPath, taskHash, inHash, stream, optio
205
183
  *
206
184
  * The IR is an East function: (inputs: Array<String>, output: String) -> Array<String>
207
185
  *
208
- * @param repoPath - Path to .e3 repository
186
+ * @param storage - Storage backend
187
+ * @param repo - Repository identifier (for local storage, the path to e3 repository directory)
209
188
  * @param commandIrHash - Hash of the IR object
210
189
  * @param inputPaths - Paths to staged input files
211
190
  * @param outputPath - Path where output should be written
212
191
  * @returns Array of strings to exec
213
192
  */
214
- export async function evaluateCommandIr(repoPath, commandIrHash, inputPaths, outputPath) {
215
- const irData = await objectRead(repoPath, commandIrHash);
193
+ export async function evaluateCommandIr(storage, repo, commandIrHash, inputPaths, outputPath) {
194
+ const irData = await storage.objects.read(repo, commandIrHash);
216
195
  try {
217
196
  // Decode the IR from beast2 format
218
197
  const decoder = decodeBeast2For(IRType);
@@ -237,349 +216,4 @@ export async function evaluateCommandIr(repoPath, commandIrHash, inputPaths, out
237
216
  throw new Error(`Failed to evaluate command IR: ${err}`);
238
217
  }
239
218
  }
240
- // ============================================================================
241
- // Process Identification (for crash detection)
242
- // ============================================================================
243
- /**
244
- * Get the current system boot ID.
245
- * Used for detecting stale locks/processes after system reboot.
246
- */
247
- export async function getBootId() {
248
- try {
249
- const data = await fs.readFile('/proc/sys/kernel/random/boot_id', 'utf-8');
250
- return data.trim();
251
- }
252
- catch {
253
- // Not on Linux, use a placeholder
254
- return 'unknown-boot-id';
255
- }
256
- }
257
- /**
258
- * Get process start time from /proc/<pid>/stat.
259
- * Returns the starttime field (field 22) which is jiffies since boot.
260
- * Used together with boot ID to uniquely identify a process (handles PID reuse).
261
- */
262
- export async function getPidStartTime(pid) {
263
- try {
264
- const data = await fs.readFile(`/proc/${pid}/stat`, 'utf-8');
265
- // Fields are space-separated, but comm (field 2) can contain spaces and is in parens
266
- // Find the closing paren, then split the rest
267
- const closeParen = data.lastIndexOf(')');
268
- const fields = data.slice(closeParen + 2).split(' ');
269
- // After the closing paren, field index 0 is state (field 3), so starttime is at index 19
270
- // (field 22 - 3 = 19)
271
- return parseInt(fields[19], 10);
272
- }
273
- catch {
274
- return 0;
275
- }
276
- }
277
- /**
278
- * Check if a process is still alive based on stored identification
279
- */
280
- export async function isProcessAlive(pid, pidStartTime, bootId) {
281
- // Different boot? Process is dead
282
- const currentBootId = await getBootId();
283
- if (currentBootId !== bootId)
284
- return false;
285
- // Check if PID exists and has same start time
286
- const currentStartTime = await getPidStartTime(pid);
287
- if (currentStartTime === 0)
288
- return false; // PID doesn't exist
289
- if (currentStartTime !== pidStartTime)
290
- return false; // PID reused
291
- return true;
292
- }
293
- /**
294
- * Execute a single task.
295
- *
296
- * This is the core execution primitive. It:
297
- * 1. Computes the execution identity from task + inputs
298
- * 2. Checks cache (unless force=true)
299
- * 3. Marshals inputs to a scratch directory
300
- * 4. Evaluates command IR to get exec args
301
- * 5. Runs the command
302
- * 6. Stores the output and updates status
303
- *
304
- * @param repoPath - Path to .e3 repository
305
- * @param taskHash - Hash of the task object
306
- * @param inputHashes - Array of input dataset hashes
307
- * @param options - Execution options
308
- * @returns Execution result
309
- */
310
- export async function taskExecute(repoPath, taskHash, inputHashes, options = {}) {
311
- const inHash = inputsHash(inputHashes);
312
- const execDir = executionPath(repoPath, taskHash, inHash);
313
- const startTime = Date.now();
314
- // Step 1: Check cache (unless force)
315
- if (!options.force) {
316
- const existingOutput = await executionGetOutput(repoPath, taskHash, inHash);
317
- if (existingOutput !== null) {
318
- const status = await executionGet(repoPath, taskHash, inHash);
319
- if (status && status.type === 'success') {
320
- return {
321
- inputsHash: inHash,
322
- cached: true,
323
- state: 'success',
324
- outputHash: existingOutput,
325
- exitCode: 0,
326
- duration: 0,
327
- error: null,
328
- };
329
- }
330
- }
331
- }
332
- // Step 2: Read task object
333
- let task;
334
- try {
335
- const taskData = await objectRead(repoPath, taskHash);
336
- const decoder = decodeBeast2For(TaskObjectType);
337
- task = decoder(Buffer.from(taskData));
338
- }
339
- catch (err) {
340
- return {
341
- inputsHash: inHash,
342
- cached: false,
343
- state: 'error',
344
- outputHash: null,
345
- exitCode: null,
346
- duration: Date.now() - startTime,
347
- error: `Failed to read task object: ${err}`,
348
- };
349
- }
350
- // Step 3: Create scratch directory
351
- // Include PID to prevent collisions when multiple e3 processes run the same
352
- // task concurrently (e.g., same task in different workspaces at same millisecond)
353
- const scratchDir = path.join(tmpdir(), `e3-exec-${taskHash.slice(0, 8)}-${inHash.slice(0, 8)}-${process.pid}-${Date.now()}`);
354
- await fs.mkdir(scratchDir, { recursive: true });
355
- try {
356
- // Step 4: Marshal inputs to scratch dir
357
- const inputPaths = [];
358
- for (let i = 0; i < inputHashes.length; i++) {
359
- const inputPath = path.join(scratchDir, `input-${i}.beast2`);
360
- const inputData = await objectRead(repoPath, inputHashes[i]);
361
- await fs.writeFile(inputPath, inputData);
362
- inputPaths.push(inputPath);
363
- }
364
- // Step 5: Evaluate command IR to get exec args
365
- const outputPath = path.join(scratchDir, 'output.beast2');
366
- let args;
367
- try {
368
- args = await evaluateCommandIr(repoPath, task.commandIr, inputPaths, outputPath);
369
- }
370
- catch (err) {
371
- return {
372
- inputsHash: inHash,
373
- cached: false,
374
- state: 'error',
375
- outputHash: null,
376
- exitCode: null,
377
- duration: Date.now() - startTime,
378
- error: `Failed to evaluate command IR: ${err}`,
379
- };
380
- }
381
- if (args.length === 0) {
382
- return {
383
- inputsHash: inHash,
384
- cached: false,
385
- state: 'error',
386
- outputHash: null,
387
- exitCode: null,
388
- duration: Date.now() - startTime,
389
- error: 'Command IR produced empty command',
390
- };
391
- }
392
- // Step 6: Create execution directory
393
- await fs.mkdir(execDir, { recursive: true });
394
- // Step 7: Get boot ID for crash detection
395
- const bootId = await getBootId();
396
- // Step 8: Execute command
397
- const result = await runCommand(args, execDir, inputHashes, bootId, options);
398
- // Step 9: Handle result
399
- if (result.exitCode === 0) {
400
- // Success - read and store output
401
- try {
402
- const outputData = await fs.readFile(outputPath);
403
- const outputHash = await objectWrite(repoPath, outputData);
404
- // Write output ref
405
- await fs.writeFile(path.join(execDir, 'output'), outputHash + '\n');
406
- // Write success status
407
- const status = variant('success', {
408
- inputHashes,
409
- outputHash,
410
- startedAt: new Date(startTime),
411
- completedAt: new Date(),
412
- });
413
- const encoder = encodeBeast2For(ExecutionStatusType);
414
- await fs.writeFile(path.join(execDir, 'status.beast2'), encoder(status));
415
- return {
416
- inputsHash: inHash,
417
- cached: false,
418
- state: 'success',
419
- outputHash,
420
- exitCode: 0,
421
- duration: Date.now() - startTime,
422
- error: null,
423
- };
424
- }
425
- catch (err) {
426
- // Output file missing or unreadable
427
- const status = variant('error', {
428
- inputHashes,
429
- startedAt: new Date(startTime),
430
- completedAt: new Date(),
431
- message: `Failed to read output: ${err}`,
432
- });
433
- const encoder = encodeBeast2For(ExecutionStatusType);
434
- await fs.writeFile(path.join(execDir, 'status.beast2'), encoder(status));
435
- return {
436
- inputsHash: inHash,
437
- cached: false,
438
- state: 'error',
439
- outputHash: null,
440
- exitCode: 0,
441
- duration: Date.now() - startTime,
442
- error: `Failed to read output: ${err}`,
443
- };
444
- }
445
- }
446
- else {
447
- // Failed - write failed status
448
- const status = variant('failed', {
449
- inputHashes,
450
- startedAt: new Date(startTime),
451
- completedAt: new Date(),
452
- exitCode: BigInt(result?.exitCode ?? -1),
453
- });
454
- const encoder = encodeBeast2For(ExecutionStatusType);
455
- await fs.writeFile(path.join(execDir, 'status.beast2'), encoder(status));
456
- return {
457
- inputsHash: inHash,
458
- cached: false,
459
- state: 'failed',
460
- outputHash: null,
461
- exitCode: result.exitCode,
462
- duration: Date.now() - startTime,
463
- error: result.error,
464
- };
465
- }
466
- }
467
- finally {
468
- // Cleanup scratch directory
469
- try {
470
- await fs.rm(scratchDir, { recursive: true, force: true });
471
- }
472
- catch {
473
- // Ignore cleanup errors
474
- }
475
- }
476
- }
477
- /**
478
- * Run a command and capture output
479
- */
480
- async function runCommand(args, execDir, inputHashes, bootId, options) {
481
- const [cmd, ...cmdArgs] = args;
482
- // Process Lifecycle Management
483
- // ============================
484
- // We use detached: true to create a new process group, allowing us to kill
485
- // the entire process tree by signaling the negative PID (process group leader).
486
- //
487
- // LIMITATION: Process groups are flat, not hierarchical. If a task spawns a
488
- // subprocess that creates its own process group (via setsid, daemonization,
489
- // or another detached spawn), that subprocess will escape our kill signal.
490
- // This is a fundamental Unix limitation - process groups were designed for
491
- // terminal job control (Ctrl+C/Ctrl+Z), not process tree management.
492
- //
493
- // For most tasks (shell scripts, pipelines, normal child processes) this works
494
- // fine. A task would have to intentionally call setsid() to escape.
495
- //
496
- // Potential improvements for hosted e3:
497
- // - Linux cgroups: Hierarchical containment with no escape. Requires root or
498
- // systemd integration (systemd-run --scope). Used by Docker/Kubernetes.
499
- // - PR_SET_CHILD_SUBREAPER: Makes e3 adopt orphaned processes instead of init,
500
- // allowing tracking and cleanup. Requires polling to detect orphans.
501
- // - Firecracker/microVMs: Complete isolation with hardware virtualization.
502
- // The VM boundary provides bulletproof containment. Best for multi-tenant.
503
- const child = spawn(cmd, cmdArgs, {
504
- stdio: ['ignore', 'pipe', 'pipe'],
505
- detached: true,
506
- });
507
- // Set up event listeners IMMEDIATELY before any async work
508
- // to avoid missing events if the process completes quickly
509
- const resultPromise = new Promise((resolve) => {
510
- child.on('error', (err) => {
511
- resolve({ exitCode: null, error: `Failed to spawn: ${err.message}` });
512
- });
513
- child.on('close', (code) => {
514
- resolve({ exitCode: code, error: code !== 0 ? `Exit code: ${code}` : null });
515
- });
516
- });
517
- // Open log files for writing
518
- const stdoutStream = createWriteStream(path.join(execDir, 'stdout.txt'));
519
- const stderrStream = createWriteStream(path.join(execDir, 'stderr.txt'));
520
- // Tee stdout
521
- child.stdout?.on('data', (data) => {
522
- stdoutStream.write(data);
523
- if (options.onStdout) {
524
- options.onStdout(data.toString('utf-8'));
525
- }
526
- });
527
- // Tee stderr
528
- child.stderr?.on('data', (data) => {
529
- stderrStream.write(data);
530
- if (options.onStderr) {
531
- options.onStderr(data.toString('utf-8'));
532
- }
533
- });
534
- // Helper to kill the entire process group (child and all its descendants).
535
- // With detached: true, child.pid is the process group leader, so killing
536
- // -child.pid sends the signal to all processes in that group.
537
- const killProcessGroup = () => {
538
- if (child.pid) {
539
- try {
540
- process.kill(-child.pid, 'SIGKILL');
541
- }
542
- catch {
543
- // Process may have already exited
544
- }
545
- }
546
- };
547
- // Handle timeout
548
- let timeoutId;
549
- if (options.timeout) {
550
- timeoutId = setTimeout(killProcessGroup, options.timeout);
551
- }
552
- // Handle abort signal
553
- if (options.signal) {
554
- if (options.signal.aborted) {
555
- // Already aborted before we started
556
- killProcessGroup();
557
- }
558
- else {
559
- options.signal.addEventListener('abort', killProcessGroup, { once: true });
560
- }
561
- }
562
- // Write running status with actual child PID (can be async now)
563
- const pidStartTime = await getPidStartTime(child.pid);
564
- const status = variant('running', {
565
- inputHashes,
566
- startedAt: new Date(),
567
- pid: BigInt(child.pid ?? -1),
568
- pidStartTime: BigInt(pidStartTime ?? -1),
569
- bootId,
570
- });
571
- const encoder = encodeBeast2For(ExecutionStatusType);
572
- await fs.writeFile(path.join(execDir, 'status.beast2'), encoder(status));
573
- // Wait for process to complete
574
- const result = await resultPromise;
575
- // Cleanup
576
- if (timeoutId)
577
- clearTimeout(timeoutId);
578
- if (options.signal) {
579
- options.signal.removeEventListener('abort', killProcessGroup);
580
- }
581
- stdoutStream.end();
582
- stderrStream.end();
583
- return result;
584
- }
585
219
  //# sourceMappingURL=executions.js.map