mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 00:50:42 +00:00
fix(runtime): preserve reviewed routing and transcript behavior (#79076)
* fix(runtime): preserve reviewed routing and transcript behavior * docs(changelog): note runtime review fixes
This commit is contained in:
@@ -192,6 +192,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discord/Gateway: preserve username target resolution for Discord outbound sends and rotate generated transcript paths when gateway sessions reset.
|
||||
- Dependencies: pin the transitive `fast-uri` production dependency to `3.1.2` so the production dependency audit no longer resolves the vulnerable `<=3.1.1` range. Thanks @shakkernerd.
|
||||
- Cron/agents: recognize same-target `edit`↔`write` recovery in `isSameToolMutationAction`, so a successful `write` to a path clears an earlier failed `edit` on the same path. Stops cron from reporting fatal failures when an agent self-heals across `edit` and `write`, while preserving same-tool fingerprint matching, blocking different-target writes, and excluding tools (including `apply_patch`) whose real call args do not produce a stable `path` fingerprint segment. Fixes #79024. Thanks @RenzoMXD.
|
||||
- Gateway/Tailscale: add opt-in `gateway.tailscale.preserveFunnel` so when `tailscale.mode = "serve"` and an externally configured Tailscale Funnel route already covers the gateway port, OpenClaw skips re-applying `tailscale serve` on startup and skips the `resetOnExit` teardown for that run, keeping operator-managed Funnel exposure alive across gateway restarts. Fixes #57241. Thanks @RenzoMXD.
|
||||
|
||||
@@ -20,6 +20,9 @@ export const loadDiscordResolveUsersModule = createLazyRuntimeModule(
|
||||
export const loadDiscordThreadBindingsManagerModule = createLazyRuntimeModule(
|
||||
() => import("./monitor/thread-bindings.manager.js"),
|
||||
);
|
||||
export const loadDiscordTargetResolverModule = createLazyRuntimeModule(
|
||||
() => import("./target-resolver.js"),
|
||||
);
|
||||
|
||||
export async function loadDiscordProviderRuntime() {
|
||||
discordProviderRuntimePromise ??= import("./monitor/provider.runtime.js");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createStartAccountContext } from "openclaw/plugin-sdk/channel-test-help
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ResolvedDiscordAccount } from "./accounts.js";
|
||||
import * as directoryLive from "./directory-live.js";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
import * as sendModule from "./send.js";
|
||||
import { createDiscordSendReceipt } from "./send.receipt.js";
|
||||
@@ -173,6 +174,33 @@ describe("discordPlugin outbound", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves Discord usernames through the messaging target resolver", async () => {
|
||||
vi.spyOn(directoryLive, "listDiscordDirectoryPeersLive").mockResolvedValueOnce([
|
||||
{ kind: "user", id: "user:999", name: "Jane" } as const,
|
||||
]);
|
||||
const resolveTarget = discordPlugin.messaging?.targetResolver?.resolveTarget;
|
||||
if (!resolveTarget) {
|
||||
throw new Error(
|
||||
"Expected discordPlugin.messaging.targetResolver.resolveTarget to be defined",
|
||||
);
|
||||
}
|
||||
|
||||
await expect(
|
||||
resolveTarget({
|
||||
cfg: createCfg(),
|
||||
accountId: "default",
|
||||
input: "jane",
|
||||
normalized: "channel:jane",
|
||||
preferredKind: "user",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
to: "user:999",
|
||||
kind: "user",
|
||||
display: "jane",
|
||||
source: "directory",
|
||||
});
|
||||
});
|
||||
|
||||
it("honors per-account replyToMode overrides", () => {
|
||||
const resolveReplyToMode = discordPlugin.threading?.resolveReplyToMode;
|
||||
if (!resolveReplyToMode) {
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
loadDiscordResolveChannelsModule,
|
||||
loadDiscordResolveUsersModule,
|
||||
loadDiscordSendModule,
|
||||
loadDiscordTargetResolverModule,
|
||||
loadDiscordThreadBindingsManagerModule,
|
||||
} from "./channel.loaders.js";
|
||||
import { shouldSuppressLocalDiscordExecApprovalPrompt } from "./exec-approvals.js";
|
||||
@@ -326,6 +327,28 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeDiscordTargetId,
|
||||
hint: "<channelId|user:ID|channel:ID>",
|
||||
resolveTarget: async ({ cfg, accountId, input, normalized, preferredKind }) => {
|
||||
const resolved = await (
|
||||
await loadDiscordTargetResolverModule()
|
||||
).resolveDiscordTarget(
|
||||
input,
|
||||
{ cfg, accountId },
|
||||
preferredKind === "user"
|
||||
? { defaultKind: "user" }
|
||||
: preferredKind === "channel" || preferredKind === "group"
|
||||
? { defaultKind: "channel" }
|
||||
: {},
|
||||
);
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
to: resolved.normalized,
|
||||
kind: resolved.kind === "user" ? "user" : "channel",
|
||||
display: resolved.raw,
|
||||
source: resolved.normalized === normalized ? "normalized" : "directory",
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
approvalCapability: getDiscordApprovalCapability(),
|
||||
|
||||
@@ -470,6 +470,38 @@ describe("gateway agent handler", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("drops a stale transcript path when a stale session rotates ids", async () => {
|
||||
vi.useFakeTimers({ toFake: ["Date"] });
|
||||
dateOnlyFakeClockActive = true;
|
||||
vi.setSystemTime(new Date("2026-05-07T12:00:00.000Z"));
|
||||
const staleEntry = {
|
||||
sessionId: "old-session-id",
|
||||
sessionFile: "/tmp/openclaw/agents/main/sessions/old-session-id.jsonl",
|
||||
updatedAt: 0,
|
||||
sessionStartedAt: 0,
|
||||
};
|
||||
mockMainSessionEntry(staleEntry);
|
||||
|
||||
let capturedEntry: Record<string, unknown> | undefined;
|
||||
mocks.updateSessionStore.mockImplementation(async (_path, updater) => {
|
||||
const store: Record<string, unknown> = {
|
||||
"agent:main:main": { ...staleEntry },
|
||||
};
|
||||
const result = await updater(store);
|
||||
capturedEntry = result as Record<string, unknown>;
|
||||
return result;
|
||||
});
|
||||
mocks.agentCommand.mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { durationMs: 100 },
|
||||
});
|
||||
|
||||
await runMainAgent("test", "test-idem-stale-transcript");
|
||||
|
||||
expect(capturedEntry?.sessionId).not.toBe("old-session-id");
|
||||
expect(capturedEntry?.sessionFile).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps stored group metadata when a trusted group session receives caller-supplied selectors", async () => {
|
||||
const sessionKey = "agent:main:slack:group:C123";
|
||||
const existingEntry = buildExistingMainStoreEntry({
|
||||
|
||||
@@ -1064,6 +1064,8 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
groupChannel: resolvedGroupChannel,
|
||||
space: resolvedGroupSpace,
|
||||
...(pluginOwnerId ? { pluginOwnerId } : {}),
|
||||
sessionFile:
|
||||
entry?.sessionId && entry.sessionId !== sessionId ? undefined : entry?.sessionFile,
|
||||
cliSessionIds: entry?.cliSessionIds,
|
||||
cliSessionBindings: entry?.cliSessionBindings,
|
||||
claudeCliSessionId: entry?.claudeCliSessionId,
|
||||
|
||||
Reference in New Issue
Block a user