@atolis-hq/wake 0.1.20 → 0.1.22

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.
@@ -35,6 +35,9 @@ export const indexHtml = `<!DOCTYPE html>
35
35
  .topbar .version { color: rgba(255, 255, 255, 0.7); font-size: 0.78rem; }
36
36
  .statusbar { display: flex; align-items: center; gap: 1rem; padding: 0.45rem 1rem; background: var(--brand-dark); border-top: 1px solid rgba(0, 0, 0, 0.18); flex-wrap: wrap; font-size: 0.8rem; }
37
37
  .statusbar .meta { color: rgba(255, 255, 255, 0.72); }
38
+ .statusbar button { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.18); color: #fff; border-radius: 6px; padding: 0.22rem 0.55rem; cursor: pointer; font-size: 0.78rem; }
39
+ .statusbar button:hover:not(:disabled) { border-color: var(--accent-light); background: rgba(45, 212, 191, 0.16); }
40
+ .statusbar button:disabled { cursor: wait; opacity: 0.62; }
38
41
  .pill { padding: 0.15rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
39
42
  .pill-idle { background: #1f3d2c; color: #7fe3a3; }
40
43
  .pill-polling { background: #1f3350; color: #7fb3ff; }
@@ -80,6 +83,7 @@ export const indexHtml = `<!DOCTYPE html>
80
83
  </header>
81
84
  <div class="statusbar">
82
85
  <span id="loop-pill" class="pill">…</span>
86
+ <button id="force-tick" type="button">Tick now</button>
83
87
  <span id="status-summary" class="meta"></span>
84
88
  </div>
85
89
  <nav>
@@ -102,6 +106,12 @@ async function getJson(path) {
102
106
  return res.json();
103
107
  }
104
108
 
109
+ async function postJson(path) {
110
+ const res = await fetch(API + path, { method: 'POST' });
111
+ if (!res.ok) throw new Error(path + ' -> ' + res.status);
112
+ return res.json();
113
+ }
114
+
105
115
  function fmtMs(ms) {
106
116
  if (ms === undefined || ms === null) return '—';
107
117
  const s = Math.floor(ms / 1000);
@@ -142,6 +152,24 @@ async function renderStatusBar() {
142
152
  }
143
153
  }
144
154
 
