feat: mirror delivered outbound messages (#1031)

Co-authored-by: T Savo <TSavo@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-17 01:48:02 +00:00
parent 3fb699a84b
commit fdaeada3ec
26 changed files with 697 additions and 29 deletions

View File

@@ -0,0 +1,114 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
appendAssistantMessageToSessionTranscript,
resolveMirroredTranscriptText,
} from "./transcript.js";
describe("resolveMirroredTranscriptText", () => {
it("prefers media filenames over text", () => {
const result = resolveMirroredTranscriptText({
text: "caption here",
mediaUrls: ["https://example.com/files/report.pdf?sig=123"],
});
expect(result).toBe("report.pdf");
});
it("returns trimmed text when no media", () => {
const result = resolveMirroredTranscriptText({ text: " hello " });
expect(result).toBe("hello");
});
});
describe("appendAssistantMessageToSessionTranscript", () => {
let tempDir: string;
let storePath: string;
let sessionsDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "transcript-test-"));
sessionsDir = path.join(tempDir, "agents", "main", "sessions");
fs.mkdirSync(sessionsDir, { recursive: true });
storePath = path.join(sessionsDir, "sessions.json");
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("returns error for missing sessionKey", async () => {
const result = await appendAssistantMessageToSessionTranscript({
sessionKey: "",
text: "test",
storePath,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.reason).toBe("missing sessionKey");
}
});
it("returns error for empty text", async () => {
const result = await appendAssistantMessageToSessionTranscript({
sessionKey: "test-session",
text: " ",
storePath,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.reason).toBe("empty text");
}
});
it("returns error for unknown sessionKey", async () => {
fs.writeFileSync(storePath, JSON.stringify({}), "utf-8");
const result = await appendAssistantMessageToSessionTranscript({
sessionKey: "nonexistent",
text: "test message",
storePath,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.reason).toContain("unknown sessionKey");
}
});
it("creates transcript file and appends message for valid session", async () => {
const sessionId = "test-session-id";
const sessionKey = "test-session";
const store = {
[sessionKey]: {
sessionId,
chatType: "direct",
channel: "discord",
},
};
fs.writeFileSync(storePath, JSON.stringify(store), "utf-8");
const result = await appendAssistantMessageToSessionTranscript({
sessionKey,
text: "Hello from delivery mirror!",
storePath,
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(fs.existsSync(result.sessionFile)).toBe(true);
const lines = fs.readFileSync(result.sessionFile, "utf-8").trim().split("\n");
expect(lines.length).toBe(2); // header + message
const header = JSON.parse(lines[0]);
expect(header.type).toBe("session");
expect(header.id).toBe(sessionId);
const messageLine = JSON.parse(lines[1]);
expect(messageLine.type).toBe("message");
expect(messageLine.message.role).toBe("assistant");
expect(messageLine.message.content[0].type).toBe("text");
expect(messageLine.message.content[0].text).toBe("Hello from delivery mirror!");
}
});
});

View File

@@ -0,0 +1,131 @@
import fs from "node:fs";
import path from "node:path";
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
import type { SessionEntry } from "./types.js";
import { loadSessionStore, updateSessionStore } from "./store.js";
import { resolveDefaultSessionStorePath, resolveSessionTranscriptPath } from "./paths.js";
function stripQuery(value: string): string {
const noHash = value.split("#")[0] ?? value;
return noHash.split("?")[0] ?? noHash;
}
function extractFileNameFromMediaUrl(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) return null;
const cleaned = stripQuery(trimmed);
try {
const parsed = new URL(cleaned);
const base = path.basename(parsed.pathname);
if (!base) return null;
try {
return decodeURIComponent(base);
} catch {
return base;
}
} catch {
const base = path.basename(cleaned);
if (!base || base === "/" || base === ".") return null;
return base;
}
}
export function resolveMirroredTranscriptText(params: {
text?: string;
mediaUrls?: string[];
}): string | null {
const mediaUrls = params.mediaUrls?.filter((url) => url && url.trim()) ?? [];
if (mediaUrls.length > 0) {
const names = mediaUrls
.map((url) => extractFileNameFromMediaUrl(url))
.filter((name): name is string => Boolean(name && name.trim()));
if (names.length > 0) return names.join(", ");
return "media";
}
const text = params.text ?? "";
const trimmed = text.trim();
return trimmed ? trimmed : null;
}
async function ensureSessionHeader(params: {
sessionFile: string;
sessionId: string;
}): Promise<void> {
if (fs.existsSync(params.sessionFile)) return;
await fs.promises.mkdir(path.dirname(params.sessionFile), { recursive: true });
const header = {
type: "session",
version: CURRENT_SESSION_VERSION,
id: params.sessionId,
timestamp: new Date().toISOString(),
cwd: process.cwd(),
};
await fs.promises.writeFile(params.sessionFile, `${JSON.stringify(header)}\n`, "utf-8");
}
export async function appendAssistantMessageToSessionTranscript(params: {
agentId?: string;
sessionKey: string;
text?: string;
mediaUrls?: string[];
/** Optional override for store path (mostly for tests). */
storePath?: string;
}): Promise<{ ok: true; sessionFile: string } | { ok: false; reason: string }> {
const sessionKey = params.sessionKey.trim();
if (!sessionKey) return { ok: false, reason: "missing sessionKey" };
const mirrorText = resolveMirroredTranscriptText({
text: params.text,
mediaUrls: params.mediaUrls,
});
if (!mirrorText) return { ok: false, reason: "empty text" };
const storePath = params.storePath ?? resolveDefaultSessionStorePath(params.agentId);
const store = loadSessionStore(storePath, { skipCache: true });
const entry = store[sessionKey] as SessionEntry | undefined;
if (!entry?.sessionId) return { ok: false, reason: `unknown sessionKey: ${sessionKey}` };
const sessionFile =
entry.sessionFile?.trim() || resolveSessionTranscriptPath(entry.sessionId, params.agentId);
await ensureSessionHeader({ sessionFile, sessionId: entry.sessionId });
const sessionManager = SessionManager.open(sessionFile);
sessionManager.appendMessage({
role: "assistant",
content: [{ type: "text", text: mirrorText }],
api: "openai-responses",
provider: "clawdbot",
model: "delivery-mirror",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
},
stopReason: "stop",
timestamp: Date.now(),
});
if (!entry.sessionFile || entry.sessionFile !== sessionFile) {
await updateSessionStore(storePath, (current) => {
current[sessionKey] = {
...entry,
sessionFile,
};
});
}
return { ok: true, sessionFile };
}