@holon-run/agentinbox 1.0.0-beta.2 → 1.0.0-beta.4

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.
@@ -6,9 +6,12 @@ exports.invokeGithubDeliveryOperation = invokeGithubDeliveryOperation;
6
6
  exports.normalizeGithubRepoEvent = normalizeGithubRepoEvent;
7
7
  exports.hydrateGithubRepoEventIfNeeded = hydrateGithubRepoEventIfNeeded;
8
8
  exports.canonicalGithubPullRequestRef = canonicalGithubPullRequestRef;
9
+ exports.canonicalGithubIssueRef = canonicalGithubIssueRef;
9
10
  exports.deriveGithubTrackedResource = deriveGithubTrackedResource;
10
11
  exports.githubSubscriptionShortcutSpec = githubSubscriptionShortcutSpec;
12
+ exports.githubFollowTemplateSpec = githubFollowTemplateSpec;
11
13
  exports.expandGithubSubscriptionShortcut = expandGithubSubscriptionShortcut;
14
+ exports.expandGithubFollowTemplate = expandGithubFollowTemplate;
12
15
  exports.projectGithubLifecycleSignal = projectGithubLifecycleSignal;
13
16
  exports.parseGithubSourceConfig = parseGithubSourceConfig;
14
17
  const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
@@ -88,6 +91,24 @@ class GithubUxcClient {
88
91
  options: { auth: input.auth },
89
92
  });
90
93
  }
94
+ async getPullRequestDetails(input) {
95
+ const response = await this.client.call({
96
+ endpoint: exports.GITHUB_ENDPOINT,
97
+ operation: "get:/repos/{owner}/{repo}/pulls/{pull_number}",
98
+ payload: {
99
+ owner: input.owner,
100
+ repo: input.repo,
101
+ pull_number: input.pullNumber,
102
+ },
103
+ options: { auth: input.auth },
104
+ });
105
+ const data = asRecord(response.data);
106
+ const head = asRecord(data.head);
107
+ return {
108
+ headRefName: asString(head.ref),
109
+ headRepositoryFullName: asString(asRecord(head.repo).full_name),
110
+ };
111
+ }
91
112
  }
92
113
  exports.GithubUxcClient = GithubUxcClient;
93
114
  class GithubDeliveryAdapter {
@@ -336,6 +357,10 @@ function canonicalGithubPullRequestRef(source, number) {
336
357
  const config = parseGithubSourceConfig(source);
337
358
  return `repo:${config.owner}/${config.repo}:pr:${number}`;
338
359
  }
360
+ function canonicalGithubIssueRef(source, number) {
361
+ const config = parseGithubSourceConfig(source);
362
+ return `repo:${config.owner}/${config.repo}:issue:${number}`;
363
+ }
339
364
  function deriveGithubTrackedResource(filter, source) {
340
365
  const metadata = asRecord(filter.metadata);
341
366
  const payload = asRecord(filter.payload);
@@ -356,7 +381,43 @@ function githubSubscriptionShortcutSpec() {
356
381
  ],
357
382
  }];
358
383
  }
