@atolis-hq/wake 0.2.4 → 0.2.6

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.
@@ -7,10 +7,16 @@ function buildStopArgs(containerName, timeoutSeconds) {
7
7
  containerName,
8
8
  ];
9
9
  }
10
+ const DOCKER_LOG_MAX_SIZE = '10m';
11
+ const DOCKER_LOG_MAX_FILE = '3';
10
12
  function buildRunArgs(input) {
11
13
  return [
12
14
  'run',
13
15
  '-d',
16
+ '--log-opt',
17
+ `max-size=${DOCKER_LOG_MAX_SIZE}`,
18
+ '--log-opt',
19
+ `max-file=${DOCKER_LOG_MAX_FILE}`,
14
20
  '--name',
15
21
  input.containerName,
16
22
  '-v',
@@ -21,7 +27,7 @@ function buildRunArgs(input) {
21
27
  '-v',
22
28
  `${mount.source}:${mount.target}${mount.readOnly === true ? ':ro' : ''}`,
23
29
  ]),
24
- // Auto-started by docker/entrypoint.sh; the UI binds 0.0.0.0 inside the
30
+ // Auto-started by wake sandbox-entrypoint; the UI binds 0.0.0.0 inside the
25
31
  // container so this published port can reach it (127.0.0.1 inside the
26
32
  // container would not be reachable via docker's port-forwarding NAT).
27
33
  ...(input.ui?.enabled === true
@@ -180,6 +180,22 @@ export async function readControlPlaneUiUrl(wakeRoot) {
180
180
  return undefined;
181
181
  }
182
182
  }
183
+ export function formatGitHubError(error) {
184
+ if (error instanceof Error) {
185
+ const octokit = error;
186
+ if (octokit.status !== undefined) {
187
+ const headers = octokit.response?.headers ?? {};
188
+ const parts = [`status=${octokit.status}`];
189
+ if (headers['x-ratelimit-remaining'] !== undefined)
190
+ parts.push(`ratelimit-remaining=${headers['x-ratelimit-remaining']}`);
191
+ if (headers['retry-after'] !== undefined)
192
+ parts.push(`retry-after=${headers['retry-after']}`);
193
+ return parts.join(' ');
194
+ }
195
+ return error.message.slice(0, 300);
196
+ }
197
+ return String(error).slice(0, 300);
198
+ }
183
199
  export function formatWakeComment(payload, controlPlaneUrl) {
184
200
  const body = typeof payload.body === 'string' ? payload.body : '';
185
201
  const kind = typeof payload.kind === 'string' ? payload.kind : undefined;
@@ -314,7 +330,7 @@ export function createGitHubIssuesWorkSource(deps) {
314
330
  });
315
331
  }
316
332
  catch (error) {
317
- console.error(`[github-work-source] poll failed for ${repoRef}, skipping this tick: ${error instanceof Error ? error.message : String(error)}`);
333
+ console.error(`[github-work-source] poll failed for ${repoRef}, skipping this tick: ${formatGitHubError(error)}`);
318
334
  }
319
335
  }
320
336
  return events;
@@ -1,6 +1,6 @@
1
1
  import { buildResourceUri } from '../../domain/resource-uri.js';
2
2
  import { createUnkeyedEventEnvelope, createEventEnvelope } from '../../lib/event-log.js';
3
- import { formatWakeComment, readControlPlaneUiUrl } from './github-issues-work-source.js';
3
+ import { formatGitHubError, formatWakeComment, readControlPlaneUiUrl, } from './github-issues-work-source.js';
4
4
  const githubPrSource = 'github-pr';
5
5
  const wakeCommentMarker = '<!-- wake:agent -->';
6
6
  function prResourceUri(repo, number) {
@@ -122,7 +122,7 @@ export function createGitHubPullRequestActivitySource(deps) {
122
122
  }
123
123
  }
124
124
  catch (error) {
125
- console.error(`[github-pr-activity-source] discovery failed for ${repoRef}, skipping this tick: ${error instanceof Error ? error.message : String(error)}`);
125
+ console.error(`[github-pr-activity-source] discovery failed for ${repoRef}, skipping this tick: ${formatGitHubError(error)}`);
126
126
  }
