stratagate-dsh 0.2.63 → 0.2.64

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.64";
7627
7714
  var LEGACY_THREAD_ID = "__legacy__";
7628
7715
  var nodeRequire2 = createRequire2(import.meta.url);
7629
7716
  function installedPackageVersion(names) {
@@ -7780,26 +7867,66 @@ async function overview(runtime, cachedEntries) {
7780
7867
  lastErrorFull: job.lastError,
7781
7868
  updatedAt: job.updatedAt,
7782
7869
  threadId: block?.threadId ?? null,
7870
+ blockIds: [job.blockId],
7871
+ threadIds: block?.threadId ? [block.threadId] : [],
7872
+ blockDetails: block ? [{
7873
+ id: block.id,
7874
+ sequence: block.sequence,
7875
+ title: block.l0Title ?? null,
7876
+ threadId: block.threadId ?? null,
7877
+ turnRange: [block.startTurn, block.endTurn]
7878
+ }] : [],
7783
7879
  sequence: block?.sequence ?? null,
7784
7880
  turnRange: block ? [block.startTurn, block.endTurn] : null
7785
7881
  };
7786
7882
  }),
7787
- ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => ({
7788
- id: job.blockId,
7789
- kind: "event-extraction",
7790
- attempts: job.attempts,
7791
- lastError: job.lastError?.slice(0, 500) ?? null,
7792
- lastErrorFull: job.lastError,
7793
- updatedAt: job.updatedAt
7794
- })),
7795
- ...snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").map((job) => ({
7796
- id: job.id,
7797
- kind: "graph-projection",
7798
- attempts: job.attempts,
7799
- lastError: job.lastError?.slice(0, 500) ?? null,
7800
- lastErrorFull: job.lastError,
7801
- updatedAt: job.updatedAt
7802
- }))
7883
+ ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => {
7884
+ const block = snapshot.blocks.find(({ id }) => id === job.blockId);
7885
+ return {
7886
+ id: job.blockId,
7887
+ kind: "event-extraction",
7888
+ attempts: job.attempts,
7889
+ nextRetryAt: job.nextRetryAt,
7890
+ lastError: job.lastError?.slice(0, 500) ?? null,
7891
+ lastErrorFull: job.lastError,
7892
+ updatedAt: job.updatedAt,
7893
+ blockIds: [job.blockId],
7894
+ threadIds: block?.threadId ? [block.threadId] : [],
7895
+ blockDetails: block ? [{
7896
+ id: block.id,
7897
+ sequence: block.sequence,
7898
+ title: block.l0Title ?? null,
7899
+ threadId: block.threadId ?? null,
7900
+ turnRange: [block.startTurn, block.endTurn]
7901
+ }] : [],
7902
+ sequence: block?.sequence ?? null,
7903
+ turnRange: block ? [block.startTurn, block.endTurn] : null
7904
+ };
7905
+ }),
7906
+ ...snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").map((job) => {
7907
+ const blockIds = [...new Set(job.sourceEventIds.flatMap((eventId) => snapshot.events.find(({ id }) => id === eventId)?.sourceBlockId ?? []))];
7908
+ const blocks = blockIds.flatMap((blockId) => snapshot.blocks.find(({ id }) => id === blockId) ?? []);
7909
+ return {
7910
+ id: job.id,
7911
+ kind: "graph-projection",
7912
+ attempts: job.attempts,
7913
+ nextRetryAt: null,
7914
+ lastError: job.lastError?.slice(0, 500) ?? null,
7915
+ lastErrorFull: job.lastError,
7916
+ updatedAt: job.updatedAt,
7917
+ blockIds,
7918
+ threadIds: [...new Set(blocks.flatMap(({ threadId }) => threadId ?? []))],
7919
+ blockDetails: blocks.map((block) => ({
7920
+ id: block.id,
7921
+ sequence: block.sequence,
7922
+ title: block.l0Title ?? null,
7923
+ threadId: block.threadId ?? null,
7924
+ turnRange: [block.startTurn, block.endTurn]
7925
+ })),
7926
+ sequences: blocks.map(({ sequence }) => sequence),
7927
+ sourceEventIds: job.sourceEventIds
7928
+ };
7929
+ })
7803
7930
  ];
7804
7931
  const timestamps = [
7805
7932
  ...snapshot.blocks.map(({ createdAt }) => createdAt),
@@ -8345,11 +8472,33 @@ async function retryBlockSummary(runtime, url) {
8345
8472
  const snapshot = await requiredSnapshot(runtime, namespace);
8346
8473
  const job = snapshot.summaryJobs.find(({ blockId: candidateId }) => candidateId === blockId);
8347
8474
  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
- }
8475
+ if (job.status !== "failed") throw new AdminHttpError(409, `Block Summary is ${job.status}, not failed`);
8351
8476
  return runtime.adminRetryBlockSummary(namespace, blockId);
8352
8477
  }
8478
+ async function retryJob(runtime, url) {
8479
+ const namespace = url.searchParams.get("namespace")?.trim() ?? "";
8480
+ const kind = url.searchParams.get("kind")?.trim() ?? "";
8481
+ const jobId = url.searchParams.get("jobId")?.trim() ?? "";
8482
+ if (!namespace) throw new AdminHttpError(400, "namespace is required");
8483
+ if (!["block-summary", "event-extraction", "graph-projection"].includes(kind)) {
8484
+ throw new AdminHttpError(400, "kind must be block-summary, event-extraction, or graph-projection");
8485
+ }
8486
+ if (!jobId) throw new AdminHttpError(400, "jobId is required");
8487
+ if (jobId.startsWith("virtual:")) throw new AdminHttpError(409, "Recovered legacy fragments cannot be retried");
8488
+ const snapshot = await requiredSnapshot(runtime, namespace);
8489
+ 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);
8490
+ if (!job) throw new AdminHttpError(404, `Unknown ${kind} job: ${jobId}`);
8491
+ if (job.status !== "failed") throw new AdminHttpError(409, `${kind} job is ${job.status}, not failed`);
8492
+ try {
8493
+ return await runtime.adminRetryJob(
8494
+ namespace,
8495
+ kind,
8496
+ jobId
8497
+ );
8498
+ } catch (error) {
8499
+ throw new AdminHttpError(422, error instanceof Error ? error.message : String(error));
8500
+ }
8501
+ }
8353
8502
  function receiptSources(snapshot, receipt) {
8354
8503
  const events = snapshot.events.filter(({ id }) => receipt.eventIds.includes(id));
8355
8504
  const elements = snapshot.elements.filter(({ id }) => receipt.elementIds.includes(id));
@@ -8465,6 +8614,9 @@ async function handleAdminRequest(runtime, req, res) {
8465
8614
  } else if (path === "/api/stratagate/blocks/retry-summary") {
8466
8615
  if (req.method !== "POST") throw new AdminHttpError(405, "StrataGate Block Summary retry requires POST");
8467
8616
  sendJson(res, 200, await retryBlockSummary(runtime, url));
8617
+ } else if (path === "/api/stratagate/jobs/retry") {
8618
+ if (req.method !== "POST") throw new AdminHttpError(405, "StrataGate job retry requires POST");
8619
+ sendJson(res, 200, await retryJob(runtime, url));
8468
8620
  } else if (path === "/api/stratagate/import") {
8469
8621
  if (req.method === "GET") {
8470
8622
  const operation = url.searchParams.get("operation");