stratagate-dsh 0.2.63 → 0.2.65

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/plugin.js CHANGED
@@ -2752,6 +2752,7 @@ var StrataGate = class _StrataGate {
2752
2752
  summaryJobs = /* @__PURE__ */ new Map();
2753
2753
  elementProjectionJobs = /* @__PURE__ */ new Map();
2754
2754
  graphProjectionJobs = /* @__PURE__ */ new Map();
2755
+ manualRetryRuns = /* @__PURE__ */ new Map();
2755
2756
  usageReceipts = /* @__PURE__ */ new Map();
2756
2757
  successfulModelResponses = [];
2757
2758
  ingestionReceipts = /* @__PURE__ */ new Map();
@@ -2997,34 +2998,90 @@ var StrataGate = class _StrataGate {
2997
2998
  listSummaryJobs() {
2998
2999
  return [...this.summaryJobs.values()];
2999
3000
  }
3000
- /** Give one terminally failed Block Summary job a fresh, user-requested retry budget. */
3001
+ runManualRetry(key, operation) {
3002
+ const existing = this.manualRetryRuns.get(key);
3003
+ if (existing) return existing;
3004
+ const run = operation().finally(() => {
3005
+ if (this.manualRetryRuns.get(key) === run) this.manualRetryRuns.delete(key);
3006
+ });
3007
+ this.manualRetryRuns.set(key, run);
3008
+ return run;
3009
+ }
3010
+ /** Give one failed Block Summary job a fresh, user-requested retry budget. */
3001
3011
  async retryBlockSummary(id) {
3002
3012
  const blockId = id.trim();
3003
3013
  if (!blockId) throw new TypeError("Block id must not be empty");
3004
- const block = this.blocks.find((candidate) => candidate.id === blockId);
3005
- if (!block) throw new Error(`Unknown block: ${blockId}`);
3006
- if (block.processingStatus !== "pending") throw new Error(`Block ${blockId} is already ready`);
3007
- await this.commitMutation(() => {
3008
- const job = this.summaryJobs.get(blockId);
3009
- if (!job) throw new Error(`Missing summary job for block: ${blockId}`);
3010
- if (job.status !== "failed" || job.nextRetryAt !== null) {
3011
- throw new Error(`Block ${blockId} Summary has not exhausted automatic retries`);
3014
+ return this.runManualRetry(`summary:${blockId}`, async () => {
3015
+ const block = this.blocks.find((candidate) => candidate.id === blockId);
3016
+ if (!block) throw new Error(`Unknown block: ${blockId}`);
3017
+ if (block.processingStatus !== "pending") throw new Error(`Block ${blockId} is already ready`);
3018
+ await this.commitMutation(() => {
3019
+ const job = this.summaryJobs.get(blockId);
3020
+ if (!job) throw new Error(`Missing summary job for block: ${blockId}`);
3021
+ if (job.status !== "failed") throw new Error(`Block ${blockId} Summary is ${job.status}, not failed`);
3022
+ this.summaryJobs.set(blockId, {
3023
+ ...job,
3024
+ status: "pending",
3025
+ attempts: 0,
3026
+ lastError: null,
3027
+ nextRetryAt: null,
3028
+ updatedAt: toUtc8Iso(this.now())
3029
+ });
3030
+ });
3031
+ const extractedEvents = await this.processBlock(block, { retryFailed: false });
3032
+ const processed = this.blocks.find((candidate) => candidate.id === blockId);
3033
+ const readyBlocks = processed?.processingStatus === "ready" ? [processed] : [];
3034
+ return { sealedBlocks: [], readyBlocks, extractedEvents, projectedElements: [] };
3035
+ });
3036
+ }
3037
+ /** Retry only Event extraction for one failed Block without rerunning its Summary. */
3038
+ async retryEventExtraction(id) {
3039
+ const blockId = id.trim();
3040
+ if (!blockId) throw new TypeError("Block id must not be empty");
3041
+ return this.runManualRetry(`extraction:${blockId}`, async () => {
3042
+ const block = this.blocks.find((candidate) => candidate.id === blockId);
3043
+ if (!block) throw new Error(`Unknown block: ${blockId}`);
3044
+ if (block.processingStatus !== "pending" || block.shouldExtract !== true) {
3045
+ throw new Error(`Block ${blockId} is not waiting for Event extraction`);
3012
3046
  }
3013
- this.summaryJobs.set(blockId, {
3014
- ...job,
3015
- status: "pending",
3016
- attempts: 0,
3017
- lastError: null,
3018
- nextRetryAt: null,
3019
- updatedAt: toUtc8Iso(this.now())
3047
+ await this.commitMutation(() => {
3048
+ const job = this.extractionJobs.get(blockId);
3049
+ if (!job) throw new Error(`Missing extraction job for block: ${blockId}`);
3050
+ if (job.status !== "failed") throw new Error(`Event extraction ${blockId} is ${job.status}, not failed`);
3051
+ this.extractionJobs.set(blockId, {
3052
+ ...job,
3053
+ attempts: 0,
3054
+ lastError: null,
3055
+ nextRetryAt: null,
3056
+ updatedAt: toUtc8Iso(this.now())
3057
+ });
3020
3058
  });
3059
+ return await this.extractEligibleBlock({ blockId, retryFailed: true }) ?? [];
3060
+ });
3061
+ }
3062
+ /** Retry exactly one failed Graph projection batch. */
3063
+ async retryGraphProjection(id) {
3064
+ const jobId = id.trim();
3065
+ if (!jobId) throw new TypeError("Graph projection id must not be empty");
3066
+ return this.runManualRetry(`graph:${jobId}`, async () => {
3067
+ if (!this.graphProjector) throw new Error("Graph projector is not configured");
3068
+ await this.commitMutation(() => {
3069
+ const job = this.requireGraphProjectionJob(jobId);
3070
+ if (job.status !== "failed") throw new Error(`Graph projection ${jobId} is ${job.status}, not failed`);
3071
+ job.status = "pending";
3072
+ job.attempts = 0;
3073
+ job.lastError = null;
3074
+ job.updatedAt = toUtc8Iso(this.now());
3075
+ });
3076
+ const batch = await this.claimGraphProjection(jobId);
3077
+ if (!batch) throw new Error(`Graph projection ${jobId} could not be claimed`);
3078
+ try {
3079
+ return await this.completeGraphProjection(jobId, await this.graphProjector(batch));
3080
+ } catch (error) {
3081
+ await this.failGraphProjection(jobId, error);
3082
+ throw error;
3083
+ }
3021
3084
  });
3022
- const extractedEvents = await this.processBlock(block, { retryFailed: false });
3023
- const processed = this.blocks.find((candidate) => candidate.id === blockId);
3024
- const readyBlocks = processed?.processingStatus === "ready" ? [processed] : [];
3025
- const projectedElements = await this.projectEligibleElements() ?? [];
3026
- await this.projectEligibleGraph();
3027
- return { sealedBlocks: [], readyBlocks, extractedEvents, projectedElements };
3028
3085
  }
3029
3086
  listElementProjectionJobs() {
3030
3087
  return [...this.elementProjectionJobs.values()];
@@ -3664,9 +3721,9 @@ var StrataGate = class _StrataGate {
3664
3721
  job.updatedAt = toUtc8Iso(this.now());
3665
3722
  });
3666
3723
  }
3667
- async claimNextGraphProjection() {
3724
+ async claimGraphProjection(jobId) {
3668
3725
  return this.commitMutation(() => {
3669
- const job = [...this.graphProjectionJobs.values()].filter((candidate) => candidate.status === "pending" || candidate.status === "failed").sort((left, right) => right.priority - left.priority || left.createdAt.localeCompare(right.createdAt))[0];
3726
+ const job = [...this.graphProjectionJobs.values()].filter((candidate) => (jobId === void 0 || candidate.id === jobId) && (candidate.status === "pending" || candidate.status === "failed")).sort((left, right) => right.priority - left.priority || left.createdAt.localeCompare(right.createdAt))[0];
3670
3727
  if (!job) return null;
3671
3728
  const events = job.sourceEventIds.flatMap((id) => this.events.find((event) => event.id === id) ?? []);
3672
3729
  if (events.length === 0) throw new Error(`Graph projection ${job.id} has no available source events`);
@@ -3692,6 +3749,9 @@ var StrataGate = class _StrataGate {
3692
3749
  };
3693
3750
  });
3694
3751
  }
3752
+ async claimNextGraphProjection() {
3753
+ return this.claimGraphProjection();
3754
+ }
3695
3755
  async completeGraphProjection(jobId, result) {
3696
3756
  return this.commitMutation(() => {
3697
3757
  const job = this.requireGraphProjectionJob(jobId);
@@ -5716,6 +5776,7 @@ var StrataGateRuntime = class {
5716
5776
  pendingSurfaceSync = /* @__PURE__ */ new Map();
5717
5777
  adminSnapshotCache = /* @__PURE__ */ new Map();
5718
5778
  externalImportRuns = /* @__PURE__ */ new Map();
5779
+ adminJobRetryRuns = /* @__PURE__ */ new Map();
5719
5780
  feedbackDrafts = /* @__PURE__ */ new Map();
5720
5781
  pendingFeedbackSuggestionSessions = /* @__PURE__ */ new Set();
5721
5782
  pendingTurns = /* @__PURE__ */ new Map();
@@ -6715,34 +6776,53 @@ var StrataGateRuntime = class {
6715
6776
  });
6716
6777
  return update;
6717
6778
  }
6718
- async adminRetryBlockSummary(namespace, id) {
6779
+ async adminRetryJob(namespace, kind, id) {
6719
6780
  const key = namespace.trim();
6720
- const blockId = id.trim();
6781
+ const jobId = id.trim();
6721
6782
  if (!key) throw new TypeError("StrataGate admin namespace must not be empty");
6722
- if (!blockId) throw new TypeError("Block id must not be empty");
6783
+ if (!jobId) throw new TypeError("StrataGate job id must not be empty");
6784
+ const retryKey = `${key}\0${kind}\0${jobId}`;
6785
+ const existing = this.adminJobRetryRuns.get(retryKey);
6786
+ if (existing) return existing;
6723
6787
  const update = this.settingsTail.catch(() => {
6724
6788
  }).then(async () => {
6725
6789
  await this.flush();
6726
6790
  const { memory, owned } = await this.openAdminMemory(key, { derivation: true });
6727
6791
  try {
6728
6792
  await this.refreshExternalImportMemory(key, memory);
6729
- const block = memory.listBlocks().find((candidate) => candidate.id === blockId);
6730
- if (!block) throw new Error(`Unknown block: ${blockId}`);
6731
- const threadId = block.threadId ?? `admin-summary-retry:${blockId}`;
6732
- const session = block.threadId ? this.knownSessions.get(block.threadId)?.deref() : void 0;
6733
- const resumed = session && this.namespaceFor(session) === key ? await this.models.run(session, () => memory.retryBlockSummary(blockId)) : await this.models.runDetached(threadId, () => memory.retryBlockSummary(blockId));
6793
+ const graphJob = kind === "graph-projection" ? memory.listGraphProjectionJobs().find((candidate) => candidate.id === jobId) : void 0;
6794
+ const blockId = kind === "graph-projection" ? graphJob?.sourceEventIds.flatMap((eventId) => memory.listEvents().find(({ id: eventCandidateId }) => eventCandidateId === eventId)?.sourceBlockId ?? [])[0] : jobId;
6795
+ const block = blockId ? memory.listBlocks().find((candidate) => candidate.id === blockId) : void 0;
6796
+ if (kind !== "graph-projection" && !block) throw new Error(`Unknown block: ${jobId}`);
6797
+ if (kind === "graph-projection" && !graphJob) throw new Error(`Unknown graph projection: ${jobId}`);
6798
+ const threadId = block?.threadId ?? `admin-job-retry:${kind}:${jobId}`;
6799
+ const session = block?.threadId ? this.knownSessions.get(block.threadId)?.deref() : void 0;
6800
+ const retry = async () => {
6801
+ if (kind === "block-summary") return memory.retryBlockSummary(jobId);
6802
+ if (kind === "event-extraction") return memory.retryEventExtraction(jobId);
6803
+ return memory.retryGraphProjection(jobId);
6804
+ };
6805
+ const result = session && this.namespaceFor(session) === key ? await this.models.run(session, retry) : await this.models.runDetached(threadId, retry);
6734
6806
  await this.persistSuccessfulResponses(memory);
6735
- if (resumed.readyBlocks.some(({ id: readyId }) => readyId === blockId) && block.threadId) {
6736
- this.pendingSurfaceSync.set(`${key}\0${blockId}`, { namespace: key, blockId });
6807
+ const currentBlock = blockId ? memory.listBlocks().find(({ id: candidateId }) => candidateId === blockId) : void 0;
6808
+ if (kind !== "graph-projection" && currentBlock?.processingStatus === "ready" && currentBlock.threadId) {
6809
+ this.pendingSurfaceSync.set(`${key}\0${currentBlock.id}`, { namespace: key, blockId: currentBlock.id });
6737
6810
  if (session && this.namespaceFor(session) === key) await this.syncPendingRetrySurface(session, memory);
6738
6811
  }
6739
- const summaryJob = memory.listSummaryJobs().find(({ blockId: candidateId }) => candidateId === blockId);
6812
+ const job = kind === "block-summary" ? memory.listSummaryJobs().find(({ blockId: candidateId }) => candidateId === jobId) : kind === "event-extraction" ? memory.listExtractionJobs().find(({ blockId: candidateId }) => candidateId === jobId) : memory.listGraphProjectionJobs().find(({ id: candidateId }) => candidateId === jobId);
6813
+ const succeeded = kind === "graph-projection" ? job?.status === "completed" : job?.status === "succeeded" || job?.status === "skipped";
6814
+ if (!succeeded) throw new Error(job?.lastError || `${kind} retry did not complete`);
6740
6815
  return {
6816
+ kind,
6817
+ jobId,
6741
6818
  blockId,
6742
- processingStatus: block.processingStatus,
6743
- ready: block.processingStatus === "ready",
6744
- summaryJob: summaryJob ?? null,
6745
- surfaceUpdated: !this.pendingSurfaceSync.has(`${key}\0${blockId}`)
6819
+ status: job?.status,
6820
+ job: job ?? null,
6821
+ ...kind === "block-summary" ? { summaryJob: job ?? null } : {},
6822
+ result,
6823
+ processingStatus: currentBlock?.processingStatus ?? null,
6824
+ ready: currentBlock?.processingStatus === "ready",
6825
+ surfaceUpdated: !blockId || !this.pendingSurfaceSync.has(`${key}\0${blockId}`)
6746
6826
  };
6747
6827
  } finally {
6748
6828
  if (owned) await memory.close();
@@ -6751,7 +6831,14 @@ var StrataGateRuntime = class {
6751
6831
  this.settingsTail = update.then(() => {
6752
6832
  }, () => {
6753
6833
  });
6754
- return update;
6834
+ const tracked = update.finally(() => {
6835
+ if (this.adminJobRetryRuns.get(retryKey) === tracked) this.adminJobRetryRuns.delete(retryKey);
6836
+ });
6837
+ this.adminJobRetryRuns.set(retryKey, tracked);
6838
+ return tracked;
6839
+ }
6840
+ async adminRetryBlockSummary(namespace, id) {
6841
+ return this.adminRetryJob(namespace, "block-summary", id);
6755
6842
  }
6756
6843
  async applyBlockDecayLambda(value) {
6757
6844
  await this.flush();
@@ -7623,7 +7710,7 @@ function clusterKnowledgeGraph(rawNodes, rawEdges) {
7623
7710
  }
7624
7711
 
7625
7712
  // src/web.ts
7626
- var STRATAGATE_DSH_VERSION = "0.2.57";
7713
+ var STRATAGATE_DSH_VERSION = "0.2.65";
7627
7714
  var LEGACY_THREAD_ID = "__legacy__";
7628
7715
  var nodeRequire2 = createRequire2(import.meta.url);
7629
7716
  function installedPackageVersion(names) {
@@ -7766,40 +7853,80 @@ async function overview(runtime, cachedEntries) {
7766
7853
  const rows = [];
7767
7854
  for (const { namespace, snapshot } of entries) {
7768
7855
  if (!snapshot) continue;
7769
- const failedJobs = snapshot.summaryJobs.filter(({ status }) => status === "failed").length + snapshot.extractionJobs.filter(({ status }) => status === "failed").length + snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").length;
7770
- const processingJobs = snapshot.summaryJobs.filter(({ status, nextRetryAt }) => status === "pending" || status === "running" || status === "failed" && nextRetryAt !== null).length + snapshot.extractionJobs.filter(({ status, nextRetryAt }) => status === "running" || status === "failed" && nextRetryAt !== null).length + snapshot.graphProjectionJobs.filter(({ status }) => status === "pending" || status === "running").length;
7771
- const failedJobDetails = [
7772
- ...snapshot.summaryJobs.filter(({ status }) => status === "failed").map((job) => {
7773
- const block = snapshot.blocks.find(({ id }) => id === job.blockId);
7774
- return {
7775
- id: job.blockId,
7776
- kind: "block-summary",
7777
- attempts: job.attempts,
7778
- nextRetryAt: job.nextRetryAt,
7779
- lastError: job.lastError?.slice(0, 500) ?? null,
7780
- lastErrorFull: job.lastError,
7781
- updatedAt: job.updatedAt,
7782
- threadId: block?.threadId ?? null,
7783
- sequence: block?.sequence ?? null,
7784
- turnRange: block ? [block.startTurn, block.endTurn] : null
7785
- };
7786
- }),
7787
- ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => ({
7856
+ const recovered = recoverSnapshotView(snapshot);
7857
+ const blockDetails = (blockIds) => [...new Set(blockIds)].flatMap((blockId) => {
7858
+ const displayBlocks = recovered.blocks.filter(({ source, threadId }) => source.id === blockId && threadId !== LEGACY_THREAD_ID);
7859
+ if (displayBlocks.length > 0) {
7860
+ return displayBlocks.map((block2) => ({
7861
+ id: block2.id,
7862
+ sourceId: block2.source.id,
7863
+ sequence: block2.source.sequence,
7864
+ title: block2.source.l0Title ?? null,
7865
+ threadId: block2.threadId,
7866
+ turnRange: block2.turnRange,
7867
+ shouldExtract: block2.source.shouldExtract ?? null
7868
+ }));
7869
+ }
7870
+ const block = snapshot.blocks.find(({ id }) => id === blockId);
7871
+ return block ? [{
7872
+ id: block.id,
7873
+ sourceId: block.id,
7874
+ sequence: block.sequence,
7875
+ title: block.l0Title ?? null,
7876
+ threadId: block.threadId ?? null,
7877
+ turnRange: [block.startTurn, block.endTurn],
7878
+ shouldExtract: block.shouldExtract ?? null
7879
+ }] : [];
7880
+ });
7881
+ const describeBlockJob = (kind, job) => {
7882
+ const details = blockDetails([job.blockId]);
7883
+ return {
7788
7884
  id: job.blockId,
7789
- kind: "event-extraction",
7885
+ kind,
7886
+ status: job.status,
7790
7887
  attempts: job.attempts,
7888
+ nextRetryAt: job.nextRetryAt,
7791
7889
  lastError: job.lastError?.slice(0, 500) ?? null,
7792
7890
  lastErrorFull: job.lastError,
7793
- updatedAt: job.updatedAt
7794
- })),
7795
- ...snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").map((job) => ({
7891
+ updatedAt: job.updatedAt,
7892
+ threadId: details[0]?.threadId ?? null,
7893
+ blockIds: [job.blockId],
7894
+ threadIds: [...new Set(details.flatMap(({ threadId }) => threadId ?? []))],
7895
+ blockDetails: details,
7896
+ sequence: details[0]?.sequence ?? null,
7897
+ turnRange: details[0]?.turnRange ?? null
7898
+ };
7899
+ };
7900
+ const describeGraphJob = (job) => {
7901
+ const blockIds = [...new Set(job.sourceEventIds.flatMap((eventId) => snapshot.events.find(({ id }) => id === eventId)?.sourceBlockId ?? []))];
7902
+ const details = blockDetails(blockIds);
7903
+ return {
7796
7904
  id: job.id,
7797
7905
  kind: "graph-projection",
7906
+ status: job.status,
7798
7907
  attempts: job.attempts,
7908
+ nextRetryAt: null,
7799
7909
  lastError: job.lastError?.slice(0, 500) ?? null,
7800
7910
  lastErrorFull: job.lastError,
7801
- updatedAt: job.updatedAt
7802
- }))
7911
+ updatedAt: job.updatedAt,
7912
+ blockIds,
7913
+ threadIds: [...new Set(details.flatMap(({ threadId }) => threadId ?? []))],
7914
+ blockDetails: details,
7915
+ sequences: details.map(({ sequence }) => sequence),
7916
+ sourceEventIds: job.sourceEventIds
7917
+ };
7918
+ };
7919
+ const failedJobs = snapshot.summaryJobs.filter(({ status }) => status === "failed").length + snapshot.extractionJobs.filter(({ status }) => status === "failed").length + snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").length;
7920
+ const processingJobs = snapshot.summaryJobs.filter(({ status, nextRetryAt }) => status === "pending" || status === "running" || status === "failed" && nextRetryAt !== null).length + snapshot.extractionJobs.filter(({ status, nextRetryAt }) => status === "running" || status === "failed" && nextRetryAt !== null).length + snapshot.graphProjectionJobs.filter(({ status }) => status === "pending" || status === "running").length;
7921
+ const failedJobDetails = [
7922
+ ...snapshot.summaryJobs.filter(({ status }) => status === "failed").map((job) => describeBlockJob("block-summary", job)),
7923
+ ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => describeBlockJob("event-extraction", job)),
7924
+ ...snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").map(describeGraphJob)
7925
+ ];
7926
+ const processingJobDetails = [
7927
+ ...snapshot.summaryJobs.filter(({ status, nextRetryAt }) => status === "pending" || status === "running" || status === "failed" && nextRetryAt !== null).map((job) => describeBlockJob("block-summary", job)),
7928
+ ...snapshot.extractionJobs.filter(({ status, nextRetryAt }) => status === "running" || status === "failed" && nextRetryAt !== null).map((job) => describeBlockJob("event-extraction", job)),
7929
+ ...snapshot.graphProjectionJobs.filter(({ status }) => status === "pending" || status === "running").map(describeGraphJob)
7803
7930
  ];
7804
7931
  const timestamps = [
7805
7932
  ...snapshot.blocks.map(({ createdAt }) => createdAt),
@@ -7834,6 +7961,7 @@ async function overview(runtime, cachedEntries) {
7834
7961
  failedJobs,
7835
7962
  processingJobs,
7836
7963
  failedJobDetails,
7964
+ processingJobDetails,
7837
7965
  successfulModelResponses: snapshot.successfulModelResponses ?? [],
7838
7966
  lastActivityAt: timestamps.at(-1) ?? null
7839
7967
  });
@@ -8345,11 +8473,33 @@ async function retryBlockSummary(runtime, url) {
8345
8473
  const snapshot = await requiredSnapshot(runtime, namespace);
8346
8474
  const job = snapshot.summaryJobs.find(({ blockId: candidateId }) => candidateId === blockId);
8347
8475
  if (!job) throw new AdminHttpError(404, `Unknown Block Summary job: ${blockId}`);
8348
- if (job.status !== "failed" || job.nextRetryAt !== null) {
8349
- throw new AdminHttpError(409, "Block Summary is not in a terminal failed state");
8350
- }
8476
+ if (job.status !== "failed") throw new AdminHttpError(409, `Block Summary is ${job.status}, not failed`);
8351
8477
  return runtime.adminRetryBlockSummary(namespace, blockId);
8352
8478
  }
8479
+ async function retryJob(runtime, url) {
8480
+ const namespace = url.searchParams.get("namespace")?.trim() ?? "";
8481
+ const kind = url.searchParams.get("kind")?.trim() ?? "";
8482
+ const jobId = url.searchParams.get("jobId")?.trim() ?? "";
8483
+ if (!namespace) throw new AdminHttpError(400, "namespace is required");
8484
+ if (!["block-summary", "event-extraction", "graph-projection"].includes(kind)) {
8485
+ throw new AdminHttpError(400, "kind must be block-summary, event-extraction, or graph-projection");
8486
+ }
8487
+ if (!jobId) throw new AdminHttpError(400, "jobId is required");
8488
+ if (jobId.startsWith("virtual:")) throw new AdminHttpError(409, "Recovered legacy fragments cannot be retried");
8489
+ const snapshot = await requiredSnapshot(runtime, namespace);
8490
+ const job = kind === "block-summary" ? snapshot.summaryJobs.find(({ blockId }) => blockId === jobId) : kind === "event-extraction" ? snapshot.extractionJobs.find(({ blockId }) => blockId === jobId) : snapshot.graphProjectionJobs.find(({ id }) => id === jobId);
8491
+ if (!job) throw new AdminHttpError(404, `Unknown ${kind} job: ${jobId}`);
8492
+ if (job.status !== "failed") throw new AdminHttpError(409, `${kind} job is ${job.status}, not failed`);
8493
+ try {
8494
+ return await runtime.adminRetryJob(
8495
+ namespace,
8496
+ kind,
8497
+ jobId
8498
+ );
8499
+ } catch (error) {
8500
+ throw new AdminHttpError(422, error instanceof Error ? error.message : String(error));
8501
+ }
8502
+ }
8353
8503
  function receiptSources(snapshot, receipt) {
8354
8504
  const events = snapshot.events.filter(({ id }) => receipt.eventIds.includes(id));
8355
8505
  const elements = snapshot.elements.filter(({ id }) => receipt.elementIds.includes(id));
@@ -8390,7 +8540,7 @@ async function dashboard(runtime, url, ifNoneMatch) {
8390
8540
  const selected = entries.find(({ namespace }) => namespace === requestedNamespace) ?? entries[0];
8391
8541
  const threadId = url.searchParams.get("threadId")?.trim() ?? "";
8392
8542
  const revisionKey = entries.map(({ namespace, revision }) => `${namespace}:${revision}`).join("|");
8393
- const etag = `"${createHash2("sha256").update(`${revisionKey}\0${selected?.namespace ?? ""}\0${threadId}`).digest("base64url").slice(0, 24)}"`;
8543
+ const etag = `"${createHash2("sha256").update(`${STRATAGATE_DSH_VERSION}\0${revisionKey}\0${selected?.namespace ?? ""}\0${threadId}`).digest("base64url").slice(0, 24)}"`;
8394
8544
  if (ifNoneMatch.split(",").map((value) => value.trim()).includes(etag)) return { etag, notModified: true };
8395
8545
  const overviewValue = await overview(runtime, entries);
8396
8546
  if (!selected) {
@@ -8465,6 +8615,9 @@ async function handleAdminRequest(runtime, req, res) {
8465
8615
  } else if (path === "/api/stratagate/blocks/retry-summary") {
8466
8616
  if (req.method !== "POST") throw new AdminHttpError(405, "StrataGate Block Summary retry requires POST");
8467
8617
  sendJson(res, 200, await retryBlockSummary(runtime, url));
8618
+ } else if (path === "/api/stratagate/jobs/retry") {
8619
+ if (req.method !== "POST") throw new AdminHttpError(405, "StrataGate job retry requires POST");
8620
+ sendJson(res, 200, await retryJob(runtime, url));
8468
8621
  } else if (path === "/api/stratagate/import") {
8469
8622
  if (req.method === "GET") {
8470
8623
  const operation = url.searchParams.get("operation");