@volter/supercode-orchestrator 0.4.28 → 0.4.30

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/board/cli.mjs CHANGED
@@ -12,7 +12,9 @@ import { read } from '../orchestration.mjs';
12
12
  import { holdDispatcherLock } from '../hermes-locks.mjs';
13
13
  import { boardPath, DEFAULT_BOARD, event, json, listBoards, now, openBoard, tx } from './store.mjs';
14
14
  import * as engine from './engine.mjs';
15
- import { adoptSession, boardLabel, boardSettings, lanesOf, localMachine, observeFleet, paused, startTriageSession, tick } from './dispatch.mjs';
15
+ import { adoptSession, boardLabel, boardSettings, lanesOf, localMachine, observeFleet, paused, startCard, tick } from './dispatch.mjs';
16
+ import { loadWorkflow, useWorkflow, workflowEntry } from './workflow.mjs';
17
+ import { evaluate } from './expr.mjs';
16
18
 
17
19
  const HELP = `supercode workflow <verb> [--root <home>] [--board <slug>] [--json]
18
20
 
@@ -148,8 +150,12 @@ function counts(root, slug) {
148
150
  /** The home's lanes (profiles' workers) and board settings (the root config's `kanban:` block), as the IR reads them. */
149
151
  async function laneState(root) {
150
152
  const orchestration = await read(root);
151
- const kanban = orchestration.profiles?.default?.residue?.config?.kanban ?? {};
152
- return { lanes: lanesOf(orchestration.profiles), settings: boardSettings(kanban), kanban, profiles: Object.keys(orchestration.profiles ?? {}) };
153
+ // the board's settings are its workflow's params (the instance's, under the home's own `kanban:` keys)
154
+ const entry = useWorkflow(root, workflowEntry(orchestration));
155
+ const settings = boardSettings(entry.params);
156
+ const tick = Number(evaluate(entry.workflow.dispatch.tick, { params: entry.params }));
157
+ if (tick > 0) settings.interval = tick;
158
+ return { lanes: lanesOf(orchestration.profiles), settings, kanban: entry.params, profiles: Object.keys(orchestration.profiles ?? {}) };
153
159
  }
154
160
 
155
161
  /** The caller's own supercode address, when it is a session supercode can reach (for its card's notices). */
@@ -207,6 +213,7 @@ export async function runWorkflow(argv) {
207
213
  const slug = boardOf(root, args);
208
214
  const open = (create = false) => { const b = openBoard(root, slug, { create }); b.label = boardLabel(root, slug); return b; };
209
215
  const withBoard = (fn, create = false) => { const b = open(create); try { return fn(b); } finally { b.close(); } };
216
+ const withBoardAsync = async (fn) => { const b = open(); try { return await fn(b); } finally { b.close(); } };
210
217
  const id = rest[0];
211
218
  const need = (n, what) => { if (rest.length < n) throw new Error(`${verb} needs ${what}`); };
212
219
  const ids = (first = rest) => [...first, ...(args.ids ?? [])];
@@ -216,6 +223,9 @@ export async function runWorkflow(argv) {
216
223
  if (sid && !READS.has(verb) && existsSync(boardPath(root, slug))) {
217
224
  withBoard((b) => { const own = ownCard(b.db, sid); if (own) authorize(b.db, verb, rest, args, own); });
218
225
  }
226
+ // the board's workflow (the IR's): what each verb does, and who may send it, is its
227
+ if (!READS.has(verb) && verb !== 'help') await loadWorkflow(root).catch((error) => { if (verb !== 'init' && verb !== 'boards') throw error; });
228
+ engine.setCaller((db, cardId) => rolesFor(db, cardId, sid));
219
229
 
220
230
  switch (verb) {
221
231
  case 'init': {
@@ -368,23 +378,23 @@ export async function runWorkflow(argv) {
368
378
  return 0;
369
379
  }
370
380
 
371
- case 'assign': need(2, 'a card id and a profile (or none)'); withBoard((b) => engine.assign(b, id, rest[1] === 'none' ? null : rest[1])); say(args, `Assigned ${id} to ${rest[1] === 'none' ? '(nobody)' : rest[1]}`, { ok: true }); return 0;
381
+ case 'assign': need(2, 'a card id and a profile (or none)'); await withBoardAsync((b) => engine.assign(b, id, rest[1] === 'none' ? null : rest[1])); say(args, `Assigned ${id} to ${rest[1] === 'none' ? '(nobody)' : rest[1]}`, { ok: true }); return 0;
372
382
  case 'reassign': {
373
383
  need(2, 'a card id and a profile');
374
- withBoard((b) => {
384
+ await withBoardAsync(async (b) => {
375
385
  const task = tx(b, (db) => engine.getTask(db, id));
376
- if (task?.status === 'running' && args.reclaim) engine.reclaim(b, id, args.reason ?? 'reassign');
377
- engine.assign(b, id, rest[1] === 'none' ? null : rest[1]);
386
+ if (task?.status === 'running' && args.reclaim) await engine.reclaim(b, id, args.reason ?? 'reassign');
387
+ await engine.assign(b, id, rest[1] === 'none' ? null : rest[1]);
378
388
  });
379
389
  say(args, `Reassigned ${id} to ${rest[1]}`, { ok: true }); return 0;
380
390
  }
381
- case 'set-model': need(1, 'a card id'); withBoard((b) => engine.setModel(b, id, rest[1] ?? null, args.provider ?? null)); say(args, rest[1] ? `${id} runs on model ${rest[1]}${args.provider ? ` (${args.provider})` : ''} from its next start` : `${id} uses its profile's model`, { ok: true }); return 0;
391
+ case 'set-model': need(1, 'a card id'); await withBoardAsync((b) => engine.setModel(b, id, rest[1] ?? null, args.provider ?? null)); say(args, rest[1] ? `${id} runs on model ${rest[1]}${args.provider ? ` (${args.provider})` : ''} from its next start` : `${id} uses its profile's model`, { ok: true }); return 0;
382
392
  case 'set-goal': {
383
393
  // a card into goal mode (or out of it with --off) from its next idle turn: the heartbeat keeps its session going
384
394
  need(1, 'a card id');
385
395
  const maxTurns = args['goal-max-turns'] ? Number(args['goal-max-turns']) : null;
386
396
  if (maxTurns !== null && !(Number.isInteger(maxTurns) && maxTurns >= 1)) throw new Error('--goal-max-turns is a whole number of turns, at least 1');
387
- for (const t of ids()) withBoard((b) => engine.setGoal(b, t, { on: !args.off, maxTurns }));
397
+ for (const t of ids()) await withBoardAsync((b) => engine.setGoal(b, t, { on: !args.off, maxTurns }));
388
398
  say(args, ids().map((t) => args.off ? `${t} is no longer a goal card` : `${t} is a goal card${maxTurns ? ` (${maxTurns} turns)` : ''}`).join('\n'), { ok: true }); return 0;
389
399
  }
