fix(telegram): stabilize topic dispatch runtime

This commit is contained in:
Peter Steinberger
2026-05-04 08:25:02 +01:00
parent 48e1256810
commit 585ce38015
14 changed files with 216 additions and 32 deletions

View File

@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
- Plugins/update: move ClawHub-preferred externalized plugin installs back to ClawHub after an earlier npm fallback once the ClawHub package becomes available. Thanks @vincentkoc.
- Plugins/update: clean stale bundled load paths for already-externalized pinned npm and ClawHub plugin installs, so release-channel sync does not leave removed bundled paths ahead of the installed external package. Thanks @vincentkoc.
- Plugins/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins.
- Telegram: accept plugin-owned numeric forum-topic targets in the agent message tool and keep reply-dispatch provider chunks behind a real stable runtime alias during in-place package updates. Fixes #77137. Thanks @richardmqq.
- Google Meet: preserve `realtime.introMessage: ""` so realtime Chrome joins can stay silent instead of restoring the default spoken intro. Thanks @vincentkoc.
- Discord/status: add degraded Discord transport and gateway event-loop starvation signals to `openclaw channels status`, `openclaw status --deep`, and fetch-timeout logs so intermittent socket resets do not look like a healthy running channel. (#76327) Thanks @joshavant.
- Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc.

View File

@@ -777,11 +777,12 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
- `channels.telegram.dms["<user_id>"].historyLimit`
- `channels.telegram.retry` config applies to Telegram send helpers (CLI/tools/actions) for recoverable outbound API errors. Inbound final-reply delivery also uses a bounded safe-send retry for Telegram pre-connect failures, but it does not retry ambiguous post-send network envelopes that could duplicate visible messages.
CLI send target can be numeric chat ID or username:
CLI and message-tool send targets can be numeric chat ID, username, or a forum topic target:
```bash
openclaw message send --channel telegram --target 123456789 --message "hi"
openclaw message send --channel telegram --target @name --message "hi"
openclaw message send --channel telegram --target -1001234567890:topic:42 --message "hi topic"
```
Telegram polls use `openclaw message poll` and support forum topics:

View File

@@ -27,7 +27,7 @@ Channel selection:
Target formats (`--target`):
- WhatsApp: E.164, group JID, or WhatsApp Channel/Newsletter JID (`...@newsletter`)
- Telegram: chat id or `@username`
- Telegram: chat id, `@username`, or forum topic target (`-1001234567890:topic:42`, or `--thread-id 42`)
- Discord: `channel:<id>` or `user:<id>` (or `<@id>` mention; raw numeric ids are treated as channels)
- Google Chat: `spaces/<spaceId>` or `users/<userId>`
- Slack: `channel:<id>` or `user:<id>` (raw channel id is accepted)

View File

@@ -41,9 +41,9 @@ const LEGACY_ROOT_RUNTIME_COMPAT_ALIASES = [
["server-close-DsVPJDIx.js", "server-close.runtime.js"],
["server-close-DvAvfgr8.js", "server-close.runtime.js"],
// v2026.5.3 beta reply-dispatch lazy chunks.
["provider-dispatcher-6EQEtc-t.js", "provider-dispatcher.js"],
["provider-dispatcher-BpL2E92x.js", "provider-dispatcher.js"],
["provider-dispatcher-JG96SkLX.js", "provider-dispatcher.js"],
["provider-dispatcher-6EQEtc-t.js", "provider-dispatcher.runtime.js"],
["provider-dispatcher-BpL2E92x.js", "provider-dispatcher.runtime.js"],
["provider-dispatcher-JG96SkLX.js", "provider-dispatcher.runtime.js"],
];
const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [
{

View File

@@ -427,6 +427,34 @@ describe("message tool path passthrough", () => {
});
});
describe("message tool Telegram topic targets", () => {
it("passes numeric forum topic targets and thread ids to outbound resolution", async () => {
mockSendResult({ to: "telegram:-1001234567890:topic:42" });
const call = await executeSend({
toolOptions: {
currentChannelProvider: "telegram",
currentChannelId: "telegram:-1001234567890:topic:42",
},
action: {
channel: "telegram",
target: "-1001234567890:topic:42",
threadId: "42",
message: "topic hello",
},
});
expect(call?.params).toEqual(
expect.objectContaining({
channel: "telegram",
target: "-1001234567890:topic:42",
threadId: "42",
message: "topic hello",
}),
);
});
});
describe("message tool schema scoping", () => {
const telegramPlugin = createChannelPlugin({
id: "telegram",

View File

@@ -0,0 +1 @@
export * from "./provider-dispatcher.js";

View File

@@ -123,4 +123,36 @@ describe("runMessageAction core send routing", () => {
}),
);
});
it("accepts Telegram numeric forum topic targets through plugin-owned grammar", async () => {
setActivePluginRegistry(createTestRegistry([]));
const result = await runMessageAction({
cfg: {
channels: {
telegram: {
botToken: "123:test",
},
},
} as OpenClawConfig,
action: "send",
params: {
channel: "telegram",
target: "-1001234567890:topic:42",
message: "topic hello",
},
dryRun: true,
});
if (result.kind !== "send") {
throw new Error(`Expected send result, got ${result.kind}`);
}
expect(result.to).toBe("telegram:-1001234567890:topic:42");
expect(result.payload).toEqual(
expect.objectContaining({
to: "telegram:-1001234567890:topic:42",
dryRun: true,
}),
);
});
});