359
- function expandGithubSubscriptionShortcut(input) {
384
+ function githubFollowTemplateSpec() {
385
+ const repoArgs = [
386
+ { name: "owner", type: "string", required: true, description: "GitHub repository owner." },
387
+ { name: "repo", type: "string", required: true, description: "GitHub repository name." },
388
+ ];
389
+ return [
390
+ {
391
+ templateId: "github.repo",
392
+ providerOrKind: "github",
393
+ label: "GitHub Repository",
394
+ description: "Follow a repository event stream.",
395
+ argsSchema: repoArgs,
396
+ },
397
+ {
398
+ templateId: "github.pr",
399
+ providerOrKind: "github",
400
+ label: "GitHub Pull Request",
401
+ description: "Follow one pull request and optionally its CI.",
402
+ argsSchema: [
403
+ ...repoArgs,
404
+ { name: "number", type: "number", required: true, description: "Pull request number." },
405
+ { name: "withCi", type: "boolean", required: false, description: "Also follow the repository CI stream for that pull request." },
406
+ ],
407
+ },
408
+ {
409
+ templateId: "github.issue",
410
+ providerOrKind: "github",
411
+ label: "GitHub Issue",
412
+ description: "Follow one issue without automatic terminal cleanup.",
413
+ argsSchema: [
414
+ ...repoArgs,
415
+ { name: "number", type: "number", required: true, description: "Issue number." },
416
+ ],
417
+ },
418
+ ];
419
+ }
420
+ async function expandGithubSubscriptionShortcut(input, client = new GithubUxcClient()) {
360
421
  if (input.name !== "pr") {
361
422
  return null;
362
423
  }
@@ -366,6 +427,9 @@ function expandGithubSubscriptionShortcut(input) {
366
427
  }
367
428
  const trackedResourceRef = canonicalGithubPullRequestRef(input.source, number);
368
429
  const withCi = input.args?.withCi === true;
430
+ const ciFilter = withCi
431
+ ? await buildGithubPrCiFilter(input.source, number, client)
432
+ : null;
369
433
  return {
370
434
  members: [
371
435
  {
@@ -382,6 +446,7 @@ function expandGithubSubscriptionShortcut(input) {
382
446
  ...(withCi
383
447
  ? [{
384
448
  streamKind: "ci_runs",
449
+ filter: ciFilter ?? undefined,
385
450
  trackedResourceRef,
386
451
  cleanupPolicy: { mode: "on_terminal" },
387
452
  }]
@@ -389,6 +454,152 @@ function expandGithubSubscriptionShortcut(input) {
389
454
  ],
390
455
  };
391
456
  }
457
+ async function expandGithubFollowTemplate(input, client = new GithubUxcClient()) {
458
+ const config = parseGithubSourceConfig(input.source);
459
+ if (input.template === "repo") {
460
+ return {
461
+ templateId: "github.repo",
462
+ sources: [
463
+ {
464
+ logicalName: "repo_events",
465
+ sourceType: "github_repo",
466
+ sourceKey: `${config.owner}/${config.repo}`,
467
+ configRef: input.source.configRef ?? null,
468
+ config: githubFollowSourceConfig(input.source),
469
+ },
470
+ ],
471
+ subscriptions: [
472
+ {
473
+ sourceLogicalName: "repo_events",
474
+ filter: {},
475
+ cleanupPolicy: { mode: "manual" },
476
+ },
477
+ ],
478
+ };
479
+ }
480
+ const number = typeof input.args?.number === "number" ? input.args.number : null;
481
+ if (input.template !== "pr" && input.template !== "issue") {
482
+ return null;
483
+ }
484
+ if (!number || !Number.isInteger(number) || number <= 0) {
485
+ throw new Error(`follow template github.${input.template} requires a positive integer number`);
486
+ }
487
+ if (input.template === "pr") {
488
+ const trackedResourceRef = canonicalGithubPullRequestRef(input.source, number);
489
+ const withCi = input.args?.withCi === true;
490
+ const ciFilter = withCi
491
+ ? await buildGithubPrCiFilter(input.source, number, client)
492
+ : null;
493
+ return {
494
+ templateId: "github.pr",
495
+ sources: [
496
+ {
497
+ logicalName: "repo_events",
498
+ sourceType: "github_repo",
499
+ sourceKey: `${config.owner}/${config.repo}`,
500
+ configRef: input.source.configRef ?? null,
501
+ config: githubFollowSourceConfig(input.source),
502
+ },
503
+ ...(withCi
504
+ ? [{
505
+ logicalName: "ci_runs",
506
+ sourceType: "github_repo_ci",
507
+ sourceKey: `${config.owner}/${config.repo}`,
508
+ configRef: input.source.configRef ?? null,
509
+ config: githubFollowSourceConfig(input.source),
510
+ }]
511
+ : []),
512
+ ],
513
+ subscriptions: [
514
+ {
515
+ sourceLogicalName: "repo_events",
516
+ filter: {
517
+ metadata: {
518
+ number,
519
+ isPullRequest: true,
520
+ },
521
+ },
522
+ trackedResourceRef,
523
+ cleanupPolicy: { mode: "on_terminal" },
524
+ },
525
+ ...(withCi
526
+ ? [{
527
+ sourceLogicalName: "ci_runs",
528
+ filter: ciFilter ?? undefined,
529
+ trackedResourceRef,
530
+ cleanupPolicy: { mode: "on_terminal" },
531
+ }]
532
+ : []),
533
+ ],
534
+ };
535
+ }
536
+ return {
537
+ templateId: "github.issue",
538
+ sources: [
539
+ {
540
+ logicalName: "repo_events",
541
+ sourceType: "github_repo",
542
+ sourceKey: `${config.owner}/${config.repo}`,
543
+ configRef: input.source.configRef ?? null,
544
+ config: githubFollowSourceConfig(input.source),
545
+ },
546
+ ],
547
+ subscriptions: [
548
+ {
549
+ sourceLogicalName: "repo_events",
550
+ filter: {
551
+ metadata: {
552
+ number,
553
+ isPullRequest: false,
554
+ },
555
+ },
556
+ trackedResourceRef: canonicalGithubIssueRef(input.source, number),
557
+ cleanupPolicy: { mode: "manual" },
558
+ },
559
+ ],
560
+ };
561
+ }
562
+ async function buildGithubPrCiFilter(source, number, client) {
563
+ const config = parseGithubSourceConfig(source);
564
+ const baseExpr = `contains(metadata.pullRequestNumbers, ${number})`;
565
+ try {
566
+ const details = await client.getPullRequestDetails({
567
+ owner: config.owner,
568
+ repo: config.repo,
569
+ pullNumber: number,
570
+ auth: config.uxcAuth,
571
+ });
572
+ if (!details.headRefName) {
573
+ throw new Error(`could not resolve head branch for PR #${number}`);
574
+ }
575
+ return {
576
+ expr: buildGithubPrCiFilterExpr(number, details.headRefName, details.headRepositoryFullName),
577
+ };
578
+ }
579
+ catch {
580
+ return { expr: baseExpr };
581
+ }
582
+ }
583
+ function buildGithubPrCiFilterExpr(number, headRefName, headRepositoryFullName) {
584
+ const base = `contains(metadata.pullRequestNumbers, ${number})`;
585
+ const branchExpr = `metadata.headBranch == ${JSON.stringify(headRefName)}`;
586
+ if (!headRepositoryFullName) {
587
+ return `${base} || ${branchExpr}`;
588
+ }
589
+ return [
590
+ base,
591
+ "||",
592
+ "(",
593
+ branchExpr,
594
+ "&&",
595
+ "(",
596
+ `metadata.headRepositoryFullName == ${JSON.stringify(headRepositoryFullName)}`,
597
+ "||",
598
+ "!exists(metadata.headRepositoryFullName)",
599
+ ")",
600
+ ")",
601
+ ].join(" ");
602
+ }
392
603
  function projectGithubLifecycleSignal(rawPayload, source) {
393
604
  const eventType = asString(rawPayload.type);
394
605
  if (eventType !== "PullRequestEvent") {
@@ -411,6 +622,12 @@ function projectGithubLifecycleSignal(rawPayload, source) {
411
622
  result: merged ? "merged" : "closed",
412
623
  };
413
624
  }
625
+ function githubFollowSourceConfig(source) {
626
+ const config = { ...(source.config ?? {}) };
627
+ delete config.number;
628
+ delete config.withCi;
629
+ return config;
630
+ }
414
631
  function buildGithubDeliveryHandle(config, eventType, number, comment) {
415
632
  if (!number) {
416
633
  return null;
@@ -7,6 +7,7 @@ const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
7
7
  exports.GITHUB_CI_ENDPOINT = "https://api.github.com";
8
8
  exports.DEFAULT_GITHUB_CI_POLL_INTERVAL_SECS = 30;
9
9
  exports.DEFAULT_GITHUB_CI_PER_PAGE = 20;
10
+ const MAX_PAGES_PER_SYNC = 10;
10
11
  const MAX_SEEN_KEYS = 512;
11
12
  const MAX_ERROR_BACKOFF_MULTIPLIER = 8;
12
13
  class GithubActionsUxcClient {
@@ -14,11 +15,12 @@ class GithubActionsUxcClient {
14
15
  constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
15
16
  this.client = client;
16
17
  }
17
- async listWorkflowRuns(config) {
18
+ async listWorkflowRuns(config, page = 1) {
18
19
  const payload = {
19
20
  owner: config.owner,
20
21
  repo: config.repo,
21
22
  per_page: config.perPage ?? exports.DEFAULT_GITHUB_CI_PER_PAGE,
23
+ page,
22
24
  };
23
25
  if (config.eventFilter) {
24
26
  payload.event = config.eventFilter;
@@ -153,30 +155,48 @@ class GithubCiSourceRuntime {
153
155
  }
154
156
  this.lastPollAt.set(sourceId, Date.now());
155
157
  const checkpoint = parseGithubCiCheckpoint(source.checkpoint);
156
- const runs = await this.client.listWorkflowRuns(config);
157
158
  let appended = 0;
158
159
  let deduped = 0;
159
- let lastSeenUpdatedAt = checkpoint.lastSeenUpdatedAt ?? undefined;
160
+ let eventsRead = 0;
161
+ let pageCount = 0;
162
+ const checkpointLastSeenUpdatedAt = checkpoint.lastSeenUpdatedAt ?? undefined;
163
+ let lastSeenUpdatedAt = checkpointLastSeenUpdatedAt;
160
164
  const seenKeys = new Set(checkpoint.seenRunKeys ?? []);
161
- for (const run of runs.slice().reverse()) {
162
- const normalized = normalizeGithubWorkflowRunEvent(source, config, run);
163
- if (!normalized) {
164
- continue;
165
+ const perPage = config.perPage ?? exports.DEFAULT_GITHUB_CI_PER_PAGE;
166
+ for (let page = 1; page <= MAX_PAGES_PER_SYNC; page += 1) {
167
+ const runs = await this.client.listWorkflowRuns(config, page);
168
+ pageCount = page;
169
+ eventsRead += runs.length;
170
+ let pageHasNewRuns = false;
171
+ for (const run of runs.slice().reverse()) {
172
+ const normalized = normalizeGithubWorkflowRunEvent(source, config, run);
173
+ if (!normalized) {
174
+ continue;
175
+ }
176
+ const eventUpdatedAt = normalized.metadata?.updatedAt;
177
+ const runKey = `${normalized.sourceNativeId}:${normalized.eventVariant}`;
178
+ const isNewerThanCheckpoint = typeof eventUpdatedAt === "string" &&
179
+ (!checkpointLastSeenUpdatedAt || eventUpdatedAt > checkpointLastSeenUpdatedAt);
180
+ const isAtCheckpointBoundary = typeof eventUpdatedAt === "string" &&
181
+ typeof checkpointLastSeenUpdatedAt === "string" &&
182
+ eventUpdatedAt === checkpointLastSeenUpdatedAt;
183
+ const shouldProcess = !checkpointLastSeenUpdatedAt ||
184
+ isNewerThanCheckpoint ||
185
+ (isAtCheckpointBoundary && !seenKeys.has(runKey));
186
+ if (!shouldProcess) {
187
+ continue;
188
+ }
189
+ pageHasNewRuns = true;
190
+ const result = await this.appendSourceEvent(normalized);
191
+ appended += result.appended;
192
+ deduped += result.deduped;
193
+ seenKeys.add(runKey);
194
+ if (typeof eventUpdatedAt === "string" && (!lastSeenUpdatedAt || eventUpdatedAt >= lastSeenUpdatedAt)) {
195
+ lastSeenUpdatedAt = eventUpdatedAt;
196
+ }
165
197
  }
166
- const eventUpdatedAt = normalized.metadata?.updatedAt;
167
- const runKey = `${normalized.sourceNativeId}:${normalized.eventVariant}`;
168
- const shouldProcess = !lastSeenUpdatedAt ||
169
- (typeof eventUpdatedAt === "string" && eventUpdatedAt > lastSeenUpdatedAt) ||
170
- !seenKeys.has(runKey);
171
- if (!shouldProcess) {
172
- continue;
173
- }
174
- const result = await this.appendSourceEvent(normalized);
175
- appended += result.appended;
176
- deduped += result.deduped;
177
- seenKeys.add(runKey);
178
- if (typeof eventUpdatedAt === "string" && (!lastSeenUpdatedAt || eventUpdatedAt >= lastSeenUpdatedAt)) {
179
- lastSeenUpdatedAt = eventUpdatedAt;
198
+ if (!pageHasNewRuns || runs.length < perPage) {
199
+ break;
180
200
  }
181
201
  }
182
202
  this.store.updateSourceRuntime(sourceId, {
@@ -195,8 +215,8 @@ class GithubCiSourceRuntime {
195
215
  sourceType: "github_repo_ci",
196
216
  appended,
197
217
  deduped,
198
- eventsRead: runs.length,
199
- note: `workflow runs fetched=${runs.length}`,
218
+ eventsRead,
219
+ note: `workflow runs fetched=${eventsRead} pages=${pageCount}`,
200
220
  };
201
221
  }
202
222
  catch (error) {
@@ -240,6 +260,8 @@ function normalizeGithubWorkflowRunEvent(source, config, raw) {
240
260
  const variant = buildWorkflowRunVariant(workflowName, status, conclusion);
241
261
  const actor = asRecord(run.actor);
242
262
  const headCommit = asRecord(run.head_commit);
263
+ const headRepository = asRecord(run.head_repository);
264
+ const pullRequestNumbers = extractPullRequestNumbers(run.pull_requests);
243
265
  return {
244
266
  sourceId: source.sourceId,
245
267
  sourceNativeId: `workflow_run:${runId}`,
@@ -259,6 +281,8 @@ function normalizeGithubWorkflowRunEvent(source, config, raw) {
259
281
  event: asString(run.event),
260
282
  headSha: asString(run.head_sha),
261
283
  headBranch: asString(run.head_branch),
284
+ headRepositoryFullName: asString(headRepository.full_name),
285
+ pullRequestNumbers,
262
286
  runNumber: asNumber(run.run_number),
263
287
  runAttempt: asNumber(run.run_attempt),
264
288
  actor: asString(actor.login),
@@ -277,6 +301,10 @@ function normalizeGithubWorkflowRunEvent(source, config, raw) {
277
301
  event: asString(run.event),
278
302
  head_sha: asString(run.head_sha),
279
303
  head_branch: asString(run.head_branch),
304
+ head_repository: {
305
+ full_name: asString(headRepository.full_name),
306
+ },
307
+ pull_requests: pullRequestNumbers.map((number) => ({ number })),
280
308
  run_number: asNumber(run.run_number),
281
309
  run_attempt: asNumber(run.run_attempt),
282
310
  html_url: asString(run.html_url),
@@ -343,6 +371,19 @@ function asString(value) {
343
371
  function asNumber(value) {
344
372
  return typeof value === "number" && Number.isFinite(value) ? value : null;
345
373
  }
374
+ function extractPullRequestNumbers(value) {
375
+ if (!Array.isArray(value)) {
376
+ return [];
377
+ }
378
+ const numbers = new Set();
379
+ for (const entry of value) {
380
+ const number = asNumber(asRecord(entry).number);
381
+ if (number && Number.isInteger(number) && number > 0) {
382
+ numbers.add(number);
383
+ }
384
+ }
385
+ return Array.from(numbers).sort((left, right) => left - right);
386
+ }
346
387
  function inferWorkflowRunStatus(run, conclusion) {
347
388
  return asString(run.status)
348
389
  ?? (conclusion ? "completed" : null)
@@ -40,6 +40,12 @@ class RpcUxcRemoteSourceClient {
40
40
  async sourceStop(namespace, sourceKey) {
41
41
  await this.client.sourceStop(namespace, sourceKey);
42
42
  }
43
+ sourceStatus(namespace, sourceKey) {
44
+ return this.client.sourceStatus(namespace, sourceKey);
45
+ }
46
+ sourceList() {
47
+ return this.client.sourceList();
48
+ }
43
49
  async sourceDelete(namespace, sourceKey) {
44
50
  await this.client.sourceDelete(namespace, sourceKey);
45
51
  }
@@ -131,6 +137,8 @@ class RemoteSourceRuntime {
131
137
  this.errorCounts.delete(sourceId);
132
138
  this.nextRetryAt.delete(sourceId);
133
139
  const checkpoint = parseRemoteCheckpoint(source.checkpoint);
140
+ const pausedAt = (0, util_1.nowIso)();
141
+ const baseRuntime = checkpoint.managedRuntime ?? checkpointRuntimeFromSource(source, checkpoint) ?? {};
134
142
  this.store.updateSourceRuntime(sourceId, {
135
143
  status: "paused",
136
144
  checkpoint: JSON.stringify({
@@ -142,6 +150,13 @@ class RemoteSourceRuntime {
142
150
  streamId: checkpoint.managedSource.streamId,
143
151
  },
144
152
  } : {}),
153
+ managedRuntime: {
154
+ ...baseRuntime,
155
+ status: "stopped",
156
+ updatedAt: pausedAt,
157
+ stoppedAt: pausedAt,
158
+ lastError: null,
159
+ },
145
160
  lastError: null,
146
161
  }),
147
162
  });
@@ -167,6 +182,47 @@ class RemoteSourceRuntime {
167
182
  erroredSourceIds: Array.from(this.errorCounts.keys()).sort(),
168
183
  };
169
184
  }
185
+ async getSourceRuntime(source) {
186
+ if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
187
+ return null;
188
+ }
189
+ const binding = managedBindingForSource(source);
190
+ try {
191
+ const managed = await this.client.sourceStatus(binding.namespace, binding.sourceKey);
192
+ return runtimeStateFromManagedView(managed, "live");
193
+ }
194
+ catch {
195
+ return runtimeStateFromCheckpoint(source);
196
+ }
197
+ }
198
+ async listSourceRuntimes(sources) {
199
+ const result = new Map();
200
+ const remoteSources = sources.filter((source) => REMOTE_SOURCE_TYPES.has(source.sourceType));
201
+ for (const source of sources) {
202
+ if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
203
+ result.set(source.sourceId, null);
204
+ }
205
+ }
206
+ if (remoteSources.length === 0) {
207
+ return result;
208
+ }
209
+ try {
210
+ const managedSources = await this.client.sourceList();
211
+ const byBinding = new Map(managedSources.map((entry) => [managedSourceBindingKey(entry.namespace, entry.source_key), entry]));
212
+ for (const source of remoteSources) {
213
+ const binding = managedBindingForSource(source);
214
+ const live = byBinding.get(managedSourceBindingKey(binding.namespace, binding.sourceKey));
215
+ result.set(source.sourceId, live ? runtimeStateFromManagedSourceListEntry(live, "live") : runtimeStateFromCheckpoint(source));
216
+ }
217
+ return result;
218
+ }
219
+ catch {
220
+ for (const source of remoteSources) {
221
+ result.set(source.sourceId, runtimeStateFromCheckpoint(source));
222
+ }
223
+ return result;
224
+ }
225
+ }
170
226
  async projectLifecycleSignal(source, rawPayload) {
171
227
  if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
172
228
  return null;
@@ -185,13 +241,37 @@ class RemoteSourceRuntime {
185
241
  if (typeof module.expandSubscriptionShortcut !== "function") {
186
242
  return null;
187
243
  }
188
- const expanded = module.expandSubscriptionShortcut({
244
+ const expanded = await module.expandSubscriptionShortcut({
189
245
  name: input.name,
190
246
  args: input.args,
191
247
  source: moduleInputSource(source),
192
248
  });
193
249
  return normalizeExpandedSubscriptionPlan(expanded);
194
250
  }
251
+ async listFollowTemplates(source) {
252
+ if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
253
+ return [];
254
+ }
255
+ const module = await this.moduleRegistry.resolve(source, this.homeDir);
256
+ if (typeof module.listFollowTemplates !== "function") {
257
+ return [];
258
+ }
259
+ return module.listFollowTemplates(moduleInputSource(source));
260
+ }
261
+ async expandFollowTemplate(source, input) {
262
+ if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
263
+ return null;
264
+ }
265
+ const module = await this.moduleRegistry.resolve(source, this.homeDir);
266
+ if (typeof module.expandFollowTemplate !== "function") {
267
+ return null;
268
+ }
269
+ return module.expandFollowTemplate({
270
+ template: input.template,
271
+ args: input.args,
272
+ source: moduleInputSource(source),
273
+ });
274
+ }
195
275
  async deriveInlinePreview(source, item) {
196
276
  if (!REMOTE_SOURCE_TYPES.has(source.sourceType)) {
197
277
  return null;
@@ -331,6 +411,7 @@ class RemoteSourceRuntime {
331
411
  streamCursor: {
332
412
  afterOffset: batch.next_after_offset,
333
413
  },
414
+ managedRuntime: checkpointRuntimeFromEnsureResponse(ensured, checkpoint.managedRuntime ?? null, (0, util_1.nowIso)()),
334
415
  lastEventAt: (0, util_1.nowIso)(),
335
416
  lastError: null,
336
417
  }),
@@ -357,6 +438,7 @@ class RemoteSourceRuntime {
357
438
  status: source.status === "paused" ? "paused" : "error",
358
439
  checkpoint: JSON.stringify({
359
440
  ...checkpoint,
441
+ managedRuntime: checkpointRuntimeWithError(checkpoint.managedRuntime ?? checkpointRuntimeFromSource(source, checkpoint), error instanceof Error ? error.message : String(error), (0, util_1.nowIso)()),
360
442
  lastError: error instanceof Error ? error.message : String(error),
361
443
  }),
362
444
  });
@@ -419,6 +501,9 @@ function managedBindingForSource(source) {
419
501
  sourceKey: checkpoint.managedSource?.sourceKey ?? defaultBinding.sourceKey,
420
502
  };
421
503
  }
504
+ function managedSourceBindingKey(namespace, sourceKey) {
505
+ return `${namespace}:${sourceKey}`;
506
+ }
422
507
  function parseRemoteCheckpoint(checkpoint) {
423
508
  if (!checkpoint) {
424
509
  return {};
@@ -430,6 +515,103 @@ function parseRemoteCheckpoint(checkpoint) {
430
515
  return {};
431
516
  }
432
517
  }
518
+ function runtimeStateFromManagedView(managed, observation) {
519
+ return {
520
+ backend: "uxc",
521
+ observation,
522
+ namespace: managed.namespace,
523
+ sourceKey: managed.source_key,
524
+ status: managed.status,
525
+ runId: managed.run_id,
526
+ streamId: managed.stream_id,
527
+ lastError: managed.last_error ?? null,
528
+ updatedAt: unixToIso(managed.updated_at_unix),
529
+ startedAt: unixToIso(managed.started_at_unix ?? null),
530
+ stoppedAt: unixToIso(managed.stopped_at_unix ?? null),
531
+ };
532
+ }
533
+ function runtimeStateFromManagedSourceListEntry(managed, observation) {
534
+ return {
535
+ backend: "uxc",
536
+ observation,
537
+ namespace: managed.namespace,
538
+ sourceKey: managed.source_key,
539
+ status: managed.status,
540
+ runId: managed.run_id,
541
+ streamId: managed.stream_id,
542
+ lastError: managed.last_error ?? null,
543
+ updatedAt: unixToIso(managed.updated_at_unix),
544
+ startedAt: null,
545
+ stoppedAt: null,
546
+ };
547
+ }
548
+ function runtimeStateFromCheckpoint(source) {
549
+ const checkpoint = parseRemoteCheckpoint(source.checkpoint);
550
+ const binding = managedBindingForSource(source);
551
+ const runtime = checkpoint.managedRuntime;
552
+ const streamId = runtime?.streamId ?? checkpoint.managedSource?.streamId ?? null;
553
+ if (!runtime && !checkpoint.managedSource) {
554
+ return null;
555
+ }
556
+ return {
557
+ backend: "uxc",
558
+ observation: "cached",
559
+ namespace: binding.namespace,
560
+ sourceKey: binding.sourceKey,
561
+ status: runtime?.status ?? null,
562
+ runId: runtime?.runId ?? null,
563
+ streamId,
564
+ lastError: runtime?.lastError ?? checkpoint.lastError ?? null,
565
+ updatedAt: runtime?.updatedAt ?? source.updatedAt,
566
+ startedAt: runtime?.startedAt ?? null,
567
+ stoppedAt: runtime?.stoppedAt ?? null,
568
+ };
569
+ }
570
+ function checkpointRuntimeFromEnsureResponse(ensured, previous, observedAt) {
571
+ return {
572
+ status: ensured.status,
573
+ runId: ensured.run_id,
574
+ streamId: ensured.stream_id,
575
+ lastError: null,
576
+ updatedAt: observedAt,
577
+ startedAt: previous?.runId === ensured.run_id ? previous.startedAt ?? null : null,
578
+ stoppedAt: null,
579
+ };
580
+ }
581
+ function checkpointRuntimeWithError(previous, lastError, observedAt) {
582
+ return {
583
+ status: previous?.status ?? null,
584
+ runId: previous?.runId ?? null,
585
+ streamId: previous?.streamId ?? null,
586
+ lastError,
587
+ updatedAt: observedAt,
588
+ startedAt: previous?.startedAt ?? null,
589
+ stoppedAt: previous?.stoppedAt ?? null,
590
+ };
591
+ }
592
+ function checkpointRuntimeFromSource(source, checkpoint = parseRemoteCheckpoint(source.checkpoint)) {
593
+ if (checkpoint.managedRuntime) {
594
+ return checkpoint.managedRuntime;
595
+ }
596
+ if (!checkpoint.managedSource) {
597
+ return null;
598
+ }
599
+ return {
600
+ status: null,
601
+ runId: null,
602
+ streamId: checkpoint.managedSource.streamId,
603
+ lastError: checkpoint.lastError ?? null,
604
+ updatedAt: source.updatedAt,
605
+ startedAt: null,
606
+ stoppedAt: null,
607
+ };
608
+ }
609
+ function unixToIso(value) {
610
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
611
+ return null;
612
+ }
613
+ return new Date(value * 1000).toISOString();
614
+ }
433
615
  function computeErrorBackoffMs(baseIntervalSecs, errorCount) {
434
616
  const baseMs = Math.max(1, baseIntervalSecs) * 1000;
435
617
  const multiplier = Math.min(2 ** Math.max(0, errorCount - 1), MAX_ERROR_BACKOFF_MULTIPLIER);