390
400
  case 'move': need(2, 'a card id and a machine (or none)'); withBoard((b) => engine.setMachine(b, id, rest[1] === 'none' ? null : rest[1])); say(args, `${id} runs on ${rest[1]}`, { ok: true }); return 0;
@@ -405,17 +415,17 @@ export async function runWorkflow(argv) {
405
415
  say(args, workspace ?? `(claimed ${id}; its ${task.workspace_kind} workspace is made where it starts)`, { ok: true, run_id: runId, workspace });
406
416
  return 0;
407
417
  }
408
- case 'reclaim': need(1, 'a card id'); withBoard((b) => engine.reclaim(b, id, args.reason ?? (rest.slice(1).join(' ') || null))); say(args, `Reclaimed ${id}`, { ok: true }); return 0;
409
- case 'heartbeat': need(1, 'a card id'); withBoard((b) => engine.heartbeat(b, id, args.note ?? null)); say(args, `Heartbeat recorded for ${id}`, { ok: true }); return 0;
418
+ case 'reclaim': need(1, 'a card id'); await withBoardAsync((b) => engine.reclaim(b, id, args.reason ?? (rest.slice(1).join(' ') || null))); say(args, `Reclaimed ${id}`, { ok: true }); return 0;
419
+ case 'heartbeat': need(1, 'a card id'); await withBoardAsync((b) => engine.heartbeat(b, id, args.note ?? null)); say(args, `Heartbeat recorded for ${id}`, { ok: true }); return 0;
410
420
 
