@enderfga/claw-orchestrator 4.0.0 → 4.0.4

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.
@@ -119,12 +119,29 @@ export async function applyPlannerToolCalls(calls, fx, iter) {
119
119
  fx.updatePushPolicy(call.args);
120
120
  break;
121
121
  }
122
- case 'write_plan_committed': {
123
- await fx.commitPlanFile('plan.md', call.args.message);
122
+ case 'write_plan': {
123
+ const { content, commit_message } = call.args;
124
+ if (typeof content !== 'string' || !content.trim()) {
125
+ throw new Error('write_plan requires non-empty `content` (full plan.md body)');
126
+ }
127
+ await fx.writePlanFile('plan.md', content, commit_message);
124
128
  break;
125
129
  }
126
- case 'write_goal_committed': {
127
- await fx.commitPlanFile('goal.json', call.args.message);
130
+ case 'write_goal': {
131
+ const { content, commit_message } = call.args;
132
+ if (typeof content !== 'string' || !content.trim()) {
133
+ throw new Error('write_goal requires non-empty `content` (full goal.json body)');
134
+ }
135
+ // Validate it parses as JSON — goal.json must be machine-readable.
136
+ // The error message includes the parse position so the Planner can
137
+ // self-correct on the next turn.
138
+ try {
139
+ JSON.parse(content);
140
+ }
141
+ catch (e) {
142
+ throw new Error(`write_goal content is not valid JSON: ${e.message}`);
143
+ }
144
+ await fx.writePlanFile('goal.json', content, commit_message);
128
145
  break;
129
146
  }
130
147
  default:
@@ -1 +1 @@
1
- {"version":3,"file":"planner-tools.js","sourceRoot":"","sources":["../../../src/autoloop/planner-tools.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"planner-tools.js","sourceRoot":"","sources":["../../../src/autoloop/planner-tools.ts"],"names":[],"mappings":"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"}
@@ -533,7 +533,9 @@
533
533
  cursor: pointer;
534
534
  }
535
535
  .forge-spec-msg { font-size: 11px; color: var(--muted); }
536
- #forgeNew {
536
+ #forgeNew,
537
+ #councilNew,
538
+ #autoloopNew {
537
539
  margin: 8px 12px;
538
540
  padding: 6px 12px;
539
541
  border-radius: 4px;
@@ -543,6 +545,109 @@
543
545
  cursor: pointer;
544
546
  width: calc(100% - 24px);
545
547
  }
548
+ #forgeNew:hover,
549
+ #councilNew:hover,
550
+ #autoloopNew:hover {
551
+ filter: brightness(0.95);
552
+ }
553
+
554
+ /* ─── Launcher modal (Council / Autoloop forms) ─────────────── */
555
+ .modal-root {
556
+ position: fixed;
557
+ inset: 0;
558
+ background: rgba(0, 0, 0, 0.45);
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ z-index: 100;
563
+ }
564
+ .modal-root[hidden] { display: none; }
565
+ .modal {
566
+ background: var(--surface);
567
+ border-radius: 8px;
568
+ min-width: 440px;
569
+ max-width: 600px;
570
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25);
571
+ }
572
+ .modal-header {
573
+ display: flex;
574
+ align-items: center;
575
+ justify-content: space-between;
576
+ padding: 14px 20px;
577
+ border-bottom: 1px solid var(--border);
578
+ }
579
+ .modal-header h2 { margin: 0; font-size: 15px; }
580
+ .modal-header button {
581
+ background: transparent;
582
+ border: none;
583
+ font-size: 22px;
584
+ cursor: pointer;
585
+ color: var(--muted);
586
+ padding: 0 6px;
587
+ }
588
+ .modal-body {
589
+ padding: 16px 20px;
590
+ display: flex;
591
+ flex-direction: column;
592
+ gap: 12px;
593
+ }
594
+ .modal-body label {
595
+ display: flex;
596
+ flex-direction: column;
597
+ gap: 4px;
598
+ font-size: 12px;
599
+ color: var(--muted);
600
+ }
601
+ .modal-body input,
602
+ .modal-body textarea {
603
+ padding: 6px 8px;
604
+ border: 1px solid var(--border);
605
+ border-radius: 4px;
606
+ font: inherit;
607
+ color: var(--text);
608
+ background: var(--bg);
609
+ }
610
+ .modal-footer {
611
+ display: flex;
612
+ justify-content: flex-end;
613
+ gap: 8px;
614
+ padding: 12px 20px 18px;
615
+ }
616
+ .modal-footer button {
617
+ padding: 6px 14px;
618
+ border-radius: 4px;
619
+ border: 1px solid var(--border);
620
+ background: var(--surface);
621
+ cursor: pointer;
622
+ font: inherit;
623
+ }
624
+ .modal-footer button.primary {
625
+ background: var(--accent);
626
+ color: white;
627
+ border-color: var(--accent);
628
+ }
629
+ .modal-footer button:disabled {
630
+ opacity: 0.6;
631
+ cursor: not-allowed;
632
+ }
633
+
634
+ /* ─── Empty-state CTA ──────────────────────────────────────── */
635
+ .empty-cta {
636
+ text-align: center;
637
+ margin: 80px auto 0;
638
+ max-width: 420px;
639
+ }
640
+ .empty-cta h2 { margin: 0 0 8px; font-size: 16px; }
641
+ .empty-cta p { margin: 0 0 16px; color: var(--muted); }
642
+ .empty-cta button {
643
+ padding: 8px 16px;
644
+ border-radius: 4px;
645
+ border: 1px solid var(--accent);
646
+ background: var(--accent);
647
+ color: white;
648
+ cursor: pointer;
649
+ font: inherit;
650
+ }
546
651
  #forgeStartBuild {
