@karmaniverous/jeeves-meta 0.12.4 → 0.13.1
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.
- package/README.md +4 -4
- package/dist/cli/jeeves-meta/index.js +390 -262
- package/dist/index.d.ts +5 -6
- package/dist/index.js +335 -205
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs, { readdirSync, readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, mkdirSync, copyFileSync, statSync, watchFile } from 'node:fs';
|
|
2
|
-
import path, { join, dirname, resolve, relative, posix } from 'node:path';
|
|
2
|
+
import path, { join, dirname, resolve, basename, relative, posix } from 'node:path';
|
|
3
3
|
import { unlink, readFile, mkdir, writeFile, copyFile } from 'node:fs/promises';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import process$1 from 'node:process';
|
|
6
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
6
7
|
import require$$0$4 from 'path';
|
|
7
8
|
import require$$0$3 from 'fs';
|
|
8
9
|
import require$$0$1 from 'constants';
|
|
@@ -11,11 +12,10 @@ import require$$4 from 'util';
|
|
|
11
12
|
import require$$5 from 'assert';
|
|
12
13
|
import require$$2 from 'events';
|
|
13
14
|
import vm from 'vm';
|
|
15
|
+
import { z } from 'zod';
|
|
14
16
|
import * as commander from 'commander';
|
|
15
17
|
import { tmpdir } from 'node:os';
|
|
16
|
-
import { z } from 'zod';
|
|
17
18
|
import 'node:child_process';
|
|
18
|
-
import { randomUUID, createHash } from 'node:crypto';
|
|
19
19
|
import pino from 'pino';
|
|
20
20
|
import Handlebars from 'handlebars';
|
|
21
21
|
import { Cron } from 'croner';
|
|
@@ -7243,6 +7243,32 @@ requireSemver();
|
|
|
7243
7243
|
|
|
7244
7244
|
/** Core config file name. */
|
|
7245
7245
|
const CONFIG_FILE = 'config.json';
|
|
7246
|
+
|
|
7247
|
+
/**
|
|
7248
|
+
* Default port assignments for Jeeves platform services.
|
|
7249
|
+
*
|
|
7250
|
+
* @remarks
|
|
7251
|
+
* Each port number is a historical reference:
|
|
7252
|
+
* - 1934: Wodehouse's *Thank You, Jeeves*; Popper's *Logic of Scientific Discovery*
|
|
7253
|
+
* - 1936: Turing's "On Computable Numbers"; Church's lambda calculus
|
|
7254
|
+
* - 1937: Turing's paper in *Proceedings of the London Mathematical Society*
|
|
7255
|
+
* - 1938: Wodehouse's *The Code of the Woosters*; Shannon's relay/switching paper
|
|
7256
|
+
*/
|
|
7257
|
+
/** Default port for jeeves-server. */
|
|
7258
|
+
const SERVER_PORT = 1934;
|
|
7259
|
+
/** Default port for jeeves-watcher. */
|
|
7260
|
+
const WATCHER_PORT = 1936;
|
|
7261
|
+
/** Default port for jeeves-runner. */
|
|
7262
|
+
const RUNNER_PORT = 1937;
|
|
7263
|
+
/** Default port for jeeves-meta. */
|
|
7264
|
+
const META_PORT = 1938;
|
|
7265
|
+
/** Map of service names to their default ports. */
|
|
7266
|
+
const DEFAULT_PORTS = {
|
|
7267
|
+
server: SERVER_PORT,
|
|
7268
|
+
watcher: WATCHER_PORT,
|
|
7269
|
+
runner: RUNNER_PORT,
|
|
7270
|
+
meta: META_PORT,
|
|
7271
|
+
};
|
|
7246
7272
|
/**
|
|
7247
7273
|
* Get the core config directory path.
|
|
7248
7274
|
*
|
|
@@ -7261,30 +7287,64 @@ function getCoreConfigDir() {
|
|
|
7261
7287
|
function getComponentConfigDir(componentName) {
|
|
7262
7288
|
throw new Error('jeeves-core: init() must be called first');
|
|
7263
7289
|
}
|
|
7290
|
+
/** Maximum rename retry attempts on EPERM. */
|
|
7291
|
+
const ATOMIC_WRITE_MAX_RETRIES = 3;
|
|
7292
|
+
/** Delay between EPERM retries in milliseconds. */
|
|
7293
|
+
const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
|
|
7264
7294
|
/**
|
|
7265
7295
|
* Write content to a file atomically via a temp file + rename.
|
|
7266
7296
|
*
|
|
7297
|
+
* @remarks
|
|
7298
|
+
* Retries the rename up to three times on EPERM (Windows file-handle
|
|
7299
|
+
* contention) with a 100 ms synchronous delay between attempts.
|
|
7300
|
+
*
|
|
7267
7301
|
* @param filePath - Absolute path to the target file.
|
|
7268
7302
|
* @param content - Content to write.
|
|
7269
7303
|
*/
|
|
7270
7304
|
function atomicWrite(filePath, content) {
|
|
7271
7305
|
const dir = dirname(filePath);
|
|
7272
|
-
const
|
|
7306
|
+
const base = basename(filePath, '.md');
|
|
7307
|
+
const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
|
|
7273
7308
|
writeFileSync(tempPath, content, 'utf-8');
|
|
7274
|
-
|
|
7275
|
-
renameSync(tempPath, filePath);
|
|
7276
|
-
}
|
|
7277
|
-
catch (err) {
|
|
7309
|
+
for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
|
|
7278
7310
|
try {
|
|
7279
|
-
|
|
7311
|
+
renameSync(tempPath, filePath);
|
|
7312
|
+
return;
|
|
7280
7313
|
}
|
|
7281
|
-
catch {
|
|
7282
|
-
|
|
7314
|
+
catch (err) {
|
|
7315
|
+
const isEperm = err instanceof Error &&
|
|
7316
|
+
'code' in err &&
|
|
7317
|
+
err.code === 'EPERM';
|
|
7318
|
+
if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
|
|
7319
|
+
try {
|
|
7320
|
+
unlinkSync(tempPath);
|
|
7321
|
+
}
|
|
7322
|
+
catch {
|
|
7323
|
+
/* best-effort cleanup */
|
|
7324
|
+
}
|
|
7325
|
+
throw err;
|
|
7326
|
+
}
|
|
7327
|
+
// Synchronous sleep before retry (acceptable in atomic write context)
|
|
7328
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
|
|
7283
7329
|
}
|
|
7284
|
-
throw err;
|
|
7285
7330
|
}
|
|
7286
7331
|
}
|
|
7287
7332
|
|
|
7333
|
+
/**
|
|
7334
|
+
* Shared internal utility functions.
|
|
7335
|
+
*
|
|
7336
|
+
* @packageDocumentation
|
|
7337
|
+
*/
|
|
7338
|
+
/**
|
|
7339
|
+
* Extract a human-readable message from an unknown caught value.
|
|
7340
|
+
*
|
|
7341
|
+
* @param err - The caught value (typically `unknown`).
|
|
7342
|
+
* @returns The error message string.
|
|
7343
|
+
*/
|
|
7344
|
+
function getErrorMessage(err) {
|
|
7345
|
+
return err instanceof Error ? err.message : String(err);
|
|
7346
|
+
}
|
|
7347
|
+
|
|
7288
7348
|
/**
|
|
7289
7349
|
* Factory for a framework-agnostic config apply HTTP handler.
|
|
7290
7350
|
*
|
|
@@ -7337,8 +7397,7 @@ function readConfigFile(filePath) {
|
|
|
7337
7397
|
return JSON.parse(raw);
|
|
7338
7398
|
}
|
|
7339
7399
|
catch (err) {
|
|
7340
|
-
|
|
7341
|
-
console.warn(`jeeves-core: Could not read config file ${filePath}: ${msg}`);
|
|
7400
|
+
console.warn(`jeeves-core: Could not read config file ${filePath}: ${getErrorMessage(err)}`);
|
|
7342
7401
|
return {};
|
|
7343
7402
|
}
|
|
7344
7403
|
}
|
|
@@ -7387,10 +7446,9 @@ function createConfigApplyHandler(descriptor) {
|
|
|
7387
7446
|
atomicWrite(configPath, json);
|
|
7388
7447
|
}
|
|
7389
7448
|
catch (err) {
|
|
7390
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7391
7449
|
return {
|
|
7392
7450
|
status: 500,
|
|
7393
|
-
body: { error: `Failed to write config: ${
|
|
7451
|
+
body: { error: `Failed to write config: ${getErrorMessage(err)}` },
|
|
7394
7452
|
};
|
|
7395
7453
|
}
|
|
7396
7454
|
// Call onConfigApply callback if defined
|
|
@@ -7399,12 +7457,11 @@ function createConfigApplyHandler(descriptor) {
|
|
|
7399
7457
|
await descriptor.onConfigApply(validatedConfig);
|
|
7400
7458
|
}
|
|
7401
7459
|
catch (err) {
|
|
7402
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
7403
7460
|
return {
|
|
7404
7461
|
status: 200,
|
|
7405
7462
|
body: {
|
|
7406
7463
|
applied: true,
|
|
7407
|
-
warning: `Config written but callback failed: ${
|
|
7464
|
+
warning: `Config written but callback failed: ${getErrorMessage(err)}`,
|
|
7408
7465
|
config: validatedConfig,
|
|
7409
7466
|
},
|
|
7410
7467
|
};
|
|
@@ -7487,8 +7544,7 @@ function createStatusHandler(options) {
|
|
|
7487
7544
|
health = await options.getHealth();
|
|
7488
7545
|
}
|
|
7489
7546
|
catch (err) {
|
|
7490
|
-
|
|
7491
|
-
health = { error: message };
|
|
7547
|
+
health = { error: getErrorMessage(err) };
|
|
7492
7548
|
overallStatus = 'degraded';
|
|
7493
7549
|
}
|
|
7494
7550
|
}
|
|
@@ -7504,6 +7560,47 @@ function createStatusHandler(options) {
|
|
|
7504
7560
|
};
|
|
7505
7561
|
};
|
|
7506
7562
|
}
|
|
7563
|
+
/** Core shared config section. */
|
|
7564
|
+
const workspaceCoreConfigSchema = z
|
|
7565
|
+
.object({
|
|
7566
|
+
/** Workspace root path. */
|
|
7567
|
+
workspace: z.string().optional().describe('Workspace root path'),
|
|
7568
|
+
/** Platform config root path. */
|
|
7569
|
+
configRoot: z.string().optional().describe('Platform config root path'),
|
|
7570
|
+
/** OpenClaw gateway URL. */
|
|
7571
|
+
gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
|
|
7572
|
+
})
|
|
7573
|
+
.partial();
|
|
7574
|
+
/** Memory shared config section. */
|
|
7575
|
+
const workspaceMemoryConfigSchema = z
|
|
7576
|
+
.object({
|
|
7577
|
+
/** MEMORY.md character budget. */
|
|
7578
|
+
budget: z.number().int().positive().optional().describe('Memory budget'),
|
|
7579
|
+
/** Warning threshold as a fraction of budget. */
|
|
7580
|
+
warningThreshold: z
|
|
7581
|
+
.number()
|
|
7582
|
+
.min(0)
|
|
7583
|
+
.max(1)
|
|
7584
|
+
.optional()
|
|
7585
|
+
.describe('Memory warning threshold'),
|
|
7586
|
+
/** Staleness threshold in days. */
|
|
7587
|
+
staleDays: z
|
|
7588
|
+
.number()
|
|
7589
|
+
.int()
|
|
7590
|
+
.positive()
|
|
7591
|
+
.optional()
|
|
7592
|
+
.describe('Memory staleness threshold in days'),
|
|
7593
|
+
})
|
|
7594
|
+
.partial();
|
|
7595
|
+
/** Workspace config Zod schema. */
|
|
7596
|
+
z.object({
|
|
7597
|
+
/** JSON Schema pointer for IDE autocomplete. */
|
|
7598
|
+
$schema: z.string().optional().describe('JSON Schema pointer'),
|
|
7599
|
+
/** Core shared defaults. */
|
|
7600
|
+
core: workspaceCoreConfigSchema.optional(),
|
|
7601
|
+
/** Memory hygiene shared defaults. */
|
|
7602
|
+
memory: workspaceMemoryConfigSchema.optional(),
|
|
7603
|
+
});
|
|
7507
7604
|
|
|
7508
7605
|
function getDefaultExportFromCjs (x) {
|
|
7509
7606
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
@@ -7821,6 +7918,38 @@ function loadConfig(configDir) {
|
|
|
7821
7918
|
}
|
|
7822
7919
|
}
|
|
7823
7920
|
|
|
7921
|
+
/**
|
|
7922
|
+
* Service URL resolution.
|
|
7923
|
+
*
|
|
7924
|
+
* @remarks
|
|
7925
|
+
* Resolves the URL for a named Jeeves service using the following
|
|
7926
|
+
* resolution order:
|
|
7927
|
+
* 1. Consumer's own component config
|
|
7928
|
+
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
7929
|
+
* 3. Default port constants
|
|
7930
|
+
*/
|
|
7931
|
+
/**
|
|
7932
|
+
* Resolve the URL for a named Jeeves service.
|
|
7933
|
+
*
|
|
7934
|
+
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
7935
|
+
* @param consumerName - Optional consumer component name for config override.
|
|
7936
|
+
* @returns The resolved service URL.
|
|
7937
|
+
* @throws Error if `init()` has not been called or the service is unknown.
|
|
7938
|
+
*/
|
|
7939
|
+
function getServiceUrl(serviceName, consumerName) {
|
|
7940
|
+
// 2. Check core config
|
|
7941
|
+
const coreDir = getCoreConfigDir();
|
|
7942
|
+
const coreConfig = loadConfig(coreDir);
|
|
7943
|
+
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
7944
|
+
if (coreUrl)
|
|
7945
|
+
return coreUrl;
|
|
7946
|
+
// 3. Fall back to port constants
|
|
7947
|
+
const port = DEFAULT_PORTS[serviceName];
|
|
7948
|
+
{
|
|
7949
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
7950
|
+
}
|
|
7951
|
+
}
|
|
7952
|
+
|
|
7824
7953
|
/**
|
|
7825
7954
|
* Resolve the bind address for a Jeeves service.
|
|
7826
7955
|
*
|
|
@@ -7858,6 +7987,10 @@ function getBindAddress(componentName) {
|
|
|
7858
7987
|
// Tier 4: Default
|
|
7859
7988
|
return DEFAULT_BIND_ADDRESS;
|
|
7860
7989
|
}
|
|
7990
|
+
/** Async sleep via setTimeout. */
|
|
7991
|
+
function sleepAsync(ms) {
|
|
7992
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
7993
|
+
}
|
|
7861
7994
|
|
|
7862
7995
|
/**
|
|
7863
7996
|
* Custom CLI commands for the jeeves-meta service.
|
|
@@ -7869,7 +8002,11 @@ function getBindAddress(componentName) {
|
|
|
7869
8002
|
*/
|
|
7870
8003
|
/** Build the full API URL for a given port string and path. */
|
|
7871
8004
|
function apiUrl(port, apiPath) {
|
|
7872
|
-
|
|
8005
|
+
const url = new URL(apiPath, getServiceUrl('meta'));
|
|
8006
|
+
if (port !== DEFAULT_PORT_STR) {
|
|
8007
|
+
url.port = port;
|
|
8008
|
+
}
|
|
8009
|
+
return url.toString();
|
|
7873
8010
|
}
|
|
7874
8011
|
/** Wrap an async CLI action with consistent error handling. */
|
|
7875
8012
|
function withErrorHandling(fn, label) {
|
|
@@ -8024,7 +8161,6 @@ function registerCustomCliCommands(program) {
|
|
|
8024
8161
|
*/
|
|
8025
8162
|
const RESTART_REQUIRED_FIELDS = [
|
|
8026
8163
|
'port',
|
|
8027
|
-
'host',
|
|
8028
8164
|
'watcherUrl',
|
|
8029
8165
|
'gatewayUrl',
|
|
8030
8166
|
'gatewayApiKey',
|
|
@@ -8136,8 +8272,6 @@ const autoSeedRuleSchema = z.object({
|
|
|
8136
8272
|
const serviceConfigSchema = metaConfigSchema.extend({
|
|
8137
8273
|
/** HTTP port for the service (default: 1938). */
|
|
8138
8274
|
port: z.number().int().min(1).max(65535).default(1938),
|
|
8139
|
-
/** Bind address for the HTTP server (default: 127.0.0.1). */
|
|
8140
|
-
host: z.string().default('127.0.0.1'),
|
|
8141
8275
|
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
8142
8276
|
schedule: z.string().default('*/30 * * * *'),
|
|
8143
8277
|
/** Optional channel identifier for reporting. */
|
|
@@ -8523,6 +8657,174 @@ async function readMetaJson(metaPath) {
|
|
|
8523
8657
|
return JSON.parse(raw);
|
|
8524
8658
|
}
|
|
8525
8659
|
|
|
8660
|
+
/**
|
|
8661
|
+
* Escape special glob characters in a path so it can be used as a literal
|
|
8662
|
+
* prefix in glob patterns.
|
|
8663
|
+
*
|
|
8664
|
+
* Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
|
|
8665
|
+
* that paths containing parentheses (e.g. Slack channel IDs) or other
|
|
8666
|
+
* special characters are matched literally by the watcher's walk endpoint.
|
|
8667
|
+
*
|
|
8668
|
+
* @module escapeGlob
|
|
8669
|
+
*/
|
|
8670
|
+
/**
|
|
8671
|
+
* Escape glob metacharacters in a string using character-class wrapping.
|
|
8672
|
+
*
|
|
8673
|
+
* Backslash escaping (`\(`) does not work reliably on Windows where `\` is
|
|
8674
|
+
* the path separator. Instead, each metacharacter is wrapped in a character
|
|
8675
|
+
* class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
|
|
8676
|
+
*
|
|
8677
|
+
* Square brackets themselves are escaped as `[[]` and `[]]`.
|
|
8678
|
+
*
|
|
8679
|
+
* @param s - Raw path string.
|
|
8680
|
+
* @returns String with glob metacharacters wrapped in character classes.
|
|
8681
|
+
*/
|
|
8682
|
+
function escapeGlob(s) {
|
|
8683
|
+
return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
|
|
8684
|
+
}
|
|
8685
|
+
|
|
8686
|
+
/**
|
|
8687
|
+
* Filter file paths by modification time.
|
|
8688
|
+
*
|
|
8689
|
+
* Shared utility for staleness detection and delta file enumeration.
|
|
8690
|
+
* Uses `fs.statSync` for fast local mtime checks on known paths.
|
|
8691
|
+
*
|
|
8692
|
+
* @module mtimeFilter
|
|
8693
|
+
*/
|
|
8694
|
+
/**
|
|
8695
|
+
* Check if any file in the list was modified after the given timestamp.
|
|
8696
|
+
*
|
|
8697
|
+
* Short-circuits on first match for efficiency (staleness checks).
|
|
8698
|
+
*
|
|
8699
|
+
* @param files - Array of file paths to check.
|
|
8700
|
+
* @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
|
|
8701
|
+
* @returns True if any file was modified after the timestamp.
|
|
8702
|
+
*/
|
|
8703
|
+
function hasModifiedAfter(files, afterMs) {
|
|
8704
|
+
for (const filePath of files) {
|
|
8705
|
+
try {
|
|
8706
|
+
if (statSync(filePath).mtimeMs > afterMs)
|
|
8707
|
+
return true;
|
|
8708
|
+
}
|
|
8709
|
+
catch {
|
|
8710
|
+
// Unreadable file — skip
|
|
8711
|
+
}
|
|
8712
|
+
}
|
|
8713
|
+
return false;
|
|
8714
|
+
}
|
|
8715
|
+
/**
|
|
8716
|
+
* Filter files to only those modified after the given timestamp.
|
|
8717
|
+
*
|
|
8718
|
+
* @param files - Array of file paths to filter.
|
|
8719
|
+
* @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
|
|
8720
|
+
* @returns Filtered array of file paths.
|
|
8721
|
+
*/
|
|
8722
|
+
function filterModifiedAfter(files, afterMs) {
|
|
8723
|
+
return files.filter((filePath) => {
|
|
8724
|
+
try {
|
|
8725
|
+
return statSync(filePath).mtimeMs > afterMs;
|
|
8726
|
+
}
|
|
8727
|
+
catch {
|
|
8728
|
+
return false;
|
|
8729
|
+
}
|
|
8730
|
+
});
|
|
8731
|
+
}
|
|
8732
|
+
|
|
8733
|
+
/**
|
|
8734
|
+
* Staleness detection via watcher walk.
|
|
8735
|
+
*
|
|
8736
|
+
* A meta is stale when any watched file in its scope was modified after
|
|
8737
|
+
* `_generatedAt`.
|
|
8738
|
+
*
|
|
8739
|
+
* @module scheduling/staleness
|
|
8740
|
+
*/
|
|
8741
|
+
/**
|
|
8742
|
+
* Check if a meta is stale.
|
|
8743
|
+
*
|
|
8744
|
+
* Uses watcher `/walk` to enumerate watched files under the scope prefix,
|
|
8745
|
+
* then applies a local mtime check (fast) to detect any modifications since
|
|
8746
|
+
* `_generatedAt`. Short-circuits on first match.
|
|
8747
|
+
*
|
|
8748
|
+
* @param scopePrefix - Path prefix for this meta's scope.
|
|
8749
|
+
* @param meta - Current meta.json content.
|
|
8750
|
+
* @param watcher - WatcherClient instance.
|
|
8751
|
+
* @returns True if any file in scope was modified after `_generatedAt`.
|
|
8752
|
+
*/
|
|
8753
|
+
async function isStale(scopePrefix, meta, watcher) {
|
|
8754
|
+
if (!meta._generatedAt)
|
|
8755
|
+
return true; // Never synthesized = stale
|
|
8756
|
+
const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
|
|
8757
|
+
// Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
|
|
8758
|
+
// Handle both forward and back slashes for cross-platform compatibility.
|
|
8759
|
+
const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
|
|
8760
|
+
const filtered = files.filter((f) => !metaSep.test(f));
|
|
8761
|
+
return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
|
|
8762
|
+
}
|
|
8763
|
+
/** Maximum staleness for never-synthesized metas (1 year in seconds). */
|
|
8764
|
+
const MAX_STALENESS_SECONDS = 365 * 86_400;
|
|
8765
|
+
/**
|
|
8766
|
+
* Compute actual staleness in seconds (now minus _generatedAt).
|
|
8767
|
+
*
|
|
8768
|
+
* Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
|
|
8769
|
+
* (1 year) so that depth weighting can differentiate them. Without
|
|
8770
|
+
* bounding, `Infinity * depthFactor` = `Infinity` for all depths.
|
|
8771
|
+
*
|
|
8772
|
+
* @param meta - Current meta.json content.
|
|
8773
|
+
* @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
|
|
8774
|
+
*/
|
|
8775
|
+
function actualStaleness(meta) {
|
|
8776
|
+
if (!meta._generatedAt)
|
|
8777
|
+
return MAX_STALENESS_SECONDS;
|
|
8778
|
+
const generatedMs = new Date(meta._generatedAt).getTime();
|
|
8779
|
+
return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
|
|
8780
|
+
}
|
|
8781
|
+
/**
|
|
8782
|
+
* Check whether the architect step should be triggered.
|
|
8783
|
+
*
|
|
8784
|
+
* @param meta - Current meta.json.
|
|
8785
|
+
* @param structureChanged - Whether the structure hash changed.
|
|
8786
|
+
* @param steerChanged - Whether the steer directive changed.
|
|
8787
|
+
* @param architectEvery - Config: run architect every N cycles.
|
|
8788
|
+
* @returns True if the architect step should run.
|
|
8789
|
+
*/
|
|
8790
|
+
function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
|
|
8791
|
+
return (!meta._builder ||
|
|
8792
|
+
structureChanged ||
|
|
8793
|
+
steerChanged ||
|
|
8794
|
+
(meta._synthesisCount ?? 0) >= architectEvery);
|
|
8795
|
+
}
|
|
8796
|
+
/**
|
|
8797
|
+
* Detect whether the steer directive changed since the last archive.
|
|
8798
|
+
*
|
|
8799
|
+
* @param currentSteer - Current _steer value (or undefined).
|
|
8800
|
+
* @param archiveSteer - Archive _steer value (or undefined).
|
|
8801
|
+
* @param hasArchive - Whether an archive snapshot exists.
|
|
8802
|
+
* @returns True if steer changed.
|
|
8803
|
+
*/
|
|
8804
|
+
function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
|
|
8805
|
+
if (!hasArchive)
|
|
8806
|
+
return Boolean(currentSteer);
|
|
8807
|
+
return currentSteer !== archiveSteer;
|
|
8808
|
+
}
|
|
8809
|
+
/**
|
|
8810
|
+
* Compute a normalized staleness score (0–1) for display purposes.
|
|
8811
|
+
*
|
|
8812
|
+
* Uses the same depth/emphasis weighting as candidate selection,
|
|
8813
|
+
* normalized to a 30-day window.
|
|
8814
|
+
*
|
|
8815
|
+
* @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
|
|
8816
|
+
* @param depth - Meta tree depth.
|
|
8817
|
+
* @param emphasis - Scheduling emphasis multiplier.
|
|
8818
|
+
* @param depthWeight - Depth weighting exponent from config.
|
|
8819
|
+
* @returns Normalized score between 0 and 1.
|
|
8820
|
+
*/
|
|
8821
|
+
function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
|
|
8822
|
+
if (stalenessSeconds === null)
|
|
8823
|
+
return 1;
|
|
8824
|
+
const depthFactor = Math.pow(1 + depthWeight, depth);
|
|
8825
|
+
return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
|
|
8826
|
+
}
|
|
8827
|
+
|
|
8526
8828
|
/**
|
|
8527
8829
|
* Build the ownership tree from discovered .meta/ paths.
|
|
8528
8830
|
*
|
|
@@ -8605,8 +8907,6 @@ function findNode(tree, targetPath) {
|
|
|
8605
8907
|
*
|
|
8606
8908
|
* @module discovery/listMetas
|
|
8607
8909
|
*/
|
|
8608
|
-
/** Maximum staleness for never-synthesized metas (1 year in seconds). */
|
|
8609
|
-
const MAX_STALENESS_SECONDS$1 = 365 * 86_400;
|
|
8610
8910
|
/**
|
|
8611
8911
|
* Discover, deduplicate, and enrich all metas.
|
|
8612
8912
|
*
|
|
@@ -8642,7 +8942,7 @@ async function listMetas(config, watcher) {
|
|
|
8642
8942
|
// Compute staleness
|
|
8643
8943
|
let stalenessSeconds;
|
|
8644
8944
|
if (neverSynth) {
|
|
8645
|
-
stalenessSeconds = MAX_STALENESS_SECONDS
|
|
8945
|
+
stalenessSeconds = MAX_STALENESS_SECONDS;
|
|
8646
8946
|
}
|
|
8647
8947
|
else {
|
|
8648
8948
|
const genAt = new Date(meta._generatedAt).getTime();
|
|
@@ -8675,79 +8975,6 @@ async function listMetas(config, watcher) {
|
|
|
8675
8975
|
};
|
|
8676
8976
|
}
|
|
8677
8977
|
|
|
8678
|
-
/**
|
|
8679
|
-
* Escape special glob characters in a path so it can be used as a literal
|
|
8680
|
-
* prefix in glob patterns.
|
|
8681
|
-
*
|
|
8682
|
-
* Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
|
|
8683
|
-
* that paths containing parentheses (e.g. Slack channel IDs) or other
|
|
8684
|
-
* special characters are matched literally by the watcher's walk endpoint.
|
|
8685
|
-
*
|
|
8686
|
-
* @module escapeGlob
|
|
8687
|
-
*/
|
|
8688
|
-
/**
|
|
8689
|
-
* Escape glob metacharacters in a string using character-class wrapping.
|
|
8690
|
-
*
|
|
8691
|
-
* Backslash escaping (`\(`) does not work reliably on Windows where `\` is
|
|
8692
|
-
* the path separator. Instead, each metacharacter is wrapped in a character
|
|
8693
|
-
* class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
|
|
8694
|
-
*
|
|
8695
|
-
* Square brackets themselves are escaped as `[[]` and `[]]`.
|
|
8696
|
-
*
|
|
8697
|
-
* @param s - Raw path string.
|
|
8698
|
-
* @returns String with glob metacharacters wrapped in character classes.
|
|
8699
|
-
*/
|
|
8700
|
-
function escapeGlob(s) {
|
|
8701
|
-
return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
|
|
8702
|
-
}
|
|
8703
|
-
|
|
8704
|
-
/**
|
|
8705
|
-
* Filter file paths by modification time.
|
|
8706
|
-
*
|
|
8707
|
-
* Shared utility for staleness detection and delta file enumeration.
|
|
8708
|
-
* Uses `fs.statSync` for fast local mtime checks on known paths.
|
|
8709
|
-
*
|
|
8710
|
-
* @module mtimeFilter
|
|
8711
|
-
*/
|
|
8712
|
-
/**
|
|
8713
|
-
* Check if any file in the list was modified after the given timestamp.
|
|
8714
|
-
*
|
|
8715
|
-
* Short-circuits on first match for efficiency (staleness checks).
|
|
8716
|
-
*
|
|
8717
|
-
* @param files - Array of file paths to check.
|
|
8718
|
-
* @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
|
|
8719
|
-
* @returns True if any file was modified after the timestamp.
|
|
8720
|
-
*/
|
|
8721
|
-
function hasModifiedAfter(files, afterMs) {
|
|
8722
|
-
for (const filePath of files) {
|
|
8723
|
-
try {
|
|
8724
|
-
if (statSync(filePath).mtimeMs > afterMs)
|
|
8725
|
-
return true;
|
|
8726
|
-
}
|
|
8727
|
-
catch {
|
|
8728
|
-
// Unreadable file — skip
|
|
8729
|
-
}
|
|
8730
|
-
}
|
|
8731
|
-
return false;
|
|
8732
|
-
}
|
|
8733
|
-
/**
|
|
8734
|
-
* Filter files to only those modified after the given timestamp.
|
|
8735
|
-
*
|
|
8736
|
-
* @param files - Array of file paths to filter.
|
|
8737
|
-
* @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
|
|
8738
|
-
* @returns Filtered array of file paths.
|
|
8739
|
-
*/
|
|
8740
|
-
function filterModifiedAfter(files, afterMs) {
|
|
8741
|
-
return files.filter((filePath) => {
|
|
8742
|
-
try {
|
|
8743
|
-
return statSync(filePath).mtimeMs > afterMs;
|
|
8744
|
-
}
|
|
8745
|
-
catch {
|
|
8746
|
-
return false;
|
|
8747
|
-
}
|
|
8748
|
-
});
|
|
8749
|
-
}
|
|
8750
|
-
|
|
8751
8978
|
/**
|
|
8752
8979
|
* Compute the file scope owned by a meta node.
|
|
8753
8980
|
*
|
|
@@ -8824,11 +9051,6 @@ function getDeltaFiles(generatedAt, scopeFiles) {
|
|
|
8824
9051
|
return filterModifiedAfter(scopeFiles, new Date(generatedAt).getTime());
|
|
8825
9052
|
}
|
|
8826
9053
|
|
|
8827
|
-
/** Sleep for a given number of milliseconds. */
|
|
8828
|
-
function sleep(ms) {
|
|
8829
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8830
|
-
}
|
|
8831
|
-
|
|
8832
9054
|
/**
|
|
8833
9055
|
* Error thrown when a spawned subprocess is aborted via AbortController.
|
|
8834
9056
|
*
|
|
@@ -8983,7 +9205,7 @@ class GatewayExecutor {
|
|
|
8983
9205
|
JSON.stringify(spawnResult));
|
|
8984
9206
|
}
|
|
8985
9207
|
// Step 2: Poll for completion via sessions_history
|
|
8986
|
-
await
|
|
9208
|
+
await sleepAsync(3000);
|
|
8987
9209
|
while (Date.now() < deadline) {
|
|
8988
9210
|
// Check for abort before each poll iteration
|
|
8989
9211
|
if (this.controller.signal.aborted) {
|
|
@@ -9047,7 +9269,7 @@ class GatewayExecutor {
|
|
|
9047
9269
|
catch {
|
|
9048
9270
|
// Transient poll failure — keep trying
|
|
9049
9271
|
}
|
|
9050
|
-
await
|
|
9272
|
+
await sleepAsync(this.pollIntervalMs);
|
|
9051
9273
|
}
|
|
9052
9274
|
throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
|
|
9053
9275
|
}
|
|
@@ -9720,101 +9942,6 @@ function discoverStalestPath(candidates, depthWeight) {
|
|
|
9720
9942
|
return winner?.node.metaPath ?? null;
|
|
9721
9943
|
}
|
|
9722
9944
|
|
|
9723
|
-
/**
|
|
9724
|
-
* Staleness detection via watcher walk.
|
|
9725
|
-
*
|
|
9726
|
-
* A meta is stale when any watched file in its scope was modified after
|
|
9727
|
-
* `_generatedAt`.
|
|
9728
|
-
*
|
|
9729
|
-
* @module scheduling/staleness
|
|
9730
|
-
*/
|
|
9731
|
-
/**
|
|
9732
|
-
* Check if a meta is stale.
|
|
9733
|
-
*
|
|
9734
|
-
* Uses watcher `/walk` to enumerate watched files under the scope prefix,
|
|
9735
|
-
* then applies a local mtime check (fast) to detect any modifications since
|
|
9736
|
-
* `_generatedAt`. Short-circuits on first match.
|
|
9737
|
-
*
|
|
9738
|
-
* @param scopePrefix - Path prefix for this meta's scope.
|
|
9739
|
-
* @param meta - Current meta.json content.
|
|
9740
|
-
* @param watcher - WatcherClient instance.
|
|
9741
|
-
* @returns True if any file in scope was modified after `_generatedAt`.
|
|
9742
|
-
*/
|
|
9743
|
-
async function isStale(scopePrefix, meta, watcher) {
|
|
9744
|
-
if (!meta._generatedAt)
|
|
9745
|
-
return true; // Never synthesized = stale
|
|
9746
|
-
const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
|
|
9747
|
-
// Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
|
|
9748
|
-
// Handle both forward and back slashes for cross-platform compatibility.
|
|
9749
|
-
const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
|
|
9750
|
-
const filtered = files.filter((f) => !metaSep.test(f));
|
|
9751
|
-
return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
|
|
9752
|
-
}
|
|
9753
|
-
/** Maximum staleness for never-synthesized metas (1 year in seconds). */
|
|
9754
|
-
const MAX_STALENESS_SECONDS = 365 * 86_400;
|
|
9755
|
-
/**
|
|
9756
|
-
* Compute actual staleness in seconds (now minus _generatedAt).
|
|
9757
|
-
*
|
|
9758
|
-
* Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
|
|
9759
|
-
* (1 year) so that depth weighting can differentiate them. Without
|
|
9760
|
-
* bounding, `Infinity * depthFactor` = `Infinity` for all depths.
|
|
9761
|
-
*
|
|
9762
|
-
* @param meta - Current meta.json content.
|
|
9763
|
-
* @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
|
|
9764
|
-
*/
|
|
9765
|
-
function actualStaleness(meta) {
|
|
9766
|
-
if (!meta._generatedAt)
|
|
9767
|
-
return MAX_STALENESS_SECONDS;
|
|
9768
|
-
const generatedMs = new Date(meta._generatedAt).getTime();
|
|
9769
|
-
return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
|
|
9770
|
-
}
|
|
9771
|
-
/**
|
|
9772
|
-
* Check whether the architect step should be triggered.
|
|
9773
|
-
*
|
|
9774
|
-
* @param meta - Current meta.json.
|
|
9775
|
-
* @param structureChanged - Whether the structure hash changed.
|
|
9776
|
-
* @param steerChanged - Whether the steer directive changed.
|
|
9777
|
-
* @param architectEvery - Config: run architect every N cycles.
|
|
9778
|
-
* @returns True if the architect step should run.
|
|
9779
|
-
*/
|
|
9780
|
-
function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
|
|
9781
|
-
return (!meta._builder ||
|
|
9782
|
-
structureChanged ||
|
|
9783
|
-
steerChanged ||
|
|
9784
|
-
(meta._synthesisCount ?? 0) >= architectEvery);
|
|
9785
|
-
}
|
|
9786
|
-
/**
|
|
9787
|
-
* Detect whether the steer directive changed since the last archive.
|
|
9788
|
-
*
|
|
9789
|
-
* @param currentSteer - Current _steer value (or undefined).
|
|
9790
|
-
* @param archiveSteer - Archive _steer value (or undefined).
|
|
9791
|
-
* @param hasArchive - Whether an archive snapshot exists.
|
|
9792
|
-
* @returns True if steer changed.
|
|
9793
|
-
*/
|
|
9794
|
-
function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
|
|
9795
|
-
if (!hasArchive)
|
|
9796
|
-
return Boolean(currentSteer);
|
|
9797
|
-
return currentSteer !== archiveSteer;
|
|
9798
|
-
}
|
|
9799
|
-
/**
|
|
9800
|
-
* Compute a normalized staleness score (0–1) for display purposes.
|
|
9801
|
-
*
|
|
9802
|
-
* Uses the same depth/emphasis weighting as candidate selection,
|
|
9803
|
-
* normalized to a 30-day window.
|
|
9804
|
-
*
|
|
9805
|
-
* @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
|
|
9806
|
-
* @param depth - Meta tree depth.
|
|
9807
|
-
* @param emphasis - Scheduling emphasis multiplier.
|
|
9808
|
-
* @param depthWeight - Depth weighting exponent from config.
|
|
9809
|
-
* @returns Normalized score between 0 and 1.
|
|
9810
|
-
*/
|
|
9811
|
-
function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
|
|
9812
|
-
if (stalenessSeconds === null)
|
|
9813
|
-
return 1;
|
|
9814
|
-
const depthFactor = Math.pow(1 + depthWeight, depth);
|
|
9815
|
-
return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
|
|
9816
|
-
}
|
|
9817
|
-
|
|
9818
9945
|
/**
|
|
9819
9946
|
* Shared error utilities.
|
|
9820
9947
|
*
|
|
@@ -10875,7 +11002,10 @@ function buildMetaRules(config) {
|
|
|
10875
11002
|
properties: {
|
|
10876
11003
|
file: {
|
|
10877
11004
|
properties: {
|
|
10878
|
-
path: {
|
|
11005
|
+
path: {
|
|
11006
|
+
type: 'string',
|
|
11007
|
+
glob: '**/jeeves-meta{.config.json,/config.json}',
|
|
11008
|
+
},
|
|
10879
11009
|
},
|
|
10880
11010
|
},
|
|
10881
11011
|
},
|
|
@@ -12008,7 +12138,7 @@ class HttpWatcherClient {
|
|
|
12008
12138
|
}
|
|
12009
12139
|
// Exponential backoff
|
|
12010
12140
|
const delayMs = this.backoffBaseMs * Math.pow(this.backoffFactor, attempt);
|
|
12011
|
-
await
|
|
12141
|
+
await sleepAsync(delayMs);
|
|
12012
12142
|
}
|
|
12013
12143
|
// Unreachable, but TypeScript needs it
|
|
12014
12144
|
throw new Error('Retry exhausted');
|
|
@@ -12264,4 +12394,4 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
12264
12394
|
customCliCommands: registerCustomCliCommands,
|
|
12265
12395
|
});
|
|
12266
12396
|
|
|
12267
|
-
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleep, startService, toMetaError, verifyRuleApplication };
|
|
12397
|
+
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleepAsync as sleep, startService, toMetaError, verifyRuleApplication };
|