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/CHANGELOG.md +72 -0
- package/README.md +63 -8
- package/docs/README.zh.md +27 -5
- package/examples/config.example.yml +3 -1
- package/lib/client.js +7 -7
- package/lib/skill.js +32 -6
- package/lib/store.mjs +17 -9
- package/lib/tools.js +106 -37
- package/package.json +1 -1
- package/python/bridge.py +171 -49
- package/python/tests/run_tests.py +269 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
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);
|
|
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
|
|
236
|
-
reps: { type: 'integer', minimum: 1, description: '
|
|
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
|
|
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
|
|
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
|
|
327
|
-
|
|
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
|
|
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' &&
|
|
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 &&
|
|
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
|
|
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
|
|
483
|
-
value.
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|