Fix cron announce fallback to use origin session context

This commit is contained in:
Tyler Yust
2026-02-18 16:10:49 -08:00
parent da440003e5
commit 5f5a1569f5
4 changed files with 74 additions and 15 deletions

View File

@@ -1,9 +1,9 @@
import "./isolated-agent.mocks.js";
import fs from "node:fs/promises";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CliDeps } from "../cli/deps.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
import type { CliDeps } from "../cli/deps.js";
import { runCronIsolatedAgentTurn } from "./isolated-agent.js";
import {
makeCfg,
@@ -184,9 +184,10 @@ describe("runCronIsolatedAgentTurn", () => {
});
});
it("uses the agent main session key when announce delivery is not pinned", async () => {
it("uses cron origin session for unpinned announce fallback and implicit delivery resolution", async () => {
await withTempCronHome(async (home) => {
const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" });
const originSessionKey = "agent:main:bluebubbles:direct:+19257864429";
await fs.writeFile(
storePath,
JSON.stringify(
@@ -195,6 +196,12 @@ describe("runCronIsolatedAgentTurn", () => {
sessionId: "main-session",
updatedAt: Date.now(),
lastChannel: "telegram",
lastTo: "999",
},
[originSessionKey]: {
sessionId: "origin-session",
updatedAt: Date.now(),
lastChannel: "telegram",
lastTo: "123",
},
},
@@ -220,6 +227,7 @@ describe("runCronIsolatedAgentTurn", () => {
deps,
job: {
...makeJob({ kind: "agentTurn", message: "do it" }),
sessionKey: originSessionKey,
delivery: { mode: "announce" },
},
message: "do it",
@@ -235,7 +243,7 @@ describe("runCronIsolatedAgentTurn", () => {
requesterOrigin?: { channel?: string; to?: string };
}
| undefined;
expect(announceArgs?.requesterSessionKey).toBe("agent:main:main");
expect(announceArgs?.requesterSessionKey).toBe(originSessionKey);
expect(announceArgs?.requesterOrigin?.channel).toBe("telegram");
expect(announceArgs?.requesterOrigin?.to).toBe("123");
});

View File

@@ -128,4 +128,31 @@ describe("resolveDeliveryTarget", () => {
expect(result.accountId).toBeUndefined();
});
it("prefers origin session context for implicit last-target resolution", async () => {
vi.mocked(loadSessionStore).mockReturnValue({
"agent:test:main": {
sessionId: "main-session",
updatedAt: 1000,
lastChannel: "telegram",
lastTo: "main-target",
lastAccountId: "main-account",
},
"agent:test:bluebubbles:direct:+19257864429": {
sessionId: "origin-session",
updatedAt: 1001,
lastChannel: "telegram",
lastTo: "origin-target",
lastAccountId: "origin-account",
},
});
const result = await resolveDeliveryTarget(makeCfg(), "agent-b", {
channel: "last",
sessionKey: "agent:test:bluebubbles:direct:+19257864429",
});
expect(result.channel).toBe("telegram");
expect(result.accountId).toBe("origin-account");
});
});

View File

@@ -21,6 +21,7 @@ export async function resolveDeliveryTarget(
jobPayload: {
channel?: "last" | ChannelId;
to?: string;
sessionKey?: string;
},
): Promise<{
channel: Exclude<OutboundChannel, "none">;
@@ -32,6 +33,8 @@ export async function resolveDeliveryTarget(
}> {
const requestedChannel = typeof jobPayload.channel === "string" ? jobPayload.channel : "last";
const explicitTo = typeof jobPayload.to === "string" ? jobPayload.to : undefined;
const originSessionKey =
typeof jobPayload.sessionKey === "string" ? jobPayload.sessionKey.trim() : "";
const allowMismatchedLastTo = requestedChannel === "last";
const sessionCfg = cfg.session;
@@ -39,14 +42,32 @@ export async function resolveDeliveryTarget(
const storePath = resolveStorePath(sessionCfg?.store, { agentId });
const store = loadSessionStore(storePath);
const main = store[mainSessionKey];
const origin = originSessionKey ? store[originSessionKey] : undefined;
const preliminary = resolveSessionDeliveryTarget({
const preliminaryFromOrigin = origin
? resolveSessionDeliveryTarget({
entry: origin,
requestedChannel,
explicitTo,
allowMismatchedLastTo,
})
: undefined;
const preliminaryFromMain = resolveSessionDeliveryTarget({
entry: main,
requestedChannel,
explicitTo,
allowMismatchedLastTo,
});
const hasResolvedTarget = (value?: { channel?: string; to?: string }) =>
Boolean(value?.channel && value?.to);
const useMainContext =
hasResolvedTarget(preliminaryFromMain) && !hasResolvedTarget(preliminaryFromOrigin);
const contextEntry = useMainContext ? main : (origin ?? main);
const preliminary = useMainContext
? preliminaryFromMain
: (preliminaryFromOrigin ?? preliminaryFromMain);
let fallbackChannel: Exclude<OutboundChannel, "none"> | undefined;
if (!preliminary.channel) {
try {
@@ -59,7 +80,7 @@ export async function resolveDeliveryTarget(
const resolved = fallbackChannel
? resolveSessionDeliveryTarget({
entry: main,
entry: contextEntry,
requestedChannel,
explicitTo,
fallbackChannel,

View File

@@ -1,7 +1,3 @@
import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { AgentDefaultsConfig } from "../../config/types.js";
import type { CronJob, CronRunOutcome, CronRunTelemetry } from "../types.js";
import {
resolveAgentConfig,
resolveAgentDir,
@@ -24,6 +20,7 @@ import {
resolveHooksGmailModel,
resolveThinkingDefault,
} from "../../agents/model-selection.js";
import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.js";
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
import { runSubagentAnnounceFlow } from "../../agents/subagent-announce.js";
import { countActiveDescendantRuns } from "../../agents/subagent-registry.js";
@@ -37,11 +34,13 @@ import {
} from "../../auto-reply/thinking.js";
import { SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
import { createOutboundSendDeps, type CliDeps } from "../../cli/outbound-send-deps.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveAgentMainSessionKey,
resolveSessionTranscriptPath,
updateSessionStore,
} from "../../config/sessions.js";
import type { AgentDefaultsConfig } from "../../config/types.js";
import { registerAgentRunContext } from "../../infra/agent-events.js";
import { deliverOutboundPayloads } from "../../infra/outbound/deliver.js";
import { resolveAgentOutboundIdentity } from "../../infra/outbound/identity.js";
@@ -55,6 +54,7 @@ import {
isExternalHookSession,
} from "../../security/external-content.js";
import { resolveCronDeliveryPlan } from "../delivery.js";
import type { CronJob, CronRunOutcome, CronRunTelemetry } from "../types.js";
import { resolveDeliveryTarget } from "./delivery-target.js";
import {
isHeartbeatOnlyResponse,
@@ -368,10 +368,17 @@ export async function runCronIsolatedAgentTurn(params: {
const agentPayload = params.job.payload.kind === "agentTurn" ? params.job.payload : null;
const deliveryPlan = resolveCronDeliveryPlan(params.job);
const deliveryRequested = deliveryPlan.requested;
const cronOriginSessionKey =
params.job.sessionKey?.trim() ||
resolveAgentMainSessionKey({
cfg: params.cfg,
agentId,
});
const resolvedDelivery = await resolveDeliveryTarget(cfgWithAgentDefaults, agentId, {
channel: deliveryPlan.channel ?? "last",
to: deliveryPlan.to,
sessionKey: cronOriginSessionKey,
});
const { formattedTime, timeLine } = resolveCronStyleNow(params.cfg, now);
@@ -667,10 +674,6 @@ export async function runCronIsolatedAgentTurn(params: {
}
}
} else if (synthesizedText) {
const announceMainSessionKey = resolveAgentMainSessionKey({
cfg: params.cfg,
agentId,
});
const announceUsesPinnedTarget = hasPinnedCronAnnounceTarget({
channel: deliveryPlan.channel,
to: deliveryPlan.to,
@@ -679,7 +682,7 @@ export async function runCronIsolatedAgentTurn(params: {
? await resolveCronAnnounceSessionKey({
cfg: cfgWithAgentDefaults,
agentId,
fallbackSessionKey: announceMainSessionKey,
fallbackSessionKey: cronOriginSessionKey,
delivery: {
channel: resolvedDelivery.channel,
to: resolvedDelivery.to,
@@ -687,7 +690,7 @@ export async function runCronIsolatedAgentTurn(params: {
threadId: resolvedDelivery.threadId,
},
})
: announceMainSessionKey;
: cronOriginSessionKey;
const taskLabel =
typeof params.job.name === "string" && params.job.name.trim()
? params.job.name.trim()