547
652
  margin-top: 8px;
548
653
  padding: 8px 14px;
@@ -602,7 +707,9 @@
602
707
  <div class="body">
603
708
  <aside class="sidebar">
604
709
  <h2 id="sidebarTitle">Runs</h2>
605
- <button id="forgeNew" hidden>+ New</button>
710
+ <button id="autoloopNew" hidden>+ New autoloop</button>
711
+ <button id="councilNew" hidden>+ New council</button>
712
+ <button id="forgeNew" hidden>+ New forge</button>
606
713
  <ul class="runlist" id="runlist"></ul>
607
714
  </aside>
608
715
 
@@ -614,6 +721,60 @@
614
721
  <div class="footer">SSE-driven dashboard • served by claw-orchestrator embedded server</div>
615
722
  </div>
616
723
 
724
+ <!-- ─── Launcher modal (tab-aware) ───────────────────────────────── -->
725
+ <div id="modalRoot" class="modal-root" hidden>
726
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
727
+ <div class="modal-header">
728
+ <h2 id="modalTitle">New run</h2>
729
+ <button id="modalClose" type="button" aria-label="Close">×</button>
730
+ </div>
731
+ <form id="modalForm" class="modal-body" autocomplete="off">
732
+ <!-- Council fields -->
733
+ <div data-for-tab="council" style="display: contents">
734
+ <label>
735
+ Task
736
+ <textarea
737
+ name="task"
738
+ rows="3"
739
+ placeholder="What should the council achieve?"
740
+ ></textarea>
741
+ </label>
742
+ <label>
743
+ Project directory
744
+ <input
745
+ name="projectDir"
746
+ type="text"
747
+ placeholder="/abs/path/to/workspace"
748
+ />
749
+ </label>
750
+ <label>
751
+ Max rounds
752
+ <input name="maxRounds" type="number" min="1" max="50" value="15" />
753
+ </label>
754
+ </div>
755
+ <!-- Autoloop fields -->
756
+ <div data-for-tab="autoloop" style="display: contents">
757
+ <label>
758
+ Workspace
759
+ <input
760
+ name="workspace"
761
+ type="text"
762
+ placeholder="/abs/path/to/git/workspace"
763
+ />
764
+ </label>
765
+ <label>
766
+ Planner model (optional)
767
+ <input name="planner_model" type="text" placeholder="claude-opus-4-7" />
768
+ </label>
769
+ </div>
770
+ <div class="modal-footer">
771
+ <button type="button" id="modalCancel">Cancel</button>
772
+ <button type="submit" id="modalSubmit" class="primary">Start</button>
773
+ </div>
774
+ </form>
775
+ </div>
776
+ </div>
777
+
617
778
  <script>
618
779
  // ─── State ────────────────────────────────────────────────────
