dsh-labscript 0.2.7 → 0.3.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/lib/tools.js CHANGED
@@ -30,6 +30,9 @@ function progressLine(run) {
30
30
  if (run.kind) parts.push('kind=' + run.kind)
31
31
  if (run.n_h5 !== undefined) parts.push('h5=' + run.n_h5 + '/' + (run.expected_n_shots || '?'))
32
32
  else if (run.expected_n_shots) parts.push('expected=' + run.expected_n_shots)
33
+ // h5 counts files; labscript writes each file before compiling it, so a compiled
34
+ // but not yet executed run shows h5>0 until the first acquisition data appears.
35
+ if (run.n_executed !== undefined) parts.push('executed=' + run.n_executed)
33
36
  if (run.complete !== undefined) parts.push('complete=' + run.complete)
34
37
  if (run.elapsed_s !== undefined) parts.push('elapsed=' + run.elapsed_s + 's')
35
38
  return parts.join(' ')
@@ -64,14 +67,21 @@ function globalLines(args, value) {
64
67
  return out
65
68
  }
66
69
 
70
+ // runmanager decides the shot count from the expansion settings, which runmanager.remote
71
+ // cannot read: a tuple is not an axis and an array can be switched to expansion "none".
72
+ // The panel's n_shots is therefore the only authoritative number; this line is a hint.
67
73
  function scanLine(value) {
68
74
  const axes = value.scan_axes && typeof value.scan_axes === 'object' ? value.scan_axes : {}
69
75
  const names = Object.keys(axes).sort()
70
76
  const product = value.n_shots_from_arrays
71
- const mismatch = typeof product === 'number' && typeof value.n_shots === 'number' && value.n_shots !== product
77
+ const known = typeof value.n_shots === 'number'
78
+ const mismatch = typeof product === 'number' && known && value.n_shots !== product
72
79
  if (names.length === 0 && !mismatch) return ''
73
80
  const detail = names.length ? names.map((key) => key + '=' + axes[key]).join(', ') : 'none'
74
- return 'scan axes: ' + detail + ' n_shots_from_arrays=' + product + (mismatch ? ' (panel n_shots=' + value.n_shots + ' differs)' : '')
81
+ const verdict = mismatch
82
+ ? ' product ' + product + ' != n_shots ' + value.n_shots + ': n_shots is authoritative (a tuple global or an expansion setting of none is not an axis)'
83
+ : ' product ' + product + ' n_shots ' + (known ? value.n_shots : '?') + ' (authoritative)'
84
+ return 'scan axes (heuristic): ' + detail + verdict
75
85
  }
76
86
 
77
87
  function resultsLine(value) {
@@ -82,6 +92,24 @@ function resultsLine(value) {
82
92
  return 'results: ' + names.map((group) => group + ' (' + layout[group].length + ' fields)').join(', ')
83
93
  }
84
94
 
95
+ // Protocol keys that the loaded labscript does not define, with the closest names it
96
+ // does define: a renamed panel global is the most common reason a protocol is stale.
97
+ function nameCheckDetail(check) {
98
+ const missing = check && Array.isArray(check.missing) ? check.missing : []
99
+ return missing.map((key) => {
100
+ const suggestions = (check.suggestions || {})[key] || []
101
+ return key + (suggestions.length ? ' (closest: ' + suggestions.join(', ') + ')' : '')
102
+ }).join(', ')
103
+ }
104
+
105
+ function nameCheckLine(value) {
106
+ const check = value.name_check
107
+ if (!check) return ''
108
+ if (check.checked === false) return 'script key check: not checked (' + (check.reason || 'no script') + ')'
109
+ const detail = nameCheckDetail(check)
110
+ return detail ? 'script key check: MISSING ' + detail : ''
111
+ }
112
+
85
113
  function dataGlobalsLines(args, value) {
86
114
  const globals = value.globals
87
115
  if (!globals) return []
@@ -166,23 +194,57 @@ async function requireApproval(runtime, exec, toolName, reason, approvalKey) {
166
194
  }
167
195
  }
168
196
 
197
+ // The pending run may be stored under either path form: labs_engage returns the
198
+ // mapped folder, the bridge also reports the raw runmanager folder, and an agent may
199
+ // quote either one back.
200
+ function pendingFolders(pending) {
201
+ if (!pending) return []
202
+ return [pending.run_folder, pending.mapped_run_folder, pending.remote_run_folder].filter((folder) => Boolean(folder))
203
+ }
204
+
205
+ function isPendingRun(pending, folder) {
206
+ return pendingFolders(pending).includes(folder)
207
+ }
208
+
209
+ // Returns the dropped run description when a dead submission was cleared.
169
210
  async function clearStalePending(runtime, exec) {
170
211
  const pending = runtime.state.pending
171
- if (!pending) return
212
+ if (!pending) return null
172
213
  if (pending.run_folder) {
214
+ let progress = null
173
215
  try {
174
- const progress = await runtime.runBridge('progress', {
216
+ progress = await runtime.runBridge('progress', {
175
217
  run_folder: pending.run_folder,
176
218
  expected_n_shots: pending.expected_n_shots,
177
219
  }, { signal: exec.signal })
178
- if (!progress.complete) {
179
- throw new LabError('RUN_PENDING', 'a run is still in progress (' + progress.n_h5 + '/' + progress.expected_n_shots + ' shots): call labs_wait first, or labs_abort')
220
+ } catch {
221
+ progress = null
222
+ }
223
+ if (progress && progress.complete !== true) {
224
+ const ageS = pending.submitted_at ? Math.round((Date.now() - Date.parse(pending.submitted_at)) / 1000) : 0
225
+ const staleS = Number((runtime.config.engage || {}).staleS || 180)
226
+ const nH5 = Number(progress.n_h5 || 0)
227
+ if (progress.folder_exists && nH5 === 0 && ageS >= staleS) {
228
+ // Nothing was ever written for this submission and it is minutes old: the
229
+ // compilation aborted, so the run folder stays empty forever. Drop it and
230
+ // report the drop instead of refusing to submit again.
231
+ const dropped = {
232
+ run_folder: progress.mapped_run_folder || pending.run_folder,
233
+ age_s: ageS,
234
+ expected_n_shots: pending.expected_n_shots,
235
+ }
236
+ runtime.clearPending()
237
+ return dropped
180
238
  }
181
- } catch (error) {
182
- if (error instanceof LabError && error.code === 'RUN_PENDING') throw error
239
+ const next = nH5 === 0
240
+ ? ' A submission with no shots is dropped automatically after ' + staleS + ' s, or call labs_abort now.'
241
+ : ' Call labs_wait to inspect it, or labs_abort to abandon it.'
242
+ throw new LabError('RUN_PENDING', 'a run is still in progress (' + nH5 + '/' + (pending.expected_n_shots || '?') +
243
+ ' shots, ' + ageS + ' s old):' + next)
183
244
  }
184
245
  }
185
246
  runtime.clearPending()
247
+ return null
186
248
  }
187
249
 
188
250
  function mockApprovalSkipped(runtime) {
@@ -193,7 +255,7 @@ export function registerTools(ctx, runtime) {
193
255
 
194
256
  ctx.tools.register({
195
257
  name: 'labs_status',
196
- description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw expressions with raw=true), n_shots, error_in_globals, run_shots, shot output folder, pending run progress, and the panel snapshot directory. Read-only; call this before any write. Pass keys=[...] (leaf global names; UI group names are not globals) to print the selected panel values (240 chars each; raw=true prints the raw expression), missing keys are shown as <missing>; keys=["*"] prints every global. scan axes lists array-valued globals (their lengths multiply to n_shots_from_arrays) and a differing panel n_shots is flagged. The shot output folder is the folder the NEXT run will use; it does not exist until engage. Paths in the output are already mapped for this machine through pathMap; do not map them again.',
258
+ description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw expressions with raw=true), n_shots, error_in_globals, run_shots, shuffle, shot output folder (called run folder in this tool: where the NEXT engage writes its shot files; it does not exist until engage), pending run progress, and the panel snapshot directory. Read-only; call this before any write. Pass keys=[...] (leaf global names; UI group names are not globals) to print the selected panel values (240 chars each; raw=true prints the raw expression), missing keys are shown as <missing> (the global is in an inactive group or misspelled); keys=["*"] prints every global. scan axes is a heuristic: it counts array-valued globals and their lengths multiply to n_shots_from_arrays, but runmanager does not expand tuples and an expansion setting of none turns an array into a non-axis, neither of which is visible from the remote API. n_shots is authoritative: when the two differ, report n_shots and say that at least one array-valued global is not an axis. shuffle true means shot order does not follow the axis order. Paths in the output are already mapped for this machine through pathMap; do not map them again.',
197
259
  parameters: {
198
260
  type: 'object',
199
261
  properties: {
@@ -207,7 +269,7 @@ export function registerTools(ctx, runtime) {
207
269
  'backend: ' + value.backend + ' plugin: v' + runtime.version,
208
270
  'labscript: ' + value.labscript,
209
271
  'shot output: ' + value.shot_output_folder,
210
- 'n_shots: ' + value.n_shots + ' error_in_globals: ' + value.error_in_globals + ' run_shots: ' + value.run_shots,
272
+ 'n_shots: ' + value.n_shots + ' error_in_globals: ' + value.error_in_globals + ' run_shots: ' + value.run_shots + ' shuffle: ' + (value.shuffle === undefined || value.shuffle === null ? 'unknown' : value.shuffle),
211
273
  progressLine(value.last_run),
212
274
  'panel history: ' + (value.panel_history ? value.panel_history.dir + ' (' + value.panel_history.count + ' snapshots)' : 'n/a'),
213
275
  scanLine(value),
@@ -227,13 +289,13 @@ export function registerTools(ctx, runtime) {
227
289
 
228
290
  ctx.tools.register({
229
291
  name: 'labs_set',
230
- description: 'Write runmanager panel globals. values are JSON scalars (bool/number/string); pass raw=true to write expression strings, or reps=N to write the repetition counter (np.arange(N)). A full panel snapshot is taken before the write. Does not run anything.',
292
+ description: 'Write runmanager panel globals. One call can do the whole step: values are JSON scalars (bool/number/string); with raw=true a string value is written as an expression (e.g. np.arange(0.5, 6.7, 0.2)) while bool/number values are still converted normally (true -> True, 2.72 -> 2.72), so scalars, an expression scan axis and reps=N can be combined. reps=N writes the configured repetition global as np.arange(N). A list global becomes a scan axis only if runmanager actually expands it (a tuple is passed through whole, and an expansion setting of none drops the axis). A global name must not shadow a Python builtin or a name from "from labscript import *": runmanager accepts the name, labscript fails at compile time, and that failure is visible only in runmanager output tab. A full panel snapshot is taken before the write. Does not run anything.',
231
293
  parameters: {
232
294
  type: 'object',
233
295
  properties: {
234
296
  values: { type: 'object', additionalProperties: true, description: 'Panel globals to write: key -> scalar' },
235
- raw: { type: 'boolean', description: 'values are raw expression strings (e.g. np.arange(0, 8, 0.3))' },
236
- reps: { type: 'integer', minimum: 1, description: 'Write the configured repetition global as np.arange(reps)' },
297
+ raw: { type: 'boolean', description: 'String values are written as expressions; bool/number values are still converted (mix them freely with reps=N)' },
298
+ reps: { type: 'integer', minimum: 1, description: 'Also write the configured repetition global as np.arange(reps)' },
237
299
  note: { type: 'string', description: 'One sentence: what this batch is for (written to the event log)' },
238
300
  },
239
301
  required: ['values'],
@@ -257,7 +319,7 @@ export function registerTools(ctx, runtime) {
257
319
 
258
320
  ctx.tools.register({
259
321
  name: 'labs_engage',
260
- description: 'Submit the current panel for a run (compiles and sends to BLACS). Returns immediately with run_folder, n_shots_at_submit and expected_n_shots; use labs_wait later to wait for completion. Refuses when error_in_globals is true or another run is still pending.',
322
+ description: 'Submit the current panel as one sequence: runmanager writes one shot file per point and hands it to BLACS (this is the "shot output folder", called run_folder in this tool). Returns immediately with run_folder, n_shots_at_submit and expected_n_shots; use labs_wait later to wait for completion. It cannot see compilation errors: runmanager shows those only in its output tab. Refuses when error_in_globals is true or another run is still pending; a pending run that produced no shot at all for config.engage.staleS (default 180 s) is dropped and reported as dropped_stale_pending.',
261
323
  parameters: {
262
324
  type: 'object',
263
325
  properties: {
@@ -267,29 +329,33 @@ export function registerTools(ctx, runtime) {
267
329
  },
268
330
  isConcurrencySafe: () => false,
269
331
  output: looseOutput((_args, value) => text(lines([
332
+ value.dropped_stale_pending ? 'dropped stale pending run: ' + value.dropped_stale_pending.run_folder + ' (' + value.dropped_stale_pending.age_s + ' s old, no shots ever appeared)' : '',
270
333
  'submitted run: ' + (value.mapped_run_folder || value.run_folder || '<run folder pending>'),
271
334
  'n_shots_at_submit: ' + value.n_shots_at_submit + ' expected: ' + value.expected_n_shots,
272
335
  'call labs_wait when results are needed.',
273
336
  ]))),
274
337
  async execute(args, exec) {
275
338
  return runtime.serial(async () => {
276
- await clearStalePending(runtime, exec)
339
+ const dropped = await clearStalePending(runtime, exec)
277
340
  const before = await runtime.runBridge('status', {}, { signal: exec.signal })
278
341
  await requireApproval(runtime, exec, 'labs_engage', 'Submit a labscript run: ' + (before.labscript || '<script>') + ', n_shots=' + before.n_shots + ', backend=' + before.backend, 'engage')
279
342
  const data = await runtime.runBridge('engage', { note: args.note }, { signal: exec.signal, timeoutMs: 180000 })
280
343
  runtime.setLastRun({
281
- run_folder: data.run_folder,
344
+ run_folder: data.mapped_run_folder || data.run_folder,
345
+ remote_run_folder: data.run_folder,
282
346
  expected_n_shots: data.expected_n_shots,
283
347
  submitted_at: data.submitted_at,
284
348
  })
285
349
  runtime.setPending({
286
350
  kind: 'engage',
287
- run_folder: data.run_folder,
351
+ run_folder: data.mapped_run_folder || data.run_folder,
352
+ remote_run_folder: data.run_folder,
288
353
  expected_n_shots: data.expected_n_shots,
289
354
  submitted_at: data.submitted_at,
290
355
  note: args.note,
291
356
  snapshot: null,
292
357
  })
358
+ if (dropped) data.dropped_stale_pending = dropped
293
359
  return data
294
360
  })
295
361
  },
@@ -297,7 +363,7 @@ export function registerTools(ctx, runtime) {
297
363
 
298
364
  ctx.tools.register({
299
365
  name: 'labs_wait',
300
- description: 'Wait for the pending run to finish (blocking, timeout in seconds). If the pending run came from labs_protocol run, this call also finalizes it: postprocess, finish write-back of panel and calibration history, and panel restore. Returns expected vs actual shots and, for protocols, the postprocess/finish/restore results. A run folder that does not exist and is not the pending run of this session fails immediately instead of waiting.',
366
+ description: 'Wait for the pending run (sequence) to finish (blocking, timeout in seconds). If it came from labs_protocol run, this call also finalizes it: postprocess, finish write-back of panel and calibration history, and panel restore. Returns expected vs actual shot files and, for protocols, the postprocess/finish/restore results. Returns early with diagnosis no_shots or compile_stalled when no shot file appeared for config.engage.noShotS seconds (default 25): compilation errors are visible only in runmanager output tab, which this tool cannot read, so report exactly that and ask the operator to paste the tab; call labs_wait again only if you expect a slow compile. diagnosis compiled_not_executed means shot files exist but BLACS acquired no data: BLACS may be paused, may have refused the shot because its connection table is not a subset of the lab connection table, or a device group failed to program within 5 minutes (the queue then pauses and puts the shot back at its head) - all visible only in the BLACS GUI. More shot files than expected means the panel n_shots changed after submit, or BLACS Repeat all / Repeat last re-ran shots. A run folder that does not exist and is not the pending run of this session fails immediately instead of waiting.',
301
367
  parameters: {
302
368
  type: 'object',
303
369
  properties: {
@@ -312,7 +378,7 @@ export function registerTools(ctx, runtime) {
312
378
  'run: ' + (value.mapped_run_folder || value.run_folder),
313
379
  value.aborted ? 'wait cancelled by labs_abort (run aborted)' : '',
314
380
  value.folder_exists === false ? 'folder: missing' : '',
315
- 'shots: ' + value.n_h5 + '/' + (value.expected_n_shots || '?') + ' complete: ' + value.complete + (value.timed_out ? ' TIMED OUT' : '') + (value.diagnosis ? ' diagnosis: ' + value.diagnosis : ''),
381
+ 'shots: ' + value.n_h5 + '/' + (value.expected_n_shots || '?') + ' h5 files' + (value.n_executed !== undefined ? ', ' + value.n_executed + ' with acquisition data' : '') + ' complete: ' + value.complete + (value.timed_out ? ' TIMED OUT' : '') + (value.diagnosis ? ' diagnosis: ' + value.diagnosis : ''),
316
382
  value.hint ? 'hint: ' + value.hint : '',
317
383
  value.finalize && value.finalize.postprocess ? 'postprocess: ' + JSON.stringify(value.finalize.postprocess).slice(0, 400) : '',
318
384
  value.finalize && value.finalize.finish ? 'finish: ' + JSON.stringify(value.finalize.finish).slice(0, 300) : '',
@@ -323,8 +389,9 @@ export function registerTools(ctx, runtime) {
323
389
  const pending = runtime.state.pending
324
390
  const runFolder = args.run_folder || (pending ? pending.run_folder : '')
325
391
  if (!runFolder) throw new LabError('NO_PENDING_RUN', 'no run folder: submit with labs_engage or labs_protocol run first, or pass run_folder')
326
- const expected = pending && pending.run_folder === runFolder ? pending.expected_n_shots : undefined
327
- if (!(pending && pending.run_folder === runFolder)) {
392
+ const pendingRun = isPendingRun(pending, runFolder)
393
+ const expected = pendingRun ? pending.expected_n_shots : undefined
394
+ if (!pendingRun) {
328
395
  let probe = null
329
396
  try {
330
397
  probe = await runtime.runBridge('progress', {
@@ -368,6 +435,7 @@ export function registerTools(ctx, runtime) {
368
435
  run_folder: waited.run_folder,
369
436
  expected_n_shots: waited.expected_n_shots,
370
437
  n_h5: waited.n_h5,
438
+ n_executed: waited.n_executed,
371
439
  complete: waited.complete,
372
440
  timed_out: waited.timed_out,
373
441
  elapsed: waited.elapsed,
@@ -378,13 +446,13 @@ export function registerTools(ctx, runtime) {
378
446
  folder_exists: waited.folder_exists,
379
447
  }
380
448
  for (const key of Object.keys(out)) if (out[key] === undefined) delete out[key]
381
- if (waited.complete && runtime.state.lastRun && runtime.state.lastRun.run_folder === runFolder) {
449
+ if (waited.complete && runtime.state.lastRun && isPendingRun(runtime.state.lastRun, runFolder)) {
382
450
  runtime.setLastRun(Object.assign({}, runtime.state.lastRun, {
383
451
  completed_at: new Date().toISOString(),
384
452
  n_h5: waited.n_h5,
385
453
  }))
386
454
  }
387
- if (waited.complete && pending && pending.kind === 'protocol' && pending.run_folder === runFolder) {
455
+ if (waited.complete && pending && pending.kind === 'protocol' && pendingRun) {
388
456
  const finalize = await runtime.runBridge('protocol_finalize', {
389
457
  protocol: pending.plan,
390
458
  run_folder: runFolder,
@@ -393,7 +461,7 @@ export function registerTools(ctx, runtime) {
393
461
  }, { signal: exec.signal, timeoutMs: 180000 })
394
462
  out.finalize = finalize
395
463
  runtime.clearPending()
396
- } else if (waited.complete && pending && pending.run_folder === runFolder) {
464
+ } else if (waited.complete && pending && pendingRun) {
397
465
  runtime.clearPending()
398
466
  }
399
467
  return out
@@ -422,7 +490,7 @@ export function registerTools(ctx, runtime) {
422
490
 
423
491
  ctx.tools.register({
424
492
  name: 'labs_data',
425
- description: 'Read a run folder HDF5: first-shot globals (pass keys=[...] to print selected values, ["*"] for all), the results groups with results: true, and with scan: a per-shot scan axis summary (unique points, min/max, median step) plus the numeric result columns; pass limit to print up to 20 raw rows. Read-only. run_folder defaults to the pending run, else the last run submitted through this plugin (persisted across restarts); for a historical run pass its folder explicitly. A missing folder is an error: runmanager\'s shot output is the NEXT run folder and only exists after engage.',
493
+ description: 'Read a run folder (shot output folder) HDF5: first-shot globals (pass keys=[...] to print selected values, ["*"] for all), the results groups with results: true, and with scan: a per-shot axis summary (unique points, min/max, median step) plus the numeric result columns; pass limit to print up to 20 raw rows. Each shot file holds the evaluated globals of that shot, its hardware commands and, after execution, images/ and results/ - a shot with no images/results was not executed. With shuffle on, read each shot\'s parameters from its own file instead of assuming axis order. Read-only. run_folder defaults to the pending run, else the last run submitted through this plugin (persisted across restarts); for a historical run pass its folder explicitly. A missing folder is an error: the shot output folder is the NEXT sequence\'s folder and only exists after engage.',
426
494
  parameters: {
427
495
  type: 'object',
428
496
  properties: {
@@ -479,8 +547,8 @@ export function registerTools(ctx, runtime) {
479
547
  value.plan && value.plan.scan ? 'scan: ' + Object.keys(value.plan.scan).join(', ') : '',
480
548
  value.hard_limits ? 'hard limits: ' + JSON.stringify(value.hard_limits).slice(0, 240) : '',
481
549
  value.warnings && value.warnings.length ? 'warnings: ' + value.warnings.join('; ') : '',
482
- value.name_check && value.name_check.checked && value.name_check.missing && value.name_check.missing.length ? 'script key check: MISSING ' + value.name_check.missing.join(', ') : '',
483
- value.name_check && value.name_check.checked === false ? 'script key check: not checked (' + (value.name_check.reason || 'no script') + ')' : '',
550
+ nameCheckLine(value),
551
+ value.dropped_stale_pending ? 'dropped stale pending run: ' + value.dropped_stale_pending.run_folder + ' (' + value.dropped_stale_pending.age_s + ' s old, no shots ever appeared)' : '',
484
552
  value.engage ? 'submitted: ' + (value.engage.mapped_run_folder || value.engage.run_folder || '<run folder pending>') + ' n_shots_at_submit: ' + value.n_shots_at_submit : '',
485
553
  value.engage ? 'call labs_wait to finalize this protocol.' : '',
486
554
  ]))),
@@ -496,38 +564,39 @@ export function registerTools(ctx, runtime) {
496
564
  return { name: loaded.name, file: loaded.file, warnings: loaded.warnings.concat(planned.warnings || []), plan: planned.plan, hard_limits: planned.hard_limits, name_check: planned.name_check }
497
565
  }
498
566
  return runtime.serial(async () => {
499
- await clearStalePending(runtime, exec)
567
+ const dropped = await clearStalePending(runtime, exec)
500
568
  const planned = await runtime.runBridge('protocol_plan', { protocol: loaded.plan }, { signal: exec.signal })
501
569
  if (planned.hard_limits && planned.hard_limits.checked) {
502
570
  // plan command already fails on violations; nothing to do
503
571
  }
504
572
  const nameCheck = planned.name_check
505
- if (nameCheck && nameCheck.checked && nameCheck.missing && nameCheck.missing.length) {
506
- const detail = nameCheck.missing.map((key) => {
507
- const suggestions = (nameCheck.suggestions || {})[key] || []
508
- return key + (suggestions.length ? ' (closest: ' + suggestions.join(', ') + ')' : '')
509
- }).join(', ')
510
- await requireApproval(runtime, exec, 'labs_protocol', 'Protocol ' + loaded.name + ' uses key(s) that are not in the currently loaded labscript: ' + detail + '. Continue anyway?', 'protocolNames')
573
+ const missingDetail = nameCheckDetail(nameCheck)
574
+ if (missingDetail) {
575
+ await requireApproval(runtime, exec, 'labs_protocol', 'Protocol ' + loaded.name + ' uses key(s) that are not in the currently loaded labscript: ' + missingDetail + '. Continue anyway?', 'protocolNames')
511
576
  }
512
577
  await requireApproval(runtime, exec, 'labs_protocol', 'Run protocol ' + loaded.name + ': expected ' + planned.plan.expected_n_shots + ' shots, labscript ' + (loaded.plan.labscript || loaded.plan.joint_script) + ', backend ' + runtime.config.backend, 'protocolRun')
513
578
  const started = await runtime.runBridge('protocol_start', { protocol: loaded.plan, note: args.note }, { signal: exec.signal, timeoutMs: 180000 })
514
579
  if (started.engage) {
515
580
  runtime.setLastRun({
516
- run_folder: started.engage.run_folder,
581
+ run_folder: started.engage.mapped_run_folder || started.engage.run_folder,
582
+ remote_run_folder: started.engage.run_folder,
517
583
  expected_n_shots: started.expected_n_shots,
518
584
  submitted_at: started.submitted_at,
519
585
  })
520
586
  runtime.setPending({
521
587
  kind: 'protocol',
522
588
  plan: loaded.plan,
523
- run_folder: started.engage.run_folder,
589
+ run_folder: started.engage.mapped_run_folder || started.engage.run_folder,
590
+ remote_run_folder: started.engage.run_folder,
524
591
  expected_n_shots: started.expected_n_shots,
525
592
  snapshot: started.snapshot,
526
593
  submitted_at: started.submitted_at,
527
594
  note: args.note,
528
595
  })
529
596
  }
530
- return Object.assign({ name: loaded.name, file: loaded.file, warnings: loaded.warnings.concat(planned.warnings || []), name_check: nameCheck }, started)
597
+ const out = Object.assign({ name: loaded.name, file: loaded.file, warnings: loaded.warnings.concat(planned.warnings || []), name_check: nameCheck }, started)
598
+ if (dropped) out.dropped_stale_pending = dropped
599
+ return out
531
600
  })
532
601
  },
533
602
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-labscript",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Control labscript-suite experiments from DeepSeek Harness: runmanager globals and scans, submits and waits, YAML fixed-operation protocols with postprocess write-back, panel snapshots, a built-in mock backend and a read-only doctor.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",