Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/plugins/architecture-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,10 @@ outbound host generic and use the messaging adapter surface for provider rules:
should be treated as `direct`, `group`, or `channel` before directory lookup.
- `messaging.targetResolver.looksLikeId(raw, normalized)` tells core whether an
input should skip straight to id-like resolution instead of directory search.
- `messaging.targetResolver.reservedLiterals` lists bare words that are
channel/session references for that provider. Resolution preserves configured
directory entries before rejecting reserved literals, then fails closed on a
directory miss.
- `messaging.targetResolver.resolveTarget(...)` is the plugin fallback when
core needs a final provider-owned resolution after normalization or after a
directory miss.
Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/sdk-channel-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ Write colocated tests in `src/channel.test.ts`:
describeMessageTool and action discovery
</Card>
<Card title="Target resolution" icon="crosshair" href="/plugins/architecture-internals#channel-target-resolution">
inferTargetChatType, looksLikeId, resolveTarget
inferTargetChatType, looksLikeId, reservedLiterals, resolveTarget
</Card>
<Card title="Runtime helpers" icon="settings" href="/plugins/sdk-runtime">
TTS, STT, media, subagent via api.runtime
Expand Down
1 change: 1 addition & 0 deletions extensions/telegram/src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,7 @@ export const telegramPlugin = createChatChannelPlugin({
targetResolver: {
looksLikeId: looksLikeTelegramTargetId,
hint: "<chatId>",
reservedLiterals: ["current", "self", "this", "me"],
},
},
resolver: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ export function expectChannelSurfaceContract(params: {
expect(typeof messaging.targetResolver.hint).toBe("string");
expect(messaging.targetResolver.hint.trim()).not.toBe("");
}
if (messaging.targetResolver.reservedLiterals !== undefined) {
expect(Array.isArray(messaging.targetResolver.reservedLiterals)).toBe(true);
expect(
messaging.targetResolver.reservedLiterals.every(
(value) => typeof value === "string" && value.trim(),
),
).toBe(true);
}
if (messaging.targetResolver.resolveTarget) {
expect(typeof messaging.targetResolver.resolveTarget).toBe("function");
}
Expand Down
2 changes: 2 additions & 0 deletions src/channels/plugins/types.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@ export type ChannelMessagingAdapter = {
targetResolver?: {
looksLikeId?: (raw: string, normalized?: string) => boolean;
hint?: string;
/** Bare words that are command/session references for this channel, not literal destinations. */
reservedLiterals?: readonly string[];
/**
* Plugin-owned fallback for explicit/native targets or post-directory-miss
* resolution. This should complement directory lookup, not duplicate it.
Expand Down
56 changes: 55 additions & 1 deletion src/cron/isolated-agent/delivery-target.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Isolated agent delivery target tests cover target resolution for cron runs.
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js";
import type {
ChannelDirectoryEntry,
ChannelOutboundAdapter,
} from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import {
Expand Down Expand Up @@ -741,6 +744,57 @@ describe("resolveDeliveryTarget", () => {
expect(result.threadId).toBeUndefined();
});

it("resolves cron reserved explicit targets through directory entries", async () => {
setMainSessionEntry(undefined);
const listGroups = vi.fn(async () => [
{
kind: "group",
id: "-1002458651455",
name: "current",
handle: "@current",
} satisfies ChannelDirectoryEntry,
]);
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "telegram",
source: "test",
plugin: {
...createOutboundTestPlugin({
id: "telegram",
outbound: createStubOutbound("Telegram"),
capabilities: { chatTypes: ["direct", "group", "channel"] },
messaging: {
...telegramMessagingForTest,
normalizeTarget: normalizeTelegramTargetForDeliveryTest,
targetResolver: {
reservedLiterals: ["current", "self", "this", "me"],
hint: "<chatId>",
},
},
}),
directory: { listGroups },
},
},
]),
);

const result = await resolveDeliveryTarget(makeCfg({ bindings: [] }), AGENT_ID, {
channel: "telegram",
to: "current",
});

expect(result.ok).toBe(true);
expect(result.to).toBe("-1002458651455");
expect(result.threadId).toBeUndefined();
expect(listGroups).toHaveBeenCalledWith(
expect.objectContaining({
accountId: undefined,
query: "current",
}),
);
});

it("uses canonical route targets even when the route has no thread", async () => {
setMainSessionEntry(undefined);
setActivePluginRegistry(
Expand Down
24 changes: 14 additions & 10 deletions src/cron/isolated-agent/delivery-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { stripTargetProviderPrefix } from "../../infra/outbound/channel-target-prefix.js";
import type { OutboundSessionRoute } from "../../infra/outbound/outbound-session.js";
import { isReservedTargetLiteralError } from "../../infra/outbound/target-errors.js";
import type { ResolvedMessagingTarget } from "../../infra/outbound/target-resolver.js";
import { tryResolveLoadedOutboundTarget } from "../../infra/outbound/targets-loaded.js";
import { resolveSessionDeliveryTarget } from "../../infra/outbound/targets-session.js";
Expand Down Expand Up @@ -349,17 +350,20 @@ export async function resolveDeliveryTarget(
allowFrom: effectiveAllowFrom,
});
if (!docked.ok) {
return {
ok: false,
channel,
to: undefined,
accountId,
threadId: explicitThreadId,
mode,
error: docked.error,
};
if (!toCandidate || !isReservedTargetLiteralError(docked.error)) {
return {
ok: false,
channel,
to: undefined,
accountId,
threadId: explicitThreadId,
mode,
error: docked.error,
};
}
} else {
toCandidate = docked.to;
}
toCandidate = docked.to;
const targetResolution = await deliveryTargetRuntime.resolveChannelTargetForDelivery({
cfg,
channel,
Expand Down
23 changes: 22 additions & 1 deletion src/gateway/server-methods/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2300,7 +2300,7 @@ export const agentHandlers: GatewayRequestHandlers = {
let resolvedTo = deliveryPlan.resolvedTo;
let effectivePlan = deliveryPlan;
let deliveryDowngradeReason: string | null = null;
let deliveryTargetResolutionError: Error | undefined;
let deliveryTargetResolutionError: Error | undefined = deliveryPlan.targetResolutionError;

if (wantsDelivery && resolvedChannel === INTERNAL_MESSAGE_CHANNEL) {
const cfgResolved = cfgForAgent ?? cfg;
Expand Down Expand Up @@ -2328,6 +2328,27 @@ export const agentHandlers: GatewayRequestHandlers = {
}
}

if (wantsDelivery && deliveryTargetResolutionError) {
if (!bestEffortDeliver) {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, String(deliveryTargetResolutionError)),
);
return;
}
deliveryDowngradeReason = String(deliveryTargetResolutionError);
resolvedChannel = INTERNAL_MESSAGE_CHANNEL;
deliveryTargetMode = undefined;
resolvedTo = undefined;
effectivePlan = {
...deliveryPlan,
resolvedChannel,
resolvedTo,
deliveryTargetMode,
};
}

if (!resolvedTo && isDeliverableMessageChannel(resolvedChannel)) {
const cfgResolved = cfgForAgent ?? cfg;
const fallback = resolveAgentOutboundTarget({
Expand Down
Loading
Loading