155
+ async function forceTickNow() {
156
+ const button = document.getElementById('force-tick');
157
+ button.disabled = true;
158
+ const original = button.textContent;
159
+ button.textContent = 'Requested';
160
+ try {
161
+ await postJson('/tick');
162
+ await renderStatusBar();
163
+ } catch (err) {
164
+ document.getElementById('status-summary').textContent = 'tick request failed: ' + err.message;
165
+ } finally {
166
+ setTimeout(() => {
167
+ button.disabled = false;
168
+ button.textContent = original;
169
+ }, 900);
170
+ }
171
+ }
172
+
145
173
  async function renderBoard() {
146
174
  const board = await getJson('/board');
147
175
  const main = document.getElementById('main');
@@ -320,6 +348,7 @@ for (const btn of document.querySelectorAll('nav button')) {
320
348
  document.getElementById('drawer-close').addEventListener('click', () => {
321
349
  document.getElementById('drawer').classList.remove('open');
322
350
  });
351
+ document.getElementById('force-tick').addEventListener('click', forceTickNow);
323
352
 
324
353
  renderStatusBar();
325
354
  switchView('board');
@@ -1,5 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { configuredTicketSource } from '../../domain/sources.js';
4
+ import { writeJsonFile } from '../../lib/json-file.js';
3
5
  import { indexHtml } from './ui-assets.js';
4
6
  import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
5
7
  function sendJson(res, status, body) {
@@ -77,12 +79,6 @@ async function handleRequest(req, res, options, now) {
77
79
  res.writeHead(404).end('not found');
78
80
  return;
79
81
  }
80
- if (req.method !== 'GET') {
81
- sendJson(res, 405, {
82
- error: 'this build only serves read endpoints; mutations are not implemented',
83
- });
84
- return;
85
- }
86
82
  const { stateStore, resourceIndex, config } = options;
87
83
  const segments = url.pathname
88
84
  .slice('/api/v1/'.length)
@@ -90,6 +86,20 @@ async function handleRequest(req, res, options, now) {
90
86
  .filter((part) => part.length > 0)
91
87
  .map((s) => decodeURIComponent(s));
92
88
  const resource = segments[0];
89
+ if (req.method === 'POST' && resource === 'tick' && segments.length === 1) {
90
+ const request = {
91
+ requestId: randomUUID(),
92
+ requestedAt: now().toISOString(),
93
+ requestedBy: 'ui',
94
+ };
95
+ await writeJsonFile(stateStore.paths.tickRequestFile, request);
96
+ sendJson(res, 202, request);
97
+ return;
98
+ }
99
+ if (req.method !== 'GET') {
100
+ sendJson(res, 405, { error: `method not allowed for ${url.pathname}` });
101
+ return;
102
+ }
93
103
  if (resource === 'status' && segments.length === 1) {
94
104
  sendJson(res, 200, await buildStatus({ stateStore, config, now: now() }));
95
105
  return;
@@ -6,11 +6,43 @@ export function createControlPlane(deps) {
6
6
  let consecutiveIdleTicks = 0;
7
7
  let consecutiveIntakeIdleTicks = 0;
8
8
  let consecutiveRunnerIdleTicks = 0;
9
+ let lastSingleTickRequest;
10
+ let lastIntakeTickRequest;
11
+ let lastRunnerTickRequest;
9
12
  const maxIntervalMs = deps.maxIntervalMs ?? deps.intervalMs * 16;
10
13
  function nextSleepMs(consecutiveTicks) {
11
14
  const backoffMs = deps.intervalMs * 2 ** Math.min(consecutiveTicks, 20);
12
15
  return Math.min(backoffMs, maxIntervalMs);
13
16
  }
17
+ async function tickRequestPending(input) {
18
+ const request = (await deps.readTickRequest?.()) ?? null;
19
+ if (request === null || request === input.getLastRequest()) {
20
+ return false;
21
+ }
22
+ input.setLastRequest(request);
23
+ return true;
24
+ }
25
+ async function sleepUntilNextTick(ms, requestState) {
26
+ if (deps.readTickRequest === undefined || ms <= 0) {
27
+ await deps.sleep(ms);
28
+ return { forced: false };
29
+ }
30
+ if (await tickRequestPending(requestState)) {
31
+ return { forced: true };
32
+ }
33
+ const deadline = Date.now() + ms;
34
+ while (running) {
35
+ const remainingMs = deadline - Date.now();
36
+ if (remainingMs <= 0) {
37
+ return { forced: false };
38
+ }
39
+ await deps.sleep(Math.min(remainingMs, 250));
40
+ if (await tickRequestPending(requestState)) {
41
+ return { forced: true };
42
+ }
43
+ }
44
+ return { forced: false };
45
+ }
14
46
  async function isPaused() {
15
47
  return Boolean(await deps.isPaused());
16
48
  }
@@ -58,8 +90,13 @@ export function createControlPlane(deps) {
58
90
  input.setIdleTicks(0);
59
91
  }
60
92
  else {
61
- await deps.sleep(nextSleepMs(input.getIdleTicks()));
62
- input.setIdleTicks(input.getIdleTicks() + 1);
93
+ const wait = await sleepUntilNextTick(nextSleepMs(input.getIdleTicks()), {
94
+ getLastRequest: input.getLastRequest,
95
+ setLastRequest: input.setLastRequest,
96
+ });
97
+ if (!wait.forced) {
98
+ input.setIdleTicks(input.getIdleTicks() + 1);
99
+ }
63
100
  }
64
101
  }
65
102
  }
@@ -94,6 +131,10 @@ export function createControlPlane(deps) {
94
131
  setIdleTicks: (value) => {
95
132
  consecutiveIntakeIdleTicks = value;
96
133
  },
134
+ getLastRequest: () => lastIntakeTickRequest,
135
+ setLastRequest: (value) => {
136
+ lastIntakeTickRequest = value;
137
+ },
97
138
  }),
98
139
  startLoop({
99
140
  run: deps.tickRunner.runRunnerTick,
@@ -102,6 +143,10 @@ export function createControlPlane(deps) {
102
143
  setIdleTicks: (value) => {
103
144
  consecutiveRunnerIdleTicks = value;
104
145
  },
146
+ getLastRequest: () => lastRunnerTickRequest,
147
+ setLastRequest: (value) => {
148
+ lastRunnerTickRequest = value;
149
+ },
105
150
  }),
106
151
  ]);
107
152
  return;
@@ -122,8 +167,15 @@ export function createControlPlane(deps) {
122
167
  consecutiveIdleTicks = 0;
123
168
  }
124
169
  else {
125
- await deps.sleep(nextSleepMs(consecutiveIdleTicks));
126
- consecutiveIdleTicks += 1;
170
+ const wait = await sleepUntilNextTick(nextSleepMs(consecutiveIdleTicks), {
171
+ getLastRequest: () => lastSingleTickRequest,
172
+ setLastRequest: (value) => {
173
+ lastSingleTickRequest = value;
174
+ },
175
+ });
176
+ if (!wait.forced) {
177
+ consecutiveIdleTicks += 1;
178
+ }
127
179
  }
128
180
  }
129
181
  },
