Skip to content

Commit a54375d

Browse files
committed
[feat.lab.Sigma.D9+] expose durable compute evidence
WHY: Structures already invoked real field, surface, and torsion commands, but discarded the accepted Job identity and hid artifact/provenance boundaries. Users could not distinguish durable service work from browser-only interaction geometry, and Cancel sometimes stopped only the local wait. This commit projects one computation-run model into the three Structures views, carries accepted Job IDs through the TypeScript SDK, polls long jobs into Running, and sends cooperative job.cancel without overwriting a race-winning result. The generated architecture twin is rebuilt from the isolated staged snapshot so the source graph stays authoritative.
1 parent 9d38ca4 commit a54375d

12 files changed

Lines changed: 4703 additions & 2689 deletions

File tree

‎docs/architecture/dirac-digital-twin.html‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎docs/architecture/dirac-digital-twin.json‎

Lines changed: 4172 additions & 2661 deletions
Large diffs are not rendered by default.

‎src/app.frontend.facets.molstar-rdkit.editable/facets/field-wells/index.ts‎

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { fieldOverlay } from './volume-overlay';
2727
// with the CLI's and the MCP adapter's semantics. Before this, the URL and the refusal
2828
// shape were rebuilt at five call sites and the sixth would have differed.
2929
import { DiracClient, DiracError, fetchField as sdkFetchField } from '../../../app/services/dirac-client';
30+
import { computationRunFromEnvelope, failedComputationRun, observedComputationRun,
31+
renderComputationRun, type ComputationRunRecord } from '../../../app/services/computation-run';
3032

3133
// A renderer that fails silently is indistinguishable from one that works but
3234
// has nothing to draw. The handle costs nothing and every harness can read it.
@@ -303,6 +305,7 @@ export type FieldView = {
303305
meta: Record<string, any> | null;
304306
digestVerified?: boolean;
305307
digestUnverifiedReason?: string;
308+
run?: ComputationRunRecord;
306309
};
307310

