fix: normalize delivery routing context
Co-authored-by: adam91holt <adam91holt@users.noreply.github.com>
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Sub-agents: normalize announce delivery origin + queue bucketing by accountId to keep multi-account routing stable. (#1061, #1058) — thanks @adam91holt.
|
- Sub-agents: normalize announce delivery origin + queue bucketing by accountId to keep multi-account routing stable. (#1061, #1058) — thanks @adam91holt.
|
||||||
|
- Sessions: include deliveryContext in sessions.list and reuse normalized delivery routing for announce/restart fallbacks. (#1058)
|
||||||
- Gateway: honor explicit delivery targets without implicit accountId fallback; preserve lastAccountId for implicit routing.
|
- Gateway: honor explicit delivery targets without implicit accountId fallback; preserve lastAccountId for implicit routing.
|
||||||
- Gateway: avoid reusing last-to/accountId when the requested channel differs; sync deliveryContext with last route fields.
|
- Gateway: avoid reusing last-to/accountId when the requested channel differs; sync deliveryContext with last route fields.
|
||||||
- Repo: fix oxlint config filename and move ignore pattern into config. (#1064) — thanks @connorshea.
|
- Repo: fix oxlint config filename and move ignore pattern into config. (#1064) — thanks @connorshea.
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ describe("resolveAnnounceTarget", () => {
|
|||||||
sessions: [
|
sessions: [
|
||||||
{
|
{
|
||||||
key: "agent:main:whatsapp:group:123@g.us",
|
key: "agent:main:whatsapp:group:123@g.us",
|
||||||
lastChannel: "whatsapp",
|
deliveryContext: {
|
||||||
lastTo: "123@g.us",
|
channel: "whatsapp",
|
||||||
lastAccountId: "work",
|
to: "123@g.us",
|
||||||
|
accountId: "work",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,9 +33,19 @@ export async function resolveAnnounceTarget(params: {
|
|||||||
sessions.find((entry) => entry?.key === params.sessionKey) ??
|
sessions.find((entry) => entry?.key === params.sessionKey) ??
|
||||||
sessions.find((entry) => entry?.key === params.displayKey);
|
sessions.find((entry) => entry?.key === params.displayKey);
|
||||||
|
|
||||||
const channel = typeof match?.lastChannel === "string" ? match.lastChannel : undefined;
|
const deliveryContext =
|
||||||
const to = typeof match?.lastTo === "string" ? match.lastTo : undefined;
|
match?.deliveryContext && typeof match.deliveryContext === "object"
|
||||||
const accountId = typeof match?.lastAccountId === "string" ? match.lastAccountId : undefined;
|
? (match.deliveryContext as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const channel =
|
||||||
|
(typeof deliveryContext?.channel === "string" ? deliveryContext.channel : undefined) ??
|
||||||
|
(typeof match?.lastChannel === "string" ? match.lastChannel : undefined);
|
||||||
|
const to =
|
||||||
|
(typeof deliveryContext?.to === "string" ? deliveryContext.to : undefined) ??
|
||||||
|
(typeof match?.lastTo === "string" ? match.lastTo : undefined);
|
||||||
|
const accountId =
|
||||||
|
(typeof deliveryContext?.accountId === "string" ? deliveryContext.accountId : undefined) ??
|
||||||
|
(typeof match?.lastAccountId === "string" ? match.lastAccountId : undefined);
|
||||||
if (channel && to) return { channel, to, accountId };
|
if (channel && to) return { channel, to, accountId };
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
|
|||||||
@@ -167,9 +167,21 @@ export function createSessionsListTool(opts?: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const entryChannel = typeof entry.channel === "string" ? entry.channel : undefined;
|
const entryChannel = typeof entry.channel === "string" ? entry.channel : undefined;
|
||||||
const lastChannel = typeof entry.lastChannel === "string" ? entry.lastChannel : undefined;
|
const deliveryContext =
|
||||||
|
entry.deliveryContext && typeof entry.deliveryContext === "object"
|
||||||
|
? (entry.deliveryContext as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const deliveryChannel =
|
||||||
|
typeof deliveryContext?.channel === "string" ? deliveryContext.channel : undefined;
|
||||||
|
const deliveryTo =
|
||||||
|
typeof deliveryContext?.to === "string" ? deliveryContext.to : undefined;
|
||||||
|
const deliveryAccountId =
|
||||||
|
typeof deliveryContext?.accountId === "string" ? deliveryContext.accountId : undefined;
|
||||||
|
const lastChannel =
|
||||||
|
deliveryChannel ?? (typeof entry.lastChannel === "string" ? entry.lastChannel : undefined);
|
||||||
const lastAccountId =
|
const lastAccountId =
|
||||||
typeof entry.lastAccountId === "string" ? entry.lastAccountId : undefined;
|
deliveryAccountId ??
|
||||||
|
(typeof entry.lastAccountId === "string" ? entry.lastAccountId : undefined);
|
||||||
const derivedChannel = deriveChannel({
|
const derivedChannel = deriveChannel({
|
||||||
key,
|
key,
|
||||||
kind,
|
kind,
|
||||||
@@ -201,7 +213,7 @@ export function createSessionsListTool(opts?: {
|
|||||||
typeof entry.abortedLastRun === "boolean" ? entry.abortedLastRun : undefined,
|
typeof entry.abortedLastRun === "boolean" ? entry.abortedLastRun : undefined,
|
||||||
sendPolicy: typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined,
|
sendPolicy: typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined,
|
||||||
lastChannel,
|
lastChannel,
|
||||||
lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined,
|
lastTo: deliveryTo ?? (typeof entry.lastTo === "string" ? entry.lastTo : undefined),
|
||||||
lastAccountId,
|
lastAccountId,
|
||||||
transcriptPath,
|
transcriptPath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -201,9 +201,10 @@ export async function initSessionState(params: {
|
|||||||
const baseEntry = !isNewSession && freshEntry ? entry : undefined;
|
const baseEntry = !isNewSession && freshEntry ? entry : undefined;
|
||||||
// Track the originating channel/to for announce routing (subagent announce-back).
|
// Track the originating channel/to for announce routing (subagent announce-back).
|
||||||
const lastChannelRaw =
|
const lastChannelRaw =
|
||||||
(ctx.OriginatingChannel as string | undefined)?.trim() || baseEntry?.lastChannel;
|
(ctx.OriginatingChannel as string | undefined) || baseEntry?.lastChannel;
|
||||||
const lastToRaw = ctx.OriginatingTo?.trim() || ctx.To?.trim() || baseEntry?.lastTo;
|
const lastToRaw = (ctx.OriginatingTo as string | undefined) || ctx.To || baseEntry?.lastTo;
|
||||||
const lastAccountIdRaw = ctx.AccountId?.trim() || baseEntry?.lastAccountId;
|
const lastAccountIdRaw =
|
||||||
|
(ctx.AccountId as string | undefined) || baseEntry?.lastAccountId;
|
||||||
const deliveryFields = normalizeSessionDeliveryFields({
|
const deliveryFields = normalizeSessionDeliveryFields({
|
||||||
deliveryContext: {
|
deliveryContext: {
|
||||||
channel: lastChannelRaw,
|
channel: lastChannelRaw,
|
||||||
|
|||||||
@@ -20,9 +20,15 @@ vi.mock("../infra/outbound/deliver.js", () => ({
|
|||||||
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
|
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../infra/outbound/targets.js", () => ({
|
vi.mock("../infra/outbound/targets.js", async () => {
|
||||||
resolveOutboundTarget: mocks.resolveOutboundTarget,
|
const actual = await vi.importActual<typeof import("../infra/outbound/targets.js")>(
|
||||||
}));
|
"../infra/outbound/targets.js",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveOutboundTarget: mocks.resolveOutboundTarget,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("deliverAgentCommandResult", () => {
|
describe("deliverAgentCommandResult", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -178,4 +184,39 @@ describe("deliverAgentCommandResult", () => {
|
|||||||
expect.objectContaining({ accountId: undefined, channel: "whatsapp" }),
|
expect.objectContaining({ accountId: undefined, channel: "whatsapp" }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses session last channel when none is provided", async () => {
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
const deps = {} as CliDeps;
|
||||||
|
const runtime = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as RuntimeEnv;
|
||||||
|
const sessionEntry = {
|
||||||
|
lastChannel: "telegram",
|
||||||
|
lastTo: "123",
|
||||||
|
} as SessionEntry;
|
||||||
|
const result = {
|
||||||
|
payloads: [{ text: "hi" }],
|
||||||
|
meta: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { deliverAgentCommandResult } = await import("./agent/delivery.js");
|
||||||
|
await deliverAgentCommandResult({
|
||||||
|
cfg,
|
||||||
|
deps,
|
||||||
|
runtime,
|
||||||
|
opts: {
|
||||||
|
message: "hello",
|
||||||
|
deliver: true,
|
||||||
|
},
|
||||||
|
sessionEntry,
|
||||||
|
result,
|
||||||
|
payloads: result.payloads,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.resolveOutboundTarget).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ channel: "telegram", to: "123" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||||
import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js";
|
|
||||||
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
|
|
||||||
import { createOutboundSendDeps, type CliDeps } from "../../cli/outbound-send-deps.js";
|
import { createOutboundSendDeps, type CliDeps } from "../../cli/outbound-send-deps.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { SessionEntry } from "../../config/sessions.js";
|
import type { SessionEntry } from "../../config/sessions.js";
|
||||||
@@ -12,34 +10,16 @@ import {
|
|||||||
normalizeOutboundPayloads,
|
normalizeOutboundPayloads,
|
||||||
normalizeOutboundPayloadsForJson,
|
normalizeOutboundPayloadsForJson,
|
||||||
} from "../../infra/outbound/payloads.js";
|
} from "../../infra/outbound/payloads.js";
|
||||||
|
import { resolveAgentDeliveryPlan } from "../../infra/outbound/agent-delivery.js";
|
||||||
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import {
|
import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
||||||
isInternalMessageChannel,
|
|
||||||
resolveGatewayMessageChannel,
|
|
||||||
} from "../../utils/message-channel.js";
|
|
||||||
import { normalizeAccountId } from "../../utils/account-id.js";
|
|
||||||
import { deliveryContextFromSession } from "../../utils/delivery-context.js";
|
|
||||||
import type { AgentCommandOpts } from "./types.js";
|
import type { AgentCommandOpts } from "./types.js";
|
||||||
|
|
||||||
type RunResult = Awaited<
|
type RunResult = Awaited<
|
||||||
ReturnType<(typeof import("../../agents/pi-embedded.js"))["runEmbeddedPiAgent"]>
|
ReturnType<(typeof import("../../agents/pi-embedded.js"))["runEmbeddedPiAgent"]>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function resolveDeliveryAccountId(params: {
|
|
||||||
opts: AgentCommandOpts;
|
|
||||||
sessionEntry?: SessionEntry;
|
|
||||||
targetMode: ChannelOutboundTargetMode;
|
|
||||||
deliveryChannel?: string;
|
|
||||||
}) {
|
|
||||||
const sessionOrigin = deliveryContextFromSession(params.sessionEntry);
|
|
||||||
const explicit = normalizeAccountId(params.opts.accountId);
|
|
||||||
if (explicit || params.targetMode !== "implicit") return explicit;
|
|
||||||
if (!params.deliveryChannel || isInternalMessageChannel(params.deliveryChannel)) return undefined;
|
|
||||||
if (sessionOrigin?.channel !== params.deliveryChannel) return undefined;
|
|
||||||
return normalizeAccountId(sessionOrigin?.accountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deliverAgentCommandResult(params: {
|
export async function deliverAgentCommandResult(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
deps: CliDeps;
|
deps: CliDeps;
|
||||||
@@ -52,7 +32,14 @@ export async function deliverAgentCommandResult(params: {
|
|||||||
const { cfg, deps, runtime, opts, sessionEntry, payloads, result } = params;
|
const { cfg, deps, runtime, opts, sessionEntry, payloads, result } = params;
|
||||||
const deliver = opts.deliver === true;
|
const deliver = opts.deliver === true;
|
||||||
const bestEffortDeliver = opts.bestEffortDeliver === true;
|
const bestEffortDeliver = opts.bestEffortDeliver === true;
|
||||||
const deliveryChannel = resolveGatewayMessageChannel(opts.channel) ?? DEFAULT_CHAT_CHANNEL;
|
const deliveryPlan = resolveAgentDeliveryPlan({
|
||||||
|
sessionEntry,
|
||||||
|
requestedChannel: opts.channel,
|
||||||
|
explicitTo: opts.to,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
wantsDelivery: deliver,
|
||||||
|
});
|
||||||
|
const deliveryChannel = deliveryPlan.resolvedChannel;
|
||||||
// Channel docking: delivery channels are resolved via plugin registry.
|
// Channel docking: delivery channels are resolved via plugin registry.
|
||||||
const deliveryPlugin = !isInternalMessageChannel(deliveryChannel)
|
const deliveryPlugin = !isInternalMessageChannel(deliveryChannel)
|
||||||
? getChannelPlugin(normalizeChannelId(deliveryChannel) ?? deliveryChannel)
|
? getChannelPlugin(normalizeChannelId(deliveryChannel) ?? deliveryChannel)
|
||||||
@@ -61,19 +48,14 @@ export async function deliverAgentCommandResult(params: {
|
|||||||
const isDeliveryChannelKnown =
|
const isDeliveryChannelKnown =
|
||||||
isInternalMessageChannel(deliveryChannel) || Boolean(deliveryPlugin);
|
isInternalMessageChannel(deliveryChannel) || Boolean(deliveryPlugin);
|
||||||
|
|
||||||
const targetMode: ChannelOutboundTargetMode =
|
const targetMode =
|
||||||
opts.deliveryTargetMode ?? (opts.to ? "explicit" : "implicit");
|
opts.deliveryTargetMode ?? deliveryPlan.deliveryTargetMode ?? (opts.to ? "explicit" : "implicit");
|
||||||
const resolvedAccountId = resolveDeliveryAccountId({
|
const resolvedAccountId = deliveryPlan.resolvedAccountId;
|
||||||
opts,
|
|
||||||
sessionEntry,
|
|
||||||
targetMode,
|
|
||||||
deliveryChannel,
|
|
||||||
});
|
|
||||||
const resolvedTarget =
|
const resolvedTarget =
|
||||||
deliver && isDeliveryChannelKnown && deliveryChannel
|
deliver && isDeliveryChannelKnown && deliveryChannel
|
||||||
? resolveOutboundTarget({
|
? resolveOutboundTarget({
|
||||||
channel: deliveryChannel,
|
channel: deliveryChannel,
|
||||||
to: opts.to,
|
to: deliveryPlan.resolvedTo,
|
||||||
cfg,
|
cfg,
|
||||||
accountId: resolvedAccountId,
|
accountId: resolvedAccountId,
|
||||||
mode: targetMode,
|
mode: targetMode,
|
||||||
|
|||||||
@@ -332,24 +332,19 @@ export async function updateLastRoute(params: {
|
|||||||
const store = loadSessionStore(storePath);
|
const store = loadSessionStore(storePath);
|
||||||
const existing = store[sessionKey];
|
const existing = store[sessionKey];
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const trimmedAccountId = accountId?.trim();
|
|
||||||
const resolvedAccountId =
|
|
||||||
trimmedAccountId && trimmedAccountId.length > 0
|
|
||||||
? trimmedAccountId
|
|
||||||
: existing?.lastAccountId ?? existing?.deliveryContext?.accountId;
|
|
||||||
const normalized = normalizeSessionDeliveryFields({
|
const normalized = normalizeSessionDeliveryFields({
|
||||||
deliveryContext: {
|
deliveryContext: {
|
||||||
channel: channel ?? existing?.lastChannel,
|
channel: channel ?? existing?.lastChannel ?? existing?.deliveryContext?.channel,
|
||||||
to,
|
to: to ?? existing?.lastTo ?? existing?.deliveryContext?.to,
|
||||||
accountId: resolvedAccountId,
|
accountId: accountId ?? existing?.lastAccountId ?? existing?.deliveryContext?.accountId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const next = mergeSessionEntry(existing, {
|
const next = mergeSessionEntry(existing, {
|
||||||
updatedAt: Math.max(existing?.updatedAt ?? 0, now),
|
updatedAt: Math.max(existing?.updatedAt ?? 0, now),
|
||||||
deliveryContext: normalized.deliveryContext,
|
deliveryContext: normalized.deliveryContext,
|
||||||
lastChannel: normalized.lastChannel ?? channel,
|
lastChannel: normalized.lastChannel,
|
||||||
lastTo: normalized.lastTo ?? (to?.trim() ? to.trim() : undefined),
|
lastTo: normalized.lastTo,
|
||||||
lastAccountId: normalized.lastAccountId ?? resolvedAccountId,
|
lastAccountId: normalized.lastAccountId,
|
||||||
});
|
});
|
||||||
store[sessionKey] = next;
|
store[sessionKey] = next;
|
||||||
await saveSessionStoreUnlocked(storePath, store);
|
await saveSessionStoreUnlocked(storePath, store);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
|
|
||||||
import { agentCommand } from "../../commands/agent.js";
|
import { agentCommand } from "../../commands/agent.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -9,10 +8,10 @@ import {
|
|||||||
updateSessionStore,
|
updateSessionStore,
|
||||||
} from "../../config/sessions.js";
|
} from "../../config/sessions.js";
|
||||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||||
import { resolveOutboundTarget, resolveSessionDeliveryTarget } from "../../infra/outbound/targets.js";
|
import { resolveAgentDeliveryPlan } from "../../infra/outbound/agent-delivery.js";
|
||||||
|
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||||
import { normalizeAccountId } from "../../utils/account-id.js";
|
|
||||||
import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.js";
|
import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.js";
|
||||||
import {
|
import {
|
||||||
INTERNAL_MESSAGE_CHANNEL,
|
INTERNAL_MESSAGE_CHANNEL,
|
||||||
@@ -202,53 +201,21 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
const runId = idem;
|
const runId = idem;
|
||||||
|
|
||||||
const wantsDelivery = request.deliver === true;
|
const wantsDelivery = request.deliver === true;
|
||||||
const requestedChannel = normalizeMessageChannel(request.channel) ?? "last";
|
|
||||||
const explicitTo =
|
const explicitTo =
|
||||||
typeof request.to === "string" && request.to.trim() ? request.to.trim() : undefined;
|
typeof request.to === "string" && request.to.trim() ? request.to.trim() : undefined;
|
||||||
|
const deliveryPlan = resolveAgentDeliveryPlan({
|
||||||
const baseDelivery = resolveSessionDeliveryTarget({
|
sessionEntry,
|
||||||
entry: sessionEntry,
|
requestedChannel: request.channel,
|
||||||
requestedChannel: requestedChannel === INTERNAL_MESSAGE_CHANNEL ? "last" : requestedChannel,
|
|
||||||
explicitTo,
|
explicitTo,
|
||||||
|
accountId: request.accountId,
|
||||||
|
wantsDelivery,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedChannel = (() => {
|
const resolvedChannel = deliveryPlan.resolvedChannel;
|
||||||
if (requestedChannel === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL;
|
const deliveryTargetMode = deliveryPlan.deliveryTargetMode;
|
||||||
if (requestedChannel === "last") {
|
const resolvedAccountId = deliveryPlan.resolvedAccountId;
|
||||||
// WebChat is not a deliverable surface. Treat it as "unset" for routing,
|
let resolvedTo = deliveryPlan.resolvedTo;
|
||||||
// so VoiceWake and CLI callers don't get stuck with deliver=false.
|
|
||||||
if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) {
|
|
||||||
return baseDelivery.channel;
|
|
||||||
}
|
|
||||||
return wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGatewayMessageChannel(requestedChannel)) return requestedChannel;
|
|
||||||
|
|
||||||
if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) {
|
|
||||||
return baseDelivery.channel;
|
|
||||||
}
|
|
||||||
return wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const deliveryTargetMode = explicitTo
|
|
||||||
? "explicit"
|
|
||||||
: isDeliverableMessageChannel(resolvedChannel)
|
|
||||||
? "implicit"
|
|
||||||
: undefined;
|
|
||||||
const resolvedAccountId =
|
|
||||||
normalizeAccountId(request.accountId) ??
|
|
||||||
(deliveryTargetMode === "implicit" && resolvedChannel === baseDelivery.lastChannel
|
|
||||||
? baseDelivery.lastAccountId
|
|
||||||
: undefined);
|
|
||||||
let resolvedTo = explicitTo;
|
|
||||||
if (
|
|
||||||
!resolvedTo &&
|
|
||||||
isDeliverableMessageChannel(resolvedChannel) &&
|
|
||||||
resolvedChannel === baseDelivery.lastChannel
|
|
||||||
) {
|
|
||||||
resolvedTo = baseDelivery.lastTo;
|
|
||||||
}
|
|
||||||
if (!resolvedTo && isDeliverableMessageChannel(resolvedChannel)) {
|
if (!resolvedTo && isDeliverableMessageChannel(resolvedChannel)) {
|
||||||
const cfg = cfgForAgent ?? loadConfig();
|
const cfg = cfgForAgent ?? loadConfig();
|
||||||
const fallback = resolveOutboundTarget({
|
const fallback = resolveOutboundTarget({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "../infra/restart-sentinel.js";
|
} from "../infra/restart-sentinel.js";
|
||||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import { deliveryContextFromSession, mergeDeliveryContext } from "../utils/delivery-context.js";
|
||||||
import { loadSessionEntry } from "./session-utils.js";
|
import { loadSessionEntry } from "./session-utils.js";
|
||||||
|
|
||||||
export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) {
|
export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) {
|
||||||
@@ -28,12 +29,11 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { cfg, entry } = loadSessionEntry(sessionKey);
|
const { cfg, entry } = loadSessionEntry(sessionKey);
|
||||||
const lastChannel = entry?.lastChannel;
|
|
||||||
const lastTo = entry?.lastTo?.trim();
|
|
||||||
const parsedTarget = resolveAnnounceTargetFromKey(sessionKey);
|
const parsedTarget = resolveAnnounceTargetFromKey(sessionKey);
|
||||||
const channelRaw = lastChannel ?? parsedTarget?.channel;
|
const origin = mergeDeliveryContext(deliveryContextFromSession(entry), parsedTarget ?? undefined);
|
||||||
|
const channelRaw = origin?.channel;
|
||||||
const channel = channelRaw ? normalizeChannelId(channelRaw) : null;
|
const channel = channelRaw ? normalizeChannelId(channelRaw) : null;
|
||||||
const to = lastTo || parsedTarget?.to;
|
const to = origin?.to;
|
||||||
if (!channel || !to) {
|
if (!channel || !to) {
|
||||||
enqueueSystemEvent(message, { sessionKey });
|
enqueueSystemEvent(message, { sessionKey });
|
||||||
return;
|
return;
|
||||||
@@ -43,7 +43,7 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) {
|
|||||||
channel,
|
channel,
|
||||||
to,
|
to,
|
||||||
cfg,
|
cfg,
|
||||||
accountId: parsedTarget?.accountId ?? entry?.lastAccountId,
|
accountId: origin?.accountId,
|
||||||
mode: "implicit",
|
mode: "implicit",
|
||||||
});
|
});
|
||||||
if (!resolved.ok) {
|
if (!resolved.ok) {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ describe("gateway server sessions", () => {
|
|||||||
thinkingLevel?: string;
|
thinkingLevel?: string;
|
||||||
verboseLevel?: string;
|
verboseLevel?: string;
|
||||||
lastAccountId?: string;
|
lastAccountId?: string;
|
||||||
|
deliveryContext?: { channel?: string; to?: string; accountId?: string };
|
||||||
}>;
|
}>;
|
||||||
}>(ws, "sessions.list", { includeGlobal: false, includeUnknown: false });
|
}>(ws, "sessions.list", { includeGlobal: false, includeUnknown: false });
|
||||||
|
|
||||||
@@ -140,6 +141,11 @@ describe("gateway server sessions", () => {
|
|||||||
expect(main?.thinkingLevel).toBe("low");
|
expect(main?.thinkingLevel).toBe("low");
|
||||||
expect(main?.verboseLevel).toBe("on");
|
expect(main?.verboseLevel).toBe("on");
|
||||||
expect(main?.lastAccountId).toBe("work");
|
expect(main?.lastAccountId).toBe("work");
|
||||||
|
expect(main?.deliveryContext).toEqual({
|
||||||
|
channel: "whatsapp",
|
||||||
|
to: "+1555",
|
||||||
|
accountId: "work",
|
||||||
|
});
|
||||||
|
|
||||||
const active = await rpcReq<{
|
const active = await rpcReq<{
|
||||||
sessions: Array<{ key: string }>;
|
sessions: Array<{ key: string }>;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
normalizeMainKey,
|
normalizeMainKey,
|
||||||
parseAgentSessionKey,
|
parseAgentSessionKey,
|
||||||
} from "../routing/session-key.js";
|
} from "../routing/session-key.js";
|
||||||
|
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js";
|
||||||
import type {
|
import type {
|
||||||
GatewayAgentRow,
|
GatewayAgentRow,
|
||||||
GatewaySessionRow,
|
GatewaySessionRow,
|
||||||
@@ -401,6 +402,7 @@ export function listSessionsFromStore(params: {
|
|||||||
key,
|
key,
|
||||||
})
|
})
|
||||||
: undefined);
|
: undefined);
|
||||||
|
const deliveryFields = normalizeSessionDeliveryFields(entry);
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
kind: classifySessionKey(key, entry),
|
kind: classifySessionKey(key, entry),
|
||||||
@@ -427,9 +429,10 @@ export function listSessionsFromStore(params: {
|
|||||||
modelProvider: entry?.modelProvider,
|
modelProvider: entry?.modelProvider,
|
||||||
model: entry?.model,
|
model: entry?.model,
|
||||||
contextTokens: entry?.contextTokens,
|
contextTokens: entry?.contextTokens,
|
||||||
lastChannel: entry?.lastChannel,
|
deliveryContext: deliveryFields.deliveryContext,
|
||||||
lastTo: entry?.lastTo,
|
lastChannel: deliveryFields.lastChannel ?? entry?.lastChannel,
|
||||||
lastAccountId: entry?.lastAccountId,
|
lastTo: deliveryFields.lastTo ?? entry?.lastTo,
|
||||||
|
lastAccountId: deliveryFields.lastAccountId ?? entry?.lastAccountId,
|
||||||
} satisfies GatewaySessionRow;
|
} satisfies GatewaySessionRow;
|
||||||
})
|
})
|
||||||
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { SessionEntry } from "../config/sessions.js";
|
import type { SessionEntry } from "../config/sessions.js";
|
||||||
|
import type { DeliveryContext } from "../utils/delivery-context.js";
|
||||||
|
|
||||||
export type GatewaySessionsDefaults = {
|
export type GatewaySessionsDefaults = {
|
||||||
modelProvider: string | null;
|
modelProvider: string | null;
|
||||||
@@ -32,6 +33,7 @@ export type GatewaySessionRow = {
|
|||||||
modelProvider?: string;
|
modelProvider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
contextTokens?: number;
|
contextTokens?: number;
|
||||||
|
deliveryContext?: DeliveryContext;
|
||||||
lastChannel?: SessionEntry["lastChannel"];
|
lastChannel?: SessionEntry["lastChannel"];
|
||||||
lastTo?: string;
|
lastTo?: string;
|
||||||
lastAccountId?: string;
|
lastAccountId?: string;
|
||||||
|
|||||||
88
src/infra/outbound/agent-delivery.ts
Normal file
88
src/infra/outbound/agent-delivery.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
|
||||||
|
import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js";
|
||||||
|
import type { SessionEntry } from "../../config/sessions.js";
|
||||||
|
import { normalizeAccountId } from "../../utils/account-id.js";
|
||||||
|
import {
|
||||||
|
INTERNAL_MESSAGE_CHANNEL,
|
||||||
|
isDeliverableMessageChannel,
|
||||||
|
isGatewayMessageChannel,
|
||||||
|
normalizeMessageChannel,
|
||||||
|
type GatewayMessageChannel,
|
||||||
|
} from "../../utils/message-channel.js";
|
||||||
|
import { resolveSessionDeliveryTarget, type SessionDeliveryTarget } from "./targets.js";
|
||||||
|
|
||||||
|
export type AgentDeliveryPlan = {
|
||||||
|
baseDelivery: SessionDeliveryTarget;
|
||||||
|
resolvedChannel: GatewayMessageChannel;
|
||||||
|
resolvedTo?: string;
|
||||||
|
resolvedAccountId?: string;
|
||||||
|
deliveryTargetMode?: ChannelOutboundTargetMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveAgentDeliveryPlan(params: {
|
||||||
|
sessionEntry?: SessionEntry;
|
||||||
|
requestedChannel?: string;
|
||||||
|
explicitTo?: string;
|
||||||
|
accountId?: string;
|
||||||
|
wantsDelivery: boolean;
|
||||||
|
}): AgentDeliveryPlan {
|
||||||
|
const requestedRaw =
|
||||||
|
typeof params.requestedChannel === "string" ? params.requestedChannel.trim() : "";
|
||||||
|
const normalizedRequested = requestedRaw ? normalizeMessageChannel(requestedRaw) : undefined;
|
||||||
|
const requestedChannel = normalizedRequested || "last";
|
||||||
|
|
||||||
|
const explicitTo =
|
||||||
|
typeof params.explicitTo === "string" && params.explicitTo.trim()
|
||||||
|
? params.explicitTo.trim()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const baseDelivery = resolveSessionDeliveryTarget({
|
||||||
|
entry: params.sessionEntry,
|
||||||
|
requestedChannel: requestedChannel === INTERNAL_MESSAGE_CHANNEL ? "last" : requestedChannel,
|
||||||
|
explicitTo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolvedChannel = (() => {
|
||||||
|
if (requestedChannel === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL;
|
||||||
|
if (requestedChannel === "last") {
|
||||||
|
if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) {
|
||||||
|
return baseDelivery.channel;
|
||||||
|
}
|
||||||
|
return params.wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGatewayMessageChannel(requestedChannel)) return requestedChannel;
|
||||||
|
|
||||||
|
if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) {
|
||||||
|
return baseDelivery.channel;
|
||||||
|
}
|
||||||
|
return params.wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const deliveryTargetMode = explicitTo
|
||||||
|
? "explicit"
|
||||||
|
: isDeliverableMessageChannel(resolvedChannel)
|
||||||
|
? "implicit"
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const resolvedAccountId =
|
||||||
|
normalizeAccountId(params.accountId) ??
|
||||||
|
(deliveryTargetMode === "implicit" ? baseDelivery.accountId : undefined);
|
||||||
|
|
||||||
|
let resolvedTo = explicitTo;
|
||||||
|
if (
|
||||||
|
!resolvedTo &&
|
||||||
|
isDeliverableMessageChannel(resolvedChannel) &&
|
||||||
|
resolvedChannel === baseDelivery.lastChannel
|
||||||
|
) {
|
||||||
|
resolvedTo = baseDelivery.lastTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseDelivery,
|
||||||
|
resolvedChannel,
|
||||||
|
resolvedTo,
|
||||||
|
resolvedAccountId,
|
||||||
|
deliveryTargetMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user