fix: honor BlueBubbles chunk mode and envelope timezone
This commit is contained in:
parent
009a10bce2
commit
dfbc23496e
@ -102,7 +102,7 @@ function resolveEnvelopeTimezone(options: NormalizedEnvelopeOptions): ResolvedEn
|
||||
return explicit ? { mode: "iana", timeZone: explicit } : { mode: "utc" };
|
||||
}
|
||||
|
||||
function formatTimestamp(
|
||||
export function formatEnvelopeTimestamp(
|
||||
ts: number | Date | undefined,
|
||||
options?: EnvelopeFormatOptions,
|
||||
): string | undefined {
|
||||
@ -179,7 +179,7 @@ export function formatAgentEnvelope(params: AgentEnvelopeParams): string {
|
||||
if (params.ip?.trim()) {
|
||||
parts.push(sanitizeEnvelopeHeaderPart(params.ip.trim()));
|
||||
}
|
||||
const ts = formatTimestamp(params.timestamp, resolved);
|
||||
const ts = formatEnvelopeTimestamp(params.timestamp, resolved);
|
||||
if (ts) {
|
||||
parts.push(ts);
|
||||
}
|
||||
|
||||
@ -44,6 +44,34 @@ describe("resolveEffectiveBlockStreamingConfig", () => {
|
||||
expect(resolved.coalescing.idleMs).toBe(0);
|
||||
});
|
||||
|
||||
it("honors newline chunkMode for plugin channels even before the plugin registry is loaded", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
chunkMode: "newline",
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
blockStreamingChunk: {
|
||||
minChars: 1,
|
||||
maxChars: 4000,
|
||||
breakPreference: "paragraph",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const resolved = resolveEffectiveBlockStreamingConfig({
|
||||
cfg,
|
||||
provider: "bluebubbles",
|
||||
});
|
||||
|
||||
expect(resolved.chunking.flushOnParagraph).toBe(true);
|
||||
expect(resolved.coalescing.flushOnEnqueue).toBe(true);
|
||||
expect(resolved.coalescing.joiner).toBe("\n\n");
|
||||
});
|
||||
|
||||
it("allows ACP maxChunkChars overrides above base defaults up to provider text limits", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@ -3,26 +3,22 @@ import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { BlockStreamingCoalesceConfig } from "../../config/types.js";
|
||||
import { resolveAccountEntry } from "../../routing/account-lookup.js";
|
||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||
import {
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
listDeliverableMessageChannels,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import { resolveChunkMode, resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
||||
|
||||
const DEFAULT_BLOCK_STREAM_MIN = 800;
|
||||
const DEFAULT_BLOCK_STREAM_MAX = 1200;
|
||||
const DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS = 1000;
|
||||
const getBlockChunkProviders = () =>
|
||||
new Set<TextChunkProvider>([...listDeliverableMessageChannels(), INTERNAL_MESSAGE_CHANNEL]);
|
||||
|
||||
function normalizeChunkProvider(provider?: string): TextChunkProvider | undefined {
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
const cleaned = provider.trim().toLowerCase();
|
||||
return getBlockChunkProviders().has(cleaned as TextChunkProvider)
|
||||
? (cleaned as TextChunkProvider)
|
||||
: undefined;
|
||||
const normalized = normalizeMessageChannel(provider);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return normalized as TextChunkProvider;
|
||||
}
|
||||
|
||||
function resolveProviderChunkContext(
|
||||
|
||||
@ -21,6 +21,7 @@ import { clearCommandLane, getQueueSize } from "../../process/command-queue.js";
|
||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
|
||||
import { hasControlCommand } from "../command-detection.js";
|
||||
import { resolveEnvelopeFormatOptions } from "../envelope.js";
|
||||
import { buildInboundMediaNote } from "../media-note.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import {
|
||||
@ -292,6 +293,7 @@ export async function runPreparedReply(
|
||||
isNewSession &&
|
||||
((baseBodyTrimmedRaw.length === 0 && rawBodyTrimmed.length > 0) || isBareNewOrReset);
|
||||
const baseBodyFinal = isBareSessionReset ? buildBareSessionResetPrompt(cfg) : baseBody;
|
||||
const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
|
||||
const inboundUserContext = buildInboundUserContextPrefix(
|
||||
isNewSession
|
||||
? {
|
||||
@ -301,6 +303,7 @@ export async function runPreparedReply(
|
||||
: {}),
|
||||
}
|
||||
: { ...sessionCtx, ThreadStarterBody: undefined },
|
||||
envelopeOptions,
|
||||
);
|
||||
const baseBodyForPrompt = isBareSessionReset
|
||||
? baseBodyFinal
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withEnv } from "../../test-utils/env.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import { buildInboundMetaSystemPrompt, buildInboundUserContextPrefix } from "./inbound-meta.js";
|
||||
|
||||
@ -217,6 +218,25 @@ describe("buildInboundUserContextPrefix", () => {
|
||||
expect(conversationInfo["timestamp"]).toEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it("honors envelope user timezone for conversation timestamps", () => {
|
||||
withEnv({ TZ: "America/Los_Angeles" }, () => {
|
||||
const text = buildInboundUserContextPrefix(
|
||||
{
|
||||
ChatType: "group",
|
||||
MessageSid: "msg-with-user-tz",
|
||||
Timestamp: Date.UTC(2026, 2, 19, 0, 0),
|
||||
} as TemplateContext,
|
||||
{
|
||||
timezone: "user",
|
||||
userTimezone: "Asia/Tokyo",
|
||||
},
|
||||
);
|
||||
|
||||
const conversationInfo = parseConversationInfoPayload(text);
|
||||
expect(conversationInfo["timestamp"]).toBe("Thu 2026-03-19 09:00 GMT+9");
|
||||
});
|
||||
});
|
||||
|
||||
it("omits invalid timestamps instead of throwing", () => {
|
||||
expect(() =>
|
||||
buildInboundUserContextPrefix({
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||
import { resolveSenderLabel } from "../../channels/sender-label.js";
|
||||
import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js";
|
||||
import type { EnvelopeFormatOptions } from "../envelope.js";
|
||||
import { formatEnvelopeTimestamp } from "../envelope.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
|
||||
function safeTrim(value: unknown): string | undefined {
|
||||
@ -11,24 +12,14 @@ function safeTrim(value: unknown): string | undefined {
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function formatConversationTimestamp(value: unknown): string | undefined {
|
||||
function formatConversationTimestamp(
|
||||
value: unknown,
|
||||
envelope?: EnvelopeFormatOptions,
|
||||
): string | undefined {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
const formatted = formatZonedTimestamp(date);
|
||||
if (!formatted) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const weekday = new Intl.DateTimeFormat("en-US", { weekday: "short" }).format(date);
|
||||
return weekday ? `${weekday} ${formatted}` : formatted;
|
||||
} catch {
|
||||
return formatted;
|
||||
}
|
||||
return formatEnvelopeTimestamp(value, envelope);
|
||||
}
|
||||
|
||||
function resolveInboundChannel(ctx: TemplateContext): string | undefined {
|
||||
@ -81,7 +72,10 @@ export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildInboundUserContextPrefix(ctx: TemplateContext): string {
|
||||
export function buildInboundUserContextPrefix(
|
||||
ctx: TemplateContext,
|
||||
envelope?: EnvelopeFormatOptions,
|
||||
): string {
|
||||
const blocks: string[] = [];
|
||||
const chatType = normalizeChatType(ctx.ChatType);
|
||||
const isDirect = !chatType || chatType === "direct";
|
||||
@ -94,7 +88,7 @@ export function buildInboundUserContextPrefix(ctx: TemplateContext): string {
|
||||
const messageId = safeTrim(ctx.MessageSid);
|
||||
const messageIdFull = safeTrim(ctx.MessageSidFull);
|
||||
const resolvedMessageId = messageId ?? messageIdFull;
|
||||
const timestampStr = formatConversationTimestamp(ctx.Timestamp);
|
||||
const timestampStr = formatConversationTimestamp(ctx.Timestamp, envelope);
|
||||
|
||||
const conversationInfo = {
|
||||
message_id: shouldIncludeConversationInfo ? resolvedMessageId : undefined,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user