308311
let plugin: PluginContext | null = null;
@@ -380,6 +383,12 @@ function viewFromBaked(m: any, kind: FieldKind): FieldView {
380383
digestUnverifiedReason: 'this field came from the pre-baked static assets, which '
381384
+ 'carry no digest — it is a fallback for an offline backend, not a computed '
382385
+ 'result',
386+
run: observedComputationRun('precomputed.field', {
387+
executor: 'static', methodId: String(meta.method || meta.kind || kind),
388+
version: meta.method_version ?? null,
389+
provenance: { structure_id: structureId, baked: true },
390+
note: 'Static fallback; no durable Job and no artifact digest are available.',
391+
}),
383392
};
384393
}
385394
const pendingFetch = new Map<string, Promise<{ cube: string, view: FieldView } | null>>();
@@ -433,7 +442,8 @@ function molfileHeavyAtoms(mf: string): number {
433442
* two demand opposite next moves.
434443
*/
435444
class FieldRefusal extends Error {
436-
constructor(message: string, readonly reason: 'budget' | 'unsupported' | 'internal' | 'network') {
445+
constructor(message: string,
446+
readonly reason: 'budget' | 'unsupported' | 'internal' | 'network' | 'cancelled') {
437447
super(message);
438448
}
439449
}
@@ -451,6 +461,8 @@ const clientTimeoutMs = (budget: number) => (budget * 2 + 20) * 1000;
451461

452462
/** The request the user can cancel — one at a time, by construction. */
453463
let inFlight: AbortController | null = null;
464+
let inFlightJobId: string | null = null;
465+
let inFlightCommand: 'structure.field.compute' | 'fields.region.mep' | null = null;
454466
/**
455467
* Whether the compute backend answered its health check. On the deployed
456468
* static site it never will, and that is the normal case there rather than an
@@ -466,7 +478,7 @@ export function setFieldStructureId(id: string | null) { structureId = id; }
466478
/** Fetch one field into the browser cache (deduplicated). Throws FieldRefusal;
467479
* returns null if the focused ligand changed while in flight. */
468480
async function fetchField(kind: FieldKind, store = false,
469-
budget = QM_BUDGET_SECONDS): Promise<{ cube: string, view: FieldView } | null> {
481+
budget = QM_BUDGET_SECONDS, reportActivity = false): Promise<{ cube: string, view: FieldView } | null> {
470482
const key = cacheKey(kind);
471483
if (!store) {
472484
const hit = cubeCache.get(key);
@@ -499,12 +511,24 @@ async function fetchField(kind: FieldKind, store = false,
499511
const basis = Kinds[kind].quantum ? currentBasis() : undefined;
500512
const controller = new AbortController();
501513
inFlight = controller;
502-
const timer = setTimeout(() => controller.abort(), clientTimeoutMs(budget));
514+
inFlightCommand = 'structure.field.compute';
515+
let timedOut = false;
516+
const timer = setTimeout(() => {
517+
timedOut = true;
518+
controller.abort();
519+
}, clientTimeoutMs(budget));
503520
const p = (async () => {
504521
try {
505522
const got = await sdkFetchField(dirac, kind, {
506523
molfile: requestMolfile!, basis, maxSeconds: budget,
507524
signal: controller.signal,
525+
onAccepted: accepted => {
526+
inFlightJobId = String(accepted.meta?.job_id || '') || null;
527+
if (reportActivity) {
528+
renderComputationRun('field-run-record',
529+
computationRunFromEnvelope('structure.field.compute', accepted, 'queued'));
530+
}
531+
},
508532
});
509533
if (molfile !== requestMolfile) return null;
510534
// The digest check is REPORTED, not assumed. On this origin (plain http on a
@@ -515,6 +539,14 @@ async function fetchField(kind: FieldKind, store = false,
515539
data: got.data, warnings: got.warnings, meta: got.envelope.meta ?? null,
516540
digestVerified: got.digestVerified,
517541
digestUnverifiedReason: got.digestUnverifiedReason,
542+
run: {
543+
...computationRunFromEnvelope('structure.field.compute', got.envelope),
544+
provenance: {
545+
...(got.envelope.meta?.provenance || {}),
546+
artifact_digest: got.digestVerified ? 'verified'
547+
: got.digestUnverifiedReason || 'not verified',
548+
},
549+
},
518550
};
519551
const entry = { cube: got.cube, view };
520552
cubeCache.set(key, entry);
@@ -539,6 +571,9 @@ async function fetchField(kind: FieldKind, store = false,
539571
bucket);
540572
}
541573
if (controller.signal.aborted) {
574+
if (!timedOut) {
575+
throw new FieldRefusal('Cancelled by user.', 'cancelled');
576+
}
542577
throw new FieldRefusal(
543578
`cancelled after ${budget * 2 + 20} s — the daemon should have `
544579
+ `stopped itself at ${budget} s, so it is over its own bound`,
@@ -558,7 +593,11 @@ async function fetchField(kind: FieldKind, store = false,
558593
e instanceof Error ? e.message : String(e), 'network');
559594
} finally {
560595
clearTimeout(timer);
561-
if (inFlight === controller) inFlight = null;
596+
if (inFlight === controller) {
597+
inFlight = null;
598+
inFlightJobId = null;
599+
inFlightCommand = null;
600+
}
562601
pendingFetch.delete(key);
563602
}
564603
})();
@@ -569,8 +608,21 @@ async function fetchField(kind: FieldKind, store = false,
569608
/** Abandon whatever is in flight. Used by the Cancel control and by any change
570609
* that makes the answer worthless before it arrives. */
571610
function abortInFlight() {
611+
const jobId = inFlightJobId;
612+
const command = inFlightCommand;
613+
if (jobId) {
614+
void dirac.execute('job.cancel', { job_ref: { kind: 'job', id: jobId } });
615+
}
616+
if (command) {
617+
renderComputationRun('field-run-record',
618+
failedComputationRun(command, jobId
619+
? `Cancellation requested for durable Job ${jobId}`
620+
: 'Cancelled before a durable Job was accepted.', 'cancelled'));
621+
}
572622
inFlight?.abort();
573623
inFlight = null;
624+
inFlightJobId = null;
625+
inFlightCommand = null;
574626
// A promise from the OLD molecule left in this map is not merely stale: a
575627
// request for the NEW molecule with the same kind|basis key joins it,
576628
// reads `molfile !== requestMolfile`, resolves null, and the panel reports
@@ -605,7 +657,7 @@ async function prefetchAll() {
605657
// deleted it and substituted a falsehood.
606658
const failed: { label: string, why: string }[] = [];
607659
for (const kind of kinds) {
608-
if (molfile !== startedFor) return; // ligand changed mid-prefetch
660+
if (molfile !== startedFor) return; // ligand changed mid-prefetch
609661
setPrefetchNote(`Precomputing fields ${done}/${kinds.length} — ${Kinds[kind].label}…`);
610662
try {
611663
await fetchField(kind);
@@ -878,11 +930,16 @@ export function buildMetaDisplay(view: FieldView | null): { rows: MetaRow[], cav
878930
function renderMeta(view: FieldView | null) {
879931
const el = byId('field-meta');
880932
if (!el) return;
881-
if (!view) { el.innerHTML = ''; return; }
933+
if (!view) {
934+
el.innerHTML = '';
935+
renderComputationRun('field-run-record', null);
936+
return;
937+
}
882938
const { rows, caveats } = buildMetaDisplay(view);
883939
el.innerHTML = rows.map(({ label, value }) =>
884940
`<div class="field-meta-row"><span>${label}</span><span>${value}</span></div>`).join('')
885941
+ caveats.map(c => `<div class="field-caveat">${escapeHtml(c)}</div>`).join('');
942+
renderComputationRun('field-run-record', view.run ?? null);
886943
}
887944

888945
function reprParams(kind: FieldKind, sign: 1 | -1, shell: number) {
@@ -1058,6 +1115,14 @@ async function requestPocketField() {
10581115
setButtonsEnabled();
10591116
setStatus(`Pocket field — ${built.sources.length} shell atoms as source, `
10601117
+ `sampled in the ligand's box…`, 'busy');
1118+
renderComputationRun('field-run-record', {
1119+
command: 'fields.region.mep', phase: 'submitting', executor: 'service',
1120+
artifacts: [], provenance: { source_atoms: built.sources.length },
1121+
note: 'Pocket sources define the field; the ligand defines only the sampling frame.',
1122+
});
1123+
const controller = new AbortController();
1124+
inFlight = controller;
1125+
inFlightCommand = 'fields.region.mep';
10611126
try {
10621127
// GROW THE FRAME UNTIL THE CONTOUR CLOSES. The ligand path does this
10631128
// in the backend; here it cannot, because the frame belongs to the
@@ -1086,7 +1151,7 @@ async function requestPocketField() {
10861151
return { ...rest, xyz: [x, y, z] };
10871152
}),
10881153
frame: grown.frame,
1089-
}, { inlineMax: 0 });
1154+
}, { inlineMax: 0, signal: controller.signal });
10901155
if (!env.ok) break;
10911156
// The box-growing loop reads the CONTRACT-DECLARED flag now, not a key that
10921157
// happened to be in an ungoverned dict.
@@ -1096,6 +1161,9 @@ async function requestPocketField() {
10961161
const e = env?.error;
10971162
setStatus(e ? `${e.user_message || e.message}${e.caller_action ? ' — ' + e.caller_action : ''}`
10981163
: 'the pocket field could not be computed', 'error');
1164+
renderComputationRun('field-run-record', env
1165+
? computationRunFromEnvelope('fields.region.mep', env)
1166+
: failedComputationRun('fields.region.mep', 'No response envelope'));
10991167
return;
11001168
}
11011169
const ref = (env.artifacts || [])[0];
@@ -1110,6 +1178,14 @@ async function requestPocketField() {
11101178
meta: env.meta ?? null,
11111179
digestVerified: got.verified,
11121180
digestUnverifiedReason: got.unverifiedReason,
1181+
run: {
1182+
...computationRunFromEnvelope('fields.region.mep', env),
1183+
provenance: {
1184+
...(env.meta?.provenance || {}),
1185+
artifact_digest: got.verified ? 'verified'
1186+
: got.unverifiedReason || 'not verified',
1187+
},
1188+
},
11131189
};
11141190
await renderCube(got.text(), 'pocket_mep', pview);
11151191
renderMeta(pview);
@@ -1134,8 +1210,17 @@ async function requestPocketField() {
11341210
setStatus(`Pocket map from ${sourceNote}, net charge ${model.net_charge} · `
11351211
+ `${chargeModelNote} · frame +${framePad} Å around the ligand.`, 'ok');
11361212
} catch (e) {
1213+
if (controller.signal.aborted) {
1214+
setStatus('Pocket field cancelled.', 'idle');
1215+
return;
1216+
}
11371217
setStatus(`Backend unreachable — ${e instanceof Error ? e.message : String(e)}`, 'error');
1218+
renderComputationRun('field-run-record', failedComputationRun('fields.region.mep', e));
11381219
} finally {
1220+
if (inFlight === controller) {
1221+
inFlight = null;
1222+
inFlightCommand = null;
1223+
}
11391224
busy = false;
11401225
setButtonsEnabled();
11411226
}
@@ -1176,8 +1261,12 @@ async function requestField(kind: FieldKind, budget = budgetFor(kind)) {
11761261
? `Solving ${spec.label} — pyscf SCF on ${ligandLabel ?? 'ligand'}… `
11771262
+ `(gives up at ${budget} s · Cancel to stop now)`
11781263
: `Computing ${spec.label}…`, 'busy');
1264+
renderComputationRun('field-run-record', {
1265+
command: 'structure.field.compute', phase: 'submitting', executor: 'service',
1266+
artifacts: [], provenance: { field_kind: kind, basis: spec.quantum ? currentBasis() : 'not applicable' },
1267+
});
11791268
try {
1180-
const entry = await fetchField(kind, false, budget);
1269+
const entry = await fetchField(kind, false, budget, true);
11811270
if (entry === null || molfile !== requestMolfile) {
11821271
setStatus('Ligand changed while computing — stale field discarded.', 'idle');
11831272
return;
@@ -1192,6 +1281,16 @@ async function requestField(kind: FieldKind, budget = budgetFor(kind)) {
11921281
const reason = e instanceof FieldRefusal ? e.reason : 'internal';
11931282
const msg = e instanceof Error ? e.message : String(e);
11941283
renderMeta(null);
1284+
if (reason === 'cancelled') {
1285+
renderComputationRun('field-run-record',
1286+
failedComputationRun('structure.field.compute', msg, 'cancelled'));
1287+
setStatus(msg, 'idle');
1288+
return;
1289+
}
1290+
const code = reason === 'budget' ? 'BUDGET'
1291+
: reason === 'unsupported' ? 'UNSUPPORTED' : 'INTERNAL';
1292+
renderComputationRun('field-run-record', computationRunFromEnvelope(
1293+
'structure.field.compute', { ok: false, error: { code, message: msg } }));
11951294
if (reason === 'network') {
11961295
setStatus(`Backend unreachable — start it with: `
11971296
+ `backend/env/bin/python backend/field_server.py (${msg})`, 'error');
@@ -1299,8 +1398,8 @@ export function initFieldWellsPanel(p: PluginContext) {
12991398
abortInFlight();
13001399
busy = false;
13011400
setButtonsEnabled();
1302-
setStatus('Cancelled. The daemon stops at its own deadline; '
1303-
+ 'nothing is left running.', 'idle');
1401+
setStatus('Cancellation requested. The durable Job records the request; '
1402+
+ 'native work stops at cooperative checkpoints.', 'idle');
13041403
});
13051404
// Changing basis invalidates the quantum entries only; classical fields
13061405
// have no basis. Cheapest correct move: refetch on demand.
@@ -1340,7 +1439,7 @@ export function initFieldWellsPanel(p: PluginContext) {
13401439
export function invalidateFieldsForGeometry(nextMolfile: string | null) {
13411440
molfile = nextMolfile;
13421441
abortInFlight();
1343-
cubeCache.clear(); // every entry was keyed to the old coordinates
1442+
cubeCache.clear(); // every entry was keyed to the old coordinates
13441443

13451444
if (!activeKind) return;
13461445
const spec = Kinds[activeKind];

0 commit comments

Comments
 (0)