Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

174 lines
5.8 KiB
JavaScript

/**
* Agent source hot-reload watcher.
*
* Watches the backend `<pkg>/src` dirs (the TypeScript the dev API child loads via
* the `eliza-source` condition) and fires a debounced callback when real code
* changes. Watching each `<pkg>/src` dir directly — never `dist/`, `node_modules/`,
* or build output — means concurrent package builds rewriting `dist/` generate
* no events here. That decoupling is the whole point: the old `node --watch`
* followed imports into `dist/` and reloaded mid-build, so it had to be
* disabled; this never does.
*
* dev-ui.mjs wires `onChange` to the API supervisor's `restart()`.
*/
import { existsSync, readdirSync, watch } from "node:fs";
import path from "node:path";
// Pure-frontend packages are served + HMR'd by Vite and are NOT loaded by the
// API child, so editing them must not bounce the agent.
export const HOT_RELOAD_FRONTEND_PACKAGES = new Set([
"ui",
"app",
"os-homepage",
"homepage",
"docs",
"docs-elizacloud-redirect",
"tui",
"robot",
"os",
]);
// Only hand-written agent source: .ts/.tsx/.mts/.cts + .json. Compiled `.js`
// is deliberately NOT matched — this monorepo emits compiled `.js`/`.d.ts`
// shadows next to `.ts` source, and reacting to those would bounce the agent on
// every build. `.d.ts` (declaration emit) is excluded explicitly since it ends
// in `.ts`.
export const HOT_RELOAD_CODE_FILE = /\.(?:tsx?|mts|cts|json)$/;
export const HOT_RELOAD_DECLARATION = /\.d\.[cm]?ts$/;
export const HOT_RELOAD_TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
export const HOT_RELOAD_IGNORED_SEGMENT =
/(?:^|[/\\])(?:dist|node_modules|\.turbo|\.git|coverage|__tests__|\.vite|build|generated)(?:[/\\]|$)/;
export const HOT_RELOAD_DEBOUNCE_MS = 350;
/**
* Collect the `<group>/<pkg>/src` dirs whose changes should reload the agent.
* Skips pure-frontend packages (Vite owns those) and any package without a
* `src/` dir.
*
* @param {string} root Repo root that holds `packages/` and `plugins/`.
* @returns {string[]} Absolute `<pkg>/src` dirs to watch.
*/
export function collectAgentSourceDirs(root) {
const dirs = [];
for (const group of ["packages", "plugins"]) {
const groupDir = path.join(root, group);
let entries;
try {
entries = readdirSync(groupDir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (
group === "packages" &&
HOT_RELOAD_FRONTEND_PACKAGES.has(entry.name)
) {
continue;
}
const srcDir = path.join(groupDir, entry.name, "src");
if (existsSync(srcDir)) dirs.push(srcDir);
}
}
return dirs;
}
/**
* Whether a watch event for `absPath` should trigger a reload. A null/empty
* path (some platforms omit the filename) is treated as reloadable so we never
* miss a real change. Build output, deps, generated, and test/coverage dirs are
* ignored, as are declaration (`.d.ts`) and test/spec files; only hand-written
* source (ts/tsx/mts/cts/json) qualifies.
*
* @param {string | null | undefined} absPath
* @returns {boolean}
*/
export function isReloadableChangePath(absPath) {
if (!absPath) return true;
if (HOT_RELOAD_IGNORED_SEGMENT.test(absPath)) return false;
if (HOT_RELOAD_DECLARATION.test(absPath)) return false;
if (HOT_RELOAD_TEST_FILE.test(absPath)) return false;
return HOT_RELOAD_CODE_FILE.test(absPath);
}
/**
* Start watching the agent source dirs. Returns a handle with the number of
* dirs watched and a `close()`.
*
* @param {Object} params
* @param {string} params.root Repo root.
* @param {(relPath: string, changedCount: number) => void} params.onChange
* Debounced; receives one sample path (relative to `root`, or "source" when
* the filename is unknown) and the number of DISTINCT files that changed in
* the window — so the caller can ignore bulk rewrites (a git reset / checkout
* / build touches many files at once; a human edit touches one or a few).
* @param {(dir: string, err: Error) => void} [params.onError] Per-dir watch
* setup failure (e.g. a platform without recursive watch).
* @param {number} [params.debounceMs]
* @returns {{ count: number, close: () => void }}
*/
export function startAgentSourceWatcher({
root,
onChange,
onError,
debounceMs = HOT_RELOAD_DEBOUNCE_MS,
}) {
const dirs = collectAgentSourceDirs(root);
/** @type {import("node:fs").FSWatcher[]} */
const watchers = [];
/** @type {ReturnType<typeof setTimeout> | null} */
let debounce = null;
/** Distinct changed paths accumulated in the current debounce window. */
const pendingFiles = new Set();
let pendingSample = null;
const fire = (absPath) => {
if (absPath && !isReloadableChangePath(absPath)) return;
if (absPath) {
pendingFiles.add(absPath);
pendingSample = absPath;
}
if (debounce) clearTimeout(debounce);
debounce = setTimeout(() => {
const count = pendingFiles.size;
const sample = pendingSample;
pendingFiles.clear();
pendingSample = null;
debounce = null;
onChange(sample ? path.relative(root, sample) : "source", count);
}, debounceMs);
debounce.unref?.();
};
for (const dir of dirs) {
try {
const fsWatcher = watch(dir, { recursive: true }, (_event, filename) => {
fire(filename ? path.join(dir, filename.toString()) : null);
});
// A dir vanishing mid-build (clean step) must not crash the dev process.
fsWatcher.on("error", () => {});
watchers.push(fsWatcher);
} catch (err) {
onError?.(dir, err);
}
}
return {
count: dirs.length,
close() {
if (debounce) {
clearTimeout(debounce);
debounce = null;
}
for (const fsWatcher of watchers) {
try {
fsWatcher.close();
} catch {
// already closed
}
}
},
};
}