127
127
  }
128
128
  return { events, seenPrData, confirmedOpenRepos };
@@ -247,7 +247,7 @@ export function createGitHubPullRequestActivitySource(deps) {
247
247
  }
248
248
  }
249
249
  catch (error) {
250
- console.error(`[github-pr-activity-source] activity poll failed for ${resourceUri}, skipping this tick: ${error instanceof Error ? error.message : String(error)}`);
250
+ console.error(`[github-pr-activity-source] activity poll failed for ${resourceUri}, skipping this tick: ${formatGitHubError(error)}`);
251
251
  }
252
252
  return events;
253
253
  }
@@ -391,7 +391,7 @@ export function createGitHubPullRequestActivitySource(deps) {
391
391
  return await pollRequiredCheckFailures(ref, watchRef.resourceUri, ingestedAt, seenPrData.get(watchRef.resourceUri));
392
392
  }
393
393
  catch (error) {
394
- console.error(`[github-pr-activity-source] check poll failed for ${watchRef.resourceUri}, skipping this tick: ${error instanceof Error ? error.message : String(error)}`);
394
+ console.error(`[github-pr-activity-source] check poll failed for ${watchRef.resourceUri}, skipping this tick: ${formatGitHubError(error)}`);
395
395
  return [];
396
396
  }
397
397
  }));