View File

@@ -1,6 +1,7 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
const getLoadedChannelPluginMock = vi.hoisted(() => vi.fn());
const getChannelPluginMock = vi.hoisted(() => vi.fn());
const getActivePluginChannelRegistryVersionMock = vi.hoisted(() => vi.fn());
@@ -15,7 +16,11 @@ let normalizeTargetForProvider: TargetNormalizationModule["normalizeTargetForPro
let resetTargetNormalizerCacheForTests: TargetNormalizationModule["__testing"]["resetTargetNormalizerCacheForTests"];
vi.mock("../../channels/plugins/registry-loaded-read.js", () => ({
getLoadedChannelPluginForRead: (...args: unknown[]) => getChannelPluginMock(...args),
getLoadedChannelPluginForRead: (...args: unknown[]) => getLoadedChannelPluginMock(...args),
}));
vi.mock("../../channels/plugins/index.js", () => ({
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
}));
vi.mock("../../plugins/runtime.js", () => ({
@@ -38,6 +43,7 @@ beforeAll(async () => {
});
beforeEach(() => {
getLoadedChannelPluginMock.mockReset();
getChannelPluginMock.mockReset();
getActivePluginChannelRegistryVersionMock.mockReset();
resetTargetNormalizerCacheForTests();
@@ -58,6 +64,7 @@ describe("normalizeTargetForProvider", () => {
{
provider: "unknown",
setup: () => {
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
getChannelPluginMock.mockReturnValueOnce(undefined);
},
expected: "raw-id",
@@ -66,6 +73,7 @@ describe("normalizeTargetForProvider", () => {
provider: "alpha",
setup: () => {
getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(1);
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
getChannelPluginMock.mockReturnValueOnce(undefined);
},
expected: "raw-id",
@@ -85,7 +93,7 @@ describe("normalizeTargetForProvider", () => {
.mockReturnValueOnce(10)
.mockReturnValueOnce(10)
.mockReturnValueOnce(11);
getChannelPluginMock
getLoadedChannelPluginMock
.mockReturnValueOnce({
messaging: { normalizeTarget: firstNormalizer },
})
@@ -97,14 +105,30 @@ describe("normalizeTargetForProvider", () => {
expect(normalizeTargetForProvider("alpha", " def ")).toBe("DEF");
expect(normalizeTargetForProvider("alpha", " ghi ")).toBe("next:ghi");
expect(getChannelPluginMock).toHaveBeenCalledTimes(2);
expect(getLoadedChannelPluginMock).toHaveBeenCalledTimes(2);
expect(getChannelPluginMock).not.toHaveBeenCalled();
expect(firstNormalizer).toHaveBeenCalledTimes(2);
expect(secondNormalizer).toHaveBeenCalledTimes(1);
});
it("uses bundled/catalog target normalization when the channel is not loaded", () => {
getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(30);
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
getChannelPluginMock.mockReturnValueOnce({
messaging: {
normalizeTarget: (raw: string) =>
raw.trim() === "-1001234567890:topic:42" ? "telegram:-1001234567890:topic:42" : undefined,
},
});
expect(normalizeTargetForProvider("telegram", " -1001234567890:topic:42 ")).toBe(
"telegram:-1001234567890:topic:42",
);
});
it("returns undefined when the provider normalizer resolves to an empty value", () => {
getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(20);
getChannelPluginMock.mockReturnValueOnce({
getLoadedChannelPluginMock.mockReturnValueOnce({
messaging: {
normalizeTarget: () => "",
},
@@ -121,7 +145,7 @@ describe("resolveNormalizedTargetInput", () => {
it("returns raw and normalized values", () => {
getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(1);
getChannelPluginMock.mockReturnValueOnce({
getLoadedChannelPluginMock.mockReturnValueOnce({
messaging: {
normalizeTarget: (raw: string) => raw.trim().toUpperCase(),
},
@@ -137,7 +161,7 @@ describe("resolveNormalizedTargetInput", () => {
describe("looksLikeTargetId", () => {
it("uses plugin looksLikeId when available", () => {
const pluginLooksLikeId = vi.fn((raw: string, normalized: string) => raw !== normalized);
getChannelPluginMock.mockReturnValueOnce({
getLoadedChannelPluginMock.mockReturnValueOnce({
messaging: {
targetResolver: {
looksLikeId: pluginLooksLikeId,
@@ -158,17 +182,38 @@ describe("looksLikeTargetId", () => {
it.each(["channel:C123", "@alice", "#general", "+15551234567", "conversation:abc", "foo@thread"])(
"falls back to built-in id-like heuristics for %s",
(raw) => {
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
getChannelPluginMock.mockReturnValueOnce(undefined);
expect(looksLikeTargetId({ channel: "workspace", raw })).toBe(true);
},
);
it("uses bundled/catalog target id detection when the channel is not loaded", () => {
getLoadedChannelPluginMock.mockReturnValueOnce(undefined);
getChannelPluginMock.mockReturnValueOnce({
messaging: {
targetResolver: {
looksLikeId: (raw: string, normalized?: string) =>
raw === "-1001234567890:topic:42" && normalized === "telegram:-1001234567890:topic:42",
},
},
});
expect(
looksLikeTargetId({
channel: "telegram",
raw: "-1001234567890:topic:42",
normalized: "telegram:-1001234567890:topic:42",
}),
).toBe(true);
});
});
describe("maybeResolvePluginMessagingTarget", () => {
const cfg = {} as OpenClawConfig;
it("returns undefined when requireIdLike is set and the target is not id-like", async () => {
getChannelPluginMock.mockReturnValueOnce({
getLoadedChannelPluginMock.mockReturnValueOnce({
messaging: {
targetResolver: {
looksLikeId: () => false,
@@ -194,7 +239,7 @@ describe("maybeResolvePluginMessagingTarget", () => {
kind: "group",
display: "general",
});
getChannelPluginMock
getLoadedChannelPluginMock
.mockReturnValueOnce({
messaging: {
normalizeTarget: (raw: string) => raw.trim().toUpperCase(),
@@ -234,7 +279,7 @@ describe("maybeResolvePluginMessagingTarget", () => {
describe("buildTargetResolverSignature", () => {
it("builds stable signatures from resolver hint and looksLikeId source", () => {
const looksLikeId = (value: string) => value.startsWith("C");
getChannelPluginMock.mockReturnValueOnce({
getLoadedChannelPluginMock.mockReturnValueOnce({
messaging: {
targetResolver: {
hint: "Use channel id",
@@ -244,7 +289,7 @@ describe("buildTargetResolverSignature", () => {
});
const first = buildTargetResolverSignature("workspace");
getChannelPluginMock.mockReturnValueOnce({
getLoadedChannelPluginMock.mockReturnValueOnce({
messaging: {
targetResolver: {
hint: "Use channel id",
@@ -258,7 +303,7 @@ describe("buildTargetResolverSignature", () => {
});
it("changes when resolver metadata changes", () => {
getChannelPluginMock.mockReturnValueOnce({
getLoadedChannelPluginMock.mockReturnValueOnce({
messaging: {
targetResolver: {
hint: "Use channel id",
@@ -268,7 +313,7 @@ describe("buildTargetResolverSignature", () => {
});
const first = buildTargetResolverSignature("workspace");
getChannelPluginMock.mockReturnValueOnce({
getLoadedChannelPluginMock.mockReturnValueOnce({
messaging: {
targetResolver: {
hint: "Use user id",

View File

@@ -1,4 +1,6 @@
import { getChannelPlugin } from "../../channels/plugins/index.js";
import { getLoadedChannelPluginForRead } from "../../channels/plugins/registry-loaded-read.js";
import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js";
import type { ChannelDirectoryEntryKind, ChannelId } from "../../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { getActivePluginChannelRegistryVersion } from "../../plugins/runtime.js";
@@ -19,6 +21,10 @@ type TargetNormalizerCacheEntry = {
const targetNormalizerCacheByChannelId = new Map<string, TargetNormalizerCacheEntry>();
function resolveChannelPluginForTargetRead(channelId: ChannelId): ChannelPlugin | undefined {
return getLoadedChannelPluginForRead(channelId) ?? getChannelPlugin(channelId);
}
function resetTargetNormalizerCacheForTests(): void {
targetNormalizerCacheByChannelId.clear();
}
@@ -33,7 +39,7 @@ function resolveTargetNormalizer(channelId: ChannelId): TargetNormalizer {
if (cached && cached.version === version) {
return cached.normalizer;
}
const plugin = getLoadedChannelPluginForRead(channelId);
const plugin = resolveChannelPluginForTargetRead(channelId);
const normalizer = plugin?.messaging?.normalizeTarget;
targetNormalizerCacheByChannelId.set(channelId, {
version,
@@ -85,7 +91,7 @@ export function looksLikeTargetId(params: {
}): boolean {
const normalizedInput =
params.normalized ?? normalizeTargetForProvider(params.channel, params.raw);
const lookup = getLoadedChannelPluginForRead(params.channel)?.messaging?.targetResolver
const lookup = resolveChannelPluginForTargetRead(params.channel)?.messaging?.targetResolver
?.looksLikeId;
if (lookup) {
return lookup(params.raw, normalizedInput ?? params.raw);
@@ -117,7 +123,7 @@ export async function maybeResolvePluginMessagingTarget(params: {
if (!normalizedInput) {
return undefined;
}
const resolver = getLoadedChannelPluginForRead(params.channel)?.messaging?.targetResolver;
const resolver = resolveChannelPluginForTargetRead(params.channel)?.messaging?.targetResolver;
if (!resolver?.resolveTarget) {
return undefined;
}
@@ -150,7 +156,7 @@ export async function maybeResolvePluginMessagingTarget(params: {
}
export function buildTargetResolverSignature(channel: ChannelId): string {
const plugin = getLoadedChannelPluginForRead(channel);
const plugin = resolveChannelPluginForTargetRead(channel);
const resolver = plugin?.messaging?.targetResolver;
const hint = resolver?.hint ?? "";
const looksLike = resolver?.looksLikeId;

View File

@@ -14,17 +14,18 @@ const mocks = vi.hoisted(() => ({
listGroupsLive: vi.fn(),
resolveTarget: vi.fn(),
getChannelPlugin: vi.fn(),
getLoadedChannelPlugin: vi.fn(),
getActivePluginChannelRegistryVersion: vi.fn(() => 1),
}));
vi.mock("../../channels/plugins/index.js", () => ({
getLoadedChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args),
getLoadedChannelPlugin: (...args: unknown[]) => mocks.getLoadedChannelPlugin(...args),
getChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args),
normalizeChannelId: (value: string) => value,
}));
vi.mock("../../channels/plugins/registry-loaded-read.js", () => ({
getLoadedChannelPluginForRead: (...args: unknown[]) => mocks.getChannelPlugin(...args),
getLoadedChannelPluginForRead: (...args: unknown[]) => mocks.getLoadedChannelPlugin(...args),
}));
vi.mock("../../plugins/runtime.js", () => ({
@@ -45,6 +46,10 @@ beforeEach(() => {
mocks.listGroupsLive.mockReset();
mocks.resolveTarget.mockReset();
mocks.getChannelPlugin.mockReset();
mocks.getLoadedChannelPlugin.mockReset();
mocks.getLoadedChannelPlugin.mockImplementation((...args: unknown[]) =>
mocks.getChannelPlugin(...args),
);
mocks.getActivePluginChannelRegistryVersion.mockReset();
mocks.getActivePluginChannelRegistryVersion.mockReturnValue(1);
resetDirectoryCache();
@@ -153,6 +158,39 @@ describe("resolveMessagingTarget (directory fallback)", () => {
expect(mocks.listGroupsLive).not.toHaveBeenCalled();
});
it("uses catalog plugin target grammar for unloaded numeric topic ids", async () => {
mocks.getLoadedChannelPlugin.mockReturnValue(undefined);
mocks.getChannelPlugin.mockReturnValue({
messaging: {
normalizeTarget: (raw: string) =>
raw.trim() === "-1001234567890:topic:42"
? "telegram:-1001234567890:topic:42"
: raw.trim() || undefined,
inferTargetChatType: ({ to }: { to: string }) => (to.includes("-100") ? "group" : "direct"),
targetResolver: {
looksLikeId: (_raw: string, normalized?: string) =>
normalized === "telegram:-1001234567890:topic:42",
hint: "<chatId>",
},
},
});
const result = await expectOkResolution({
cfg,
channel: "telegram",
input: "-1001234567890:topic:42",
});
expect(result.target).toEqual({
to: "telegram:-1001234567890:topic:42",
kind: "group",
display: "telegram:-1001234567890:topic:42",
source: "normalized",
});
expect(mocks.listGroups).not.toHaveBeenCalled();
expect(mocks.listGroupsLive).not.toHaveBeenCalled();
});
it("uses plugin chat-type inference for directory lookups and plugin fallback on miss", async () => {
mocks.getChannelPlugin.mockReturnValue({
directory: {

View File

@@ -90,7 +90,7 @@ describe("tsdown config", () => {
"media-understanding/apply.runtime",
"index",
"commands/status.summary.runtime",
"auto-reply/reply/provider-dispatcher",
"provider-dispatcher.runtime",
"plugins/provider-discovery.runtime",
"plugins/provider-runtime.runtime",
"plugins/runtime/index",
@@ -112,12 +112,12 @@ describe("tsdown config", () => {
);
});
it("keeps reply dispatcher lazy runtime behind one stable dist entry", () => {
it("keeps reply dispatcher lazy runtime behind one root stable dist entry", () => {
const distGraph = unifiedDistGraph();
expect(entrySources(distGraph as TsdownConfigEntry)).toEqual(
expect.objectContaining({
"auto-reply/reply/provider-dispatcher": "src/auto-reply/reply/provider-dispatcher.ts",
"provider-dispatcher.runtime": "src/auto-reply/reply/provider-dispatcher.runtime.ts",
}),
);
});

View File

@@ -15,12 +15,12 @@ export type { ReplyPayload } from "./reply-payload.js";
export const dispatchReplyWithBufferedBlockDispatcher: DispatchReplyWithBufferedBlockDispatcher =
async (params) => {
const { dispatchReplyWithBufferedBlockDispatcher: dispatch } =
await import("../auto-reply/reply/provider-dispatcher.js");
await import("../auto-reply/reply/provider-dispatcher.runtime.js");
return await dispatch(params);
};
export const dispatchReplyWithDispatcher: DispatchReplyWithDispatcher = async (params) => {
const { dispatchReplyWithDispatcher: dispatch } =
await import("../auto-reply/reply/provider-dispatcher.js");
await import("../auto-reply/reply/provider-dispatcher.runtime.js");
return await dispatch(params);
};

View File

@@ -227,6 +227,38 @@ describe("runtime postbuild static assets", () => {
);
});
it("rewrites reply-dispatch imports to the stable provider dispatcher runtime alias", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "provider-dispatcher.runtime-NewHash.js"),
'export * from "./provider-dispatcher-ImplHash.js";\n',
"utf8",
);
await fs.writeFile(
path.join(distDir, "reply-dispatch-runtime-OldHash.js"),
['const dispatcher = () => import("./provider-dispatcher.runtime-NewHash.js");', ""].join(
"\n",
),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
writeStableRootRuntimeAliases({ rootDir });
writeLegacyRootRuntimeCompatAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "reply-dispatch-runtime-OldHash.js"), "utf8")).toBe(
['const dispatcher = () => import("./provider-dispatcher.runtime.js");', ""].join("\n"),
);
expect(await fs.readFile(path.join(distDir, "provider-dispatcher.runtime.js"), "utf8")).toBe(
'export * from "./provider-dispatcher.runtime-NewHash.js";\n',
);
expect(await fs.readFile(path.join(distDir, "provider-dispatcher-6EQEtc-t.js"), "utf8")).toBe(
'export * from "./provider-dispatcher.runtime.js";\n',
);
});
it("keeps hashed imports when a stable runtime alias would collide", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
@@ -294,8 +326,8 @@ describe("runtime postbuild static assets", () => {
"utf8",
);
await fs.writeFile(
path.join(distDir, "provider-dispatcher.js"),
'export * from "./provider-dispatcher-NewHash.js";\n',
path.join(distDir, "provider-dispatcher.runtime.js"),
'export * from "./provider-dispatcher.runtime-NewHash.js";\n',
"utf8",
);
@@ -308,7 +340,7 @@ describe("runtime postbuild static assets", () => {
await fs.readFile(path.join(distDir, "runtime-plugins.runtime-CNAfmQRG.js"), "utf8"),
).toBe('export * from "./runtime-plugins.runtime.js";\n');
expect(await fs.readFile(path.join(distDir, "provider-dispatcher-6EQEtc-t.js"), "utf8")).toBe(
'export * from "./provider-dispatcher.js";\n',
'export * from "./provider-dispatcher.runtime.js";\n',
);
});

View File

@@ -204,6 +204,7 @@ function buildCoreDistEntries(): Record<string, string> {
"agents/model-catalog.runtime": "src/agents/model-catalog.runtime.ts",
"agents/models-config.runtime": "src/agents/models-config.runtime.ts",
"cli/gateway-lifecycle.runtime": "src/cli/gateway-cli/lifecycle.runtime.ts",
"provider-dispatcher.runtime": "src/auto-reply/reply/provider-dispatcher.runtime.ts",
"server-close.runtime": "src/gateway/server-close.runtime.ts",
"plugins/memory-state": "src/plugins/memory-state.ts",
"subagent-registry.runtime": "src/agents/subagent-registry.runtime.ts",
@@ -242,7 +243,6 @@ function buildDockerE2eHarnessEntries(): Record<string, string> {
"src/agents/pi-embedded-runner/effective-tool-policy.ts",
"agents/pi-embedded-runner/run/runtime-context-prompt":
"src/agents/pi-embedded-runner/run/runtime-context-prompt.ts",
"auto-reply/reply/provider-dispatcher": "src/auto-reply/reply/provider-dispatcher.ts",
"auto-reply/reply/commands-crestodian": "src/auto-reply/reply/commands-crestodian.ts",
"cli/run-main": "src/cli/run-main.ts",
"commitments/runtime": "src/commitments/runtime.ts",