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:
@@ -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)
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
8
plugin/package-lock.json
generated
8
plugin/package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user