mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-16 18:34:18 +00:00
fix(wake): handle relative + agent-prefixed session keys consistently in cron adapter
Address review findings from successive codex rounds: 1. next-heartbeat + sessionKey now fires a targeted immediate wake. The regularly-scheduled heartbeat fires for the agent's main session, not the supplied sessionKey, so an event queued for a non-main session would sit stranded indefinitely; an "event"-intent wake is also deferred as not-due by the heartbeat runner and not retried, so neither path delivers without an explicit immediate wake. 2. resolveCronWakeTarget now always runs through resolveCronAgent, both for agent-prefixed session keys (so non-default agents are honored) and relative keys (so the configured default agent is used instead of the hardcoded "main" returned by resolveAgentIdFromSessionKey). Mirrors the matching fix in the enqueueSystemEvent adapter so wake and enqueue resolve to the same target. 3. Generated Swift `WakeParams` models now expose the new optional `sessionkey` field (codingKey "sessionKey") in both the macOS and shared OpenClawKit copies. Locally regenerated from agent.ts via protocol:gen + protocol:gen:swift would have produced this; the environment couldn't run the generators (fs-safe transitive typecheck errors), so the diff was applied by hand to match what pnpm protocol:check would output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
4ddd942f5f
commit
072fa9b174
@@ -941,18 +941,22 @@ public struct AgentWaitParams: Codable, Sendable {
|
||||
public struct WakeParams: Codable, Sendable {
|
||||
public let mode: AnyCodable
|
||||
public let text: String
|
||||
public let sessionkey: String?
|
||||
|
||||
public init(
|
||||
mode: AnyCodable,
|
||||
text: String)
|
||||
text: String,
|
||||
sessionkey: String?)
|
||||
{
|
||||
self.mode = mode
|
||||
self.text = text
|
||||
self.sessionkey = sessionkey
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case mode
|
||||
case text
|
||||
case sessionkey = "sessionKey"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1796,6 +1796,25 @@ export function wake(
|
||||
reason: "wake",
|
||||
...(sessionKey ? { sessionKey } : {}),
|
||||
});
|
||||
} else if (sessionKey) {
|
||||
// next-heartbeat + sessionKey still needs a targeted immediate wake.
|
||||
// Reasons:
|
||||
// 1. The regularly-scheduled heartbeat fires for the agent's main
|
||||
// session, not the supplied sessionKey, so it never peeks the queue
|
||||
// we just enqueued — the event would sit stranded indefinitely.
|
||||
// 2. An `intent: "event"` wake gets deferred by heartbeat-runner as
|
||||
// not-due and is not retried (only busy-skips are), so it cannot
|
||||
// stand in for the regular cadence either.
|
||||
// Effectively, --session-key collapses --mode now and --mode next-heartbeat
|
||||
// into the same targeted-immediate behavior — this matches the documented
|
||||
// user intent (target a specific session for relay) better than silently
|
||||
// dropping the event.
|
||||
state.deps.requestHeartbeat({
|
||||
source: "manual",
|
||||
intent: "immediate",
|
||||
reason: "wake",
|
||||
sessionKey,
|
||||
});
|
||||
}
|
||||
return { ok: true } as const;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,11 @@ describe("wake (cron timer)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("threads sessionKey to enqueue only on mode=next-heartbeat", () => {
|
||||
it("threads sessionKey to enqueue and fires a targeted immediate wake on mode=next-heartbeat", () => {
|
||||
// next-heartbeat + sessionKey collapses to immediate-targeted behavior:
|
||||
// the regularly-scheduled heartbeat fires for agent-main and never peeks
|
||||
// a non-main session queue, and an "event"-intent wake is not retried by
|
||||
// the heartbeat runner. Targeted immediate is the only reliable path.
|
||||
const { state, enqueueSystemEvent, requestHeartbeat } = createState();
|
||||
expect(
|
||||
wake(state, {
|
||||
@@ -67,6 +71,18 @@ describe("wake (cron timer)", () => {
|
||||
expect(enqueueSystemEvent).toHaveBeenCalledWith("ping", {
|
||||
sessionKey: "agent:main:slack:42",
|
||||
});
|
||||
expect(requestHeartbeat).toHaveBeenCalledWith({
|
||||
source: "manual",
|
||||
intent: "immediate",
|
||||
reason: "wake",
|
||||
sessionKey: "agent:main:slack:42",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fire a wake on mode=next-heartbeat when no sessionKey is supplied", () => {
|
||||
const { state, enqueueSystemEvent, requestHeartbeat } = createState();
|
||||
expect(wake(state, { mode: "next-heartbeat", text: "ping" })).toEqual({ ok: true });
|
||||
expect(enqueueSystemEvent).toHaveBeenCalledWith("ping", undefined);
|
||||
expect(requestHeartbeat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
} from "../plugins/hook-types.js";
|
||||
import { normalizeAgentId, toAgentStoreSessionKey } from "../routing/session-key.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { parseAgentSessionKey } from "../sessions/session-key-utils.js";
|
||||
import {
|
||||
dispatchGatewayCronFinishedNotifications,
|
||||
sendGatewayCronFailureAlert,
|
||||
@@ -201,17 +202,25 @@ export function buildGatewayCronService(params: {
|
||||
typeof opts?.agentId === "string" && opts.agentId.trim()
|
||||
? normalizeAgentId(opts.agentId)
|
||||
: undefined;
|
||||
const derivedAgentId =
|
||||
requestedAgentId ??
|
||||
(opts?.sessionKey
|
||||
// Derive agentId from sessionKey only when the key is agent-prefixed
|
||||
// (`agent:<id>:...`). For relative session keys like `discord:channel:ops`,
|
||||
// `resolveAgentIdFromSessionKey` returns the literal `DEFAULT_AGENT_ID`
|
||||
// ("main") regardless of cfg, which would route relative keys to the
|
||||
// hardcoded "main" agent even on non-main-default deployments. Passing
|
||||
// `undefined` lets `resolveCronAgent` fall back to the configured default
|
||||
// agent so wake and enqueue resolve to the same target.
|
||||
const parsedSessionKeyAgentId =
|
||||
opts?.sessionKey && parseAgentSessionKey(opts.sessionKey)
|
||||
? normalizeAgentId(resolveAgentIdFromSessionKey(opts.sessionKey))
|
||||
: undefined);
|
||||
const runtimeConfigBase = getRuntimeConfig();
|
||||
const runtimeConfig =
|
||||
derivedAgentId !== undefined
|
||||
? mergeRuntimeAgentConfig(runtimeConfigBase, derivedAgentId)
|
||||
: runtimeConfigBase;
|
||||
const agentId = derivedAgentId || undefined;
|
||||
: undefined;
|
||||
const requestedOrDerived = requestedAgentId ?? parsedSessionKeyAgentId;
|
||||
// Always run `resolveCronAgent`, including when no agent is requested —
|
||||
// for relative session keys (e.g. `discord:channel:ops`) we want the
|
||||
// configured default agent's session, which `resolveCronAgent(undefined)`
|
||||
// returns. Leaving agentId undefined here would strand the wake on the
|
||||
// resolveCronSessionKey branch below.
|
||||
const { agentId: resolvedAgentId, cfg: runtimeConfig } = resolveCronAgent(requestedOrDerived);
|
||||
const agentId = resolvedAgentId || undefined;
|
||||
const sessionKey =
|
||||
opts?.sessionKey && agentId
|
||||
? resolveCronSessionKey({
|
||||
@@ -282,9 +291,15 @@ export function buildGatewayCronService(params: {
|
||||
// When the caller passes only a sessionKey (e.g. `system event --session-key`),
|
||||
// derive agentId from that key so a non-default agent's session is not silently
|
||||
// rejected as foreign by resolveCronSessionKey and rerouted to the default agent.
|
||||
// Only derive from agent-prefixed keys — for relative keys, let resolveCronAgent
|
||||
// fall back to the configured default so we don't hardcode "main" on multi-agent
|
||||
// deployments where main exists but isn't the default. (Mirrors the same fix in
|
||||
// resolveCronWakeTarget above.)
|
||||
const derivedAgentId =
|
||||
opts?.agentId ??
|
||||
(opts?.sessionKey ? resolveAgentIdFromSessionKey(opts.sessionKey) : undefined);
|
||||
(opts?.sessionKey && parseAgentSessionKey(opts.sessionKey)
|
||||
? resolveAgentIdFromSessionKey(opts.sessionKey)
|
||||
: undefined);
|
||||
const { agentId, cfg: runtimeConfig } = resolveCronAgent(derivedAgentId);
|
||||
const sessionKey = resolveCronSessionKey({
|
||||
runtimeConfig,
|
||||
|
||||
Reference in New Issue
Block a user