411
- case 'link': need(2, 'a parent and a child'); withBoard((b) => engine.link(b, rest[0], rest[1])); say(args, `Linked ${rest[0]} -> ${rest[1]}`, { ok: true }); return 0;
412
- case 'unlink': { need(2, 'a parent and a child'); const gone = withBoard((b) => engine.unlink(b, rest[0], rest[1])); say(args, gone ? `Unlinked ${rest[0]} -> ${rest[1]}` : `no link ${rest[0]} -> ${rest[1]}`, { ok: gone }); return gone ? 0 : 1; }
421
+ case 'link': need(2, 'a parent and a child'); await withBoardAsync((b) => engine.link(b, rest[0], rest[1])); say(args, `Linked ${rest[0]} -> ${rest[1]}`, { ok: true }); return 0;
422
+ case 'unlink': { need(2, 'a parent and a child'); const gone = await withBoardAsync((b) => engine.unlink(b, rest[0], rest[1])); say(args, gone ? `Unlinked ${rest[0]} -> ${rest[1]}` : `no link ${rest[0]} -> ${rest[1]}`, { ok: gone }); return gone ? 0 : 1; }
413
423
 
414
424
  case 'comment': {
415
425
  need(2, 'a card id and the text');
416
426
  let text = rest.slice(1).join(' ');
417
427
  if (args['max-len']) text = text.slice(0, Number(args['max-len']));
418
- const n = withBoard((b) => engine.comment(b, id, args.author ?? callerAddress() ?? callerName(), text));
428
+ const n = await withBoardAsync((b) => engine.comment(b, id, args.author ?? callerAddress() ?? callerName(), text));
419
429
  say(args, `Comment added to ${id}`, { ok: true, comment: n }); return 0;
420
430
  }
421
431
  case 'attach': {
@@ -448,45 +458,49 @@ export async function runWorkflow(argv) {
448
458
  case 'complete': {
449
459
  need(1, 'a card id');
450
460
  const metadata = metadataOf(args.metadata);
451
- for (const t of rest) withBoard((b) => engine.complete(b, t, { summary: args.summary ?? null, result: args.result ?? null, metadata: metadata ?? null }));
461
+ for (const t of rest) await withBoardAsync((b) => engine.complete(b, t, { summary: args.summary ?? null, result: args.result ?? null, metadata: metadata ?? null }));
452
462
  say(args, rest.map((t) => `Completed ${t}`).join('\n'), { ok: true, completed: rest }); return 0;
453
463
  }
454
464
  case 'edit': {
455
465
  need(1, 'a card id');
456
466
  if (args.result === undefined) throw new Error('edit needs --result');
457
- withBoard((b) => engine.editResult(b, id, { result: args.result, summary: args.summary ?? null, metadata: metadataOf(args.metadata) }));
467
+ await withBoardAsync((b) => engine.editResult(b, id, { result: args.result, summary: args.summary ?? null, metadata: metadataOf(args.metadata) }));
458
468
  say(args, `Edited ${id}`, { ok: true }); return 0;
459
469
  }
460
470
  case 'block': {
461
471
  need(1, 'a card id');
462
472
  const reason = rest.slice(1).join(' ') || null;
463
- const results = [id, ...(args.ids ?? [])].map((t) => [t, withBoard((b) => engine.block(b, t, { reason, kind: args.kind ?? null }))]);
473
+ const results = [];
474
+ for (const t of [id, ...(args.ids ?? [])]) results.push([t, (await withBoardAsync((b) => engine.block(b, t, { reason, kind: args.kind ?? null }))).to]);
464
475
  say(args, results.map(([t, s]) => `${s === 'todo' ? 'Waiting on dependencies' : s === 'triage' ? 'Routed to triage' : 'Blocked'} ${t}`).join('\n'), { ok: true, results }); return 0;
465
476
  }
466
477
  case 'schedule': {
467
478
  need(1, 'a card id');
468
479
  const reason = rest.slice(1).join(' ') || null;
469
- for (const t of [id, ...(args.ids ?? [])]) withBoard((b) => engine.schedule(b, t, reason));
480
+ for (const t of [id, ...(args.ids ?? [])]) await withBoardAsync((b) => engine.schedule(b, t, reason));
470
481
  say(args, [id, ...(args.ids ?? [])].map((t) => `Scheduled ${t}`).join('\n'), { ok: true }); return 0;
471
482
  }
472
483
  case 'unblock': {
473
484
  need(1, 'a card id');
474
- const results = ids().map((t) => [t, withBoard((b) => engine.unblock(b, t))]);
485
+ const results = [];
486
+ for (const t of ids()) results.push([t, (await withBoardAsync((b) => engine.unblock(b, t, { reason: args.reason ?? null }))).to]);
475
487
  // Hermes records the reason as a comment on each card it unblocks
476
- if (args.reason) for (const [t] of results) withBoard((b) => engine.comment(b, t, callerName(), `UNBLOCK: ${args.reason}`));
488
+ if (args.reason) for (const [t] of results) await withBoardAsync((b) => engine.comment(b, t, callerName(), `UNBLOCK: ${args.reason}`));
477
489
  say(args, results.map(([t, s]) => `Unblocked ${t} → ${s}`).join('\n'), { ok: true, results }); return 0;
478
490
  }
479
- case 'request-review': need(1, 'a card id'); withBoard((b) => engine.requestReview(b, id, { summary: args.summary ?? null, reviewer: args.reviewer ?? null, metadata: metadataOf(args.metadata) ?? null, force: Boolean(args.force), expectedRunId: workerRun(b.db, id, sid) })); say(args, `${id} → review`, { ok: true }); return 0;
480
- case 'request-changes': need(2, 'a card id and the reason'); withBoard((b) => engine.requestChanges(b, id, { reason: rest.slice(1).join(' '), reviewer: args.reviewer ?? callerName() })); say(args, `${id} sent back for changes`, { ok: true }); return 0;
491
+ case 'request-review': need(1, 'a card id'); await withBoardAsync((b) => engine.requestReview(b, id, { summary: args.summary ?? null, reviewer: args.reviewer ?? null, metadata: metadataOf(args.metadata) ?? null, force: Boolean(args.force), expectedRunId: workerRun(b.db, id, sid) })); say(args, `${id} → review`, { ok: true }); return 0;
492
+ case 'request-changes': need(2, 'a card id and the reason'); await withBoardAsync((b) => engine.requestChanges(b, id, { reason: rest.slice(1).join(' '), reviewer: args.reviewer ?? callerName() })); say(args, `${id} sent back for changes`, { ok: true }); return 0;
481
493
  case 'reopen': {
482
494
  // a done card back to work; the cards that built on it are invalidated with it (Hermes's reopen)
483
495
  need(1, 'a card id');
484
- const results = ids().map((t) => [t, withBoard((b) => engine.reopen(b, t, { author: callerName(), reason: args.reason ?? null }))]);
485
- say(args, results.map(([t, r]) => `Reopened ${t} → ${r.status}${r.invalidated?.length ? ` (invalidated ${r.invalidated.map((d) => d.id).join(', ')})` : ''}`).join('\n'), { ok: true, results }); return 0;
496
+ const results = [];
497
+ for (const t of ids()) results.push([t, (await withBoardAsync((b) => engine.reopen(b, t, { author: callerName(), reason: args.reason ?? null }))).to]);
498
+ say(args, results.map(([t, s]) => `Reopened ${t} → ${s}`).join('\n'), { ok: true, results }); return 0;
486
499
  }
487
500
  case 'reopen-review': {
488
501
  need(1, 'a card id');
489
- const results = ids().map((t) => [t, withBoard((b) => engine.reopenReview(b, t))]);
502
+ const results = [];
503
+ for (const t of ids()) results.push([t, (await withBoardAsync((b) => engine.reopenReview(b, t))).to]);
490
504
  say(args, results.map(([t, s]) => `Reopened ${t} → ${s}`).join('\n'), { ok: true, results }); return 0;
491
505
  }
492
506
  case 'promote': {
@@ -503,11 +517,11 @@ export async function runWorkflow(argv) {
503
517
  say(args, verdicts.map((v) => `${v.ok ? 'would promote' : 'would refuse'} ${v.id}${v.reason ? `: ${v.reason}` : ''}`).join('\n'), verdicts);
504
518
  return 0;
505
519
  }
506
- for (const t of targets) withBoard((b) => engine.promote(b, t, { actor: callerName(), reason, force: args.force }));
520
+ for (const t of targets) await withBoardAsync((b) => engine.promote(b, t, { actor: callerName(), reason, force: args.force }));
507
521
  say(args, targets.map((t) => `Promoted ${t} → ready`).join('\n'), { ok: true }); return 0;
508
522
  }
509
523
  case 'archive': {
510
- for (const t of rest) withBoard((b) => engine.archive(b, t));
524
+ for (const t of rest) await withBoardAsync((b) => engine.archive(b, t));
511
525
  const purged = [];
512
526
  for (const t of args.rm ?? []) {
513
527
  withBoard((b) => tx(b, (db) => {
@@ -530,13 +544,13 @@ export async function runWorkflow(argv) {
530
544
  const results = [];
531
545
  for (const t of targets) {
532
546
  if (verb === 'specify' && (args.title !== undefined || args.body !== undefined)) {
533
- withBoard((b) => engine.specify(b, t, { title: args.title ?? null, body: args.body ?? null, author: args.author ?? callerAddress() ?? callerName() }));
547
+ await withBoardAsync((b) => engine.specify(b, t, { title: args.title ?? null, body: args.body ?? null, author: args.author ?? callerAddress() ?? callerName() }));
534
548
  results.push({ id: t, specified: true });
535
549
  } else if (verb === 'decompose' && args.child?.length) {
536
550
  const children = args.child.map((c) => { const [title, assignee] = c.split(/@(?=[^@]*$)/); return { title: title.trim(), assignee: assignee?.trim() || undefined }; });
537
- results.push({ id: t, children: withBoard((b) => engine.decompose(b, t, children, { author: args.author ?? callerAddress() ?? callerName() })) });
551
+ results.push({ id: t, children: await withBoardAsync((b) => engine.decompose(b, t, children, { author: args.author ?? callerAddress() ?? callerName() })) });
538
552
  } else {
539
- const session = withBoard((b) => startTriageSession(b, t, lanes, settings, verb));
553
+ const session = await withBoardAsync((b) => startCard(b, t, { lanes, settings }));
540
554
  results.push({ id: t, session: session.address, mode: session.mode });
541
555
  }
542
556
  }
@@ -944,6 +958,23 @@ const READS = new Set(['list', 'ls', 'show', 'context', 'runs', 'log', 'tail', '
944
958
  * comment and attach. Never its arc's completion, and never another arc's card: anything across arcs goes through the
945
959
  * fleet's manager.
946
960
  */
961
+ /**
962
+ * Who a caller is on card `cardId`, in the workflow's roles: a person or the fleet's manager at the CLI (no session)
963
+ * is `manager`; the session of the card's current run has that run's role; the card's working session between runs
964
+ * is its `implementer`; the session working a subtask's arc is `arc`; any other session is nobody on it.
965
+ */
966
+ function rolesFor(db, cardId, sessionId) {
967
+ if (!sessionId) return ['manager'];
968
+ const task = engine.getTask(db, cardId);
969
+ if (!task) return [];
970
+ const roles = [];
971
+ const current = task.current_run_id ? json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(task.current_run_id)?.metadata, {})?.supercode : null;
972
+ if (current?.session_id === sessionId) roles.push(current.role ?? 'implementer');
973
+ else if (engine.lastSession(db, cardId)?.session_id === sessionId) roles.push('implementer');
974
+ if (task.subtask_of && engine.lastSession(db, task.subtask_of)?.session_id === sessionId) roles.push('arc');
975
+ return roles;
976
+ }
977
+
947
978
  function authorize(db, verb, rest, args, own) {
948
979
  const refuse = (why) => { throw new Error(`the session working ${own.id} ${why}`); };
949
980
  if (verb === 'create') {
@@ -952,21 +983,13 @@ function authorize(db, verb, rest, args, own) {
952
983
  }
953
984
  if (verb === 'boards') { if (!['list', 'ls', 'show', 'current', undefined].includes(rest[0])) refuse('does not change the home\'s boards'); return; }
954
985
  // verbs that act on the board rather than on a card are the manager's and the board's, never a card's session's
955
- if (['pause', 'resume', 'swarm', 'dispatch', 'daemon', 'serve', 'gc', 'repair', 'adopt', 'close', 'move', 'reassign', 'assign', 'set-model', 'set-goal', 'claim', 'reclaim', 'archive', 'promote', 'schedule', 'link', 'unlink', 'edit', 'reopen-review', 'reopen', 'attach-rm'].includes(verb)) refuse(`does not ${verb}; the fleet's manager does`);
986
+ // (what a session may do to a card is its workflow's: each transition's `by`)
987
+ if (['pause', 'resume', 'swarm', 'dispatch', 'daemon', 'serve', 'gc', 'repair', 'adopt', 'close', 'move', 'claim', 'attach-rm'].includes(verb)) refuse(`does not ${verb}; the fleet's manager does`);
956
988
  if (args.all) refuse(`does not ${verb} every card; only its own`);
957
989
  const card = engine.getTask(db, rest[0]);
958
990
  if (!card) return; // the verb itself answers "no card"
959
- const mine = card.id === own.id;
960
- const sub = card.subtask_of === own.id;
961
- // a session started from the review lane is the card's reviewer: its verdict is its to give
962
- const reviewing = mine && json(db.prepare("SELECT payload FROM task_events WHERE task_id = ? AND kind = 'claimed' ORDER BY id DESC LIMIT 1").get(card.id)?.payload, {})?.source_status === 'review';
963
- if (reviewing && ['complete', 'request-changes'].includes(verb)) return;
964
- const allowed = {
965
- comment: mine || sub, block: mine || sub, heartbeat: mine, 'request-review': mine || sub, attach: mine || sub,
966
- specify: mine, decompose: mine, complete: sub, unblock: sub, 'notify-subscribe': mine || sub, 'notify-unsubscribe': mine || sub,
967
- }[verb];
968
- if (!allowed) refuse(mine ? `cannot ${verb} its own card; its reviewers and the fleet's manager do` : `writes only its own card and its subtasks, not ${card.id}`);
969
- if (verb === 'block' && mine && !args.kind) refuse('blocks its card with a typed reason (--kind needs_input|capability|transient|dependency)');
991
+ // a card's session writes its own card and its subtasks; what it may do with them is the workflow's (`by`)
992
+ if (card.id !== own.id && card.subtask_of !== own.id) refuse(`writes only its own card and its subtasks, not ${card.id}`);
970
993
  for (const other of args.ids ?? []) if (other !== own.id && engine.getTask(db, other)?.subtask_of !== own.id) refuse(`writes only its own card and its subtasks, not ${other}`);
971
994
  }
972
995
 
@@ -1020,6 +1043,7 @@ async function serve(root, args = {}) {
1020
1043
  while (!stopped) {
1021
1044
  pausedSaid = Boolean(paused(root)) && pausedSaid;
1022
1045
  let interval = 60;
1046
+ const began = Date.now();
1023
1047
  try {
1024
1048
  const state = await laneState(root);
1025
1049
  const settings = { ...state.settings, ...override };
@@ -1027,6 +1051,7 @@ async function serve(root, args = {}) {
1027
1051
  for (const r of await tick({ root, lanes: state.lanes, settings })) {
1028
1052
  const said = summary(r);
1029
1053
  if (!said.endsWith('nothing to do') || args.verbose) log(said);
1054
+ if (args.verbose && r.ms) log(` took ${Object.entries(r.ms).map(([k, v]) => `${k} ${(v / 1000).toFixed(1)}s`).join(', ')}`);
1030
1055
  if (r.paused && !pausedSaid) { log(`new starts paused by ${r.paused} (\`supercode workflow resume\` lifts it)`); pausedSaid = true; }
1031
1056
  // Hermes's stuck warning: ready work for six ticks running and nothing started
1032
1057
  stuck[r.board] = r.ready_waiting && !r.started.length && !r.paused ? (stuck[r.board] ?? 0) + 1 : 0;
@@ -1038,7 +1063,9 @@ async function serve(root, args = {}) {
1038
1063
  } catch (error) {
1039
1064
  log(`tick failed: ${error.message}`);
1040
1065
  }
1041
- for (let waited = 0; waited < interval && !stopped; waited += 1) await new Promise((done) => setTimeout(done, 1000));
1066
+ // the next tick is due an interval after this one began (a long tick is not followed by a full interval's wait)
1067
+ const rest = Math.max(1, interval - Math.floor((Date.now() - began) / 1000));
1068
+ for (let waited = 0; waited < rest && !stopped; waited += 1) await new Promise((done) => setTimeout(done, 1000));
1042
1069
  }
1043
1070
  } finally {
1044
1071
  lock.release();