Merge pull request 'fix: wake dedupe + inline slot context + complete contracts.tools' (#6) from fix/wake-dedupe-and-contracts into main

This commit is contained in:
h z
2026-05-20 14:48:06 +00:00
4 changed files with 89 additions and 17 deletions

View File

@@ -197,7 +197,18 @@ function register(api: PluginAPI): void {
* Wake agent via dispatchInboundMessage — same mechanism used by Discord plugin. * Wake agent via dispatchInboundMessage — same mechanism used by Discord plugin.
* Direct in-process call, no WebSocket or CLI needed. * Direct in-process call, no WebSocket or CLI needed.
*/ */
async function wakeAgent(agentId: string): Promise<boolean> { async function wakeAgent(
agentId: string,
dueSlots?: Array<{
id?: number | null;
virtual_id?: string | null;
event_data?: any;
scheduled_at?: string;
priority?: number;
slot_type?: string;
[k: string]: unknown;
}>
): Promise<boolean> {
logger.info(`Waking agent ${agentId}: has due slots`); logger.info(`Waking agent ${agentId}: has due slots`);
const sessionKey = `agent:${agentId}:hf-wakeup`; const sessionKey = `agent:${agentId}:hf-wakeup`;
@@ -208,13 +219,39 @@ function register(api: PluginAPI): void {
/* webpackIgnore: true */ sdkPath /* webpackIgnore: true */ sdkPath
); );
const cfg = api.runtime?.config?.loadConfig?.(); // api.config first (current public API). Fall back to deprecated
// runtime.config.loadConfig() for older host versions. Both should
// contain agents.list / channels for dispatch routing.
const cfg = (api as any).config ?? api.runtime?.config?.loadConfig?.();
if (!cfg) { if (!cfg) {
logger.error('Cannot load OpenClaw config for dispatch'); logger.error('Cannot load OpenClaw config for dispatch');
return false; return false;
} }
const wakeupMessage = `You have due slots. Follow the \`hf-wakeup\` workflow of skill \`hf-hangman-lab\` to proceed. Only reply \`WAKEUP_OK\` in this session.`; // Inline the highest-priority due slot's context so the agent does
// not need a second round-trip to harborforge_calendar_status. The
// agent can read event_data.task_code / task_title etc. directly.
let slotBlock = '';
const top = dueSlots && dueSlots.length ? dueSlots[0] : undefined;
if (top) {
slotBlock = `\n\nMatching slot:\n\`\`\`json\n${JSON.stringify(
{
slot_id: top.id ?? null,
virtual_id: top.virtual_id ?? null,
scheduled_at: top.scheduled_at ?? null,
priority: top.priority ?? null,
slot_type: top.slot_type ?? null,
event_data: top.event_data ?? null,
},
null,
2
)}\n\`\`\``;
}
const wakeupMessage =
`You have due slots. Follow the \`hf-wakeup\` workflow of skill ` +
`\`hf-hangman-lab\` to proceed. Only reply \`WAKEUP_OK\` in this ` +
`session.${slotBlock}`;
const result = await dispatchInboundMessageWithDispatcher({ const result = await dispatchInboundMessageWithDispatcher({
ctx: { ctx: {
@@ -308,28 +345,59 @@ function register(api: PluginAPI): void {
} }
} }
// Track wakes already dispatched for a slot in the current sync
// window — the simplified inline scheduler does not PATCH slot
// status server-side, so without dedupe the check loop re-wakes
// the same slot every 30s. Set is cleared by runSync (fresh wake
// budget per sync).
const wakedSlotKeys = new Set<string>();
// Check: find agents with due slots and wake them // Check: find agents with due slots and wake them
async function runCheck() { async function runCheck() {
const now = new Date(); const now = new Date();
const agentsWithDue = scheduleCache.getAgentsWithDueSlots(now); const agentsWithDue = scheduleCache.getAgentsWithDueSlots(now);
for (const { agentId } of agentsWithDue) { for (const { agentId, slots } of agentsWithDue) {
// Check if agent is busy // Filter out slots we've already woken this sync window
const status = await calendarBridge.getAgentStatus(agentId); const fresh = slots.filter((s) => {
const key = `${agentId}::${s.id ?? s.virtual_id ?? s.scheduled_at}`;
if (wakedSlotKeys.has(key)) return false;
return true;
});
if (fresh.length === 0) continue;
// Check if agent is busy (best effort; backend may 405 the GET
// — treat unknown as not-busy so wakeup still fires)
let status: string | null = null;
try {
status = await calendarBridge.getAgentStatus(agentId);
} catch {
status = null;
}
if (status === 'busy' || status === 'offline' || status === 'exhausted') { if (status === 'busy' || status === 'offline' || status === 'exhausted') {
continue; continue;
} }
// Wake the agent // Wake the agent with the slot context inlined
await wakeAgent(agentId); const ok = await wakeAgent(agentId, fresh);
if (ok) {
for (const s of fresh) {
const key = `${agentId}::${s.id ?? s.virtual_id ?? s.scheduled_at}`;
wakedSlotKeys.add(key);
}
}
} }
} }
// Initial sync // Initial sync (also resets the wake-dedupe window)
runSync(); const runSyncReset = async () => {
wakedSlotKeys.clear();
await runSync();
};
runSyncReset();
// Start intervals // Start intervals
const syncHandle = setInterval(runSync, SYNC_INTERVAL_MS); const syncHandle = setInterval(runSyncReset, SYNC_INTERVAL_MS);
const checkHandle = setInterval(runCheck, CHECK_INTERVAL_MS); const checkHandle = setInterval(runCheck, CHECK_INTERVAL_MS);
// Store handles for cleanup (reuse calendarScheduler variable) // Store handles for cleanup (reuse calendarScheduler variable)

View File

@@ -11,7 +11,11 @@
"harborforge_telemetry", "harborforge_telemetry",
"harborforge_monitor_telemetry", "harborforge_monitor_telemetry",
"harborforge_calendar_status", "harborforge_calendar_status",
"harborforge_calendar_complete" "harborforge_calendar_complete",
"harborforge_calendar_abort",
"harborforge_calendar_pause",
"harborforge_calendar_resume",
"harborforge_restart_status"
] ]
}, },
"configSchema": { "configSchema": {

View File

@@ -9,14 +9,14 @@
"version": "0.2.0", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.19.41",
"typescript": "^5.0.0" "typescript": "^5.0.0"
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.37", "version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -10,7 +10,7 @@
"watch": "tsc --watch" "watch": "tsc --watch"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.19.41",
"typescript": "^5.0.0" "typescript": "^5.0.0"
}, },
"license": "MIT" "license": "MIT"