feat(runtime): SIGHUP hot-reload of room bindings (no full restart)
Registering/deregistering a channel via a DB write from a non-main room did not take effect until a full restart, and restarting from inside an agent turn is unreliable (the agent is a child of ejclaw.service, so `systemctl restart` kills it mid-command and it can't be confirmed) — the source of repeated "auto-restart missing / claimed restart but it didn't happen" failures. Add a SIGHUP handler that calls runtimeState.reloadRoomBindings(), which re-reads ONLY room bindings from the DB (cursors/sessions untouched, no re-processing). `kill -HUP <MainPID>` then makes a registration live instantly with no restart and no agent death, so it is verifiable in the same turn. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
11
src/index.ts
11
src/index.ts
@@ -376,6 +376,17 @@ async function main(): Promise<void> {
|
||||
};
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
// SIGHUP hot-reloads room bindings so a channel registered/deregistered via a
|
||||
// DB write takes effect immediately — no full restart needed. Send it only to
|
||||
// the main process, e.g. `kill -HUP <MainPID>`, so agent subprocesses are not
|
||||
// signalled.
|
||||
process.on('SIGHUP', () => {
|
||||
try {
|
||||
runtimeState.reloadRoomBindings();
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Failed to reload room bindings on SIGHUP');
|
||||
}
|
||||
});
|
||||
|
||||
// Channel callbacks (shared by all channels)
|
||||
const channelOpts = {
|
||||
|
||||
@@ -34,4 +34,32 @@ describe('createRuntimeState', () => {
|
||||
expect(db.deleteAllSessionsForGroup).toHaveBeenCalledWith('group-a');
|
||||
expect(db.deleteSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reloadRoomBindings re-reads bindings live without regressing cursors', () => {
|
||||
db.getAllRoomBindings.mockReturnValue({
|
||||
'dc:a': { name: 'A', folder: 'a' },
|
||||
});
|
||||
const state = createRuntimeState();
|
||||
state.loadState();
|
||||
expect(Object.keys(state.getRoomBindings())).toEqual(['dc:a']);
|
||||
|
||||
// Cursor advances in memory after load.
|
||||
state.setLastTimestamp('cursor-2');
|
||||
|
||||
// A new channel is registered in the DB (DB write, not live IPC).
|
||||
db.getAllRoomBindings.mockReturnValue({
|
||||
'dc:a': { name: 'A', folder: 'a' },
|
||||
'dc:b': { name: 'B', folder: 'b' },
|
||||
});
|
||||
|
||||
state.reloadRoomBindings();
|
||||
|
||||
// Binding picked up live...
|
||||
expect(Object.keys(state.getRoomBindings()).sort()).toEqual([
|
||||
'dc:a',
|
||||
'dc:b',
|
||||
]);
|
||||
// ...without resetting the message cursor (no re-processing).
|
||||
expect(state.getLastTimestamp()).toBe('cursor-2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,13 @@ import type { AgentType, RegisteredGroup } from './types.js';
|
||||
|
||||
export interface RuntimeState {
|
||||
loadState: () => void;
|
||||
/**
|
||||
* Re-read ONLY the room bindings from the DB (not cursors/sessions), so a
|
||||
* channel registered/deregistered via a DB write takes effect live without a
|
||||
* full process restart. Safe to call at runtime — it does not touch the
|
||||
* message cursors, so no re-processing risk.
|
||||
*/
|
||||
reloadRoomBindings: () => void;
|
||||
saveState: () => void;
|
||||
clearSession: (groupFolder: string, opts?: { allRoles?: boolean }) => void;
|
||||
assignRoomForIpc: (jid: string, input: AssignRoomInput) => void;
|
||||
@@ -96,6 +103,14 @@ export function createRuntimeState(): RuntimeState {
|
||||
);
|
||||
};
|
||||
|
||||
const reloadRoomBindings = (): void => {
|
||||
roomBindings = getAllRoomBindings();
|
||||
logger.info(
|
||||
{ groupCount: Object.keys(roomBindings).length },
|
||||
'Room bindings reloaded',
|
||||
);
|
||||
};
|
||||
|
||||
const assignRoomForIpc = (jid: string, input: AssignRoomInput): void => {
|
||||
const assignedGroup = assignRoom(jid, input);
|
||||
if (!assignedGroup) {
|
||||
@@ -121,6 +136,7 @@ export function createRuntimeState(): RuntimeState {
|
||||
|
||||
return {
|
||||
loadState,
|
||||
reloadRoomBindings,
|
||||
saveState,
|
||||
clearSession,
|
||||
assignRoomForIpc,
|
||||
|
||||
Reference in New Issue
Block a user