@atolis-hq/wake 0.2.25 → 0.2.27

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.
@@ -80,6 +80,10 @@ export const indexHtml = `<!DOCTYPE html>
80
80
  @media (max-width: 767px) {
81
81
  .modal-overlay { padding: 0; }
82
82
  .modal { width: 100%; height: 100%; max-height: none; border-radius: 0; border-left: 0; border-right: 0; }
83
+ .columns { display: flex; flex-direction: column; overflow-x: unset; }
84
+ .col { min-height: unset; }
85
+ .col-empty { padding: 0.25rem 0.5rem; }
86
+ .col-empty h2 { margin: 0.1rem 0.4rem; }
83
87
  }
84
88
  .tiles { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 1rem; }
85
89
  .tile { background: #1a1d23; border-radius: 10px; padding: 0.6rem 0.9rem; min-width: 120px; }
@@ -164,6 +168,7 @@ const METRIC_OPTIONS = [
164
168
  ['duration-over-time', 'Duration over time'],
165
169
  ['work-items-over-time', 'Work items completed over time'],
166
170
  ['work-item-durations', 'Work item e2e duration'],
171
+ ['work-item-totals', 'Work item totals'],
167
172
  ];
168
173
 
169
174
  async function getJson(path, signal) {
@@ -248,7 +253,6 @@ async function renderBoard(context) {
248
253
  const board = await getJson('/board', context.signal);
249
254
  if (!isActiveRequest(context.requestId)) return;
250
255
  const main = document.getElementById('main');
251
- main.innerHTML = '';
252
256
  const columns = el('div', { class: 'columns' }, CONDITIONS.map((cond) => {
253
257
  const items = board.filter((c) => c.condition === cond);
254
258
  const cards = items.map((item) => el('div', {
@@ -262,12 +266,12 @@ async function renderBoard(context) {
262
266
  ]),
263
267
  el('div', { class: 'meta', text: item.lastRunSentinel ? 'last: ' + item.lastRunAction + ' → ' + item.lastRunSentinel : item.conditionReason }),
264
268
  ]));
265
- return el('div', { class: 'col' }, [
269
+ return el('div', { class: 'col' + (items.length === 0 ? ' col-empty' : '') }, [
266
270
  el('h2', { text: cond + ' (' + items.length + ')' }),
267
271
  ...cards,
268
272
  ]);
269
273
  }));
270
- main.appendChild(columns);
274
+ main.replaceChildren(columns);
271
275
  }
272
276
 
273
277
  function resourceUriToUrl(resourceUri) {
@@ -434,15 +438,13 @@ async function renderActivity(context) {
434
438
  const events = await getJson('/events?limit=200', context.signal);
435
439
  if (!isActiveRequest(context.requestId)) return;
436
440
  const main = document.getElementById('main');
437
- main.innerHTML = '';
438
- const table = el('table', {}, [
441
+ main.replaceChildren(el('table', {}, [
439
442
  el('tr', {}, ['time', 'direction', 'type', 'work item'].map((h) => el('th', { text: h }))),
440
443
  ...events.map((ev) => el('tr', {}, [
441
444
  el('td', { text: ev.ingestedAt }), el('td', { text: ev.direction }),
442
445
  el('td', { text: ev.sourceEventType }), el('td', { text: ev.workItemKey }),
443
446
  ])),
444
- ]);
445
- main.appendChild(table);
447
+ ]));
446
448
  }
447
449
 
448
450
  function fmtCost(usd) {
@@ -640,6 +642,20 @@ function renderWorkItemDurations(rows) {
640
642
  ]);
641
643
  }
642
644
 
645
+ function renderWorkItemTotals(rows) {
646
+ if (rows.length === 0) return el('p', { class: 'meta', text: 'No runs for this window.' });
647
+ const maxTokens = Math.max(...rows.map((row) => row.totalTokens));
648
+ return el('table', {}, [
649
+ el('tr', {}, ['work item', 'runs', 'total tokens', 'total duration'].map((h) => el('th', { text: h }))),
650
+ ...rows.map((row) => el('tr', {}, [
651
+ el('td', { text: row.key }),
652
+ el('td', { text: String(row.runCount) }),
653
+ metricValueCell(row.totalTokens, maxTokens, fmtNumber),
654
+ el('td', { text: fmtMs(row.totalDurationMs) }),
655
+ ])),
656
+ ]);
657
+ }
658
+
643
659
  function renderMetricDetail(detail) {
644
660
  if (detail.kind === 'runs-over-time') return renderRunsOverTime(detail.rows);
645
661
  if (detail.kind === 'runs-by-status-over-time') return renderRunsByStatusOverTime(detail.rows);
@@ -650,6 +666,7 @@ function renderMetricDetail(detail) {
650
666
  if (detail.kind === 'duration-over-time') return renderDurationTable(detail.rows, 'bucket');
651
667
  if (detail.kind === 'work-items-over-time') return renderWorkItemsOverTime(detail.rows);
652
668
  if (detail.kind === 'work-item-durations') return renderWorkItemDurations(detail.rows);
669
+ if (detail.kind === 'work-item-totals') return renderWorkItemTotals(detail.rows);
653
670
  return el('p', { class: 'meta', text: 'Unsupported metric.' });
654
671
  }
655
672
 
@@ -657,43 +674,43 @@ async function renderAnalytics(context) {
657
674
  const metrics = await getJson('/metrics?window=' + encodeURIComponent(analyticsWindow) + '&metric=' + encodeURIComponent(analyticsMetric), context.signal);
658
675
  if (!isActiveRequest(context.requestId)) return;
659
676
  const main = document.getElementById('main');
660
- main.innerHTML = '';
661
- main.appendChild(el('div', { class: 'toolbar' }, [
662
- el('label', {}, [
663
- document.createTextNode('Window'),
664
- selectBox(analyticsWindow, [['1d', '1d'], ['3d', '3d'], ['5d', '5d'], ['7d', '7d']], (value) => {
665
- analyticsWindow = value;
666
- switchView('analytics');
667
- }),
677
+ main.replaceChildren(
678
+ el('div', { class: 'toolbar' }, [
679
+ el('label', {}, [
680
+ document.createTextNode('Window'),
681
+ selectBox(analyticsWindow, [['1d', '1d'], ['3d', '3d'], ['5d', '5d'], ['7d', '7d']], (value) => {
682
+ analyticsWindow = value;
683
+ switchView('analytics');
684
+ }),
685
+ ]),
686
+ el('label', {}, [
687
+ document.createTextNode('Metric'),
688
+ selectBox(analyticsMetric, METRIC_OPTIONS, (value) => {
689
+ analyticsMetric = value;
690
+ switchView('analytics');
691
+ }),
692
+ ]),
668
693
  ]),
669
- el('label', {}, [
670
- document.createTextNode('Metric'),
671
- selectBox(analyticsMetric, METRIC_OPTIONS, (value) => {
672
- analyticsMetric = value;
673
- switchView('analytics');
674
- }),
694
+ el('div', { class: 'tiles' }, [
695
+ tile('Runs', fmtNumber(metrics.summary.totalRuns)),
696
+ tile('Completed', fmtNumber(metrics.summary.completedRuns)),
697
+ tile('Blocked/approval', fmtNumber(metrics.summary.blockedRuns + metrics.summary.awaitingApprovalRuns)),
698
+ tile('Failed', fmtNumber(metrics.summary.failedRuns)),
699
+ tile('Tokens', fmtNumber(metrics.summary.totalTokens)),
700
+ tile('Cost', fmtCost(metrics.summary.totalCostUsd)),
701
+ tile('Median run', fmtDuration(metrics.summary.medianRunDurationMs) || '—'),
702
+ tile('Work done', fmtNumber(metrics.summary.completedWorkItems)),
703
+ tile('Median e2e', fmtDuration(metrics.summary.medianWorkItemDurationMs) || '—'),
675
704
  ]),
676
- ]));
677
- main.appendChild(el('div', { class: 'tiles' }, [
678
- tile('Runs', fmtNumber(metrics.summary.totalRuns)),
679
- tile('Completed', fmtNumber(metrics.summary.completedRuns)),
680
- tile('Blocked/approval', fmtNumber(metrics.summary.blockedRuns + metrics.summary.awaitingApprovalRuns)),
681
- tile('Failed', fmtNumber(metrics.summary.failedRuns)),
682
- tile('Tokens', fmtNumber(metrics.summary.totalTokens)),
683
- tile('Cost', fmtCost(metrics.summary.totalCostUsd)),
684
- tile('Median run', fmtDuration(metrics.summary.medianRunDurationMs) || '—'),
685
- tile('Work done', fmtNumber(metrics.summary.completedWorkItems)),
686
- tile('Median e2e', fmtDuration(metrics.summary.medianWorkItemDurationMs) || '—'),
687
- ]));
688
- main.appendChild(renderMetricDetail(metrics.detail));
705
+ renderMetricDetail(metrics.detail),
706
+ );
689
707
  }
690
708
 
691
709
  async function renderRuns(context) {
692
710
  const runs = await getJson('/runs', context.signal);
693
711
  if (!isActiveRequest(context.requestId)) return;
694
712
  const main = document.getElementById('main');
695
- main.innerHTML = '';
696
- const table = el('table', {}, [
713
+ main.replaceChildren(el('table', {}, [
697
714
  el('tr', {}, ['repo#issue', 'action', 'status', 'sentinel', 'runner', 'tokens', 'cost', 'started', 'finished'].map((h) => el('th', { text: h }))),
698
715
  ...runs.map((r) => el('tr', {}, [
699
716
  el('td', { text: r.repo + '#' + r.issueNumber }), el('td', { text: r.action }), el('td', { text: r.status }),
@@ -701,87 +718,87 @@ async function renderRuns(context) {
701
718
  el('td', { text: fmtTokens(r.tokenUsage) }), el('td', { text: fmtCost(r.tokenUsage && r.tokenUsage.costUsd) }),
702
719
  el('td', { text: r.startedAt }), el('td', { text: r.finishedAt || '' }),
703
720
  ])),
704
- ]);
705
- main.appendChild(table);
721
+ ]));
706
722
  }
707
723
 
708
724
  async function renderConfig(context) {
709
725
  const data = await getJson('/config', context.signal);
710
726
  if (!isActiveRequest(context.requestId)) return;
711
727
  const main = document.getElementById('main');
712
- main.innerHTML = '';
713
- main.appendChild(el('h3', { text: 'Routing table' }));
714
- main.appendChild(el('table', {}, [
715
- el('tr', {}, ['stage', 'action', 'tier', 'runner', 'model', 'fallback order'].map((h) => el('th', { text: h }))),
716
- ...data.routingTable.map((r) => el('tr', {}, [
717
- el('td', { text: r.stage }), el('td', { text: r.action || '' }), el('td', { text: r.tier || '' }),
718
- el('td', { text: r.runnerName || '' }), el('td', { text: r.model || '' }),
719
- el('td', {}, (r.candidates || []).map((c) => {
720
- if (!c.paused) return el('span', { class: 'chip', text: c.runnerName });
721
- const btn = el('button', { type: 'button', class: 'btn', text: 'Unpause', style: 'margin-top:0;font-size:0.7rem;padding:0.1rem 0.4rem;' });
722
- btn.addEventListener('click', async () => {
723
- btn.disabled = true;
724
- btn.textContent = 'Unpausing…';
725
- try {
726
- await postJson('/runners/' + encodeURIComponent(c.runnerName) + '/unpause');
727
- switchView('config');
728
- } catch (err) {
729
- btn.disabled = false;
730
- btn.textContent = 'Unpause';
731
- document.getElementById('status-summary').textContent = 'unpause failed: ' + err.message;
732
- }
733
- });
734
- return el('span', { style: 'display:inline-flex;align-items:center;gap:0.25rem;margin-right:0.25rem;' }, [
735
- el('span', { class: 'chip amber', text: c.runnerName + ' (paused)' }),
736
- btn,
737
- ]);
738
- })),
739
- ])),
740
- ]));
741
- main.appendChild(el('h3', { text: 'Effective config (redacted)' }));
742
- main.appendChild(el('pre', { text: JSON.stringify(data.config, null, 2) }));
728
+ main.replaceChildren(
729
+ el('h3', { text: 'Routing table' }),
730
+ el('table', {}, [
731
+ el('tr', {}, ['stage', 'action', 'tier', 'runner', 'model', 'fallback order'].map((h) => el('th', { text: h }))),
732
+ ...data.routingTable.map((r) => el('tr', {}, [
733
+ el('td', { text: r.stage }), el('td', { text: r.action || '' }), el('td', { text: r.tier || '' }),
734
+ el('td', { text: r.runnerName || '' }), el('td', { text: r.model || '' }),
735
+ el('td', {}, (r.candidates || []).map((c) => {
736
+ if (!c.paused) return el('span', { class: 'chip', text: c.runnerName });
737
+ const btn = el('button', { type: 'button', class: 'btn', text: 'Unpause', style: 'margin-top:0;font-size:0.7rem;padding:0.1rem 0.4rem;' });
738
+ btn.addEventListener('click', async () => {
739
+ btn.disabled = true;
740
+ btn.textContent = 'Unpausing…';
741
+ try {
742
+ await postJson('/runners/' + encodeURIComponent(c.runnerName) + '/unpause');
743
+ switchView('config');
744
+ } catch (err) {
745
+ btn.disabled = false;
746
+ btn.textContent = 'Unpause';
747
+ document.getElementById('status-summary').textContent = 'unpause failed: ' + err.message;
748
+ }
749
+ });
750
+ return el('span', { style: 'display:inline-flex;align-items:center;gap:0.25rem;margin-right:0.25rem;' }, [
751
+ el('span', { class: 'chip amber', text: c.runnerName + ' (paused)' }),
752
+ btn,
753
+ ]);
754
+ })),
755
+ ])),
756
+ ]),
757
+ el('h3', { text: 'Effective config (redacted)' }),
758
+ el('pre', { text: JSON.stringify(data.config, null, 2) }),
759
+ );
743
760
  }
744
761
 
745
762
  async function renderHealth(context) {
746
763
  const health = await getJson('/health', context.signal);
747
764
  if (!isActiveRequest(context.requestId)) return;
748
765
  const main = document.getElementById('main');
749
- main.innerHTML = '';
750
766
  const runnerNames = Object.keys(health.pause.runnerHealth || {});
751
- main.appendChild(el('div', { class: 'tiles' }, [
752
- tile('Tick lock', health.lock.present ? (health.lock.stale ? 'stale' : 'held') : 'free'),
753
- tile('Paused', String(health.pause.paused)),
754
- tile('Runners paused now', String(runnerNames.filter((name) => {
755
- const until = health.pause.runnerHealth[name].pausedUntil;
756
- return until && Date.parse(until) > Date.now();
757
- }).length)),
758
- tile('Integrity issues', String(health.integrityIssues.length)),
759
- ]));
760
- main.appendChild(el('h3', { text: 'Runner health (quota fallback, #67)' }));
761
- if (runnerNames.length === 0) {
762
- main.appendChild(el('p', { class: 'meta', text: 'No quota failures recorded.' }));
763
- } else {
764
- main.appendChild(el('table', {}, [
765
- el('tr', {}, ['runner', 'status', 'paused until', 'failure count', 'last failure'].map((h) => el('th', { text: h }))),
766
- ...runnerNames.map((name) => {
767
- const entry = health.pause.runnerHealth[name];
768
- const paused = entry.pausedUntil && Date.parse(entry.pausedUntil) > Date.now();
769
- return el('tr', {}, [
770
- el('td', { text: name }),
771
- el('td', {}, [el('span', { class: 'chip' + (paused ? ' amber' : ' ok') , text: paused ? 'paused' : 'available' })]),
772
- el('td', { text: entry.pausedUntil || '' }),
773
- el('td', { text: String(entry.failureCount || 0) }),
774
- el('td', { text: entry.lastFailureAt || '' }),
775
- ]);
776
- }),
777
- ]));
778
- }
779
- main.appendChild(el('h3', { text: 'Storage' }));
780
- main.appendChild(el('pre', { text: JSON.stringify(health.storage, null, 2) }));
781
- main.appendChild(el('h3', { text: 'Source polling' }));
782
- main.appendChild(el('pre', { text: JSON.stringify(health.sources, null, 2) }));
783
- main.appendChild(el('h3', { text: 'Integrity issues' }));
784
- main.appendChild(el('pre', { text: JSON.stringify(health.integrityIssues, null, 2) }));
767
+ const runnerSection = runnerNames.length === 0
768
+ ? el('p', { class: 'meta', text: 'No quota failures recorded.' })
769
+ : el('table', {}, [
770
+ el('tr', {}, ['runner', 'status', 'paused until', 'failure count', 'last failure'].map((h) => el('th', { text: h }))),
771
+ ...runnerNames.map((name) => {
772
+ const entry = health.pause.runnerHealth[name];
773
+ const paused = entry.pausedUntil && Date.parse(entry.pausedUntil) > Date.now();
774
+ return el('tr', {}, [
775
+ el('td', { text: name }),
776
+ el('td', {}, [el('span', { class: 'chip' + (paused ? ' amber' : ' ok') , text: paused ? 'paused' : 'available' })]),
777
+ el('td', { text: entry.pausedUntil || '' }),
778
+ el('td', { text: String(entry.failureCount || 0) }),
779
+ el('td', { text: entry.lastFailureAt || '' }),
780
+ ]);
781
+ }),
782
+ ]);
783
+ main.replaceChildren(
784
+ el('div', { class: 'tiles' }, [
785
+ tile('Tick lock', health.lock.present ? (health.lock.stale ? 'stale' : 'held') : 'free'),
786
+ tile('Paused', String(health.pause.paused)),
787
+ tile('Runners paused now', String(runnerNames.filter((name) => {
788
+ const until = health.pause.runnerHealth[name].pausedUntil;
789
+ return until && Date.parse(until) > Date.now();
790
+ }).length)),
791
+ tile('Integrity issues', String(health.integrityIssues.length)),
792
+ ]),
793
+ el('h3', { text: 'Runner health (quota fallback, #67)' }),
794
+ runnerSection,
795
+ el('h3', { text: 'Storage' }),
796
+ el('pre', { text: JSON.stringify(health.storage, null, 2) }),
797
+ el('h3', { text: 'Source polling' }),
798
+ el('pre', { text: JSON.stringify(health.sources, null, 2) }),
799
+ el('h3', { text: 'Integrity issues' }),
800
+ el('pre', { text: JSON.stringify(health.integrityIssues, null, 2) }),
801
+ );
785
802
  }
786
803
 
787
804
  function tile(label, value) {
@@ -381,6 +381,7 @@ const metricsMetrics = new Set([
381
381
  'duration-over-time',
382
382
  'work-items-over-time',
383
383
  'work-item-durations',
384
+ 'work-item-totals',
384
385
  ]);
385
386
  function parseMetricsWindow(value) {
386
387
  return metricsWindows.has(value) ? value : '7d';
@@ -867,6 +868,27 @@ export async function buildMetrics(input) {
867
868
  summary,
868
869
  detail: { kind: 'work-item-durations', rows: workItemDurations },
869
870
  };
871
+ case 'work-item-totals': {
872
+ const groups = new Map();
873
+ for (const run of runs) {
874
+ const key = run.workItemKey;
875
+ const existing = groups.get(key) ?? { runCount: 0, totalTokens: 0, totalDurationMs: 0 };
876
+ existing.runCount += 1;
877
+ existing.totalTokens += runTokenTotal(run);
878
+ existing.totalDurationMs += runDurationMs(run) ?? 0;
879
+ groups.set(key, existing);
880
+ }
881
+ const rows = [...groups.entries()]
882
+ .map(([key, value]) => ({ key, ...value }))
883
+ .sort((left, right) => right.totalTokens - left.totalTokens || left.key.localeCompare(right.key));
884
+ return {
885
+ window,
886
+ metric,
887
+ generatedAt: input.now.toISOString(),
888
+ summary,
889
+ detail: { kind: 'work-item-totals', rows },
890
+ };
891
+ }
870
892
  case 'runs-over-time':
871
893
  return {
872
894
  window,
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "ged4cd7b";
127
+ export const wakeVersion = "g76dc1bc";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {