diff --git a/src/gep/a2aProtocol.js b/src/gep/a2aProtocol.js index d44f7d3..843f88c 100644 --- a/src/gep/a2aProtocol.js +++ b/src/gep/a2aProtocol.js @@ -18,7 +18,7 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); -const { getGepAssetsDir } = require('./paths'); +const { getGepAssetsDir, getEvolverLogPath } = require('./paths'); const { computeAssetId } = require('./contentHash'); const { captureEnvFingerprint } = require('./envFingerprint'); const os = require('os'); @@ -547,6 +547,28 @@ function sendHeartbeat() { _latestAvailableWork = data.available_work; } _heartbeatConsecutiveFailures = 0; + try { + var logPath = getEvolverLogPath(); + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + var now = new Date(); + try { + fs.utimesSync(logPath, now, now); + } catch (e) { + if (e && e.code === 'ENOENT') { + try { + var fd = fs.openSync(logPath, 'a'); + fs.closeSync(fd); + fs.utimesSync(logPath, now, now); + } catch (innerErr) { + console.warn('[Heartbeat] Failed to create evolver_loop.log: ' + innerErr.message); + } + } else { + console.warn('[Heartbeat] Failed to touch evolver_loop.log: ' + e.message); + } + } + } catch (outerErr) { + console.warn('[Heartbeat] Failed to ensure evolver_loop.log: ' + outerErr.message); + } return { ok: true, response: data }; }) .catch(function (err) { diff --git a/src/gep/paths.js b/src/gep/paths.js index c9b6ad9..129582c 100644 --- a/src/gep/paths.js +++ b/src/gep/paths.js @@ -36,6 +36,10 @@ function getLogsDir() { return process.env.EVOLVER_LOGS_DIR || path.join(getWorkspaceRoot(), 'logs'); } +function getEvolverLogPath() { + return path.join(getLogsDir(), 'evolver_loop.log'); +} + function getMemoryDir() { return process.env.MEMORY_DIR || path.join(getWorkspaceRoot(), 'memory'); } @@ -96,6 +100,7 @@ module.exports = { getRepoRoot, getWorkspaceRoot, getLogsDir, + getEvolverLogPath, getMemoryDir, getEvolutionDir, getGepAssetsDir, diff --git a/src/ops/lifecycle.js b/src/ops/lifecycle.js index 4f44404..82b6ecb 100644 --- a/src/ops/lifecycle.js +++ b/src/ops/lifecycle.js @@ -5,10 +5,10 @@ const fs = require('fs'); const path = require('path'); const { execSync, spawn } = require('child_process'); -const { getRepoRoot, getWorkspaceRoot, getLogsDir } = require('../gep/paths'); +const { getRepoRoot, getWorkspaceRoot, getEvolverLogPath } = require('../gep/paths'); var WORKSPACE_ROOT = getWorkspaceRoot(); -var LOG_FILE = path.join(getLogsDir(), 'evolver_loop.log'); +var LOG_FILE = getEvolverLogPath(); var PID_FILE = path.join(WORKSPACE_ROOT, 'memory', 'evolver_loop.pid'); var MAX_SILENCE_MS = 30 * 60 * 1000; diff --git a/test/a2aProtocol.test.js b/test/a2aProtocol.test.js index 7c43263..fc145f7 100644 --- a/test/a2aProtocol.test.js +++ b/test/a2aProtocol.test.js @@ -1,5 +1,8 @@ -const { describe, it } = require('node:test'); +const { describe, it, before, after } = require('node:test'); const assert = require('node:assert/strict'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); const { PROTOCOL_NAME, PROTOCOL_VERSION, @@ -13,6 +16,7 @@ const { buildRevoke, isValidProtocolMessage, unwrapAssetFromMessage, + sendHeartbeat, } = require('../src/gep/a2aProtocol'); describe('protocol constants', () => { @@ -132,3 +136,64 @@ describe('unwrapAssetFromMessage', () => { assert.equal(unwrapAssetFromMessage('string'), null); }); }); + +describe('sendHeartbeat log touch', () => { + var tmpDir; + var originalFetch; + var originalHubUrl; + var originalLogsDir; + + before(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-hb-test-')); + originalHubUrl = process.env.A2A_HUB_URL; + originalLogsDir = process.env.EVOLVER_LOGS_DIR; + process.env.A2A_HUB_URL = 'http://localhost:19999'; + process.env.EVOLVER_LOGS_DIR = tmpDir; + originalFetch = global.fetch; + }); + + after(() => { + global.fetch = originalFetch; + if (originalHubUrl === undefined) { + delete process.env.A2A_HUB_URL; + } else { + process.env.A2A_HUB_URL = originalHubUrl; + } + if (originalLogsDir === undefined) { + delete process.env.EVOLVER_LOGS_DIR; + } else { + process.env.EVOLVER_LOGS_DIR = originalLogsDir; + } + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('updates mtime of existing evolver_loop.log on successful heartbeat', async () => { + var logPath = path.join(tmpDir, 'evolver_loop.log'); + fs.writeFileSync(logPath, ''); + var oldTime = new Date(Date.now() - 5000); + fs.utimesSync(logPath, oldTime, oldTime); + + global.fetch = async () => ({ + json: async () => ({ status: 'ok' }), + }); + + var result = await sendHeartbeat(); + assert.ok(result.ok, 'heartbeat should succeed'); + + var mtime = fs.statSync(logPath).mtimeMs; + assert.ok(mtime > oldTime.getTime(), 'mtime should be newer than the pre-set old time'); + }); + + it('creates evolver_loop.log when it does not exist on successful heartbeat', async () => { + var logPath = path.join(tmpDir, 'evolver_loop.log'); + if (fs.existsSync(logPath)) fs.unlinkSync(logPath); + + global.fetch = async () => ({ + json: async () => ({ status: 'ok' }), + }); + + var result = await sendHeartbeat(); + assert.ok(result.ok, 'heartbeat should succeed'); + assert.ok(fs.existsSync(logPath), 'evolver_loop.log should be created when missing'); + }); +});