@@ -0,0 +1,46 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { DEFAULT_LOG_MAX_BYTES, DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS, rotateLogFileIfNeeded, } from './log-rotation.js';
3
+ export function createDetachedProcessLogSink(logFile, options) {
4
+ const maxBytes = options?.maxBytes ?? DEFAULT_LOG_MAX_BYTES;
5
+ const rotateCheckIntervalMs = options?.rotateCheckIntervalMs ?? DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS;
6
+ const stdout = options?.stdout ?? process.stdout;
7
+ let closed = false;
8
+ let fileStream = openLogFile(logFile, maxBytes);
9
+ const rotateInterval = setInterval(() => {
10
+ if (closed) {
11
+ return;
12
+ }
13
+ const stream = fileStream;
14
+ stream.end(() => {
15
+ if (closed) {
16
+ return;
17
+ }
18
+ rotateLogFileIfNeeded(logFile, maxBytes);
19
+ fileStream = openLogFile(logFile, maxBytes);
20
+ });
21
+ }, rotateCheckIntervalMs);
22
+ rotateInterval.unref();
23
+ return {
24
+ write: (chunk) => {
25
+ if (closed) {
26
+ return;
27
+ }
28
+ fileStream.write(chunk);
29
+ stdout.write(chunk);
30
+ },
31
+ close: () => {
32
+ if (closed) {
33
+ return Promise.resolve();
34
+ }
35
+ closed = true;
36
+ clearInterval(rotateInterval);
37
+ return new Promise((resolve) => {
38
+ fileStream.end(resolve);
39
+ });
40
+ },
41
+ };
42
+ }
43
+ function openLogFile(logFile, maxBytes) {
44
+ rotateLogFileIfNeeded(logFile, maxBytes);
45
+ return createWriteStream(logFile, { flags: 'a' });
46
+ }
@@ -0,0 +1,42 @@
1
+ import { renameSync, rmSync, statSync } from 'node:fs';
2
+ export const DEFAULT_LOG_MAX_BYTES = 20 * 1024 * 1024;
3
+ export const DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
4
+ export function resolveLogMaxBytes(env = process.env) {
5
+ const raw = env.WAKE_LOG_MAX_BYTES;
6
+ if (raw === undefined || raw.trim() === '') {
7
+ return DEFAULT_LOG_MAX_BYTES;
8
+ }
9
+ const parsed = Number(raw);
10
+ if (!Number.isFinite(parsed) || parsed <= 0) {
11
+ return DEFAULT_LOG_MAX_BYTES;
12
+ }
13
+ return Math.floor(parsed);
14
+ }
15
+ export function resolveLogRotateCheckIntervalMs(env = process.env) {
16
+ const raw = env.WAKE_LOG_ROTATE_CHECK_INTERVAL_MS;
17
+ if (raw === undefined || raw.trim() === '') {
18
+ return DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS;
19
+ }
20
+ const parsed = Number(raw);
21
+ if (!Number.isFinite(parsed) || parsed <= 0) {
22
+ return DEFAULT_LOG_ROTATE_CHECK_INTERVAL_MS;
23
+ }
24
+ return Math.floor(parsed);
25
+ }
26
+ export function rotateLogFileIfNeeded(logFile, maxBytes = DEFAULT_LOG_MAX_BYTES) {
27
+ try {
28
+ if (statSync(logFile).size < maxBytes) {
29
+ return false;
30
+ }
31
+ }
32
+ catch (error) {
33
+ if (error.code === 'ENOENT') {
34
+ return false;
35
+ }
36
+ throw error;
37
+ }
38
+ const backupFile = `${logFile}.1`;
39
+ rmSync(backupFile, { force: true });
40
+ renameSync(logFile, backupFile);
41
+ return true;
42
+ }
package/dist/src/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
- import { closeSync, existsSync, openSync } from 'node:fs';
3
+ import { existsSync } from 'node:fs';
4
4
  import { access, chmod, copyFile, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { createInterface } from 'node:readline/promises';
@@ -32,7 +32,9 @@ import { createControlPlane } from './core/control-plane.js';
32
32
  import { createOutboundSinkRouter, createWorkSourceFanIn } from './core/sink-router.js';
33
33
  import { createTickRunner } from './core/tick-runner.js';
34
34
  import { systemClock } from './lib/clock.js';
35
+ import { createDetachedProcessLogSink } from './lib/detached-process-logging.js';
35
36
  import { readJsonFile } from './lib/json-file.js';
37
+ import { resolveLogMaxBytes, resolveLogRotateCheckIntervalMs } from './lib/log-rotation.js';
36
38
  import { configuredTicketSource } from './domain/sources.js';
37
39
  import { wakeVersion } from './version.js';
38
40
  function commandArgsBeforeTerminator(args) {
@@ -243,16 +245,26 @@ function createSandboxEntrypointDeps() {
243
245
  return {
244
246
  env: process.env,
245
247
  spawnDetached: (command, args, options) => {
246
- const logFd = options?.logFile !== undefined ? openSync(options.logFile, 'a') : 'ignore';
248
+ const logSink = options?.logFile !== undefined
249
+ ? createDetachedProcessLogSink(options.logFile, {
250
+ maxBytes: resolveLogMaxBytes(process.env),
251
+ rotateCheckIntervalMs: resolveLogRotateCheckIntervalMs(process.env),
252
+ })
253
+ : undefined;
247
254
  const child = spawn(command, args, {
248
255
  cwd: process.cwd(),
249
256
  env: process.env,
250
- stdio: ['ignore', logFd, logFd],
257
+ stdio: ['ignore', 'pipe', 'pipe'],
251
258
  detached: true,
252
259
  });
253
260
  if (typeof child.pid === 'number') {
254
261
  children.set(child.pid, child);
255
262
  }
263
+ const forwardOutput = (chunk) => {
264
+ logSink?.write(chunk);
265
+ };
266
+ child.stdout?.on('data', forwardOutput);
267
+ child.stderr?.on('data', forwardOutput);
256
268
  // Registered here (at spawn time) so it runs before any exit listener
257
269
  // waitForExit attaches later — though it wouldn't matter either way:
258
270
  // waitForExit captures the ChildProcess reference synchronously from
@@ -264,9 +276,7 @@ function createSandboxEntrypointDeps() {
264
276
  if (typeof child.pid === 'number') {
265
277
  children.delete(child.pid);
266
278
  }
267
- if (logFd !== 'ignore') {
268
- closeSync(logFd);
269
- }
279
+ logSink?.close().catch(() => { });
270
280
  });
271
281
  return { pid: child.pid ?? -1 };
272
282
  },
@@ -1 +1 @@
1
- export const wakeVersion = "0.2.4+g59df9f2";
1
+ export const wakeVersion = "0.2.6+g29d62c6";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {