feat(cron): introduce origin context and delivery mode for cron jobs (#6)
- Added `originContext` to `createCronTool` for routing replies back to the originating session/channel. - Updated `CronJob` and related types to include `origin` and `deliveryMode` properties. - Enhanced `resolveDeliveryTarget` to utilize origin context for determining delivery targets. - Modified various components to support the new origin context and delivery mode functionality, ensuring replies are routed correctly based on the job's origin.
This commit is contained in:
parent
d0408c3dbf
commit
174fa8c9cb
@ -88,6 +88,12 @@ export function createMoltbotTools(options?: {
|
|||||||
}),
|
}),
|
||||||
createCronTool({
|
createCronTool({
|
||||||
agentSessionKey: options?.agentSessionKey,
|
agentSessionKey: options?.agentSessionKey,
|
||||||
|
originContext: {
|
||||||
|
channel: options?.agentChannel,
|
||||||
|
to: options?.agentTo,
|
||||||
|
accountId: options?.agentAccountId,
|
||||||
|
threadId: options?.agentThreadId,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
createMessageTool({
|
createMessageTool({
|
||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
|
|||||||
@ -42,6 +42,13 @@ const CronToolSchema = Type.Object({
|
|||||||
|
|
||||||
type CronToolOptions = {
|
type CronToolOptions = {
|
||||||
agentSessionKey?: string;
|
agentSessionKey?: string;
|
||||||
|
/** Origin context for routing cron job replies back to where the job was created */
|
||||||
|
originContext?: {
|
||||||
|
channel?: string;
|
||||||
|
to?: string;
|
||||||
|
accountId?: string;
|
||||||
|
threadId?: string | number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChatMessage = {
|
type ChatMessage = {
|
||||||
@ -210,6 +217,18 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
|
|||||||
(job as { agentId?: string }).agentId = agentId;
|
(job as { agentId?: string }).agentId = agentId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Inject origin context for reply routing (unless already specified)
|
||||||
|
if (job && typeof job === "object" && !("origin" in job) && opts?.originContext) {
|
||||||
|
const origin: Record<string, unknown> = {};
|
||||||
|
if (opts.originContext.channel) origin.channel = opts.originContext.channel;
|
||||||
|
if (opts.originContext.to) origin.to = opts.originContext.to;
|
||||||
|
if (opts.originContext.accountId) origin.accountId = opts.originContext.accountId;
|
||||||
|
if (opts.originContext.threadId != null) origin.threadId = opts.originContext.threadId;
|
||||||
|
if (opts.agentSessionKey) origin.sessionKey = opts.agentSessionKey;
|
||||||
|
if (Object.keys(origin).length > 0) {
|
||||||
|
(job as { origin?: Record<string, unknown> }).origin = origin;
|
||||||
|
}
|
||||||
|
}
|
||||||
const contextMessages =
|
const contextMessages =
|
||||||
typeof params.contextMessages === "number" && Number.isFinite(params.contextMessages)
|
typeof params.contextMessages === "number" && Number.isFinite(params.contextMessages)
|
||||||
? params.contextMessages
|
? params.contextMessages
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
resolveOutboundTarget,
|
resolveOutboundTarget,
|
||||||
resolveSessionDeliveryTarget,
|
resolveSessionDeliveryTarget,
|
||||||
} from "../../infra/outbound/targets.js";
|
} from "../../infra/outbound/targets.js";
|
||||||
|
import type { CronDeliveryMode, CronOrigin } from "../types.js";
|
||||||
|
|
||||||
export async function resolveDeliveryTarget(
|
export async function resolveDeliveryTarget(
|
||||||
cfg: MoltbotConfig,
|
cfg: MoltbotConfig,
|
||||||
@ -20,15 +21,67 @@ export async function resolveDeliveryTarget(
|
|||||||
channel?: "last" | ChannelId;
|
channel?: "last" | ChannelId;
|
||||||
to?: string;
|
to?: string;
|
||||||
},
|
},
|
||||||
|
options?: {
|
||||||
|
/** Origin context from when the job was created */
|
||||||
|
origin?: CronOrigin;
|
||||||
|
/** Delivery mode: "origin" (default) or "current" */
|
||||||
|
deliveryMode?: CronDeliveryMode;
|
||||||
|
},
|
||||||
): Promise<{
|
): Promise<{
|
||||||
channel: Exclude<OutboundChannel, "none">;
|
channel: Exclude<OutboundChannel, "none">;
|
||||||
to?: string;
|
to?: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
|
threadId?: string | number;
|
||||||
mode: "explicit" | "implicit";
|
mode: "explicit" | "implicit";
|
||||||
error?: Error;
|
error?: Error;
|
||||||
}> {
|
}> {
|
||||||
const requestedChannel = typeof jobPayload.channel === "string" ? jobPayload.channel : "last";
|
const origin = options?.origin;
|
||||||
const explicitTo = typeof jobPayload.to === "string" ? jobPayload.to : undefined;
|
const deliveryMode = options?.deliveryMode ?? "origin";
|
||||||
|
|
||||||
|
// Explicit payload values always take precedence
|
||||||
|
const hasExplicitChannel =
|
||||||
|
typeof jobPayload.channel === "string" && jobPayload.channel !== "last";
|
||||||
|
const hasExplicitTo = typeof jobPayload.to === "string" && jobPayload.to.trim() !== "";
|
||||||
|
|
||||||
|
// Determine if we should use origin context
|
||||||
|
// Use origin when: deliveryMode is "origin" (default), origin exists, and no explicit overrides
|
||||||
|
const useOrigin =
|
||||||
|
deliveryMode === "origin" &&
|
||||||
|
origin &&
|
||||||
|
!hasExplicitChannel &&
|
||||||
|
!hasExplicitTo &&
|
||||||
|
(origin.channel || origin.to);
|
||||||
|
|
||||||
|
if (useOrigin && origin.channel && origin.to) {
|
||||||
|
// Route directly to origin - no need to look up main session
|
||||||
|
const docked = resolveOutboundTarget({
|
||||||
|
channel: origin.channel,
|
||||||
|
to: origin.to,
|
||||||
|
cfg,
|
||||||
|
accountId: origin.accountId,
|
||||||
|
mode: "explicit",
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
channel: origin.channel as Exclude<OutboundChannel, "none">,
|
||||||
|
to: docked.ok ? docked.to : undefined,
|
||||||
|
accountId: origin.accountId,
|
||||||
|
threadId: origin.threadId,
|
||||||
|
mode: "explicit",
|
||||||
|
error: docked.ok ? undefined : docked.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to main session lookup (current behavior)
|
||||||
|
const requestedChannel = hasExplicitChannel
|
||||||
|
? (jobPayload.channel as ChannelId)
|
||||||
|
: useOrigin && origin?.channel
|
||||||
|
? origin.channel
|
||||||
|
: "last";
|
||||||
|
const explicitTo = hasExplicitTo
|
||||||
|
? jobPayload.to
|
||||||
|
: useOrigin && origin?.to
|
||||||
|
? origin.to
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const sessionCfg = cfg.session;
|
const sessionCfg = cfg.session;
|
||||||
const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId });
|
const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId });
|
||||||
@ -45,11 +98,16 @@ export async function resolveDeliveryTarget(
|
|||||||
|
|
||||||
let fallbackChannel: Exclude<OutboundChannel, "none"> | undefined;
|
let fallbackChannel: Exclude<OutboundChannel, "none"> | undefined;
|
||||||
if (!preliminary.channel) {
|
if (!preliminary.channel) {
|
||||||
try {
|
// Try origin channel as fallback before config/default
|
||||||
const selection = await resolveMessageChannelSelection({ cfg });
|
if (useOrigin && origin?.channel) {
|
||||||
fallbackChannel = selection.channel;
|
fallbackChannel = origin.channel as Exclude<OutboundChannel, "none">;
|
||||||
} catch {
|
} else {
|
||||||
fallbackChannel = preliminary.lastChannel ?? DEFAULT_CHAT_CHANNEL;
|
try {
|
||||||
|
const selection = await resolveMessageChannelSelection({ cfg });
|
||||||
|
fallbackChannel = selection.channel;
|
||||||
|
} catch {
|
||||||
|
fallbackChannel = preliminary.lastChannel ?? DEFAULT_CHAT_CHANNEL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,22 +125,25 @@ export async function resolveDeliveryTarget(
|
|||||||
const channel = resolved.channel ?? fallbackChannel ?? DEFAULT_CHAT_CHANNEL;
|
const channel = resolved.channel ?? fallbackChannel ?? DEFAULT_CHAT_CHANNEL;
|
||||||
const mode = resolved.mode as "explicit" | "implicit";
|
const mode = resolved.mode as "explicit" | "implicit";
|
||||||
const toCandidate = resolved.to;
|
const toCandidate = resolved.to;
|
||||||
|
const accountId = useOrigin && origin?.accountId ? origin.accountId : resolved.accountId;
|
||||||
|
const threadId = useOrigin && origin?.threadId != null ? origin.threadId : resolved.threadId;
|
||||||
|
|
||||||
if (!toCandidate) {
|
if (!toCandidate) {
|
||||||
return { channel, to: undefined, accountId: resolved.accountId, mode };
|
return { channel, to: undefined, accountId, threadId, mode };
|
||||||
}
|
}
|
||||||
|
|
||||||
const docked = resolveOutboundTarget({
|
const docked = resolveOutboundTarget({
|
||||||
channel,
|
channel,
|
||||||
to: toCandidate,
|
to: toCandidate,
|
||||||
cfg,
|
cfg,
|
||||||
accountId: resolved.accountId,
|
accountId,
|
||||||
mode,
|
mode,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
channel,
|
channel,
|
||||||
to: docked.ok ? docked.to : undefined,
|
to: docked.ok ? docked.to : undefined,
|
||||||
accountId: resolved.accountId,
|
accountId,
|
||||||
|
threadId,
|
||||||
mode,
|
mode,
|
||||||
error: docked.ok ? undefined : docked.error,
|
error: docked.ok ? undefined : docked.error,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -234,10 +234,18 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
deliveryMode === "explicit" || (deliveryMode === "auto" && hasExplicitTarget);
|
deliveryMode === "explicit" || (deliveryMode === "auto" && hasExplicitTarget);
|
||||||
const bestEffortDeliver = agentPayload?.bestEffortDeliver === true;
|
const bestEffortDeliver = agentPayload?.bestEffortDeliver === true;
|
||||||
|
|
||||||
const resolvedDelivery = await resolveDeliveryTarget(cfgWithAgentDefaults, agentId, {
|
const resolvedDelivery = await resolveDeliveryTarget(
|
||||||
channel: agentPayload?.channel ?? "last",
|
cfgWithAgentDefaults,
|
||||||
to: agentPayload?.to,
|
agentId,
|
||||||
});
|
{
|
||||||
|
channel: agentPayload?.channel ?? "last",
|
||||||
|
to: agentPayload?.to,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
origin: params.job.origin,
|
||||||
|
deliveryMode: params.job.deliveryMode,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const userTimezone = resolveUserTimezone(params.cfg.agents?.defaults?.userTimezone);
|
const userTimezone = resolveUserTimezone(params.cfg.agents?.defaults?.userTimezone);
|
||||||
const userTimeFormat = resolveUserTimeFormat(params.cfg.agents?.defaults?.timeFormat);
|
const userTimeFormat = resolveUserTimeFormat(params.cfg.agents?.defaults?.timeFormat);
|
||||||
|
|||||||
@ -95,6 +95,8 @@ export function createJob(state: CronServiceState, input: CronJobCreate): CronJo
|
|||||||
state: {
|
state: {
|
||||||
...input.state,
|
...input.state,
|
||||||
},
|
},
|
||||||
|
origin: input.origin,
|
||||||
|
deliveryMode: input.deliveryMode,
|
||||||
};
|
};
|
||||||
assertSupportedJobSpec(job);
|
assertSupportedJobSpec(job);
|
||||||
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
|
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
|
||||||
@ -115,6 +117,8 @@ export function applyJobPatch(job: CronJob, patch: CronJobPatch) {
|
|||||||
if ("agentId" in patch) {
|
if ("agentId" in patch) {
|
||||||
job.agentId = normalizeOptionalAgentId((patch as { agentId?: unknown }).agentId);
|
job.agentId = normalizeOptionalAgentId((patch as { agentId?: unknown }).agentId);
|
||||||
}
|
}
|
||||||
|
if (patch.origin) job.origin = patch.origin;
|
||||||
|
if (patch.deliveryMode) job.deliveryMode = patch.deliveryMode;
|
||||||
assertSupportedJobSpec(job);
|
assertSupportedJobSpec(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
|
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
|
||||||
import type { CronJob, CronJobCreate, CronJobPatch, CronStoreFile } from "../types.js";
|
import type {
|
||||||
|
CronDeliveryMode,
|
||||||
|
CronJob,
|
||||||
|
CronJobCreate,
|
||||||
|
CronJobPatch,
|
||||||
|
CronOrigin,
|
||||||
|
CronStoreFile,
|
||||||
|
} from "../types.js";
|
||||||
|
|
||||||
export type CronEvent = {
|
export type CronEvent = {
|
||||||
jobId: string;
|
jobId: string;
|
||||||
@ -24,7 +31,16 @@ export type CronServiceDeps = {
|
|||||||
log: Logger;
|
log: Logger;
|
||||||
storePath: string;
|
storePath: string;
|
||||||
cronEnabled: boolean;
|
cronEnabled: boolean;
|
||||||
enqueueSystemEvent: (text: string, opts?: { agentId?: string }) => void;
|
enqueueSystemEvent: (
|
||||||
|
text: string,
|
||||||
|
opts?: {
|
||||||
|
agentId?: string;
|
||||||
|
/** Origin context for routing replies back to where the job was created */
|
||||||
|
origin?: CronOrigin;
|
||||||
|
/** Delivery mode: "origin" (default) or "current" */
|
||||||
|
deliveryMode?: CronDeliveryMode;
|
||||||
|
},
|
||||||
|
) => void;
|
||||||
requestHeartbeatNow: (opts?: { reason?: string }) => void;
|
requestHeartbeatNow: (opts?: { reason?: string }) => void;
|
||||||
runHeartbeatOnce?: (opts?: { reason?: string }) => Promise<HeartbeatRunResult>;
|
runHeartbeatOnce?: (opts?: { reason?: string }) => Promise<HeartbeatRunResult>;
|
||||||
runIsolatedAgentJob: (params: { job: CronJob; message: string }) => Promise<{
|
runIsolatedAgentJob: (params: { job: CronJob; message: string }) => Promise<{
|
||||||
|
|||||||
@ -149,7 +149,11 @@ export async function executeJob(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.deps.enqueueSystemEvent(text, { agentId: job.agentId });
|
state.deps.enqueueSystemEvent(text, {
|
||||||
|
agentId: job.agentId,
|
||||||
|
origin: job.origin,
|
||||||
|
deliveryMode: job.deliveryMode,
|
||||||
|
});
|
||||||
if (job.wakeMode === "now" && state.deps.runHeartbeatOnce) {
|
if (job.wakeMode === "now" && state.deps.runHeartbeatOnce) {
|
||||||
const reason = `cron:${job.id}`;
|
const reason = `cron:${job.id}`;
|
||||||
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||||
|
|||||||
@ -53,6 +53,26 @@ export type CronIsolation = {
|
|||||||
postToMainMaxChars?: number;
|
postToMainMaxChars?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Origin context: where the cron job was created.
|
||||||
|
* Used for routing replies back to the originating session/channel.
|
||||||
|
*/
|
||||||
|
export type CronOrigin = {
|
||||||
|
/** The channel where the job was created (telegram, whatsapp, etc.) */
|
||||||
|
channel?: ChannelId;
|
||||||
|
/** The chat/conversation ID where the job was created */
|
||||||
|
to?: string;
|
||||||
|
/** The account ID (for multi-account setups) */
|
||||||
|
accountId?: string;
|
||||||
|
/** The thread ID (for threaded conversations) */
|
||||||
|
threadId?: string | number;
|
||||||
|
/** The session key where the job was created */
|
||||||
|
sessionKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Delivery routing mode for cron job replies */
|
||||||
|
export type CronDeliveryMode = "origin" | "current";
|
||||||
|
|
||||||
export type CronJobState = {
|
export type CronJobState = {
|
||||||
nextRunAtMs?: number;
|
nextRunAtMs?: number;
|
||||||
runningAtMs?: number;
|
runningAtMs?: number;
|
||||||
@ -77,6 +97,17 @@ export type CronJob = {
|
|||||||
payload: CronPayload;
|
payload: CronPayload;
|
||||||
isolation?: CronIsolation;
|
isolation?: CronIsolation;
|
||||||
state: CronJobState;
|
state: CronJobState;
|
||||||
|
/**
|
||||||
|
* Origin context: where the job was created.
|
||||||
|
* Replies are routed back here by default.
|
||||||
|
*/
|
||||||
|
origin?: CronOrigin;
|
||||||
|
/**
|
||||||
|
* Delivery routing mode:
|
||||||
|
* - "origin" (default): Route replies to where the job was created
|
||||||
|
* - "current": Route replies to the session's current lastChannel/lastTo
|
||||||
|
*/
|
||||||
|
deliveryMode?: CronDeliveryMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CronStoreFile = {
|
export type CronStoreFile = {
|
||||||
|
|||||||
@ -85,6 +85,27 @@ export const CronIsolationSchema = Type.Object(
|
|||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Origin context: where the cron job was created.
|
||||||
|
* Used for routing replies back to the originating session/channel.
|
||||||
|
*/
|
||||||
|
export const CronOriginSchema = Type.Object(
|
||||||
|
{
|
||||||
|
channel: Type.Optional(NonEmptyString),
|
||||||
|
to: Type.Optional(Type.String()),
|
||||||
|
accountId: Type.Optional(Type.String()),
|
||||||
|
threadId: Type.Optional(Type.Union([Type.String(), Type.Integer()])),
|
||||||
|
sessionKey: Type.Optional(Type.String()),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Delivery routing mode for cron job replies */
|
||||||
|
export const CronDeliveryModeSchema = Type.Union([
|
||||||
|
Type.Literal("origin"),
|
||||||
|
Type.Literal("current"),
|
||||||
|
]);
|
||||||
|
|
||||||
export const CronJobStateSchema = Type.Object(
|
export const CronJobStateSchema = Type.Object(
|
||||||
{
|
{
|
||||||
nextRunAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
nextRunAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||||
@ -115,6 +136,8 @@ export const CronJobSchema = Type.Object(
|
|||||||
payload: CronPayloadSchema,
|
payload: CronPayloadSchema,
|
||||||
isolation: Type.Optional(CronIsolationSchema),
|
isolation: Type.Optional(CronIsolationSchema),
|
||||||
state: CronJobStateSchema,
|
state: CronJobStateSchema,
|
||||||
|
origin: Type.Optional(CronOriginSchema),
|
||||||
|
deliveryMode: Type.Optional(CronDeliveryModeSchema),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
@ -140,6 +163,8 @@ export const CronAddParamsSchema = Type.Object(
|
|||||||
wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]),
|
wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]),
|
||||||
payload: CronPayloadSchema,
|
payload: CronPayloadSchema,
|
||||||
isolation: Type.Optional(CronIsolationSchema),
|
isolation: Type.Optional(CronIsolationSchema),
|
||||||
|
origin: Type.Optional(CronOriginSchema),
|
||||||
|
deliveryMode: Type.Optional(CronDeliveryModeSchema),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
@ -157,6 +182,8 @@ export const CronJobPatchSchema = Type.Object(
|
|||||||
payload: Type.Optional(CronPayloadPatchSchema),
|
payload: Type.Optional(CronPayloadPatchSchema),
|
||||||
isolation: Type.Optional(CronIsolationSchema),
|
isolation: Type.Optional(CronIsolationSchema),
|
||||||
state: Type.Optional(Type.Partial(CronJobStateSchema)),
|
state: Type.Optional(Type.Partial(CronJobStateSchema)),
|
||||||
|
origin: Type.Optional(CronOriginSchema),
|
||||||
|
deliveryMode: Type.Optional(CronDeliveryModeSchema),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,17 +1,24 @@
|
|||||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import type { CliDeps } from "../cli/deps.js";
|
import type { CliDeps } from "../cli/deps.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { resolveAgentMainSessionKey } from "../config/sessions.js";
|
import {
|
||||||
|
loadSessionStore,
|
||||||
|
resolveAgentMainSessionKey,
|
||||||
|
resolveStorePath,
|
||||||
|
updateSessionStore,
|
||||||
|
} from "../config/sessions.js";
|
||||||
import { runCronIsolatedAgentTurn } from "../cron/isolated-agent.js";
|
import { runCronIsolatedAgentTurn } from "../cron/isolated-agent.js";
|
||||||
import { appendCronRunLog, resolveCronRunLogPath } from "../cron/run-log.js";
|
import { appendCronRunLog, resolveCronRunLogPath } from "../cron/run-log.js";
|
||||||
import { CronService } from "../cron/service.js";
|
import { CronService } from "../cron/service.js";
|
||||||
import { resolveCronStorePath } from "../cron/store.js";
|
import { resolveCronStorePath } from "../cron/store.js";
|
||||||
|
import type { CronDeliveryMode, CronOrigin } from "../cron/types.js";
|
||||||
import { runHeartbeatOnce } from "../infra/heartbeat-runner.js";
|
import { runHeartbeatOnce } from "../infra/heartbeat-runner.js";
|
||||||
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
|
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
|
||||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||||
import { getChildLogger } from "../logging.js";
|
import { getChildLogger } from "../logging.js";
|
||||||
import { normalizeAgentId } from "../routing/session-key.js";
|
import { normalizeAgentId } from "../routing/session-key.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js";
|
||||||
|
|
||||||
export type GatewayCronState = {
|
export type GatewayCronState = {
|
||||||
cron: CronService;
|
cron: CronService;
|
||||||
@ -52,6 +59,46 @@ export function buildGatewayCronService(params: {
|
|||||||
cfg: runtimeConfig,
|
cfg: runtimeConfig,
|
||||||
agentId,
|
agentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If origin is provided and deliveryMode is "origin" (default), update the
|
||||||
|
// session's delivery context so the heartbeat routes replies to the origin.
|
||||||
|
const origin = opts?.origin;
|
||||||
|
const deliveryMode = opts?.deliveryMode ?? "origin";
|
||||||
|
if (origin && deliveryMode === "origin" && (origin.channel || origin.to)) {
|
||||||
|
const storePath = resolveStorePath(runtimeConfig.session?.store, { agentId });
|
||||||
|
const store = loadSessionStore(storePath);
|
||||||
|
const entry = store[sessionKey];
|
||||||
|
if (entry) {
|
||||||
|
const deliveryFields = normalizeSessionDeliveryFields({
|
||||||
|
deliveryContext: {
|
||||||
|
channel: origin.channel,
|
||||||
|
to: origin.to,
|
||||||
|
accountId: origin.accountId,
|
||||||
|
threadId: origin.threadId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Update session entry with origin delivery context
|
||||||
|
const updatedEntry = {
|
||||||
|
...entry,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
deliveryContext: deliveryFields.deliveryContext,
|
||||||
|
lastChannel: deliveryFields.lastChannel ?? entry.lastChannel,
|
||||||
|
lastTo: deliveryFields.lastTo ?? entry.lastTo,
|
||||||
|
lastAccountId: deliveryFields.lastAccountId ?? entry.lastAccountId,
|
||||||
|
lastThreadId: deliveryFields.lastThreadId ?? entry.lastThreadId,
|
||||||
|
};
|
||||||
|
store[sessionKey] = updatedEntry;
|
||||||
|
void updateSessionStore(storePath, (s) => {
|
||||||
|
s[sessionKey] = updatedEntry;
|
||||||
|
}).catch((err) => {
|
||||||
|
cronLogger.warn(
|
||||||
|
{ err: String(err), sessionKey },
|
||||||
|
"cron: failed to update session delivery context for origin routing",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enqueueSystemEvent(text, { sessionKey });
|
enqueueSystemEvent(text, { sessionKey });
|
||||||
},
|
},
|
||||||
requestHeartbeatNow,
|
requestHeartbeatNow,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user