@@ -12,6 +12,7 @@ export function createWakePaths(wakeRoot) {
12
12
  configFile: join(wakeRoot, 'config.json'),
13
13
  ledgerFile: join(wakeRoot, 'ledger.json'),
14
14
  pauseFile: join(wakeRoot, 'PAUSE'),
15
+ tickRequestFile: join(wakeRoot, 'control', 'tick-request.json'),
15
16
  tickLockFile: join(wakeRoot, 'locks', 'tick.lock'),
16
17
  runnerLockFile: join(wakeRoot, 'locks', 'runner.lock'),
17
18
  issueFixtureFile: join(wakeRoot, 'fixtures', 'issues.json'),
package/dist/src/main.js CHANGED
@@ -26,6 +26,7 @@ import { createControlPlane } from './core/control-plane.js';
26
26
  import { createOutboundSinkRouter, createWorkSourceFanIn } from './core/sink-router.js';
27
27
  import { createTickRunner } from './core/tick-runner.js';
28
28
  import { systemClock } from './lib/clock.js';
29
+ import { readJsonFile } from './lib/json-file.js';
29
30
  import { configuredTicketSource } from './domain/sources.js';
30
31
  import { wakeVersion } from './version.js';
31
32
  function commandArgsBeforeTerminator(args) {
@@ -135,6 +136,17 @@ async function runCommandCapture(command, args) {
135
136
  });
136
137
  });
137
138
  }
139
+ async function readTickRequestId(path) {
140
+ try {
141
+ const request = await readJsonFile(path);
142
+ return typeof request.requestId === 'string' && request.requestId.length > 0
143
+ ? request.requestId
144
+ : null;
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
138
150
  async function inspectDockerImage(image) {
139
151
  return await new Promise((resolveInspect, reject) => {
140
152
  const child = spawn('docker', ['image', 'inspect', image], {
@@ -318,6 +330,9 @@ async function runStart(args) {
318
330
  setTimeout(resolveSleep, ms);
319
331
  });
320
332
  },
333
+ readTickRequest() {
334
+ return readTickRequestId(runtime.stateStore.paths.tickRequestFile);
335
+ },
321
336
  });
322
337
  const stop = () => controlPlane.stop();
323
338
  process.on('SIGINT', stop);
@@ -1 +1 @@
1
- export const wakeVersion = "0.1.20+g944b4a0";
1
+ export const wakeVersion = "0.1.22+gf88059c";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {