@holon-run/agentinbox 0.1.0

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.
@@ -0,0 +1,372 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GithubCiSourceRuntime = exports.GithubActionsUxcClient = void 0;
4
+ exports.normalizeGithubWorkflowRunEvent = normalizeGithubWorkflowRunEvent;
5
+ const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
6
+ const GITHUB_ENDPOINT = "https://api.github.com";
7
+ const DEFAULT_POLL_INTERVAL_SECS = 30;
8
+ const DEFAULT_PER_PAGE = 20;
9
+ const MAX_SEEN_KEYS = 512;
10
+ const MAX_ERROR_BACKOFF_MULTIPLIER = 8;
11
+ class GithubActionsUxcClient {
12
+ client;
13
+ constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
14
+ this.client = client;
15
+ }
16
+ async listWorkflowRuns(config) {
17
+ const payload = {
18
+ owner: config.owner,
19
+ repo: config.repo,
20
+ per_page: config.perPage ?? DEFAULT_PER_PAGE,
21
+ };
22
+ if (config.eventFilter) {
23
+ payload.event = config.eventFilter;
24
+ }
25
+ if (config.branch) {
26
+ payload.branch = config.branch;
27
+ }
28
+ if (config.statusFilter) {
29
+ payload.status = config.statusFilter;
30
+ }
31
+ const response = await this.client.call({
32
+ endpoint: GITHUB_ENDPOINT,
33
+ operation: "get:/repos/{owner}/{repo}/actions/runs",
34
+ payload,
35
+ options: { auth: config.uxcAuth },
36
+ });
37
+ const data = asRecord(response.data);
38
+ return Array.isArray(data.workflow_runs)
39
+ ? data.workflow_runs.map((value) => asRecord(value)).filter((value) => Object.keys(value).length > 0)
40
+ : [];
41
+ }
42
+ }
43
+ exports.GithubActionsUxcClient = GithubActionsUxcClient;
44
+ class GithubCiSourceRuntime {
45
+ store;
46
+ appendSourceEvent;
47
+ client;
48
+ interval = null;
49
+ inFlight = new Set();
50
+ lastPollAt = new Map();
51
+ errorCounts = new Map();
52
+ nextRetryAt = new Map();
53
+ constructor(store, appendSourceEvent, client) {
54
+ this.store = store;
55
+ this.appendSourceEvent = appendSourceEvent;
56
+ this.client = client ?? new GithubActionsUxcClient();
57
+ }
58
+ async ensureSource(source) {
59
+ if (source.sourceType !== "github_repo_ci") {
60
+ return;
61
+ }
62
+ const checkpoint = parseGithubCiCheckpoint(source.checkpoint);
63
+ this.store.updateSourceRuntime(source.sourceId, {
64
+ status: "active",
65
+ checkpoint: JSON.stringify(checkpoint),
66
+ });
67
+ }
68
+ async start() {
69
+ if (this.interval) {
70
+ return;
71
+ }
72
+ this.interval = setInterval(() => {
73
+ void this.syncAll();
74
+ }, 2_000);
75
+ try {
76
+ await this.syncAll();
77
+ }
78
+ catch (error) {
79
+ console.warn("github_repo_ci initial sync failed:", error);
80
+ }
81
+ }
82
+ async stop() {
83
+ if (this.interval) {
84
+ clearInterval(this.interval);
85
+ this.interval = null;
86
+ }
87
+ }
88
+ async pollSource(sourceId) {
89
+ return this.syncSource(sourceId, true);
90
+ }
91
+ status() {
92
+ return {
93
+ activeSourceIds: Array.from(this.inFlight.values()).sort(),
94
+ erroredSourceIds: Array.from(this.errorCounts.keys()).sort(),
95
+ };
96
+ }
97
+ async syncAll() {
98
+ const sources = this.store
99
+ .listSources()
100
+ .filter((source) => source.sourceType === "github_repo_ci" && source.status !== "paused");
101
+ for (const source of sources) {
102
+ try {
103
+ await this.syncSource(source.sourceId, false);
104
+ }
105
+ catch (error) {
106
+ console.warn(`github_repo_ci sync failed for ${source.sourceId}:`, error);
107
+ }
108
+ }
109
+ }
110
+ async syncSource(sourceId, force) {
111
+ if (this.inFlight.has(sourceId)) {
112
+ return {
113
+ sourceId,
114
+ sourceType: "github_repo_ci",
115
+ appended: 0,
116
+ deduped: 0,
117
+ eventsRead: 0,
118
+ note: "source sync already in flight",
119
+ };
120
+ }
121
+ this.inFlight.add(sourceId);
122
+ try {
123
+ const source = this.store.getSource(sourceId);
124
+ if (!source) {
125
+ throw new Error(`unknown source: ${sourceId}`);
126
+ }
127
+ const config = parseGithubCiSourceConfig(source);
128
+ if (!force) {
129
+ const retryAt = this.nextRetryAt.get(sourceId) ?? 0;
130
+ if (Date.now() < retryAt) {
131
+ return {
132
+ sourceId,
133
+ sourceType: "github_repo_ci",
134
+ appended: 0,
135
+ deduped: 0,
136
+ eventsRead: 0,
137
+ note: "error backoff not elapsed",
138
+ };
139
+ }
140
+ const lastPollAt = this.lastPollAt.get(sourceId) ?? 0;
141
+ const pollIntervalMs = (config.pollIntervalSecs ?? DEFAULT_POLL_INTERVAL_SECS) * 1000;
142
+ if (Date.now() - lastPollAt < pollIntervalMs) {
143
+ return {
144
+ sourceId,
145
+ sourceType: "github_repo_ci",
146
+ appended: 0,
147
+ deduped: 0,
148
+ eventsRead: 0,
149
+ note: "poll interval not elapsed",
150
+ };
151
+ }
152
+ }
153
+ this.lastPollAt.set(sourceId, Date.now());
154
+ const checkpoint = parseGithubCiCheckpoint(source.checkpoint);
155
+ const runs = await this.client.listWorkflowRuns(config);
156
+ let appended = 0;
157
+ let deduped = 0;
158
+ let lastSeenUpdatedAt = checkpoint.lastSeenUpdatedAt ?? undefined;
159
+ const seenKeys = new Set(checkpoint.seenRunKeys ?? []);
160
+ for (const run of runs.slice().reverse()) {
161
+ const normalized = normalizeGithubWorkflowRunEvent(source, config, run);
162
+ if (!normalized) {
163
+ continue;
164
+ }
165
+ const eventUpdatedAt = normalized.metadata?.updatedAt;
166
+ const runKey = `${normalized.sourceNativeId}:${normalized.eventVariant}`;
167
+ const shouldProcess = !lastSeenUpdatedAt ||
168
+ (typeof eventUpdatedAt === "string" && eventUpdatedAt > lastSeenUpdatedAt) ||
169
+ !seenKeys.has(runKey);
170
+ if (!shouldProcess) {
171
+ continue;
172
+ }
173
+ const result = await this.appendSourceEvent(normalized);
174
+ appended += result.appended;
175
+ deduped += result.deduped;
176
+ seenKeys.add(runKey);
177
+ if (typeof eventUpdatedAt === "string" && (!lastSeenUpdatedAt || eventUpdatedAt >= lastSeenUpdatedAt)) {
178
+ lastSeenUpdatedAt = eventUpdatedAt;
179
+ }
180
+ }
181
+ this.store.updateSourceRuntime(sourceId, {
182
+ status: "active",
183
+ checkpoint: JSON.stringify({
184
+ lastSeenUpdatedAt,
185
+ seenRunKeys: Array.from(seenKeys).slice(-MAX_SEEN_KEYS),
186
+ lastEventAt: new Date().toISOString(),
187
+ lastError: undefined,
188
+ }),
189
+ });
190
+ this.errorCounts.delete(sourceId);
191
+ this.nextRetryAt.delete(sourceId);
192
+ return {
193
+ sourceId,
194
+ sourceType: "github_repo_ci",
195
+ appended,
196
+ deduped,
197
+ eventsRead: runs.length,
198
+ note: `workflow runs fetched=${runs.length}`,
199
+ };
200
+ }
201
+ catch (error) {
202
+ const source = this.store.getSource(sourceId);
203
+ if (source) {
204
+ const checkpoint = parseGithubCiCheckpoint(source.checkpoint);
205
+ const config = parseGithubCiSourceConfig(source);
206
+ const nextErrorCount = (this.errorCounts.get(sourceId) ?? 0) + 1;
207
+ this.errorCounts.set(sourceId, nextErrorCount);
208
+ this.nextRetryAt.set(sourceId, Date.now() + computeErrorBackoffMs(config.pollIntervalSecs ?? DEFAULT_POLL_INTERVAL_SECS, nextErrorCount));
209
+ this.store.updateSourceRuntime(sourceId, {
210
+ status: "error",
211
+ checkpoint: JSON.stringify({
212
+ ...checkpoint,
213
+ lastError: error instanceof Error ? error.message : String(error),
214
+ }),
215
+ });
216
+ }
217
+ throw error;
218
+ }
219
+ finally {
220
+ this.inFlight.delete(sourceId);
221
+ }
222
+ }
223
+ }
224
+ exports.GithubCiSourceRuntime = GithubCiSourceRuntime;
225
+ function computeErrorBackoffMs(pollIntervalSecs, errorCount) {
226
+ const baseMs = Math.max(1, pollIntervalSecs) * 1000;
227
+ const multiplier = Math.min(2 ** Math.max(0, errorCount - 1), MAX_ERROR_BACKOFF_MULTIPLIER);
228
+ return baseMs * multiplier;
229
+ }
230
+ function normalizeGithubWorkflowRunEvent(source, config, raw) {
231
+ const run = asRecord(raw);
232
+ const runId = asNumber(run.id);
233
+ if (!runId) {
234
+ return null;
235
+ }
236
+ const workflowName = asString(run.name) ?? asString(run.display_title);
237
+ const conclusion = asString(run.conclusion);
238
+ const status = inferWorkflowRunStatus(run, conclusion);
239
+ const variant = buildWorkflowRunVariant(workflowName, status, conclusion);
240
+ const actor = asRecord(run.actor);
241
+ const headCommit = asRecord(run.head_commit);
242
+ return {
243
+ sourceId: source.sourceId,
244
+ sourceNativeId: `workflow_run:${runId}`,
245
+ eventVariant: variant,
246
+ occurredAt: asString(run.updated_at) ?? asString(run.created_at) ?? new Date().toISOString(),
247
+ metadata: {
248
+ provider: "github",
249
+ owner: config.owner,
250
+ repo: config.repo,
251
+ repoFullName: `${config.owner}/${config.repo}`,
252
+ workflowRunId: runId,
253
+ workflowId: asNumber(run.workflow_id),
254
+ name: workflowName,
255
+ displayTitle: asString(run.display_title),
256
+ status,
257
+ conclusion,
258
+ event: asString(run.event),
259
+ headSha: asString(run.head_sha),
260
+ headBranch: asString(run.head_branch),
261
+ runNumber: asNumber(run.run_number),
262
+ runAttempt: asNumber(run.run_attempt),
263
+ actor: asString(actor.login),
264
+ htmlUrl: asString(run.html_url),
265
+ createdAt: asString(run.created_at),
266
+ updatedAt: asString(run.updated_at),
267
+ commitMessage: asString(headCommit.message),
268
+ },
269
+ rawPayload: {
270
+ id: runId,
271
+ workflow_id: asNumber(run.workflow_id),
272
+ name: workflowName,
273
+ display_title: asString(run.display_title),
274
+ status,
275
+ conclusion,
276
+ event: asString(run.event),
277
+ head_sha: asString(run.head_sha),
278
+ head_branch: asString(run.head_branch),
279
+ run_number: asNumber(run.run_number),
280
+ run_attempt: asNumber(run.run_attempt),
281
+ html_url: asString(run.html_url),
282
+ actor: asString(actor.login),
283
+ head_commit: {
284
+ id: asString(headCommit.id),
285
+ message: asString(headCommit.message),
286
+ },
287
+ },
288
+ deliveryHandle: null,
289
+ };
290
+ }
291
+ function parseGithubCiSourceConfig(source) {
292
+ const config = source.config ?? {};
293
+ const owner = asString(config.owner);
294
+ const repo = asString(config.repo);
295
+ if (!owner || !repo) {
296
+ const [fallbackOwner, fallbackRepo] = source.sourceKey.split("/", 2);
297
+ if (!fallbackOwner || !fallbackRepo) {
298
+ throw new Error(`github_repo_ci source requires config.owner and config.repo: ${source.sourceId}`);
299
+ }
300
+ return {
301
+ owner: fallbackOwner,
302
+ repo: fallbackRepo,
303
+ uxcAuth: asString(config.uxcAuth) ?? asString(config.credentialRef) ?? undefined,
304
+ pollIntervalSecs: asNumber(config.pollIntervalSecs) ?? DEFAULT_POLL_INTERVAL_SECS,
305
+ perPage: asNumber(config.perPage) ?? DEFAULT_PER_PAGE,
306
+ eventFilter: asString(config.eventFilter) ?? undefined,
307
+ branch: asString(config.branch) ?? undefined,
308
+ statusFilter: asString(config.statusFilter) ?? undefined,
309
+ };
310
+ }
311
+ return {
312
+ owner,
313
+ repo,
314
+ uxcAuth: asString(config.uxcAuth) ?? asString(config.credentialRef) ?? undefined,
315
+ pollIntervalSecs: asNumber(config.pollIntervalSecs) ?? DEFAULT_POLL_INTERVAL_SECS,
316
+ perPage: asNumber(config.perPage) ?? DEFAULT_PER_PAGE,
317
+ eventFilter: asString(config.eventFilter) ?? undefined,
318
+ branch: asString(config.branch) ?? undefined,
319
+ statusFilter: asString(config.statusFilter) ?? undefined,
320
+ };
321
+ }
322
+ function parseGithubCiCheckpoint(checkpoint) {
323
+ if (!checkpoint) {
324
+ return {};
325
+ }
326
+ try {
327
+ return JSON.parse(checkpoint);
328
+ }
329
+ catch {
330
+ return {};
331
+ }
332
+ }
333
+ function asRecord(value) {
334
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
335
+ return {};
336
+ }
337
+ return value;
338
+ }
339
+ function asString(value) {
340
+ return typeof value === "string" ? value : null;
341
+ }
342
+ function asNumber(value) {
343
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
344
+ }
345
+ function inferWorkflowRunStatus(run, conclusion) {
346
+ return asString(run.status)
347
+ ?? (conclusion ? "completed" : null)
348
+ ?? "observed";
349
+ }
350
+ function buildWorkflowRunVariant(workflowName, status, conclusion) {
351
+ const parts = ["workflow_run"];
352
+ const workflowSlug = slugifyWorkflowName(workflowName);
353
+ if (workflowSlug) {
354
+ parts.push(workflowSlug);
355
+ }
356
+ parts.push(status);
357
+ if (conclusion) {
358
+ parts.push(conclusion);
359
+ }
360
+ return parts.join(".");
361
+ }
362
+ function slugifyWorkflowName(value) {
363
+ if (!value) {
364
+ return null;
365
+ }
366
+ const slug = value
367
+ .trim()
368
+ .toLowerCase()
369
+ .replace(/[^a-z0-9]+/g, "_")
370
+ .replace(/^_+|_+$/g, "");
371
+ return slug.length > 0 ? slug : null;
372
+ }