feat(core,server,web): images reach every input — steering, goal objectives, one fold (#71)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rank-YuandClaude Fable 5 authored and GitHub committed 2026-07-30 16:50:35 +08:00
1 parent de6380f278
commit 97852f43b8
40 files changed
+1474 -254

No files matched your search

+30 -1
View File
@@ -10,6 +10,7 @@ import {
buildSkillsMessage,
emptyTokenCounts,
goalFinished,
imageUrlMessage,
tokenUsage,
userText,
} from "@prismshadow/penguin-core";
@@ -154,17 +155,20 @@ describe("SessionManager.startGoal", () => {
*/
function goalFakeSession(
stream: (input: OmniMessage[]) => OmniMessage[],
): RuntimeSession & { runOpts: RunOpts[] } {
): RuntimeSession & { runOpts: RunOpts[]; runs: OmniMessage[][] } {
const runOpts: RunOpts[] = [];
const runs: OmniMessage[][] = [];
return {
sessionId: ROW.sessionId,
runOpts,
runs,
toolPermission: () => "rw",
generateTitle: async () => ({ title: null, usage: null }),
compactability: () => "ok" as const,
steer: () => false,
skipReconnectWait: () => false,
async *run(input: OmniMessage[], opts) {
runs.push(input);
runOpts.push({
...(opts.thinkingLevel !== undefined ? { thinkingLevel: opts.thinkingLevel } : {}),
...(opts.goal !== undefined ? { goal: opts.goal } : {}),
@@ -250,6 +254,31 @@ describe("SessionManager.startGoal", () => {
expect((published[0]!.payload as { text: string }).text).toContain("[use_skills]");
});
it("records the objective without the attached images: the display copy stays path-free", async () => {
// Core folds the attached images into `[attached image: …]` lines inside the objective it
// re-injects each round. The objective recorded here is the one shown to people — status
// card, goal_started, title material — so it keeps the user's words only.
const session = goalFakeSession(() => [goalFinished("complete", 1, 10)]);
const manager = makeManager(session);
const events: ChannelEvent[] = [];
channels.get(ROW.sessionId).subscribe((e) => events.push(e));
await manager.startGoal(ROW.sessionId, {
input: [userText("Match this mockup"), imageUrlMessage("data:image/png;base64,aGk=")],
budget: -1,
});
await waitFor(() => manager.statusOf(ROW.sessionId) === "idle");
// The whole input still reaches core (the images included) — only the recorded copy differs.
expect(session.runs[0]).toHaveLength(2);
const started = events
.filter((e) => e.event === "server_event")
.map((e) => JSON.parse(e.data) as { type: string; objective?: string })
.find((e) => e.type === "goal_started");
expect(started?.objective).toBe("Match this mockup");
expect(goals.latestForSession(ROW.sessionId)?.objective).toBe("Match this mockup");
});
it("409s while a goal is running (mutual exclusion); runs without a goals repo", async () => {
let release: () => void = () => {};
const gate = new Promise<void>((r) => {