Merge branch 'main' into agents/daily/ci-health-agent-2026-02-17-8365488986185039286

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-17 15:09:34 -05:00
commit 8fb2a2ab99
30 files changed
+2496 -32

No files matched your search

+1 -1
View File
@@ -1 +1 @@
- Memory retrieval seed for daily :: Required startup + artifacts + memory + issue capture
- Memory retrieval seed for daily :: scheduler memory retrieval
@@ -0,0 +1,18 @@
{
"cadence": "daily",
"operation": "retrieve",
"runId": "session-mlr0921p",
"servicePath": "src/services/memory/index.js#getRelevantMemories",
"inputs": {
"agentId": "scheduler-memory-daily",
"promptPath": "",
"promptIntent": "scheduler memory retrieval",
"query": "scheduler | daily | agent | scheduler-memory-daily | intent | scheduler memory retrieval",
"events": 1
},
"outputs": {
"ingestedCount": 1,
"retrievedCount": 1
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_RETRIEVED
@@ -0,0 +1,18 @@
{
"cadence": "daily",
"operation": "retrieve",
"runId": "session-mlr0dhq6",
"servicePath": "src/services/memory/index.js#getRelevantMemories",
"inputs": {
"agentId": "const-refactor-agent",
"promptPath": "src/prompts/daily/const-refactor-agent.md",
"promptIntent": "Required startup + artifacts + memory + issue capture",
"query": "scheduler | daily | agent | const-refactor-agent | intent | Required startup + artifacts + memory + issue capture",
"events": 1
},
"outputs": {
"ingestedCount": 1,
"retrievedCount": 1
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_RETRIEVED
@@ -0,0 +1,20 @@
{
"cadence": "weekly",
"operation": "store",
"runId": "session-mlr0g2tv",
"servicePath": "src/services/memory/index.js#ingestEvents",
"inputs": {
"agentId": "scheduler-memory-weekly",
"promptPath": "",
"promptIntent": "scheduler memory store",
"events": 2,
"sourceFile": "fallback"
},
"outputs": {
"storedCount": 1,
"summaries": [
"Store memory event A for weekly :: scheduler memory store Store memory event B for weekly :: scheduler memory store"
]
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_STORED
@@ -0,0 +1,20 @@
{
"cadence": "daily",
"operation": "store",
"runId": "session-mlr0iplm",
"servicePath": "src/services/memory/index.js#ingestEvents",
"inputs": {
"agentId": "const-refactor-agent",
"promptPath": "src/prompts/daily/const-refactor-agent.md",
"promptIntent": "Required startup + artifacts + memory + issue capture",
"events": 1,
"sourceFile": "memory-updates/2026-02-17T19-44-58Z__const-refactor-agent.md"
},
"outputs": {
"storedCount": 1,
"summaries": [
"# Memory Update: Constants Refactor\n\n- Discovered hardcoded constants in `src/lock-ops.mjs` duplicating defaults.\n- Extracted `DEFAULT_RETRY_ATTEMPTS`, `DEFAULT_RETRY_BASE_DELAY_MS`, etc. to `src/constants.mjs`.\n- Refactored `src/lock-ops.mjs` to use these constants.\n- Fixed a b…"
]
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_STORED
+8
View File
@@ -0,0 +1,8 @@
> [email protected] dashboard:serve
> node bin/torch-lock.mjs dashboard
Dashboard running at http://null:4173/dashboard/
Authentication: DISABLED (Dashboard is public)
Serving files from /app
Using configuration from /app
+2 -1
View File
@@ -2,6 +2,7 @@
> [email protected] dashboard:serve
> node bin/torch-lock.mjs dashboard
Dashboard running at http://localhost:4173/dashboard/
Dashboard running at http://null:4173/dashboard/
Authentication: DISABLED (Dashboard is public)
Serving files from /app
Using configuration from /app
@@ -0,0 +1,7 @@
# Memory Update: Constants Refactor
- Discovered hardcoded constants in `src/lock-ops.mjs` duplicating defaults.
- Extracted `DEFAULT_RETRY_ATTEMPTS`, `DEFAULT_RETRY_BASE_DELAY_MS`, etc. to `src/constants.mjs`.
- Refactored `src/lock-ops.mjs` to use these constants.
- Fixed a bug in `src/cmd-check.mjs` where `queryLocksFn` was undefined.
- Verified with unit tests and lint.
+64
View File
@@ -16,6 +16,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.58.2",
"eslint": "^10.0.0",
"globals": "^17.3.0"
}
@@ -255,6 +256,22 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
@@ -670,6 +687,21 @@
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -952,6 +984,38 @@
"node": ">=8"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+1
View File
@@ -56,6 +56,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.58.2",
"eslint": "^10.0.0",
"globals": "^17.3.0"
}
File diff suppressed because it is too large. Load diff
+49
View File
@@ -0,0 +1,49 @@
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
let errorCount = 0;
page.on('console', msg => {
const type = msg.type();
if (type === 'error' || type === 'warning') {
console.log(`CONSOLE [${type.toUpperCase()}]: ${msg.text()}`);
if (type === 'error') errorCount++;
}
});
page.on('pageerror', exception => {
console.log(`PAGE ERROR: ${exception}`);
errorCount++;
});
page.on('requestfailed', request => {
// Check if failure() is not null before accessing errorText
const failure = request.failure();
const errorText = failure ? failure.errorText : 'Unknown error';
console.log(`REQUEST FAILED: ${request.url()} - ${errorText}`);
errorCount++;
});
try {
console.log('Navigating to http://localhost:4173/dashboard/ ...');
await page.goto('http://localhost:4173/dashboard/');
console.log('Page loaded. Waiting 5s for any async errors...');
await page.waitForTimeout(5000);
} catch (e) {
console.error('Navigation failed:', e);
errorCount++;
}
await browser.close();
if (errorCount > 0) {
console.log(`\nFound ${errorCount} errors.`);
process.exit(1);
} else {
console.log('\nNo errors found.');
process.exit(0);
}
})();
+36 -1
View File
@@ -79,11 +79,46 @@ function parseJsonEventsFromOutput(text) {
return events;
}
const ALLOWED_ENV_KEYS = new Set([
'PATH', 'Path', // Windows compatibility
'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ',
'NODE_ENV', 'NODE_OPTIONS', 'NODE_PATH',
'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'SystemRoot', 'windir', 'ComSpec', 'PATHEXT',
'TMPDIR', 'TEMP', 'TMP',
'EDITOR', 'VISUAL',
// CI variables
'CI', 'GITHUB_ACTIONS', 'GITHUB_REF', 'GITHUB_HEAD_REF', 'GITHUB_BASE_REF', 'GITHUB_EVENT_NAME', 'GITHUB_SHA',
]);
function getSafeEnv() {
const safeEnv = {};
for (const key in process.env) {
if (ALLOWED_ENV_KEYS.has(key) ||
key.startsWith('npm_') ||
key.startsWith('NOSTR_') ||
key.startsWith('TORCH_') ||
key.startsWith('SCHEDULER_') ||
key.startsWith('AGENT_')) {
safeEnv[key] = process.env[key];
}
}
return safeEnv;
}
/**
* Spawns a child process with a sanitized environment by default.
* @param {string} command
* @param {string[]} args
* @param {object} options
* @param {object} [options.env] - Additional environment variables to merge.
* @param {boolean} [options.inheritProcessEnv=false] - If true, use full process.env. If false (default), use sanitized env.
*/
async function runCommand(command, args = [], options = {}) {
const baseEnv = options.inheritProcessEnv ? process.env : getSafeEnv();
return new Promise((resolve) => {
const child = spawn(command, args, {
cwd: process.cwd(),
env: { ...process.env, ...(options.env || {}) },
env: { ...baseEnv, ...(options.env || {}) },
stdio: ['ignore', 'pipe', 'pipe'],
});
+1 -1
View File
@@ -29,7 +29,7 @@ export async function cmdCheck(cadence, deps = {}) {
console.error(`Paused agents: ${pausedAgents.join(', ')}`);
}
const locks = await queryLocksFn(relays, cadence, dateStr, namespace);
const locks = await queryLocks(relays, cadence, dateStr, namespace);
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
const roster = await getRosterFn(cadence);
+8
View File
@@ -11,6 +11,14 @@ export const DEFAULT_QUERY_TIMEOUT_MS = 30_000;
export const DEFAULT_PUBLISH_TIMEOUT_MS = 15_000;
export const DEFAULT_MIN_SUCCESSFUL_PUBLISHES = 1;
export const DEFAULT_MIN_ACTIVE_RELAY_POOL = 1;
export const DEFAULT_RETRY_ATTEMPTS = 4;
export const DEFAULT_RETRY_BASE_DELAY_MS = 500;
export const DEFAULT_RETRY_CAP_DELAY_MS = 8000;
export const DEFAULT_ROLLING_WINDOW_SIZE = 25;
export const DEFAULT_FAILURE_THRESHOLD = 3;
export const DEFAULT_QUARANTINE_COOLDOWN_MS = 30_000;
export const DEFAULT_MAX_QUARANTINE_COOLDOWN_MS = 300_000;
export const DEFAULT_SNAPSHOT_INTERVAL_MS = 60_000;
export const VALID_CADENCES = new Set(['daily', 'weekly']);
export const KIND_APP_DATA = 30078;
@@ -0,0 +1,10 @@
# Context for Constants Refactor Run
**Agent:** const-refactor-agent
**Date:** 2026-02-17T19-41-31Z
**Baseline:**
- `js/` directory does not exist. Scanning `src/` instead.
- `AGENTS.md` and `CLAUDE.md` not found.
- Target branch: default
**Goal:** Find duplicated numeric constants in `src/` and extract them to `src/constants.mjs`.
+1
View File
@@ -136,6 +136,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
path.join(packageRoot, 'dashboard'),
path.join(packageRoot, 'assets'),
path.join(packageRoot, 'src', 'docs'),
path.join(packageRoot, 'src', 'constants.mjs'),
path.join(packageRoot, 'torch-config.json')
];
@@ -0,0 +1,3 @@
- Extracted retry/health constants from `src/lock-ops.mjs` to `src/constants.mjs`.
- Constants: `DEFAULT_RETRY_ATTEMPTS`, `DEFAULT_RETRY_BASE_DELAY_MS`, `DEFAULT_RETRY_CAP_DELAY_MS`, `DEFAULT_ROLLING_WINDOW_SIZE`, `DEFAULT_FAILURE_THRESHOLD`, `DEFAULT_QUARANTINE_COOLDOWN_MS`, `DEFAULT_MAX_QUARANTINE_COOLDOWN_MS`, `DEFAULT_SNAPSHOT_INTERVAL_MS`.
- Reason: These values were hardcoded in multiple places in `src/lock-ops.mjs` and represent system-wide defaults.
+55 -14
View File
@@ -1,3 +1,4 @@
import { randomInt, randomUUID } from 'node:crypto';
import { SimplePool } from 'nostr-tools/pool';
import {
getQueryTimeoutMs,
@@ -6,7 +7,17 @@ import {
getRelayFallbacks,
getMinActiveRelayPool,
} from './torch-config.mjs';
import { KIND_APP_DATA } from './constants.mjs';
import {
KIND_APP_DATA,
DEFAULT_RETRY_ATTEMPTS,
DEFAULT_RETRY_BASE_DELAY_MS,
DEFAULT_RETRY_CAP_DELAY_MS,
DEFAULT_ROLLING_WINDOW_SIZE,
DEFAULT_FAILURE_THRESHOLD,
DEFAULT_QUARANTINE_COOLDOWN_MS,
DEFAULT_MAX_QUARANTINE_COOLDOWN_MS,
DEFAULT_SNAPSHOT_INTERVAL_MS,
} from './constants.mjs';
function nowUnix() {
return Math.floor(Date.now() / 1000);
@@ -129,7 +140,13 @@ function isTransientPublishCategory(category) {
].includes(category);
}
function calculateBackoffDelayMs(attemptNumber, baseMs, capMs, randomFn = Math.random) {
const MAX_RANDOM = 281474976710655; // 2**48 - 1
function secureRandom() {
return randomInt(0, MAX_RANDOM) / MAX_RANDOM;
}
function calculateBackoffDelayMs(attemptNumber, baseMs, capMs, randomFn = secureRandom) {
const maxDelay = Math.min(capMs, baseMs * (2 ** Math.max(0, attemptNumber - 1)));
return Math.floor(randomFn() * maxDelay);
}
@@ -290,11 +307,11 @@ export const defaultHealthManager = new RelayHealthManager();
function buildRelayHealthConfig(deps) {
return {
rollingWindowSize: deps.rollingWindowSize ?? 25,
failureThreshold: deps.failureThreshold ?? 3,
quarantineCooldownMs: deps.quarantineCooldownMs ?? 30_000,
maxQuarantineCooldownMs: deps.maxQuarantineCooldownMs ?? 5 * 60_000,
snapshotIntervalMs: deps.snapshotIntervalMs ?? 60_000,
rollingWindowSize: deps.rollingWindowSize ?? DEFAULT_ROLLING_WINDOW_SIZE,
failureThreshold: deps.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD,
quarantineCooldownMs: deps.quarantineCooldownMs ?? DEFAULT_QUARANTINE_COOLDOWN_MS,
maxQuarantineCooldownMs: deps.maxQuarantineCooldownMs ?? DEFAULT_MAX_QUARANTINE_COOLDOWN_MS,
snapshotIntervalMs: deps.snapshotIntervalMs ?? DEFAULT_SNAPSHOT_INTERVAL_MS,
minActiveRelayPool: Math.max(1, deps.minActiveRelayPool),
};
}
@@ -408,12 +425,31 @@ async function publishToRelays(pool, relays, event, publishTimeoutMs, phase) {
});
}
class LockPublisher {
constructor(relays, event, deps) {
export class LockPublisher {
constructor(relays, event, deps = {}) {
this.relays = relays;
this.event = event;
this.deps = deps;
this.pool = null;
this.healthConfig = null;
this.allRelays = [];
this.fallbackRelays = [];
this.publishTimeoutMs = 0;
this.minSuccesses = 0;
this.minActiveRelayPool = 0;
this.maxAttempts = 0;
this.retryBaseDelayMs = 0;
this.retryCapDelayMs = 0;
this.sleepFn = null;
this.randomFn = null;
this.telemetryLogger = null;
this.healthLogger = null;
this.healthManager = null;
this.correlationId = null;
this.attemptId = null;
}
async publish() {
const {
poolFactory = () => new SimplePool(),
getPublishTimeoutMsFn = getPublishTimeoutMs,
@@ -448,14 +484,17 @@ class LockPublisher {
this.randomFn = randomFn;
this.telemetryLogger = telemetryLogger;
this.healthLogger = healthLogger;
this.healthManager = healthManager;
this.correlationId = diagnostics.correlationId || randomUUID();
this.attemptId = diagnostics.attemptId || randomUUID();
this.correlationId = diagnostics.correlationId || process.env.SCHEDULER_LOCK_CORRELATION_ID || 'none';
this.attemptId = diagnostics.attemptId || process.env.SCHEDULER_LOCK_ATTEMPT_ID || '1';
this.healthConfig = buildRelayHealthConfig({
...this.deps,
minActiveRelayPool: this.minActiveRelayPool
});
this.allRelays = mergeRelayList(relays, this.fallbackRelays);
}
this.allRelays = mergeRelayList(this.relays, this.fallbackRelays);
async publish() {
try {
this.healthManager.maybeLogSnapshot(this.allRelays, this.healthConfig, this.healthLogger, 'publish:periodic');
let lastAttemptState = null;
@@ -663,3 +702,5 @@ export async function publishLock(relays, event, deps = {}) {
export function _resetRelayHealthState() {
defaultHealthManager.reset();
}
export const _secureRandom = secureRandom;
+30 -14
View File
@@ -152,36 +152,33 @@ async function interactiveInit(cwd) {
}
}
export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers = null) {
// Use interactive mode to get configuration unless mock answers are provided
let config;
async function resolveConfiguration(cwd, mockAnswers) {
if (mockAnswers) {
config = mockAnswers;
} else {
config = await interactiveInit(cwd);
return mockAnswers;
}
const { installDir, namespace, relays } = config;
return await interactiveInit(cwd);
}
const paths = getPaths(cwd, installDir);
console.log(`\nInitializing torch in ${paths.torchDir}...`);
// Check if directory exists and is not empty (unless force is used or we are installing to .)
function ensureInstallDirectory(paths, force, installDir) {
if (fs.existsSync(paths.torchDir) && !force) {
const entries = fs.readdirSync(paths.torchDir);
if (entries.length > 0 && installDir !== '.') {
throw new Error(`Directory ${paths.torchDir} already exists and is not empty. Use --force to overwrite.`);
}
}
ensureDir(paths.torchDir);
ensureDir(paths.promptsDir);
}
function installAppAssets(torchDir, installDir) {
// 1. Copy App Directories
syncAppDirectories(paths.torchDir, 'Copied');
syncAppDirectories(torchDir, 'Copied');
// 2. Copy App Files
syncAppFiles(paths.torchDir, installDir, 'Copied');
syncAppFiles(torchDir, installDir, 'Copied');
}
function installTorchAssets(paths, installDir) {
// 3. Copy Roster
const srcRoster = path.join(SRC_PROMPTS_DIR, 'roster.json');
if (fs.existsSync(srcRoster)) {
@@ -215,7 +212,9 @@ export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers =
console.log(`Created ${files.length} files in ${path.relative(paths.root, destDir)}/`);
}
}
}
function configureTorch(cwd, paths, installDir, namespace, relays) {
// 6. Create/Update torch-config.json
const configPath = path.join(paths.root, 'torch-config.json');
@@ -268,12 +267,29 @@ export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers =
fs.writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf8');
console.log(`Saved configuration to ${path.relative(cwd, configPath)}`);
}
function injectHostScriptsIfNeeded(paths, installDir) {
// 7. Inject Scripts into Host Package.json
// If we are NOT installing to '.', the host package.json is in paths.root
if (installDir !== '.') {
injectScriptsIntoHost(paths.root, installDir);
}
}
export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers = null) {
const config = await resolveConfiguration(cwd, mockAnswers);
const { installDir, namespace, relays } = config;
const paths = getPaths(cwd, installDir);
console.log(`\nInitializing torch in ${paths.torchDir}...`);
ensureInstallDirectory(paths, force, installDir);
installAppAssets(paths.torchDir, installDir);
installTorchAssets(paths, installDir);
configureTorch(cwd, paths, installDir, namespace, relays);
injectHostScriptsIfNeeded(paths, installDir);
console.log('\nInitialization complete.');
console.log('You can now customize the files in ' + path.relative(cwd, paths.torchDir) + '/');
@@ -0,0 +1,227 @@
> [email protected] test:unit:lock-backend
> node --test test/lock-ops.test.mjs test/lock-ops-publish-retry.test.mjs
TAP version 13
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# Published to 1/1 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# Published to 1/1 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# (node:3973) ExperimentalWarning: The MockTimers API is an experimental feature and might change at any time
# (Use `node --trace-warnings ...` to show where the warning was created)
# Subtest: publishLock retry behavior
# Subtest: succeeds immediately on first attempt without retries
ok 1 - succeeds immediately on first attempt without retries
---
duration_ms: 6.654526
type: 'test'
...
# Subtest: retries a transient failure then succeeds within retry budget with bounded jitter
ok 2 - retries a transient failure then succeeds within retry budget with bounded jitter
---
duration_ms: 4.073858
type: 'test'
...
# Subtest: returns terminal relay_publish_quorum_failure after exhausting retry budget
ok 3 - returns terminal relay_publish_quorum_failure after exhausting retry budget
---
duration_ms: 5.742571
type: 'test'
...
# Subtest: short-circuits non-retryable failures without extra attempts
ok 4 - short-circuits non-retryable failures without extra attempts
---
duration_ms: 2.299061
type: 'test'
...
1..4
ok 1 - publishLock retry behavior
---
duration_ms: 20.363862
type: 'suite'
...
# Subtest: lock-ops
# Subtest: parseLockEvent
# Subtest: parses a valid lock event correctly
ok 1 - parses a valid lock event correctly
---
duration_ms: 1.874948
type: 'test'
...
1..1
ok 1 - parseLockEvent
---
duration_ms: 2.822323
type: 'suite'
...
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
# Published to 2/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
# [publish:primary] Publishing to 1 relays (wss://primary-bad)...
# [publish:primary] Publishing to 5 relays (relay-timeout, wss://dns, wss://tcp, wss://tls, wss://ws)...
# [publish:primary] Publishing to 2 relays (wss://relay-bad, wss://relay-ok)...
# {"event":"lock_publish_quorum_met","correlationId":"7d70b395-2145-4470-9f33-9b602a34792d","attemptId":"c0035955-5f07-4d6b-a450-e07811f00e2f","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://primary-bad)...
# [publish:fallback] Publishing to 1 relays (wss://fallback-ok)...
# {"event":"lock_publish_quorum_met","correlationId":"239da9db-92a7-44f8-b694-52a4a6ddf437","attemptId":"12267c26-1926-4f5b-835b-f92682e5ffc4","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 2 relays (wss://bad, wss://healthy)...
# {"event":"lock_publish_quorum_met","correlationId":"54df96c1-356d-40a2-a300-0a5fce94d1f9","attemptId":"72e3fd52-0c75-4f3f-a43f-241ed8d973e1","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 2 relays (wss://healthy, wss://bad)...
# {"event":"lock_publish_quorum_met","correlationId":"35e05397-38ab-4fd5-b979-9f731dc5765a","attemptId":"c6410080-1244-4f4b-9139-dbb85187b441","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 2 relays (wss://healthy, wss://bad)...
# {"event":"lock_publish_quorum_met","correlationId":"e6f1916c-879a-4ed2-953e-7a519c11d857","attemptId":"1aa9c36c-8755-45ca-9fe6-6a89f883c152","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://healthy)...
# {"event":"lock_publish_quorum_met","correlationId":"56c8195a-74eb-4b72-a495-3e1efcb4a3f0","attemptId":"d2e0b655-7115-4f61-91fc-139dc5ec9db4","publishAttempt":1,"successCount":1,"relayAttemptedCount":1,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":1,"elapsedMs":0}],"totalElapsedMs":0}
# Published to 1/1 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
# [publish:fallback] Publishing to 1 relays (wss://fallback-healthy)...
# {"event":"lock_publish_quorum_met","correlationId":"94251c93-ebf1-481d-8fb5-c8a73bdf0a20","attemptId":"533fb740-98ec-4358-9d32-78c8b0ac72a4","publishAttempt":1,"successCount":1,"relayAttemptedCount":3,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":3,"elapsedMs":0}],"totalElapsedMs":0}
# Published to 1/3 relays (required=1, timeout=200ms)
# Subtest: queryLocks
# Subtest: falls back to fallback relays when primary query fails
ok 1 - falls back to fallback relays when primary query fails
---
duration_ms: 27.235965
type: 'test'
...
1..1
ok 2 - queryLocks
---
duration_ms: 27.514651
type: 'suite'
...
# Subtest: publishLock
# Subtest: retries transient failures and succeeds on a later attempt
not ok 1 - retries transient failures and succeeds on a later attempt
---
duration_ms: 6.53189
type: 'test'
location: '/app/test/lock-ops.test.mjs:78:5'
failureType: 'testCodeFailure'
error: |-
Expected values to be strictly deep-equal:
+ actual - expected
{
+ attemptId: '52c2baa6-8ff8-4f42-b6b3-ead606942315',
+ correlationId: '6428aa2b-60f0-40a5-b385-525fc25c46b0',
- attemptId: 'attempt-1',
- correlationId: 'corr-1',
elapsedMs: 1,
errorCategory: 'connection_reset',
event: 'lock_publish_retry',
nextDelayMs: 250,
publishAttempt: 1,
code: 'ERR_ASSERTION'
name: 'AssertionError'
expected:
event: 'lock_publish_retry'
correlationId: 'corr-1'
attemptId: 'attempt-1'
publishAttempt: 1
relayUrl: 'wss://primary-a'
errorCategory: 'connection_reset'
elapsedMs: 1
nextDelayMs: 250
actual:
event: 'lock_publish_retry'
correlationId: '6428aa2b-60f0-40a5-b385-525fc25c46b0'
attemptId: '52c2baa6-8ff8-4f42-b6b3-ead606942315'
publishAttempt: 1
relayUrl: 'wss://primary-a'
errorCategory: 'connection_reset'
elapsedMs: 1
nextDelayMs: 250
operator: 'deepStrictEqual'
stack: |-
TestContext.<anonymous> (file:///app/test/lock-ops.test.mjs:124:14)
async Test.run (node:internal/test_runner/test:1054:7)
async Promise.all (index 0)
async Suite.run (node:internal/test_runner/test:1442:7)
async Suite.processPendingSubtests (node:internal/test_runner/test:744:7)
...
# Subtest: fails after retry budget is exhausted for persistent transient failures
ok 2 - fails after retry budget is exhausted for persistent transient failures
---
duration_ms: 2.875565
type: 'test'
...
# Subtest: fails without retry for persistent validation failures
ok 3 - fails without retry for persistent validation failures
---
duration_ms: 1.360655
type: 'test'
...
# Subtest: includes per-relay reason categories and retry metadata in failure diagnostics
ok 4 - includes per-relay reason categories and retry metadata in failure diagnostics
---
duration_ms: 2.145677
type: 'test'
...
# Subtest: returns success when mixed relay outcomes still satisfy quorum
ok 5 - returns success when mixed relay outcomes still satisfy quorum
---
duration_ms: 1.586006
type: 'test'
...
# Subtest: uses fallback relays to satisfy min success quorum
ok 6 - uses fallback relays to satisfy min success quorum
---
duration_ms: 1.059862
type: 'test'
...
# Subtest: quarantines repeatedly failing relays and still reaches quorum with one healthy relay
ok 7 - quarantines repeatedly failing relays and still reaches quorum with one healthy relay
---
duration_ms: 2.500909
type: 'test'
...
# Subtest: uses fallback and min active pool to reintroduce quarantined relay when needed
ok 8 - uses fallback and min active pool to reintroduce quarantined relay when needed
---
duration_ms: 1.073546
type: 'test'
...
1..8
not ok 3 - publishLock
---
duration_ms: 19.967883
type: 'suite'
location: '/app/test/lock-ops.test.mjs:77:3'
failureType: 'subtestsFailed'
error: '1 subtest failed'
code: 'ERR_TEST_FAILURE'
...
1..3
not ok 2 - lock-ops
---
duration_ms: 52.981447
type: 'suite'
location: '/app/test/lock-ops.test.mjs:10:1'
failureType: 'subtestsFailed'
error: '1 subtest failed'
code: 'ERR_TEST_FAILURE'
...
1..2
# tests 14
# suites 5
# pass 13
# fail 1
# cancelled 0
# skipped 0
# todo 0
# duration_ms 196.37029
@@ -0,0 +1,175 @@
> [email protected] test:unit:lock-backend
> node --test test/lock-ops.test.mjs test/lock-ops-publish-retry.test.mjs
TAP version 13
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# Published to 1/1 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# Published to 1/1 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# [publish:primary] Publishing to 1 relays (wss://relay-a)...
# (node:5876) ExperimentalWarning: The MockTimers API is an experimental feature and might change at any time
# (Use `node --trace-warnings ...` to show where the warning was created)
# Subtest: publishLock retry behavior
# Subtest: succeeds immediately on first attempt without retries
ok 1 - succeeds immediately on first attempt without retries
---
duration_ms: 6.413745
type: 'test'
...
# Subtest: retries a transient failure then succeeds within retry budget with bounded jitter
ok 2 - retries a transient failure then succeeds within retry budget with bounded jitter
---
duration_ms: 3.062285
type: 'test'
...
# Subtest: returns terminal relay_publish_quorum_failure after exhausting retry budget
ok 3 - returns terminal relay_publish_quorum_failure after exhausting retry budget
---
duration_ms: 4.611267
type: 'test'
...
# Subtest: short-circuits non-retryable failures without extra attempts
ok 4 - short-circuits non-retryable failures without extra attempts
---
duration_ms: 2.07494
type: 'test'
...
1..4
ok 1 - publishLock retry behavior
---
duration_ms: 17.852297
type: 'suite'
...
# Subtest: lock-ops
# Subtest: parseLockEvent
# Subtest: parses a valid lock event correctly
ok 1 - parses a valid lock event correctly
---
duration_ms: 1.583449
type: 'test'
...
1..1
ok 1 - parseLockEvent
---
duration_ms: 2.601962
type: 'suite'
...
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
# Published to 2/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
# [publish:primary] Publishing to 1 relays (wss://primary-down)...
# [publish:primary] Publishing to 1 relays (wss://primary-bad)...
# [publish:primary] Publishing to 5 relays (relay-timeout, wss://dns, wss://tcp, wss://tls, wss://ws)...
# [publish:primary] Publishing to 2 relays (wss://relay-bad, wss://relay-ok)...
# {"event":"lock_publish_quorum_met","correlationId":"1be06dfa-a31e-42b0-b789-327fd9e11131","attemptId":"10405dd5-2f84-4e23-9a5a-696abb34c1bc","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://primary-bad)...
# [publish:fallback] Publishing to 1 relays (wss://fallback-ok)...
# {"event":"lock_publish_quorum_met","correlationId":"becb5a52-cef6-4bc6-84b1-ca4d14d63e04","attemptId":"25dca8be-61ee-42b8-a764-7ebdc46f234b","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 2 relays (wss://bad, wss://healthy)...
# {"event":"lock_publish_quorum_met","correlationId":"67ecc056-eaa3-40c2-9042-15e033b7a252","attemptId":"07181123-946d-4823-bc4a-c79d13a84920","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 2 relays (wss://healthy, wss://bad)...
# {"event":"lock_publish_quorum_met","correlationId":"6075ee96-8def-4111-84e2-d7b395950a84","attemptId":"bb03ae8b-ff47-41e2-ba91-b3031d38c497","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":0}],"totalElapsedMs":0}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 2 relays (wss://healthy, wss://bad)...
# {"event":"lock_publish_quorum_met","correlationId":"38f8db94-b61b-4b02-9a33-05fd8da73e5e","attemptId":"e645bbac-64ec-4f61-a865-a91d0adc739c","publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":2,"elapsedMs":1}],"totalElapsedMs":1}
# Published to 1/2 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 1 relays (wss://healthy)...
# {"event":"lock_publish_quorum_met","correlationId":"4c6a0906-bd5b-431b-9af1-1d4a87aa59d3","attemptId":"13836194-8337-49af-8f81-8038a7101d4b","publishAttempt":1,"successCount":1,"relayAttemptedCount":1,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":1,"elapsedMs":0}],"totalElapsedMs":0}
# Published to 1/1 relays (required=1, timeout=200ms)
# [publish:primary] Publishing to 2 relays (wss://primary-a, wss://primary-b)...
# [publish:fallback] Publishing to 1 relays (wss://fallback-healthy)...
# {"event":"lock_publish_quorum_met","correlationId":"2e7e8c9f-5b93-4cd3-8324-a63b981e9e4c","attemptId":"d53e657e-431a-4203-a017-256ba03a05db","publishAttempt":1,"successCount":1,"relayAttemptedCount":3,"requiredSuccesses":1,"timeoutMs":200,"retryTimeline":[{"publishAttempt":1,"successCount":1,"relayAttemptedCount":3,"elapsedMs":1}],"totalElapsedMs":2}
# Published to 1/3 relays (required=1, timeout=200ms)
# Subtest: queryLocks
# Subtest: falls back to fallback relays when primary query fails
ok 1 - falls back to fallback relays when primary query fails
---
duration_ms: 27.905124
type: 'test'
...
1..1
ok 2 - queryLocks
---
duration_ms: 28.20218
type: 'suite'
...
# Subtest: publishLock
# Subtest: retries transient failures and succeeds on a later attempt
ok 1 - retries transient failures and succeeds on a later attempt
---
duration_ms: 4.944259
type: 'test'
...
# Subtest: fails after retry budget is exhausted for persistent transient failures
ok 2 - fails after retry budget is exhausted for persistent transient failures
---
duration_ms: 3.551748
type: 'test'
...
# Subtest: fails without retry for persistent validation failures
ok 3 - fails without retry for persistent validation failures
---
duration_ms: 1.406267
type: 'test'
...
# Subtest: includes per-relay reason categories and retry metadata in failure diagnostics
ok 4 - includes per-relay reason categories and retry metadata in failure diagnostics
---
duration_ms: 2.101245
type: 'test'
...
# Subtest: returns success when mixed relay outcomes still satisfy quorum
ok 5 - returns success when mixed relay outcomes still satisfy quorum
---
duration_ms: 1.547853
type: 'test'
...
# Subtest: uses fallback relays to satisfy min success quorum
ok 6 - uses fallback relays to satisfy min success quorum
---
duration_ms: 1.380374
type: 'test'
...
# Subtest: quarantines repeatedly failing relays and still reaches quorum with one healthy relay
ok 7 - quarantines repeatedly failing relays and still reaches quorum with one healthy relay
---
duration_ms: 3.040514
type: 'test'
...
# Subtest: uses fallback and min active pool to reintroduce quarantined relay when needed
ok 8 - uses fallback and min active pool to reintroduce quarantined relay when needed
---
duration_ms: 2.862114
type: 'test'
...
1..8
ok 3 - publishLock
---
duration_ms: 21.884946
type: 'suite'
...
1..3
ok 2 - lock-ops
---
duration_ms: 53.519877
type: 'suite'
...
1..2
# tests 14
# suites 5
# pass 14
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 212.244831
+1
View File
@@ -0,0 +1 @@
- [x] Extract lock-ops constants to src/constants.mjs
@@ -0,0 +1,15 @@
---
cadence: daily
agent: const-refactor-agent
status: completed
reason: 'Scheduler cycle completed successfully'
created_at: 2026-02-17T19-48-02Z
timestamp: 2026-02-17T19-48-02Z
platform: linux
---
# Scheduler completed
- reason: Scheduler cycle completed successfully
- platform: linux
- details: Extracted lock backend constants to src/constants.mjs, refactored lock-ops.mjs, fixed cmd-check.mjs bug.
@@ -0,0 +1,34 @@
---
agent: frontend-console-debug-agent
status: completed
date: 2026-02-17
---
# Weekly Frontend Console Debug
## Summary
Detected and resolved a `403 Forbidden` error preventing the dashboard from loading `src/constants.mjs`.
## Actions
1. **Fixed Infrastructure**:
- Refactored `src/lock-ops.mjs` to fix a critical syntax error in the `LockPublisher` class.
- Verified the fix by successfully running `npm run lock:check:weekly`.
2. **Frontend Debugging**:
- Installed `@playwright/test` to capture console errors.
- Created `scripts/agent/debug_frontend.mjs` which launches a headless browser to inspect the dashboard.
- Identified a blocking `403 Forbidden` error when the dashboard attempted to load `src/constants.mjs`.
3. **Remediation**:
- Updated `src/dashboard.mjs` to explicitly allow serving `src/constants.mjs` by adding it to the `allowedPaths` whitelist. This allows the frontend to access shared constants while maintaining security restrictions on other source files.
- Fixed a typo in `src/cmd-check.mjs` where `queryLocksFn` was referenced instead of `queryLocks`.
4. **Verification**:
- Re-ran `scripts/agent/debug_frontend.mjs` and confirmed the dashboard loads with zero console errors.
- Ran `npm run lint` (passed with warnings).
- Ran `npm run build` (successful).
- Ran `npm run test:unit:lock-backend` (all tests passed).
## Artifacts
- `scripts/agent/debug_frontend.mjs`
- `package.json` (added `@playwright/test`)
+21
View File
@@ -0,0 +1,21 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { _secureRandom } from '../src/lock-ops.mjs';
describe('secureRandom', () => {
it('returns values in [0, 1)', () => {
for (let i = 0; i < 1000; i++) {
const val = _secureRandom();
assert.ok(val >= 0, `Value ${val} should be >= 0`);
assert.ok(val < 1, `Value ${val} should be < 1`);
}
});
it('returns varied values', () => {
const values = new Set();
for (let i = 0; i < 100; i++) {
values.add(_secureRandom());
}
assert.ok(values.size > 90, 'Values should be reasonably unique');
});
});