619
780
  const state = {
@@ -671,6 +832,8 @@
671
832
  ? 'Council sessions'
672
833
  : 'Forge apps';
673
834
  $('forgeNew').hidden = tab !== 'forge';
835
+ $('councilNew').hidden = tab !== 'council';
836
+ $('autoloopNew').hidden = tab !== 'autoloop';
674
837
  state.selectedId = null;
675
838
  closeStreams();
676
839
  renderEmptyMain();
@@ -705,7 +868,33 @@
705
868
  }
706
869
 
707
870
  function renderEmptyMain() {
708
- $('main').innerHTML = '<div class="empty" style="margin-top:80px">Select a run to view.</div>';
871
+ // When the sidebar list is empty, invite the user to start their
872
+ // first run. Otherwise just say "pick one". The button delegates
873
+ // to the same launcher trigger as the sidebar "+ New" button.
874
+ const labels = {
875
+ autoloop: 'autoloop run',
876
+ council: 'council session',
877
+ forge: 'forge app',
878
+ };
879
+ const noun = labels[state.tab] || 'run';
880
+ if (state.runs.length === 0) {
881
+ const triggerId =
882
+ state.tab === 'autoloop'
883
+ ? 'autoloopNew'
884
+ : state.tab === 'council'
885
+ ? 'councilNew'
886
+ : 'forgeNew';
887
+ $('main').innerHTML = `
888
+ <div class="empty empty-cta">
889
+ <h2>No ${escapeHtml(noun)}s yet</h2>
890
+ <p>Click <strong>+ New</strong> in the sidebar to start your first one.</p>
891
+ <button type="button" onclick="document.getElementById('${triggerId}').click()">+ New ${escapeHtml(noun)}</button>
892
+ </div>
893
+ `;
894
+ } else {
895
+ $('main').innerHTML =
896
+ '<div class="empty" style="margin-top:80px">Select a run to view.</div>';
897
+ }
709
898
  }
710
899
 
711
900
  // ─── List polling ─────────────────────────────────────────────
@@ -727,6 +916,9 @@
727
916
  ? j.councils || []
728
917
  : j.runs || [];
729
918
  renderList();
919
+ // Refresh the empty-state CTA — runs may have just appeared or
920
+ // disappeared, and renderEmptyMain's text depends on state.runs.length.
921
+ if (!state.selectedId) renderEmptyMain();
730
922
  $('status').textContent = 'connected';
731
923
  } catch (err) {
732
924
  $('status').textContent = 'disconnected';
@@ -756,6 +948,9 @@
756
948
  <span>iter ${r.iter}</span>
757
949
  <span>↑${r.push_log_count}</span>
758
950
  </div>
951
+ <div class="lifecycle-actions">
952
+ <button data-act="delete">Delete</button>
953
+ </div>
759
954
  </li>`;
760
955
  } else if (state.tab === 'council') {
761
956
  return `
@@ -801,6 +996,30 @@
801
996
  ev.stopPropagation();
802
997
  const action = btn.dataset.act;
803
998
  const id = el.dataset.id;
999
+ if (state.tab === 'autoloop') {
1000
+ if (action !== 'delete') return;
1001
+ if (!confirm(`Delete autoloop run "${id}"? The registry entry is removed; the ledger directory under tasks/${id}/ is kept on disk.`)) return;
1002
+ try {
1003
+ const r = await fetch(`/autoloop/${encodeURIComponent(id)}/delete`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
1004
+ if (!r.ok) {
1005
+ let detail = `HTTP ${r.status}`;
1006
+ try { const j = await r.json(); if (j && j.error) detail = j.error; } catch { /* ignore */ }
1007
+ alert(`Delete failed: ${detail}`);
1008
+ return;
1009
+ }
1010
+ // If we were viewing this run, close the detail pane and pick another.
1011
+ if (state.selectedId === id) {
1012
+ closeStreams();
1013
+ state.selectedId = null;
1014
+ $('main').innerHTML = '<div class="empty">Select a run from the left.</div>';
1015
+ }
1016
+ } catch (err) {
1017
+ alert(`Delete failed: ${(err && err.message) || err}`);
1018
+ return;
1019
+ }
1020
+ refreshList();
1021
+ return;
1022
+ }
804
1023
  if (action === 'delete' && !confirm('Delete this run? Container, image, and on-disk artifacts are removed.')) return;
805
1024
  await forgeLifecycleAction(id, action);
806
1025
  refreshList();
@@ -1272,6 +1491,57 @@
1272
1491
  state.autoloop.pushLog = logJ.entries || [];
1273
1492
  paintPushLog();
1274
1493
  }
1494
+ // Replay chat history from <ledger>/chat.jsonl so a refresh /
1495
+ // reopening a terminated run shows the prior conversation. New
1496
+ // entries that arrive via SSE (planner_reply) get appended on top.
1497
+ const histR = await fetch(`/autoloop/${encodeURIComponent(id)}/chat_history`);
1498
+ if (histR.ok) {
1499
+ const histJ = await histR.json();
1500
+ if (histJ.ok && Array.isArray(histJ.entries)) {
1501
+ state.autoloop.plannerLog = histJ.entries
1502
+ .filter((e) => e && typeof e.text === 'string')
1503
+ .map((e) => ({ who: e.who || 'planner', text: e.text, ts: e.ts || '' }));
1504
+ paintPlanner();
1505
+ }
1506
+ }
1507
+ // For terminated runs, surface a Resume affordance under the
1508
+ // top bar. The button kicks off /autoloop/<id>/resume and then
1509
+ // re-runs openAutoloop so SSE reconnects with the live state.
1510
+ if (state.autoloop.state && state.autoloop.state.status === 'terminated') {
1511
+ const bar = document.querySelector('.topbar');
1512
+ if (bar && !document.getElementById('auto-resume')) {
1513
+ const btn = document.createElement('button');
1514
+ btn.id = 'auto-resume';
1515
+ btn.textContent = 'Resume run';
1516
+ btn.style.cssText =
1517
+ 'padding:6px 12px;border:1px solid var(--border);background:var(--accent);color:#fff;border-radius:6px;cursor:pointer;font:inherit;font-weight:600;margin-left:auto';
1518
+ btn.addEventListener('click', async () => {
1519
+ btn.disabled = true;
1520
+ btn.textContent = 'Resuming…';
1521
+ try {
1522
+ const r2 = await fetch(`/autoloop/${encodeURIComponent(id)}/resume`, {
1523
+ method: 'POST',
1524
+ headers: { 'content-type': 'application/json' },
1525
+ body: '{}',
1526
+ });
1527
+ if (!r2.ok) {
1528
+ const j2 = await r2.json().catch(() => ({}));
1529
+ alert(`Resume failed: ${(j2 && j2.error) || `HTTP ${r2.status}`}`);
1530
+ btn.disabled = false;
1531
+ btn.textContent = 'Resume run';
1532
+ return;
1533
+ }
1534
+ closeStreams();
1535
+ openAutoloop(id);
1536
+ } catch (err) {
1537
+ alert(`Resume failed: ${(err && err.message) || err}`);
1538
+ btn.disabled = false;
1539
+ btn.textContent = 'Resume run';
1540
+ }
1541
+ });
1542
+ bar.appendChild(btn);
1543
+ }
1544
+ }
1275
1545
  } catch (err) {
1276
1546
  /* ignore */
1277
1547
  }
@@ -1291,9 +1561,21 @@
1291
1561
  });
1292
1562
  es.addEventListener('planner_reply', (e) => {
1293
1563
  const { text } = JSON.parse(e.data);
1564
+ // Clear the "waiting…" placeholder, if any, before appending the reply.
1565
+ state.autoloop.plannerLog = state.autoloop.plannerLog.filter((m) => m.who !== 'pending');
1294
1566
  state.autoloop.plannerLog.push({ who: 'planner', text, ts: new Date().toISOString() });
1295
1567
  paintPlanner();
1296
1568
  });
1569
+ es.addEventListener('planner_error', (e) => {
1570
+ const { message } = JSON.parse(e.data);
1571
+ state.autoloop.plannerLog = state.autoloop.plannerLog.filter((m) => m.who !== 'pending');
1572
+ state.autoloop.plannerLog.push({
1573
+ who: 'system',
1574
+ text: `[planner error] ${message}`,
1575
+ ts: new Date().toISOString(),
1576
+ });
1577
+ paintPlanner();
1578
+ });
1297
1579
  es.addEventListener('coder_reply', (e) => {
1298
1580
  const { text } = JSON.parse(e.data);
1299
1581
  state.autoloop.coderLog.push({ who: 'coder', text, ts: new Date().toISOString() });
@@ -1442,16 +1724,47 @@
1442
1724
  const btn = $('auto-send');
1443
1725
  btn.disabled = true;
1444
1726
  ta.disabled = true;
1727
+ // Clear input optimistically — the message is already echoed in the
1728
+ // planner log below, and the new server contract is fire-and-forget
1729
+ // (returns 202 immediately; reply streams via SSE). If the POST fails
1730
+ // synchronously we surface the error in the log.
1731
+ ta.value = '';
1445
1732
  state.autoloop.plannerLog.push({ who: 'user', text, ts: new Date().toISOString() });
1733
+ state.autoloop.plannerLog.push({
1734
+ who: 'pending',
1735
+ text: 'Planner is thinking…',
1736
+ ts: new Date().toISOString(),
1737
+ });
1446
1738
  paintPlanner();
1447
1739
  try {
1448
- await fetch('/v1/openclaw/tools/autoloop_chat', {
1740
+ const r = await fetch(`/autoloop/${encodeURIComponent(id)}/chat`, {
1449
1741
  method: 'POST',
1450
1742
  headers: { 'content-type': 'application/json' },
1451
- body: JSON.stringify({ run_id: id, text }),
1743
+ body: JSON.stringify({ text }),
1452
1744
  });
1453
- ta.value = '';
1745
+ // Server returns 202 on accepted (queued); 200 is also fine for
1746
+ // legacy clients. Anything else is a real failure.
1747
+ if (!r.ok) {
1748
+ let detail = `HTTP ${r.status}`;
1749
+ try {
1750
+ const j = await r.json();
1751
+ if (j && typeof j.error === 'string') detail = j.error;
1752
+ } catch {
1753
+ /* non-JSON body — keep the status code */
1754
+ }
1755
+ state.autoloop.plannerLog = state.autoloop.plannerLog.filter((m) => m.who !== 'pending');
1756
+ state.autoloop.plannerLog.push({
1757
+ who: 'system',
1758
+ text: `[error] ${detail}`,
1759
+ ts: new Date().toISOString(),
1760
+ });
1761
+ paintPlanner();
1762
+ }
1763
+ // On success, do nothing — the planner_reply / planner_error SSE
1764
+ // handler upstairs clears the "thinking…" placeholder when the
1765
+ // Planner actually replies (or errors).
1454
1766
  } catch (err) {
1767
+ state.autoloop.plannerLog = state.autoloop.plannerLog.filter((m) => m.who !== 'pending');
1455
1768
  state.autoloop.plannerLog.push({
1456
1769
  who: 'system',
1457
1770
  text: `[error] ${(err && err.message) || 'send failed'}`,
@@ -1604,7 +1917,98 @@
1604
1917
  el.scrollTop = el.scrollHeight;
1605
1918
  }
1606
1919
 
1920
+ // ─── Modal launcher (Council / Autoloop) ─────────────────────
1921
+ //
1922
+ // forgeNew uses a different shape (no form: ultraapp is interview-
1923
+ // driven, so POST /ultraapp/new with an empty body is enough). For
1924
+ // Council / Autoloop we need real form input; one modal handles
1925
+ // both, switching visible fields by state.tab.
1926
+ function openLauncherModal() {
1927
+ const root = $('modalRoot');
1928
+ $('modalTitle').textContent =
1929
+ state.tab === 'council' ? 'New Council session' : 'New Autoloop run';
1930
+ root.querySelectorAll('[data-for-tab]').forEach((el) => {
1931
+ const visible = el.dataset.forTab === state.tab;
1932
+ el.style.display = visible ? 'contents' : 'none';
1933
+ // Only the visible group's required fields actually count.
1934
+ el.querySelectorAll('input,textarea').forEach((f) => {
1935
+ if (visible && (f.name === 'task' || f.name === 'projectDir' || f.name === 'workspace')) {
1936
+ f.required = true;
1937
+ } else {
1938
+ f.required = false;
1939
+ }
1940
+ });
1941
+ });
1942
+ root.hidden = false;
1943
+ setTimeout(() => {
1944
+ const firstVisible = root.querySelector(
1945
+ `[data-for-tab="${state.tab}"] input, [data-for-tab="${state.tab}"] textarea`,
1946
+ );
1947
+ firstVisible?.focus();
1948
+ }, 0);
1949
+ }
1950
+ function closeLauncherModal() {
1951
+ $('modalRoot').hidden = true;
1952
+ $('modalForm').reset();
1953
+ }
1954
+ $('councilNew').addEventListener('click', openLauncherModal);
1955
+ $('autoloopNew').addEventListener('click', openLauncherModal);
1956
+ $('modalClose').addEventListener('click', closeLauncherModal);
1957
+ $('modalCancel').addEventListener('click', closeLauncherModal);
1958
+ $('modalRoot').addEventListener('click', (e) => {
1959
+ if (e.target.id === 'modalRoot') closeLauncherModal();
1960
+ });
1961
+ document.addEventListener('keydown', (e) => {
1962
+ if (e.key === 'Escape' && !$('modalRoot').hidden) closeLauncherModal();
1963
+ });
1964
+ $('modalForm').addEventListener('submit', async (e) => {
1965
+ e.preventDefault();
1966
+ const fd = new FormData(e.target);
1967
+ const body = {};
1968
+ if (state.tab === 'council') {
1969
+ body.task = (fd.get('task') || '').toString().trim();
1970
+ body.projectDir = (fd.get('projectDir') || '').toString().trim();
1971
+ const mr = (fd.get('maxRounds') || '').toString().trim();
1972
+ if (mr) body.maxRounds = Number(mr);
1973
+ } else if (state.tab === 'autoloop') {
1974
+ body.workspace = (fd.get('workspace') || '').toString().trim();
1975
+ const pm = (fd.get('planner_model') || '').toString().trim();
1976
+ if (pm) body.planner_model = pm;
1977
+ }
1978
+ const url = state.tab === 'council' ? '/council/new' : '/autoloop/new';
1979
+ const submitBtn = $('modalSubmit');
1980
+ submitBtn.disabled = true;
1981
+ submitBtn.textContent = 'Starting...';
1982
+ try {
1983
+ const r = await fetch(url, {
1984
+ method: 'POST',
1985
+ headers: { 'Content-Type': 'application/json' },
1986
+ body: JSON.stringify(body),
1987
+ });
1988
+ const j = await r.json();
1989
+ if (!j.ok) {
1990
+ alert('Failed: ' + (j.error || `HTTP ${r.status}`));
1991
+ return;
1992
+ }
1993
+ const id = j.id ?? j.run_id;
1994
+ closeLauncherModal();
1995
+ await refreshList();
1996
+ state.selectedId = id;
1997
+ renderList();
1998
+ openDetail(id);
1999
+ } catch (err) {
2000
+ alert('Failed: ' + err.message);
2001
+ } finally {
2002
+ submitBtn.disabled = false;
2003
+ submitBtn.textContent = 'Start';
2004
+ }
2005
+ });
2006
+
1607
2007
  // ─── Boot ────────────────────────────────────────────────────
2008
+ // Make the right "+ New" button visible for the default tab.
2009
+ $('autoloopNew').hidden = state.tab !== 'autoloop';
2010
+ $('councilNew').hidden = state.tab !== 'council';
2011
+ $('forgeNew').hidden = state.tab !== 'forge';
1608
2012
  refreshList();
1609
2013
  </script>
1610
2014
  </body>
@@ -18,6 +18,13 @@ export declare class EmbeddedServer {
18
18
  private host;
19
19
  constructor(manager: SessionManager, port?: number, host?: string);
20
20
  private _checkRateLimit;
21
+ /**
22
+ * Try to reuse the token persisted by a previous server. Returns null if
23
+ * the file is absent, unreadable, the wrong shape, or the wrong perms.
24
+ * We accept any 32+ hex-char string (the format crypto.randomBytes(32)
25
+ * produces) — anything else is treated as garbage and rotated out.
26
+ */
27
+ private _readPersistedToken;
21
28
  private _writeTokenFile;
22
29
  start(): Promise<number>;
23
30
  stop(): Promise<void>;