@opengeni/worker-bundle 0.27.3 → 0.28.1-canary.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.
- package/dist/activities/mcp-credentials.d.ts +3 -1
- package/dist/activities/workspace-capture.d.ts +12 -1
- package/dist/{activities-control-WZRFEIJH.js → activities-control-VVK7RA7F.js} +5 -5
- package/dist/{activities-control-WZRFEIJH.js.map → activities-control-VVK7RA7F.js.map} +1 -1
- package/dist/{activities-turn-V7J34ZOF.js → activities-turn-FOIZYY56.js} +250 -174
- package/dist/activities-turn-FOIZYY56.js.map +1 -0
- package/dist/{chunk-C54VDT6I.js → chunk-AKP6BKM2.js} +3 -1
- package/dist/chunk-AKP6BKM2.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +17 -17
- package/src/activities/agent-turn/agent-build.ts +30 -1
- package/src/activities/agent-turn/finalization.ts +52 -36
- package/src/activities/agent-turn/skill-read.ts +2 -21
- package/src/activities/agent-turn/tool-environment.ts +52 -10
- package/src/activities/mcp-credentials.ts +42 -2
- package/src/activities/scheduled-tasks.ts +8 -6
- package/src/activities/workspace-capture.ts +148 -126
- package/src/pr-review-credentials.ts +2 -0
- package/dist/activities-turn-V7J34ZOF.js.map +0 -1
- package/dist/chunk-C54VDT6I.js.map +0 -1
|
@@ -346,6 +346,62 @@ export async function readCaptureRepository(
|
|
|
346
346
|
|
|
347
347
|
let loggedStorageNullOnce = false;
|
|
348
348
|
|
|
349
|
+
/** Review work may use idle time, but must yield to the next turn. Poll durable
|
|
350
|
+
* queue state rather than relying on best-effort event delivery. The capture
|
|
351
|
+
* already fences every continuation on this signal; no replacement writer is
|
|
352
|
+
* admitted until its caller returns. */
|
|
353
|
+
export async function captureWhileIdle(input: {
|
|
354
|
+
hasPendingWork: () => Promise<boolean>;
|
|
355
|
+
capture: (signal: AbortSignal) => Promise<void>;
|
|
356
|
+
signal?: AbortSignal;
|
|
357
|
+
}): Promise<void> {
|
|
358
|
+
if (input.signal?.aborted) return;
|
|
359
|
+
const controller = new AbortController();
|
|
360
|
+
const signal = input.signal
|
|
361
|
+
? AbortSignal.any([input.signal, controller.signal])
|
|
362
|
+
: controller.signal;
|
|
363
|
+
let checking = false;
|
|
364
|
+
let stopped = false;
|
|
365
|
+
const check = async (): Promise<void> => {
|
|
366
|
+
if (checking || stopped || signal.aborted) return;
|
|
367
|
+
checking = true;
|
|
368
|
+
try {
|
|
369
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
370
|
+
let onAbort: (() => void) | undefined;
|
|
371
|
+
try {
|
|
372
|
+
const pending = await Promise.race([
|
|
373
|
+
input.hasPendingWork(),
|
|
374
|
+
new Promise<boolean>((resolve) => {
|
|
375
|
+
onAbort = () => resolve(true);
|
|
376
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
377
|
+
timer = setTimeout(() => resolve(true), 1_000);
|
|
378
|
+
}),
|
|
379
|
+
]);
|
|
380
|
+
if (pending) controller.abort();
|
|
381
|
+
} finally {
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
// A failed queue read is not permission to delay accepted user work.
|
|
387
|
+
controller.abort();
|
|
388
|
+
} finally {
|
|
389
|
+
checking = false;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
await check();
|
|
393
|
+
if (signal.aborted) return;
|
|
394
|
+
const timer = setInterval(() => void check(), 250);
|
|
395
|
+
timer.unref?.();
|
|
396
|
+
try {
|
|
397
|
+
await input.capture(signal);
|
|
398
|
+
} finally {
|
|
399
|
+
stopped = true;
|
|
400
|
+
clearInterval(timer);
|
|
401
|
+
controller.abort();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
349
405
|
/**
|
|
350
406
|
* Turn-end entrypoint. Gates on the flag + configured object storage, races the
|
|
351
407
|
* whole capture against a 120s cap, and swallows every failure ("turn outcome
|
|
@@ -399,13 +455,25 @@ export async function captureWorkspaceRevision(
|
|
|
399
455
|
name: "opengeni_workspace_captures_inflight",
|
|
400
456
|
help: "Current workspace-capture operations still resident in this worker process.",
|
|
401
457
|
});
|
|
458
|
+
const ownership = { keys: new Set<string>(), retain: false };
|
|
402
459
|
const capture = runCapture(
|
|
403
460
|
input,
|
|
404
461
|
{ ...input, objectStorage: input.objectStorage },
|
|
405
462
|
startedAt,
|
|
406
463
|
controller.signal,
|
|
407
464
|
stage,
|
|
408
|
-
|
|
465
|
+
ownership,
|
|
466
|
+
).finally(async () => {
|
|
467
|
+
// Cleanup follows physical write settlement, even after inference resumes.
|
|
468
|
+
// Unique keys cannot belong to a successor capture.
|
|
469
|
+
if (!ownership.retain && ownership.keys.size > 0) {
|
|
470
|
+
await safeDelete(
|
|
471
|
+
input.objectStorage!,
|
|
472
|
+
[...ownership.keys],
|
|
473
|
+
observability,
|
|
474
|
+
new AbortController().signal,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
409
477
|
observability.incrementGauge({
|
|
410
478
|
name: "opengeni_workspace_captures_inflight",
|
|
411
479
|
help: "Current workspace-capture operations still resident in this worker process.",
|
|
@@ -434,13 +502,25 @@ export async function captureWorkspaceRevision(
|
|
|
434
502
|
}
|
|
435
503
|
// The one and only place a capture failure surfaces — as a log line, never
|
|
436
504
|
// an exception past this boundary (the turn already completed).
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
"
|
|
443
|
-
|
|
505
|
+
// Public telemetry intentionally strips arbitrary error text and unknown
|
|
506
|
+
// attributes. Keep the bounded stage/reason in the message so operators can
|
|
507
|
+
// distinguish a deadline from provider loss without logging file contents,
|
|
508
|
+
// credentials, paths, or upstream exception messages.
|
|
509
|
+
const reason = controller.signal.aborted
|
|
510
|
+
? "deadline_exceeded"
|
|
511
|
+
: isBoxExitingError(error)
|
|
512
|
+
? "sandbox_exited"
|
|
513
|
+
: "operation_failed";
|
|
514
|
+
observability.warn(
|
|
515
|
+
`workspace capture failed — stage=${stage.value} reason=${reason} durationMs=${Date.now() - startedAt}; review cache not updated`,
|
|
516
|
+
{
|
|
517
|
+
"opengeni.session_id": input.sessionId,
|
|
518
|
+
"opengeni.turn_id": input.turnId ?? "",
|
|
519
|
+
"error.message": error instanceof Error ? error.message : String(error),
|
|
520
|
+
"workspace_capture.duration_ms": Date.now() - startedAt,
|
|
521
|
+
"workspace_capture.stage": stage.value,
|
|
522
|
+
},
|
|
523
|
+
);
|
|
444
524
|
observability.incrementCounter({
|
|
445
525
|
name: "opengeni_workspace_capture_total",
|
|
446
526
|
labels: { result: "failed" },
|
|
@@ -459,7 +539,9 @@ async function runCapture(
|
|
|
459
539
|
startedAt: number,
|
|
460
540
|
signal: AbortSignal,
|
|
461
541
|
stage: { value: string },
|
|
542
|
+
ownership: { keys: Set<string>; retain: boolean },
|
|
462
543
|
): Promise<void> {
|
|
544
|
+
const ownedKeys = ownership.keys;
|
|
463
545
|
const { observability } = input;
|
|
464
546
|
const storage = ctx.objectStorage;
|
|
465
547
|
const keepN = input.keepLatest ?? KEEP_LATEST_REVISIONS;
|
|
@@ -581,11 +663,20 @@ async function runCapture(
|
|
|
581
663
|
// dirty tree on every read-only turn. Instead we skip when the change surface
|
|
582
664
|
// is byte-identical to the previous revision — "no new revision when nothing
|
|
583
665
|
// changed" holds even with a persistently dirty tree, and content-addressed
|
|
584
|
-
// blobs
|
|
666
|
+
// blobs dedupe against the latest retained capture. This preserves empty-turn revision behavior
|
|
585
667
|
// while correctly handling a persistently dirty tree.
|
|
586
668
|
// ── 3. after-images of touched files (size-gated), content-addressed ───────
|
|
587
669
|
const files: WorkspaceCaptureFile[] = [];
|
|
588
670
|
const blobKeys = new Set<string>();
|
|
671
|
+
const captureId = crypto.randomUUID();
|
|
672
|
+
// A predecessor GC plan keeps the latest revision's refs. Never reuse refs
|
|
673
|
+
// from older revisions: an already-issued delete may still target those.
|
|
674
|
+
const retainedRefs = retainedCaptureBlobRefs(
|
|
675
|
+
input.workspaceId,
|
|
676
|
+
input.sessionId,
|
|
677
|
+
prev?.blobKeys ?? [],
|
|
678
|
+
);
|
|
679
|
+
const storedKeys = new Set(retainedRefs.values());
|
|
589
680
|
let totalBytes = 0;
|
|
590
681
|
let tooLargeCount = 0;
|
|
591
682
|
let binaryCount = 0;
|
|
@@ -626,6 +717,7 @@ async function runCapture(
|
|
|
626
717
|
});
|
|
627
718
|
throwIfCaptureAborted(signal);
|
|
628
719
|
} catch (error) {
|
|
720
|
+
if (signal.aborted) throw error;
|
|
629
721
|
const boxExiting = classifyCaptureEntryError(error);
|
|
630
722
|
if (boxExiting) throw boxExiting;
|
|
631
723
|
continue; // vanished/unreadable single file — omit it, capture continues
|
|
@@ -647,11 +739,12 @@ async function runCapture(
|
|
|
647
739
|
}
|
|
648
740
|
const bytes = Buffer.from(read.content, "base64");
|
|
649
741
|
const hash = sha256(bytes);
|
|
650
|
-
const contentRef =
|
|
742
|
+
const contentRef =
|
|
743
|
+
retainedRefs.get(hash) ?? blobKey(input.workspaceId, input.sessionId, `${captureId}/${hash}`);
|
|
651
744
|
if (read.isBinary) binaryCount += 1;
|
|
652
745
|
if (!blobKeys.has(contentRef)) {
|
|
653
746
|
totalBytes += bytes.byteLength;
|
|
654
|
-
//
|
|
747
|
+
// Never publish a revision exceeding the whole-capture byte limit.
|
|
655
748
|
if (totalBytes > WHOLE_CAPTURE_GUARD_BYTES) {
|
|
656
749
|
observability.warn("workspace capture skipped — whole-capture guard tripped", {
|
|
657
750
|
"opengeni.session_id": input.sessionId,
|
|
@@ -665,6 +758,17 @@ async function runCapture(
|
|
|
665
758
|
}
|
|
666
759
|
}
|
|
667
760
|
blobKeys.add(contentRef);
|
|
761
|
+
if (!storedKeys.has(contentRef)) {
|
|
762
|
+
stage.value = "content_upload";
|
|
763
|
+
ownedKeys.add(contentRef);
|
|
764
|
+
await storage.putObject({
|
|
765
|
+
key: contentRef,
|
|
766
|
+
contentType: "application/octet-stream",
|
|
767
|
+
body: bytes,
|
|
768
|
+
});
|
|
769
|
+
throwIfCaptureAborted(signal);
|
|
770
|
+
storedKeys.add(contentRef);
|
|
771
|
+
}
|
|
668
772
|
files.push({
|
|
669
773
|
// baseHash (git HEAD blob sha) is intentionally null in M1: the wake-on-edit
|
|
670
774
|
// flush guard (M3/C2) compares live content against the after-image `hash`,
|
|
@@ -684,10 +788,11 @@ async function runCapture(
|
|
|
684
788
|
|
|
685
789
|
// ── 4. empty-turn gate (B3) ───────────────────────────────
|
|
686
790
|
// Skip when the change surface is byte-identical to the previous revision.
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
|
|
791
|
+
// New writes use unique keys; reused refs belong to the latest retained revision.
|
|
792
|
+
// An unchanged dirty turn therefore performs no object-storage writes.
|
|
793
|
+
// Each file is read once and its exact bytes are hashed and uploaded together;
|
|
794
|
+
// peak memory remains one guarded file rather than the entire workspace.
|
|
795
|
+
const fingerprint = changeFingerprint(repos, files);
|
|
691
796
|
if (prev && prev.stats.fingerprint === fingerprint) {
|
|
692
797
|
observability.incrementCounter({
|
|
693
798
|
name: "opengeni_workspace_capture_total",
|
|
@@ -703,117 +808,11 @@ async function runCapture(
|
|
|
703
808
|
|
|
704
809
|
// ── 6. PUT blobs + tree + manifest (F9: all writes BEFORE any delete) ──────
|
|
705
810
|
// Key manifest/tree by the turn (one capture per turn) so the key is known
|
|
706
|
-
// before
|
|
707
|
-
const turnKey = input.turnId
|
|
811
|
+
// before commit. Unique capture namespaces make late deletes safe across workers.
|
|
812
|
+
const turnKey = `${input.turnId}/${captureId}`;
|
|
708
813
|
const treeKey = `workspace-captures/${input.workspaceId}/${input.sessionId}/trees/${turnKey}.json`;
|
|
709
814
|
const manifestKey = `workspace-captures/${input.workspaceId}/${input.sessionId}/manifests/${turnKey}.json`;
|
|
710
815
|
|
|
711
|
-
// Re-read and upload ONE after-image at a time. The first pass above computes
|
|
712
|
-
// the stable capture fingerprint without retaining every changed file. This
|
|
713
|
-
// second pass takes the actual after-image that will be uploaded, refreshing
|
|
714
|
-
// metadata when a live process changed a file between passes. Peak worker
|
|
715
|
-
// memory is bounded by PER_FILE_CONTENT_GUARD_BYTES instead of
|
|
716
|
-
// WHOLE_CAPTURE_GUARD_BYTES per concurrent turn.
|
|
717
|
-
const uploadedKeys = new Set<string>();
|
|
718
|
-
blobKeys.clear();
|
|
719
|
-
totalBytes = 0;
|
|
720
|
-
let refreshedFiles = 0;
|
|
721
|
-
const omittedFiles = new Set<string>();
|
|
722
|
-
for (const file of files) {
|
|
723
|
-
stage.value = "content_upload";
|
|
724
|
-
throwIfCaptureAborted(signal);
|
|
725
|
-
if (file.deleted || file.tooLarge || !file.contentRef || !file.hash) {
|
|
726
|
-
continue;
|
|
727
|
-
}
|
|
728
|
-
let reread: Awaited<ReturnType<typeof svc.fsRead>>;
|
|
729
|
-
try {
|
|
730
|
-
reread = await svc.fsRead({
|
|
731
|
-
path: file.path,
|
|
732
|
-
encoding: "base64",
|
|
733
|
-
maxBytes: PER_FILE_CONTENT_GUARD_BYTES,
|
|
734
|
-
});
|
|
735
|
-
throwIfCaptureAborted(signal);
|
|
736
|
-
} catch (error) {
|
|
737
|
-
if (signal.aborted) throw error;
|
|
738
|
-
const boxExiting = classifyCaptureEntryError(error);
|
|
739
|
-
if (boxExiting) throw boxExiting;
|
|
740
|
-
// Match the first pass: a single vanished or unreadable file is omitted,
|
|
741
|
-
// never misreported as deleted and never allowed to fail the full capture.
|
|
742
|
-
omittedFiles.add(file.path);
|
|
743
|
-
refreshedFiles += 1;
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
if (reread.truncated) {
|
|
747
|
-
file.hash = null;
|
|
748
|
-
file.contentRef = null;
|
|
749
|
-
file.sizeBytes = reread.sizeBytes;
|
|
750
|
-
file.isBinary = reread.isBinary;
|
|
751
|
-
file.tooLarge = true;
|
|
752
|
-
refreshedFiles += 1;
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
const bytes = Buffer.from(reread.content, "base64");
|
|
756
|
-
const hash = sha256(bytes);
|
|
757
|
-
const contentRef = blobKey(input.workspaceId, input.sessionId, hash);
|
|
758
|
-
if (
|
|
759
|
-
hash !== file.hash ||
|
|
760
|
-
bytes.byteLength !== file.sizeBytes ||
|
|
761
|
-
reread.isBinary !== file.isBinary
|
|
762
|
-
) {
|
|
763
|
-
file.hash = hash;
|
|
764
|
-
file.contentRef = contentRef;
|
|
765
|
-
file.sizeBytes = bytes.byteLength;
|
|
766
|
-
file.isBinary = reread.isBinary;
|
|
767
|
-
refreshedFiles += 1;
|
|
768
|
-
}
|
|
769
|
-
if (!blobKeys.has(contentRef)) {
|
|
770
|
-
totalBytes += bytes.byteLength;
|
|
771
|
-
if (totalBytes > WHOLE_CAPTURE_GUARD_BYTES) {
|
|
772
|
-
observability.warn("workspace capture skipped — whole-capture guard tripped", {
|
|
773
|
-
"opengeni.session_id": input.sessionId,
|
|
774
|
-
"workspace_capture.total_bytes": totalBytes,
|
|
775
|
-
});
|
|
776
|
-
observability.incrementCounter({
|
|
777
|
-
name: "opengeni_workspace_capture_total",
|
|
778
|
-
labels: { result: "guard_tripped" },
|
|
779
|
-
});
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
blobKeys.add(contentRef);
|
|
784
|
-
if (!uploadedKeys.has(contentRef)) {
|
|
785
|
-
await storage.putObject({
|
|
786
|
-
key: contentRef,
|
|
787
|
-
contentType: "application/octet-stream",
|
|
788
|
-
body: bytes,
|
|
789
|
-
});
|
|
790
|
-
throwIfCaptureAborted(signal);
|
|
791
|
-
uploadedKeys.add(contentRef);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
if (omittedFiles.size > 0) {
|
|
795
|
-
for (let index = files.length - 1; index >= 0; index -= 1) {
|
|
796
|
-
const file = files[index];
|
|
797
|
-
if (file && omittedFiles.has(file.path)) files.splice(index, 1);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
if (refreshedFiles > 0) {
|
|
801
|
-
observability.incrementCounter({
|
|
802
|
-
name: "opengeni_workspace_capture_files_refreshed_total",
|
|
803
|
-
amount: refreshedFiles,
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
tooLargeCount = files.filter((file) => file.tooLarge).length;
|
|
807
|
-
binaryCount = files.filter((file) => !file.deleted && file.isBinary).length;
|
|
808
|
-
fingerprint = changeFingerprint(repos, files);
|
|
809
|
-
if (prev && prev.stats.fingerprint === fingerprint) {
|
|
810
|
-
observability.incrementCounter({
|
|
811
|
-
name: "opengeni_workspace_capture_total",
|
|
812
|
-
labels: { result: "skipped_empty" },
|
|
813
|
-
});
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
816
|
// ── 7. serialize the exact uploaded after-images ──────────────────────────
|
|
818
817
|
const capturedAt = new Date();
|
|
819
818
|
const stats: WorkspaceCaptureStats = {
|
|
@@ -850,11 +849,13 @@ async function runCapture(
|
|
|
850
849
|
}),
|
|
851
850
|
);
|
|
852
851
|
stage.value = "tree_upload";
|
|
852
|
+
ownedKeys.add(treeKey);
|
|
853
853
|
await storage.putObject({ key: treeKey, contentType: "application/json", body: treeBytes });
|
|
854
854
|
throwIfCaptureAborted(signal);
|
|
855
855
|
stats.durationMs = Date.now() - startedAt;
|
|
856
856
|
const manifestBytes = utf8(JSON.stringify(manifest));
|
|
857
857
|
stage.value = "manifest_upload";
|
|
858
|
+
ownedKeys.add(manifestKey);
|
|
858
859
|
await storage.putObject({
|
|
859
860
|
key: manifestKey,
|
|
860
861
|
contentType: "application/json",
|
|
@@ -865,6 +866,9 @@ async function runCapture(
|
|
|
865
866
|
|
|
866
867
|
// ── 8. epoch-fenced insert (superseded lease → zero rows) ─────────────────
|
|
867
868
|
stage.value = "database_commit";
|
|
869
|
+
// A transport failure during commit has an unknown outcome: retain rather
|
|
870
|
+
// than risk deleting a published review. A definite rejected commit is safe.
|
|
871
|
+
ownership.retain = true;
|
|
868
872
|
const inserted = await insertWorkspaceCapture(input.db, {
|
|
869
873
|
accountId: input.accountId,
|
|
870
874
|
workspaceId: input.workspaceId,
|
|
@@ -881,6 +885,7 @@ async function runCapture(
|
|
|
881
885
|
stats,
|
|
882
886
|
capturedAt,
|
|
883
887
|
});
|
|
888
|
+
ownership.retain = inserted !== null;
|
|
884
889
|
if (process.env.OPENGENI_TEST_SCENARIO === "sandbox") {
|
|
885
890
|
console.log(
|
|
886
891
|
`[sandbox-e2e] capture commit inserted=${Boolean(inserted)} revision=${revision} epoch=${input.leaseEpoch} group=${input.sandboxGroupId}`,
|
|
@@ -889,8 +894,8 @@ async function runCapture(
|
|
|
889
894
|
throwIfCaptureAborted(signal);
|
|
890
895
|
if (!inserted) {
|
|
891
896
|
// Lease superseded/released between capture and commit. Best-effort clean up
|
|
892
|
-
// the turn-keyed blobs we just PUT
|
|
893
|
-
//
|
|
897
|
+
// the turn-keyed blobs we just PUT. The finalizer also cleans newly owned
|
|
898
|
+
// content blobs, leaving reused refs untouched; never throw.
|
|
894
899
|
observability.incrementCounter({
|
|
895
900
|
name: "opengeni_workspace_capture_total",
|
|
896
901
|
labels: { result: "superseded" },
|
|
@@ -904,6 +909,7 @@ async function runCapture(
|
|
|
904
909
|
}
|
|
905
910
|
|
|
906
911
|
// ── 9. inline keep-latest-N GC (best-effort; F9 — after the commit) ────────
|
|
912
|
+
stage.value = "garbage_collection";
|
|
907
913
|
let gcDeleted = 0;
|
|
908
914
|
try {
|
|
909
915
|
throwIfCaptureAborted(signal);
|
|
@@ -1106,11 +1112,27 @@ function utf8(s: string): Uint8Array {
|
|
|
1106
1112
|
return new TextEncoder().encode(s);
|
|
1107
1113
|
}
|
|
1108
1114
|
|
|
1109
|
-
/** Content-addressed after-image blob key
|
|
1115
|
+
/** Content-addressed after-image blob key, optionally in a unique capture namespace. */
|
|
1110
1116
|
export function blobKey(workspaceId: string, sessionId: string, sha256Hex: string): string {
|
|
1111
1117
|
return `workspace-captures/${workspaceId}/${sessionId}/blobs/${sha256Hex}`;
|
|
1112
1118
|
}
|
|
1113
1119
|
|
|
1120
|
+
/** Only pass the latest retained revision's keys: older refs may be under deletion. */
|
|
1121
|
+
export function retainedCaptureBlobRefs(
|
|
1122
|
+
workspaceId: string,
|
|
1123
|
+
sessionId: string,
|
|
1124
|
+
latestKeys: string[],
|
|
1125
|
+
): Map<string, string> {
|
|
1126
|
+
const prefix = blobKey(workspaceId, sessionId, "");
|
|
1127
|
+
const refs = new Map<string, string>();
|
|
1128
|
+
for (const key of latestKeys) {
|
|
1129
|
+
if (!key.startsWith(prefix)) continue;
|
|
1130
|
+
const match = /^(?:[0-9a-f-]{36}\/)?([0-9a-f]{64})$/.exec(key.slice(prefix.length));
|
|
1131
|
+
if (match?.[1]) refs.set(match[1], key);
|
|
1132
|
+
}
|
|
1133
|
+
return refs;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1114
1136
|
async function safeDelete(
|
|
1115
1137
|
storage: ObjectStorage,
|
|
1116
1138
|
keys: string[],
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
GitCredentialsRequest,
|
|
6
6
|
} from "@opengeni/contracts";
|
|
7
7
|
import { prReviewRegistrationIdFromCredentialBinding } from "@opengeni/core";
|
|
8
|
+
import { createRemoteMcpCredentialsPort } from "@opengeni/core/remote-mcp-credentials";
|
|
8
9
|
import { decryptVariableSetValue, resolvePrReviewGitCredential, type Database } from "@opengeni/db";
|
|
9
10
|
import {
|
|
10
11
|
createGitHubAppInstallationTokenWithExpiry,
|
|
@@ -18,6 +19,7 @@ export function createStandaloneConnectionCredentialsPort(
|
|
|
18
19
|
db: Database,
|
|
19
20
|
): ConnectionCredentialsPort {
|
|
20
21
|
return {
|
|
22
|
+
...createRemoteMcpCredentialsPort(settings),
|
|
21
23
|
gitCredentials: async (request) => await resolveStandaloneGitCredentials(settings, db, request),
|
|
22
24
|
};
|
|
23
25
|
}
|