@straiffi/archon 1.7.4 → 1.7.6

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 (108) hide show
  1. package/dist/client/assets/{ActivityView-BgMz3NNT.js → ActivityView-BrZY8vYf.js} +1 -1
  2. package/dist/client/assets/{GitHubActionsDialog-CNqWDS3E.js → GitHubActionsDialog-CUfdVw97.js} +1 -1
  3. package/dist/client/assets/{TestsDialog-Cll7xJe0.js → TestsDialog-FXcnLOyf.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BL5dYiul.js → architectureDiagram-3BPJPVTR-D0PXGvSL.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-Bi5x2v80.js → blockDiagram-GPEHLZMM-BCLAxnp5.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-esEm54Ay.js → c4Diagram-AAUBKEIU-DVZ5yZWV.js} +1 -1
  7. package/dist/client/assets/channel-BCPCrThZ.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-DACfY8xM.js → chunk-2J33WTMH-BGJYj1Lu.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DZNUiaUs.js → chunk-3OPIFGDE-DI5-yK3m.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-ClDfkMAp.js → chunk-5ZQYHXKU-Dnr5DDsb.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-CCwA3GWT.js → chunk-727SXJPM-8HdFwn-P.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-Byl4DRZ9.js → chunk-AQP2D5EJ-C7x9t2uJ.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-CXymZQ7A.js → chunk-CSCIHK7Q-CrhwRPh5.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-VV3D8g6a.js → chunk-KSCS5N6A-CdD62r5U.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-BL9N5GRI.js → chunk-L5ZTLDWV-CqQVvwc_.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-DKNN-zno.js → chunk-LZXEDZCA-DLHrqoJ4.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-CHCpQ1yr.js → chunk-ND2GUHAM-B0yOdiPF.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-B0Dm74mt.js → chunk-NZK2D7GU-boM-MocY.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-DAa5z2xG.js → chunk-O5CBEL6O-B0jgJn0t.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-DrVJWKVR.js → chunk-WU5MYG2G-C_S_v-m2.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BdJlc_1i.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Bidw5Zc1.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-B0fsl8mE.js → dagre-BM42HDAG-DKl6Y8ya.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-Cx_VEbSS.js → diagram-2AECGRRQ-6Lyn9DBp.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-Cv7VVn5r.js → diagram-5GNKFQAL-wdS1vEkN.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-C8Sl5czV.js → diagram-KO2AKTUF-CPih29jF.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-C3dZaLz2.js → diagram-LMA3HP47-Cr5Px74d.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-BAU9DtTr.js → diagram-OG6HWLK6-i0PjSScd.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DpNc-eBD.js → erDiagram-TEJ5UH35-Dt4yOC9J.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-DGwTAT1S.js → flowDiagram-I6XJVG4X-Bf6kvajC.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-MPZhEgzJ.js → ganttDiagram-6RSMTGT7-DgxePMk_.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CY0tHL5N.js → gitGraphDiagram-PVQCEYII-BZq2cmFS.js} +1 -1
  33. package/dist/client/assets/index-BJGHMaMz.css +2 -0
  34. package/dist/client/assets/{index-CJRgmWwY.js → index-CjRMYO1U.js} +106 -106
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-BBZeKjf4.js → infoDiagram-5YYISTIA-Chij69KN.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CM_iFrES.js → ishikawaDiagram-YF4QCWOH-3qHCLFgD.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-Do_lY94X.js → journeyDiagram-JHISSGLW-DvRW8tbA.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-brxg3j37.js → kanban-definition-UN3LZRKU-DjR2T5I1.js} +1 -1
  39. package/dist/client/assets/{line-PHHbm9rj.js → line-B9OzrQ1Y.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-CLxAkW69.js → mermaid-parser.core-DYcDor4d.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-CebvSmx9.js → mermaid.core-DG8QYo2u.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-CX_-ZaJF.js → mindmap-definition-RKZ34NQL-f4QPF6b3.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-BJuHvql_.js → pieDiagram-4H26LBE5-CMkl2jBu.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DiJll7l9.js → quadrantDiagram-W4KKPZXB-BXaUHzJi.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-Bg-tXgcj.js → requirementDiagram-4Y6WPE33-B-Inuj_6.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BoK2egfZ.js → sankeyDiagram-5OEKKPKP-BjsmZFYI.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DcrDWNzv.js → sequenceDiagram-3UESZ5HK-B1PAm6sX.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-B995qt1S.js → stateDiagram-AJRCARHV-pLONNEN8.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-kWPDF3wr.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-DqSVAhqL.js → timeline-definition-PNZ67QCA-BLMkSXo_.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-D9wVraVY.js → vennDiagram-CIIHVFJN-CEwGRegG.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CLff9zVd.js → wardleyDiagram-YWT4CUSO-_md2IBXC.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-BuHUMMH2.js → xychartDiagram-2RQKCTM6-DVzwfXHc.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +282 -0
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +332 -130
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/attachments.js +16 -0
  60. package/dist/server/lib/attachments.js.map +1 -1
  61. package/dist/server/lib/automationResumeDecision.js +141 -0
  62. package/dist/server/lib/automationResumeDecision.js.map +1 -0
  63. package/dist/server/lib/buildSequences.js +52 -0
  64. package/dist/server/lib/buildSequences.js.map +1 -1
  65. package/dist/server/lib/executionArtifactCleanupState.js +12 -0
  66. package/dist/server/lib/executionArtifactCleanupState.js.map +1 -0
  67. package/dist/server/lib/executionArtifactRetention.js +405 -0
  68. package/dist/server/lib/executionArtifactRetention.js.map +1 -0
  69. package/dist/server/lib/executionArtifacts.js +705 -0
  70. package/dist/server/lib/executionArtifacts.js.map +1 -0
  71. package/dist/server/lib/executionRetry.js +9 -1
  72. package/dist/server/lib/executionRetry.js.map +1 -1
  73. package/dist/server/lib/executionRetryContext.js +21 -0
  74. package/dist/server/lib/executionRetryContext.js.map +1 -1
  75. package/dist/server/lib/executionRuns.js +62 -2
  76. package/dist/server/lib/executionRuns.js.map +1 -1
  77. package/dist/server/lib/executionTimeline.js +38 -10
  78. package/dist/server/lib/executionTimeline.js.map +1 -1
  79. package/dist/server/lib/lightweightPrompt.js +2 -2
  80. package/dist/server/lib/lightweightPrompt.js.map +1 -1
  81. package/dist/server/lib/projects.js +16 -3
  82. package/dist/server/lib/projects.js.map +1 -1
  83. package/dist/server/lib/pullRequestContent.js +148 -25
  84. package/dist/server/lib/pullRequestContent.js.map +1 -1
  85. package/dist/server/lib/queue.js +1 -0
  86. package/dist/server/lib/queue.js.map +1 -1
  87. package/dist/server/lib/runtimePaths.js +7 -0
  88. package/dist/server/lib/runtimePaths.js.map +1 -1
  89. package/dist/server/lib/ticketAutomationCommit.js +11 -4
  90. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  91. package/dist/server/lib/ticketFollowUpOperations.js +19 -15
  92. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  93. package/dist/server/lib/ticketUndo.js +83 -31
  94. package/dist/server/lib/ticketUndo.js.map +1 -1
  95. package/dist/server/lib/tickets.js +67 -6
  96. package/dist/server/lib/tickets.js.map +1 -1
  97. package/dist/server/lib/types.js +11 -0
  98. package/dist/server/lib/types.js.map +1 -1
  99. package/dist/server/workers/followUp.js +94 -6
  100. package/dist/server/workers/followUp.js.map +1 -1
  101. package/dist/server/workers/review.js +1 -0
  102. package/dist/server/workers/review.js.map +1 -1
  103. package/package.json +1 -1
  104. package/dist/client/assets/channel-CqNHF3FD.js +0 -1
  105. package/dist/client/assets/classDiagram-4FO5ZUOK-BjEgQwKg.js +0 -1
  106. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-gGomH-F_.js +0 -1
  107. package/dist/client/assets/index-mRkJWu70.css +0 -2
  108. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Dn8P7Sa1.js +0 -1
@@ -0,0 +1,705 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { constants } from 'fs';
3
+ import { isIP } from 'net';
4
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from 'path';
5
+ import { lstat, mkdir, link, open, realpath, unlink } from 'fs/promises';
6
+ import db from '../db.js';
7
+ import { withExecutionRunTransaction } from './executionRuns.js';
8
+ import { resolveArchonArtifactsRoot } from './runtimePaths.js';
9
+ import { isProjectArtifactCleanupActive } from './executionArtifactCleanupState.js';
10
+ import { truncateUtf8 } from './transcriptRuns.js';
11
+ import { ARTIFACT_RETENTION_DURATIONS_MS, DEFAULT_ARTIFACT_RETENTION_CLASS, MAX_ARTIFACT_FILENAME_LENGTH, MAX_ARTIFACTS_PER_RUN, MAX_ARTIFACT_SUMMARIES_PER_TIMELINE_ITEM, MAX_ARTIFACT_TITLE_LENGTH, MAX_ARTIFACT_URL_LENGTH, MAX_MANAGED_ARTIFACT_BYTES, } from './types.js';
12
+ const ARTIFACT_KINDS = new Set(['report', 'screenshot', 'log_excerpt', 'structured_result']);
13
+ const ARTIFACT_SENSITIVITIES = new Set(['normal', 'sensitive']);
14
+ const ARTIFACT_RETENTION_CLASSES = new Set(['short', 'standard']);
15
+ const ARTIFACT_AVAILABILITIES = new Set([
16
+ 'available',
17
+ 'expired',
18
+ 'missing',
19
+ 'deletion_failed',
20
+ 'deleted',
21
+ ]);
22
+ const MEDIA_TYPES_BY_KIND = {
23
+ report: new Set(['text/plain', 'text/markdown', 'application/json', 'application/pdf']),
24
+ screenshot: new Set(['image/png', 'image/jpeg', 'image/webp']),
25
+ log_excerpt: new Set(['text/plain']),
26
+ structured_result: new Set(['application/json']),
27
+ };
28
+ const MAX_BATCH_RUN_IDS = 200;
29
+ const MAX_CLEANUP_ERROR_BYTES = 2_048;
30
+ export const ARTIFACT_REGISTRATION_HEARTBEAT_MS = 30_000;
31
+ export class ExecutionArtifactError extends Error {
32
+ code;
33
+ constructor(code, message, cause) {
34
+ super(message, cause === undefined ? undefined : { cause });
35
+ this.name = 'ExecutionArtifactError';
36
+ this.code = code;
37
+ }
38
+ }
39
+ const intentSelect = `
40
+ SELECT *
41
+ FROM execution_artifact_registration_intents
42
+ WHERE artifact_id = ? AND project_id = ? AND execution_run_id = ?
43
+ `;
44
+ const artifactSelect = `
45
+ SELECT *
46
+ FROM execution_artifacts
47
+ WHERE id = ? AND project_id = ? AND execution_run_id = ?
48
+ `;
49
+ const normalizeRequiredText = (value, name, maxLength) => {
50
+ const normalized = value.trim();
51
+ if (!normalized || normalized.length > maxLength || normalized.includes('\0')) {
52
+ throw new ExecutionArtifactError('invalid_input', `${name} must contain between 1 and ${maxLength} characters`);
53
+ }
54
+ return normalized;
55
+ };
56
+ const normalizeRegistrationInput = (input) => {
57
+ const artifactId = normalizeRequiredText(input.artifactId, 'Artifact ID', 255);
58
+ const projectId = normalizeRequiredText(input.projectId, 'Project ID', 255);
59
+ const runId = normalizeRequiredText(input.runId, 'Execution run ID', 255);
60
+ const title = normalizeRequiredText(input.title, 'Artifact title', MAX_ARTIFACT_TITLE_LENGTH);
61
+ const mediaType = normalizeRequiredText(input.mediaType, 'Artifact media type', 255).toLowerCase();
62
+ const sensitivity = input.sensitivity ?? 'normal';
63
+ const retentionClass = input.retentionClass ?? DEFAULT_ARTIFACT_RETENTION_CLASS;
64
+ if (!ARTIFACT_KINDS.has(input.kind)) {
65
+ throw new ExecutionArtifactError('invalid_input', 'Unsupported artifact kind');
66
+ }
67
+ if (!ARTIFACT_SENSITIVITIES.has(sensitivity)) {
68
+ throw new ExecutionArtifactError('invalid_input', 'Unsupported artifact sensitivity');
69
+ }
70
+ if (!ARTIFACT_RETENTION_CLASSES.has(retentionClass)) {
71
+ throw new ExecutionArtifactError('invalid_input', 'Unsupported artifact retention class');
72
+ }
73
+ if (!MEDIA_TYPES_BY_KIND[input.kind].has(mediaType)) {
74
+ throw new ExecutionArtifactError('invalid_input', `Unsupported media type for ${input.kind} artifacts`);
75
+ }
76
+ return { artifactId, projectId, runId, kind: input.kind, title, mediaType, sensitivity, retentionClass };
77
+ };
78
+ const normalizeFilename = (value) => {
79
+ const filename = normalizeRequiredText(value, 'Artifact filename', MAX_ARTIFACT_FILENAME_LENGTH);
80
+ if (filename !== basename(filename) || filename === '.' || filename === '..' || filename.includes('/') || filename.includes('\\')) {
81
+ throw new ExecutionArtifactError('invalid_input', 'Artifact filename must not contain path components');
82
+ }
83
+ return filename;
84
+ };
85
+ const pathSegment = (value) => createHash('sha256').update(value).digest('hex').slice(0, 32);
86
+ const generateRelativePaths = (projectId, runId, artifactId) => {
87
+ const directory = [pathSegment(projectId), pathSegment(runId)].join('/');
88
+ const reservationId = randomUUID();
89
+ const artifactSegment = pathSegment(artifactId);
90
+ const finalRelativePath = `${directory}/${artifactSegment}.${reservationId}.artifact`;
91
+ const temporaryRelativePath = `${directory}/.${artifactSegment}.${reservationId}.tmp`;
92
+ return { temporaryRelativePath, finalRelativePath };
93
+ };
94
+ const currentSqlTimestamp = () => new Date(Math.floor(Date.now() / 1_000) * 1_000).toISOString();
95
+ const expiryFor = (createdAt, retentionClass) => (new Date(Date.parse(createdAt) + ARTIFACT_RETENTION_DURATIONS_MS[retentionClass]).toISOString());
96
+ const isConstraintError = (error) => (error instanceof Error && (error.message.includes('UNIQUE constraint failed')
97
+ || error.message.includes('Artifact identity exists')
98
+ || error.message.includes('FOREIGN KEY constraint failed')));
99
+ export const getExecutionArtifactRegistrationIntent = (projectId, runId, artifactId) => {
100
+ const row = db.prepare(intentSelect).get(artifactId, projectId, runId);
101
+ return row ?? null;
102
+ };
103
+ export const reserveExecutionArtifactRegistration = (input) => {
104
+ const normalized = normalizeRegistrationInput(input);
105
+ const filename = normalizeFilename(input.filename);
106
+ const paths = generateRelativePaths(normalized.projectId, normalized.runId, normalized.artifactId);
107
+ const createdAt = currentSqlTimestamp();
108
+ const expiresAt = expiryFor(createdAt, normalized.retentionClass);
109
+ try {
110
+ return withExecutionRunTransaction(() => {
111
+ const projectDeleting = db.prepare('SELECT deletion_started_at FROM projects WHERE id = ?').get(normalized.projectId);
112
+ if (isProjectArtifactCleanupActive(normalized.projectId) || projectDeleting?.deletion_started_at) {
113
+ throw new ExecutionArtifactError('conflict', 'Artifact registration is unavailable while the project is being deleted');
114
+ }
115
+ const run = db.prepare('SELECT 1 FROM execution_runs WHERE id = ? AND project_id = ?').get(normalized.runId, normalized.projectId);
116
+ if (!run) {
117
+ throw new ExecutionArtifactError('not_found', 'Execution run not found');
118
+ }
119
+ db.prepare(`
120
+ INSERT INTO execution_artifact_registration_intents (
121
+ artifact_id, execution_run_id, project_id, kind, title, media_type,
122
+ sensitivity, retention_class, expires_at, filename,
123
+ temporary_relative_path, final_relative_path, created_at, updated_at
124
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
125
+ `).run(normalized.artifactId, normalized.runId, normalized.projectId, normalized.kind, normalized.title, normalized.mediaType, normalized.sensitivity, normalized.retentionClass, expiresAt, filename, paths.temporaryRelativePath, paths.finalRelativePath, createdAt, createdAt);
126
+ const intent = getExecutionArtifactRegistrationIntent(normalized.projectId, normalized.runId, normalized.artifactId);
127
+ if (!intent) {
128
+ throw new Error('Artifact reservation was not persisted');
129
+ }
130
+ return intent;
131
+ });
132
+ }
133
+ catch (error) {
134
+ if (error instanceof ExecutionArtifactError) {
135
+ throw error;
136
+ }
137
+ if (isConstraintError(error)) {
138
+ throw new ExecutionArtifactError('conflict', 'Artifact identity is already registered or the run is at capacity', error);
139
+ }
140
+ throw error;
141
+ }
142
+ };
143
+ const updateIntentState = (intent, expectedState, state) => {
144
+ const result = db.prepare(`
145
+ UPDATE execution_artifact_registration_intents
146
+ SET state = ?, updated_at = ?
147
+ WHERE artifact_id = ? AND project_id = ? AND execution_run_id = ?
148
+ AND temporary_relative_path = ? AND final_relative_path = ?
149
+ AND state = ?
150
+ `).run(state, currentSqlTimestamp(), intent.artifact_id, intent.project_id, intent.execution_run_id, intent.temporary_relative_path, intent.final_relative_path, expectedState);
151
+ if (result.changes !== 1) {
152
+ throw new ExecutionArtifactError('conflict', 'Artifact reservation is no longer owned by this registration');
153
+ }
154
+ };
155
+ const renewIntentLease = (intent) => {
156
+ const result = db.prepare(`
157
+ UPDATE execution_artifact_registration_intents
158
+ SET updated_at = ?
159
+ WHERE artifact_id = ? AND project_id = ? AND execution_run_id = ?
160
+ AND temporary_relative_path = ? AND final_relative_path = ?
161
+ AND state IN ('writing', 'publishing')
162
+ AND cleanup_claim_id IS NULL
163
+ `).run(currentSqlTimestamp(), intent.artifact_id, intent.project_id, intent.execution_run_id, intent.temporary_relative_path, intent.final_relative_path);
164
+ if (result.changes !== 1) {
165
+ throw new ExecutionArtifactError('conflict', 'Artifact reservation is no longer owned by this registration');
166
+ }
167
+ };
168
+ const startIntentHeartbeat = (intent, content) => {
169
+ let ownershipError = null;
170
+ const renew = () => {
171
+ if (ownershipError) {
172
+ throw ownershipError;
173
+ }
174
+ try {
175
+ renewIntentLease(intent);
176
+ }
177
+ catch (error) {
178
+ ownershipError = error instanceof ExecutionArtifactError
179
+ ? error
180
+ : new ExecutionArtifactError('conflict', 'Artifact reservation heartbeat failed', error);
181
+ destroyContent(content, ownershipError);
182
+ throw ownershipError;
183
+ }
184
+ };
185
+ const timer = setInterval(() => {
186
+ try {
187
+ renew();
188
+ }
189
+ catch {
190
+ // The registration observes ownership loss before publication or finalization.
191
+ }
192
+ }, ARTIFACT_REGISTRATION_HEARTBEAT_MS);
193
+ timer.unref();
194
+ return {
195
+ assertOwned: renew,
196
+ stop: () => clearInterval(timer),
197
+ };
198
+ };
199
+ const resolveOwnedPath = (root, relativePath) => {
200
+ if (isAbsolute(relativePath) || relativePath.includes('\\')) {
201
+ throw new ExecutionArtifactError('storage_error', 'Artifact storage path is invalid');
202
+ }
203
+ const resolved = resolve(root, relativePath);
204
+ const fromRoot = relative(resolve(root), resolved);
205
+ if (!fromRoot || fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
206
+ throw new ExecutionArtifactError('storage_error', 'Artifact storage path escapes the artifact root');
207
+ }
208
+ return resolved;
209
+ };
210
+ const prepareOwnedPaths = async (intent) => {
211
+ const configuredRoot = resolveArchonArtifactsRoot();
212
+ await mkdir(configuredRoot, { recursive: true });
213
+ const root = await realpath(configuredRoot);
214
+ const temporaryPath = resolveOwnedPath(root, intent.temporary_relative_path);
215
+ const finalPath = resolveOwnedPath(root, intent.final_relative_path);
216
+ const relativeParent = relative(root, dirname(temporaryPath));
217
+ let ownedParent = root;
218
+ for (const segment of relativeParent.split(sep)) {
219
+ if (!segment || segment === '.' || segment === '..') {
220
+ throw new ExecutionArtifactError('storage_error', 'Artifact storage directory is invalid');
221
+ }
222
+ ownedParent = resolve(ownedParent, segment);
223
+ try {
224
+ await mkdir(ownedParent, { mode: 0o700 });
225
+ }
226
+ catch (error) {
227
+ if (error.code !== 'EEXIST') {
228
+ throw error;
229
+ }
230
+ }
231
+ const stats = await lstat(ownedParent);
232
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
233
+ throw new ExecutionArtifactError('storage_error', 'Artifact storage directory must not be a symbolic link');
234
+ }
235
+ }
236
+ const actualParent = await realpath(ownedParent);
237
+ const parentFromRoot = relative(root, actualParent);
238
+ if (parentFromRoot === '..' || parentFromRoot.startsWith(`..${sep}`) || isAbsolute(parentFromRoot)) {
239
+ throw new ExecutionArtifactError('storage_error', 'Artifact storage directory escapes the artifact root');
240
+ }
241
+ return { temporaryPath, finalPath };
242
+ };
243
+ const destroyContent = (content, error) => {
244
+ const destroy = content.destroy;
245
+ if (typeof destroy === 'function') {
246
+ destroy.call(content, error);
247
+ }
248
+ };
249
+ const writeManagedContent = async (temporaryPath, content, onCreated) => {
250
+ const handle = await open(temporaryPath, 'wx', 0o600);
251
+ onCreated();
252
+ const hash = createHash('sha256');
253
+ let byteSize = 0;
254
+ try {
255
+ const chunks = Buffer.isBuffer(content)
256
+ ? (async function* () { yield content; })()
257
+ : content;
258
+ for await (const chunk of chunks) {
259
+ const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : Buffer.from(chunk);
260
+ byteSize += bytes.byteLength;
261
+ if (byteSize > MAX_MANAGED_ARTIFACT_BYTES) {
262
+ const error = new ExecutionArtifactError('invalid_input', 'Managed artifact exceeds the 25 MiB limit');
263
+ destroyContent(content, error);
264
+ throw error;
265
+ }
266
+ let offset = 0;
267
+ while (offset < bytes.byteLength) {
268
+ const { bytesWritten } = await handle.write(bytes, offset, bytes.byteLength - offset);
269
+ if (bytesWritten === 0) {
270
+ throw new ExecutionArtifactError('storage_error', 'Managed artifact write made no progress');
271
+ }
272
+ offset += bytesWritten;
273
+ }
274
+ hash.update(bytes);
275
+ }
276
+ await handle.sync();
277
+ }
278
+ finally {
279
+ await handle.close();
280
+ }
281
+ return { byteSize, sha256: hash.digest('hex') };
282
+ };
283
+ const boundedCleanupError = (error) => {
284
+ const message = error instanceof Error ? error.message : String(error);
285
+ return truncateUtf8(message, MAX_CLEANUP_ERROR_BYTES).text ?? 'Artifact cleanup failed';
286
+ };
287
+ const removeIfOwned = async (path, owned) => {
288
+ if (!owned) {
289
+ return null;
290
+ }
291
+ try {
292
+ await unlink(path);
293
+ return null;
294
+ }
295
+ catch (error) {
296
+ if (error.code === 'ENOENT') {
297
+ return null;
298
+ }
299
+ return error;
300
+ }
301
+ };
302
+ const compensateRegistration = async (intent, paths, ownership) => {
303
+ const errors = paths
304
+ ? (await Promise.all([
305
+ removeIfOwned(paths.temporaryPath, ownership.temporary),
306
+ removeIfOwned(paths.finalPath, ownership.final),
307
+ ])).filter(error => error !== null)
308
+ : [];
309
+ if (errors.length === 0) {
310
+ db.prepare(`
311
+ DELETE FROM execution_artifact_registration_intents
312
+ WHERE artifact_id = ? AND project_id = ? AND execution_run_id = ?
313
+ AND temporary_relative_path = ? AND final_relative_path = ?
314
+ AND state NOT IN ('cleanup_pending', 'cleanup_failed')
315
+ `).run(intent.artifact_id, intent.project_id, intent.execution_run_id, intent.temporary_relative_path, intent.final_relative_path);
316
+ return;
317
+ }
318
+ const now = currentSqlTimestamp();
319
+ db.prepare(`
320
+ UPDATE execution_artifact_registration_intents
321
+ SET state = CASE WHEN cleanup_attempts >= 9 THEN 'cleanup_failed' ELSE 'cleanup_pending' END,
322
+ cleanup_attempts = MIN(cleanup_attempts + 1, 10),
323
+ last_cleanup_error = ?, last_cleanup_attempt_at = ?, updated_at = ?
324
+ WHERE artifact_id = ? AND project_id = ? AND execution_run_id = ?
325
+ AND temporary_relative_path = ? AND final_relative_path = ?
326
+ `).run(boundedCleanupError(errors[0]), now, now, intent.artifact_id, intent.project_id, intent.execution_run_id, intent.temporary_relative_path, intent.final_relative_path);
327
+ };
328
+ const finalizeArtifact = ({ intent, expectedState, storageType, byteSize, sha256, externalUrl, emitter }) => (withExecutionRunTransaction(transaction => {
329
+ const ownedIntent = getExecutionArtifactRegistrationIntent(intent.project_id, intent.execution_run_id, intent.artifact_id);
330
+ if (!ownedIntent
331
+ || ownedIntent.temporary_relative_path !== intent.temporary_relative_path
332
+ || ownedIntent.final_relative_path !== intent.final_relative_path
333
+ || ownedIntent.state !== expectedState) {
334
+ throw new ExecutionArtifactError('conflict', 'Artifact reservation is no longer owned by this registration');
335
+ }
336
+ const removed = db.prepare(`
337
+ DELETE FROM execution_artifact_registration_intents
338
+ WHERE artifact_id = ? AND project_id = ? AND execution_run_id = ?
339
+ AND temporary_relative_path = ? AND final_relative_path = ?
340
+ AND state = ?
341
+ `).run(intent.artifact_id, intent.project_id, intent.execution_run_id, intent.temporary_relative_path, intent.final_relative_path, expectedState);
342
+ if (removed.changes !== 1) {
343
+ throw new ExecutionArtifactError('conflict', 'Artifact reservation could not be finalized');
344
+ }
345
+ db.prepare(`
346
+ INSERT INTO execution_artifacts (
347
+ id, execution_run_id, project_id, kind, title, storage_type, availability,
348
+ media_type, byte_size, sha256, sensitivity, retention_class, expires_at,
349
+ filename, managed_relative_path, external_url, created_at, updated_at
350
+ ) VALUES (?, ?, ?, ?, ?, ?, 'available', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
351
+ `).run(intent.artifact_id, intent.execution_run_id, intent.project_id, intent.kind, intent.title, storageType, intent.media_type, byteSize, sha256, intent.sensitivity, intent.retention_class, intent.expires_at, storageType === 'managed_file' ? intent.filename : null, storageType === 'managed_file' ? intent.final_relative_path : null, externalUrl, intent.created_at, intent.created_at);
352
+ transaction.appendLifecycleEvent({
353
+ projectId: intent.project_id,
354
+ runId: intent.execution_run_id,
355
+ eventType: 'artifact_created',
356
+ metadata: intent.sensitivity === 'normal' ? { artifact_id: intent.artifact_id } : null,
357
+ });
358
+ const artifact = getExecutionArtifact(intent.project_id, intent.execution_run_id, intent.artifact_id);
359
+ if (!artifact) {
360
+ throw new Error('Finalized artifact metadata was not persisted');
361
+ }
362
+ return artifact;
363
+ }, emitter));
364
+ const publicationError = (error) => {
365
+ const code = error.code;
366
+ if (code === 'EEXIST') {
367
+ return new ExecutionArtifactError('conflict', 'Artifact destination already exists', error);
368
+ }
369
+ if (code === 'EXDEV' || code === 'ENOTSUP' || code === 'EOPNOTSUPP' || code === 'EPERM') {
370
+ return new ExecutionArtifactError('storage_error', 'Artifact storage does not support no-clobber publication', error);
371
+ }
372
+ return new ExecutionArtifactError('storage_error', 'Artifact could not be published', error);
373
+ };
374
+ export const registerManagedExecutionArtifact = async (input, emitter) => {
375
+ const intent = reserveExecutionArtifactRegistration(input);
376
+ let paths = null;
377
+ let ownsTemporary = false;
378
+ let ownsFinal = false;
379
+ let heartbeat = null;
380
+ try {
381
+ updateIntentState(intent, 'reserved', 'writing');
382
+ heartbeat = startIntentHeartbeat(intent, input.content);
383
+ paths = await prepareOwnedPaths(intent);
384
+ const digest = await writeManagedContent(paths.temporaryPath, input.content, () => {
385
+ ownsTemporary = true;
386
+ });
387
+ heartbeat.assertOwned();
388
+ updateIntentState(intent, 'writing', 'publishing');
389
+ heartbeat.assertOwned();
390
+ try {
391
+ await link(paths.temporaryPath, paths.finalPath);
392
+ ownsFinal = true;
393
+ }
394
+ catch (error) {
395
+ throw publicationError(error);
396
+ }
397
+ await unlink(paths.temporaryPath);
398
+ ownsTemporary = false;
399
+ heartbeat.assertOwned();
400
+ return finalizeArtifact({
401
+ intent,
402
+ expectedState: 'publishing',
403
+ storageType: 'managed_file',
404
+ byteSize: digest.byteSize,
405
+ sha256: digest.sha256,
406
+ externalUrl: null,
407
+ emitter,
408
+ });
409
+ }
410
+ catch (error) {
411
+ await compensateRegistration(intent, paths, { temporary: ownsTemporary, final: ownsFinal });
412
+ throw error;
413
+ }
414
+ finally {
415
+ heartbeat?.stop();
416
+ }
417
+ };
418
+ const isPrivateIpv4 = (address) => {
419
+ const octets = address.split('.').map(Number);
420
+ return octets[0] === 0
421
+ || octets[0] === 10
422
+ || octets[0] === 127
423
+ || (octets[0] === 169 && octets[1] === 254)
424
+ || (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31)
425
+ || (octets[0] === 192 && octets[1] === 168);
426
+ };
427
+ const isPrivateIpv6 = (address) => {
428
+ const normalized = address.toLowerCase();
429
+ const [leftValue, rightValue = ''] = normalized.split('::');
430
+ const left = leftValue ? leftValue.split(':') : [];
431
+ const right = rightValue ? rightValue.split(':') : [];
432
+ const missingGroups = 8 - left.length - right.length;
433
+ const groups = normalized.includes('::')
434
+ ? [...left, ...Array(Math.max(0, missingGroups)).fill('0'), ...right]
435
+ : left;
436
+ if (groups.length !== 8) {
437
+ return true;
438
+ }
439
+ const bytes = groups.flatMap(group => {
440
+ const value = Number.parseInt(group || '0', 16);
441
+ return [value >> 8, value & 0xff];
442
+ });
443
+ const isUnspecified = bytes.every(byte => byte === 0);
444
+ const isLoopback = bytes.slice(0, 15).every(byte => byte === 0) && bytes[15] === 1;
445
+ const isUniqueLocal = (bytes[0] & 0xfe) === 0xfc;
446
+ const isLinkLocal = bytes[0] === 0xfe && (bytes[1] & 0xc0) === 0x80;
447
+ const isMappedIpv4 = bytes.slice(0, 10).every(byte => byte === 0) && bytes[10] === 0xff && bytes[11] === 0xff;
448
+ const mappedIpv4 = bytes.slice(12).join('.');
449
+ return isUnspecified || isLoopback || isUniqueLocal || isLinkLocal || (isMappedIpv4 && isPrivateIpv4(mappedIpv4));
450
+ };
451
+ const validateExternalUrl = (value) => {
452
+ const normalized = normalizeRequiredText(value, 'External artifact URL', MAX_ARTIFACT_URL_LENGTH);
453
+ let url;
454
+ try {
455
+ url = new URL(normalized);
456
+ }
457
+ catch (error) {
458
+ throw new ExecutionArtifactError('invalid_input', 'External artifact URL is invalid', error);
459
+ }
460
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash) {
461
+ throw new ExecutionArtifactError('invalid_input', 'External artifacts require an HTTPS URL without credentials or a fragment');
462
+ }
463
+ const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.+$/, '');
464
+ const ipVersion = isIP(hostname);
465
+ if (hostname === 'localhost'
466
+ || hostname.endsWith('.localhost')
467
+ || (ipVersion === 4 && isPrivateIpv4(hostname))
468
+ || (ipVersion === 6 && isPrivateIpv6(hostname))) {
469
+ throw new ExecutionArtifactError('invalid_input', 'External artifact URL target is not allowed');
470
+ }
471
+ return url.toString();
472
+ };
473
+ export const registerExternalExecutionArtifact = (input, emitter) => {
474
+ const normalized = normalizeRegistrationInput(input);
475
+ if (normalized.sensitivity !== 'normal') {
476
+ throw new ExecutionArtifactError('invalid_input', 'External artifacts must use normal sensitivity');
477
+ }
478
+ const externalUrl = validateExternalUrl(input.url);
479
+ const intent = reserveExecutionArtifactRegistration({
480
+ ...input,
481
+ filename: `${pathSegment(normalized.artifactId)}.external`,
482
+ });
483
+ try {
484
+ return finalizeArtifact({
485
+ intent,
486
+ expectedState: 'reserved',
487
+ storageType: 'external_url',
488
+ byteSize: null,
489
+ sha256: null,
490
+ externalUrl,
491
+ emitter,
492
+ });
493
+ }
494
+ catch (error) {
495
+ db.prepare(`
496
+ DELETE FROM execution_artifact_registration_intents
497
+ WHERE artifact_id = ? AND project_id = ? AND execution_run_id = ?
498
+ AND temporary_relative_path = ? AND final_relative_path = ?
499
+ AND state = 'reserved'
500
+ `).run(intent.artifact_id, intent.project_id, intent.execution_run_id, intent.temporary_relative_path, intent.final_relative_path);
501
+ throw error;
502
+ }
503
+ };
504
+ export const getExecutionArtifact = (projectId, runId, artifactId) => {
505
+ const row = db.prepare(artifactSelect).get(artifactId, projectId, runId);
506
+ return row ?? null;
507
+ };
508
+ const isArtifactRetrievable = (row, now) => (row.availability === 'available'
509
+ && Number.isFinite(Date.parse(row.expires_at))
510
+ && Date.parse(row.expires_at) > now);
511
+ const toPublicSummary = (row, now) => ({
512
+ id: row.id,
513
+ execution_run_id: row.execution_run_id,
514
+ project_id: row.project_id,
515
+ kind: row.kind,
516
+ title: row.sensitivity === 'sensitive' ? null : row.title,
517
+ storage_type: row.storage_type,
518
+ availability: row.availability === 'available' && !isArtifactRetrievable(row, now) ? 'expired' : row.availability,
519
+ media_type: row.media_type,
520
+ byte_size: row.byte_size,
521
+ sensitivity: row.sensitivity,
522
+ expires_at: row.expires_at,
523
+ filename: row.sensitivity === 'sensitive' ? null : row.filename,
524
+ created_at: row.created_at,
525
+ actions: isArtifactRetrievable(row, now)
526
+ ? [row.storage_type === 'managed_file' ? 'download' : 'open_external']
527
+ : [],
528
+ });
529
+ export const getExecutionArtifactSummary = (projectId, runId, artifactId) => {
530
+ const artifact = getExecutionArtifact(projectId, runId, artifactId);
531
+ return artifact ? toPublicSummary(artifact, Date.now()) : null;
532
+ };
533
+ export const loadExecutionArtifactSummariesForRuns = (projectId, runIds, perRunLimit = MAX_ARTIFACT_SUMMARIES_PER_TIMELINE_ITEM) => {
534
+ const uniqueRunIds = [...new Set(runIds)];
535
+ const now = Date.now();
536
+ if (uniqueRunIds.length === 0) {
537
+ return new Map();
538
+ }
539
+ if (uniqueRunIds.length > MAX_BATCH_RUN_IDS) {
540
+ throw new ExecutionArtifactError('invalid_input', `Artifact summary lookup supports at most ${MAX_BATCH_RUN_IDS} runs`);
541
+ }
542
+ if (!Number.isInteger(perRunLimit) || perRunLimit < 1 || perRunLimit > MAX_ARTIFACTS_PER_RUN) {
543
+ throw new ExecutionArtifactError('invalid_input', `Artifact summary limit must be between 1 and ${MAX_ARTIFACTS_PER_RUN}`);
544
+ }
545
+ const placeholders = uniqueRunIds.map(() => '?').join(', ');
546
+ const rows = db.prepare(`
547
+ SELECT * FROM (
548
+ SELECT execution_artifacts.*,
549
+ COUNT(*) OVER (PARTITION BY execution_run_id) AS artifact_count,
550
+ ROW_NUMBER() OVER (
551
+ PARTITION BY execution_run_id
552
+ ORDER BY created_at DESC, id DESC
553
+ ) AS artifact_rank
554
+ FROM execution_artifacts
555
+ WHERE project_id = ? AND execution_run_id IN (${placeholders})
556
+ )
557
+ WHERE artifact_rank <= ?
558
+ ORDER BY execution_run_id, created_at DESC, id DESC
559
+ `).all(projectId, ...uniqueRunIds, perRunLimit);
560
+ const summaries = new Map(uniqueRunIds.map(runId => [runId, { artifacts: [], artifact_count: 0 }]));
561
+ for (const row of rows) {
562
+ const batch = summaries.get(row.execution_run_id);
563
+ if (batch) {
564
+ batch.artifacts.push(toPublicSummary(row, now));
565
+ batch.artifact_count = row.artifact_count;
566
+ }
567
+ }
568
+ return summaries;
569
+ };
570
+ const isOwnedPath = (root, path) => {
571
+ const fromRoot = relative(root, path);
572
+ return fromRoot !== '' && fromRoot !== '..' && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot);
573
+ };
574
+ const sanitizeDownloadFilename = (filename, sensitive) => {
575
+ if (sensitive || !filename) {
576
+ return 'artifact';
577
+ }
578
+ const sanitized = filename
579
+ .normalize('NFKD')
580
+ .replace(/[^\x20-\x7E]/g, '_')
581
+ .replace(/["\\/;\r\n]/g, '_')
582
+ .replace(/^\.+/, '')
583
+ .slice(0, MAX_ARTIFACT_FILENAME_LENGTH);
584
+ return sanitized || 'artifact';
585
+ };
586
+ export const openExecutionArtifactForRetrieval = async (projectId, runId, artifactId, emitter) => {
587
+ const artifact = getExecutionArtifact(projectId, runId, artifactId);
588
+ if (!artifact) {
589
+ return { kind: 'not_found' };
590
+ }
591
+ if (!isArtifactRetrievable(artifact, Date.now())) {
592
+ return { kind: 'unavailable' };
593
+ }
594
+ if (!MEDIA_TYPES_BY_KIND[artifact.kind].has(artifact.media_type)) {
595
+ return { kind: 'unavailable' };
596
+ }
597
+ if (artifact.storage_type === 'external_url') {
598
+ if (!artifact.external_url) {
599
+ return { kind: 'unavailable' };
600
+ }
601
+ try {
602
+ return { kind: 'external', url: validateExternalUrl(artifact.external_url) };
603
+ }
604
+ catch {
605
+ return { kind: 'unavailable' };
606
+ }
607
+ }
608
+ if (!artifact.managed_relative_path) {
609
+ return { kind: 'unavailable' };
610
+ }
611
+ try {
612
+ const root = await realpath(resolveArchonArtifactsRoot());
613
+ const path = resolveOwnedPath(root, artifact.managed_relative_path);
614
+ const parent = await realpath(dirname(path));
615
+ if (!isOwnedPath(root, parent)) {
616
+ return { kind: 'unavailable' };
617
+ }
618
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
619
+ try {
620
+ const stats = await handle.stat();
621
+ if (!stats.isFile()) {
622
+ await handle.close();
623
+ return { kind: 'unavailable' };
624
+ }
625
+ const current = getExecutionArtifact(projectId, runId, artifactId);
626
+ if (!current || !isArtifactRetrievable(current, Date.now())) {
627
+ await handle.close();
628
+ return { kind: 'unavailable' };
629
+ }
630
+ return {
631
+ kind: 'managed',
632
+ handle,
633
+ byteSize: stats.size,
634
+ filename: sanitizeDownloadFilename(artifact.filename, artifact.sensitivity === 'sensitive'),
635
+ mediaType: artifact.media_type,
636
+ };
637
+ }
638
+ catch (error) {
639
+ await handle.close().catch(() => undefined);
640
+ throw error;
641
+ }
642
+ }
643
+ catch (error) {
644
+ if (error.code === 'ENOENT') {
645
+ observeExecutionArtifactMissing(projectId, runId, artifactId, emitter);
646
+ }
647
+ return { kind: 'unavailable' };
648
+ }
649
+ };
650
+ const observeExecutionArtifactMissing = (projectId, runId, artifactId, emitter) => withExecutionRunTransaction(transaction => {
651
+ const now = currentSqlTimestamp();
652
+ const result = db.prepare(`
653
+ UPDATE execution_artifacts
654
+ SET availability = 'missing', updated_at = ?
655
+ WHERE id = ? AND project_id = ? AND execution_run_id = ?
656
+ AND availability = 'available'
657
+ `).run(now, artifactId, projectId, runId);
658
+ if (result.changes !== 1) {
659
+ return;
660
+ }
661
+ const artifact = getExecutionArtifact(projectId, runId, artifactId);
662
+ transaction.appendLifecycleEvent({
663
+ projectId,
664
+ runId,
665
+ eventType: 'artifact_availability_changed',
666
+ domainStatus: 'missing',
667
+ metadata: artifact?.sensitivity === 'normal' ? { artifact_id: artifactId } : null,
668
+ });
669
+ }, emitter);
670
+ export const persistExecutionArtifactAvailability = (input, emitter) => {
671
+ if (!ARTIFACT_AVAILABILITIES.has(input.availability)) {
672
+ throw new ExecutionArtifactError('invalid_input', 'Unsupported artifact availability');
673
+ }
674
+ return withExecutionRunTransaction(transaction => {
675
+ const current = getExecutionArtifact(input.projectId, input.runId, input.artifactId);
676
+ if (!current) {
677
+ throw new ExecutionArtifactError('not_found', 'Execution artifact not found');
678
+ }
679
+ if (current.availability === input.availability) {
680
+ return current;
681
+ }
682
+ if (current.availability !== 'available' || input.availability !== 'missing') {
683
+ throw new ExecutionArtifactError('conflict', 'Artifact availability transition is managed by retention cleanup');
684
+ }
685
+ const now = currentSqlTimestamp();
686
+ db.prepare(`
687
+ UPDATE execution_artifacts
688
+ SET availability = ?, updated_at = ?, deleted_at = CASE WHEN ? = 'deleted' THEN ? ELSE deleted_at END
689
+ WHERE id = ? AND project_id = ? AND execution_run_id = ?
690
+ `).run(input.availability, now, input.availability, now, input.artifactId, input.projectId, input.runId);
691
+ transaction.appendLifecycleEvent({
692
+ projectId: input.projectId,
693
+ runId: input.runId,
694
+ eventType: 'artifact_availability_changed',
695
+ domainStatus: input.availability,
696
+ metadata: current.sensitivity === 'normal' ? { artifact_id: current.id } : null,
697
+ });
698
+ const updated = getExecutionArtifact(input.projectId, input.runId, input.artifactId);
699
+ if (!updated) {
700
+ throw new Error('Updated artifact metadata was not persisted');
701
+ }
702
+ return updated;
703
+ }, emitter);
704
+ };
705
+ //# sourceMappingURL=executionArtifacts.js.map