From cb7a446d3ea99895998baa163fafdd5f5f8265ae Mon Sep 17 00:00:00 2001 From: john Date: Sun, 5 Jul 2026 11:58:17 +0800 Subject: [PATCH] fix: ensure userVisible metadata on Goosed session replies Goosed rejects /sessions/:id/reply when user_message.metadata lacks userVisible. Normalize metadata before submit and during agent orchestration so page-generation runs no longer fail before streaming starts. Co-authored-by: Cursor --- chat-intent-router.mjs | 2 ++ goose-message.mjs | 19 +++++++++++++++++++ goose-message.test.mjs | 27 +++++++++++++++++++++++++++ tkmind-proxy.mjs | 5 +++++ tkmind-proxy.test.mjs | 41 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 goose-message.mjs create mode 100644 goose-message.test.mjs diff --git a/chat-intent-router.mjs b/chat-intent-router.mjs index 3c76d09..9e5dcc9 100644 --- a/chat-intent-router.mjs +++ b/chat-intent-router.mjs @@ -406,6 +406,8 @@ export function applyAgentOrchestrationToUserMessage(userMessage, classification displayText: displayText || undefined, chatIntentRoute: CHAT_INTENT_ROUTE.AGENT, chatIntentSource: classification?.source ?? null, + userVisible: userMessage?.metadata?.userVisible ?? true, + agentVisible: userMessage?.metadata?.agentVisible ?? true, }; return { ...userMessage, diff --git a/goose-message.mjs b/goose-message.mjs new file mode 100644 index 0000000..cc4e91b --- /dev/null +++ b/goose-message.mjs @@ -0,0 +1,19 @@ +/** + * Goosed requires user_message.metadata.userVisible (and agentVisible) on /reply. + */ + +export function ensureGooseUserMessageMetadata(userMessage) { + if (!userMessage || typeof userMessage !== 'object' || Array.isArray(userMessage)) { + return userMessage; + } + const metadata = + userMessage.metadata && typeof userMessage.metadata === 'object' && !Array.isArray(userMessage.metadata) + ? { ...userMessage.metadata } + : {}; + if (metadata.userVisible == null) metadata.userVisible = true; + if (metadata.agentVisible == null) metadata.agentVisible = true; + return { + ...userMessage, + metadata, + }; +} diff --git a/goose-message.test.mjs b/goose-message.test.mjs new file mode 100644 index 0000000..535b992 --- /dev/null +++ b/goose-message.test.mjs @@ -0,0 +1,27 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { ensureGooseUserMessageMetadata } from './goose-message.mjs'; + +test('ensureGooseUserMessageMetadata adds required visibility flags', () => { + const normalized = ensureGooseUserMessageMetadata({ + role: 'user', + content: [{ type: 'text', text: '帮我生成页面' }], + }); + assert.equal(normalized.metadata.userVisible, true); + assert.equal(normalized.metadata.agentVisible, true); +}); + +test('ensureGooseUserMessageMetadata preserves existing metadata fields', () => { + const normalized = ensureGooseUserMessageMetadata({ + role: 'user', + content: [{ type: 'text', text: '编排文本' }], + metadata: { + displayText: '用户原文', + chatIntentRoute: 'agent_orchestration', + }, + }); + assert.equal(normalized.metadata.displayText, '用户原文'); + assert.equal(normalized.metadata.chatIntentRoute, 'agent_orchestration'); + assert.equal(normalized.metadata.userVisible, true); + assert.equal(normalized.metadata.agentVisible, true); +}); diff --git a/tkmind-proxy.mjs b/tkmind-proxy.mjs index 26198d8..b23619a 100644 --- a/tkmind-proxy.mjs +++ b/tkmind-proxy.mjs @@ -16,6 +16,7 @@ import { buildCurrentTimeAgentPrefix, buildTaskRoutingAgentText } from './user-m import { reconcileAgentSession } from './session-reconcile.mjs'; import { createImgproxySigner } from './imgproxy-signer.mjs'; import { isDirectChatSessionId } from './direct-chat-service.mjs'; +import { ensureGooseUserMessageMetadata } from './goose-message.mjs'; import { memoryLimitForIntervention, resolveMemoryInterventionMode, @@ -1375,6 +1376,10 @@ export function createTkmindProxy({ await userAuth.getAgentSessionPolicy(userId), ); } + body = { + ...body, + user_message: ensureGooseUserMessageMetadata(body.user_message), + }; const target = await resolveTarget(sessionId); const upstream = await apiFetch( diff --git a/tkmind-proxy.test.mjs b/tkmind-proxy.test.mjs index eec7734..2cba4e1 100644 --- a/tkmind-proxy.test.mjs +++ b/tkmind-proxy.test.mjs @@ -16,6 +16,7 @@ import { createMemoryV2 } from './memory-v2.mjs'; async function withFakeGoosedSession(handler) { const workingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memind-memory-v2-')); const harnessEntries = []; + const replyBodies = []; let server; try { @@ -52,6 +53,7 @@ async function withFakeGoosedSession(handler) { return; } if (req.method === 'POST' && req.url === '/sessions/session-1/reply') { + replyBodies.push(body); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true })); return; @@ -77,6 +79,7 @@ async function withFakeGoosedSession(handler) { apiTarget: `http://127.0.0.1:${port}`, workingDir, harnessEntries, + replyBodies, }); } finally { if (server?.listening) { @@ -538,6 +541,44 @@ test('getRuntimeStatus preserves Memory V2 status contract for release gates', a }); }); +test('submitSessionReplyForUser adds goose metadata visibility flags before reply', async () => { + await withFakeGoosedSession(async ({ apiTarget, workingDir, replyBodies }) => { + const proxy = createTkmindProxy({ + apiTarget, + apiSecret: 'test-secret', + userAuth: { + ...createMemoryTestUserAuth(workingDir), + async ownsSession() { + return true; + }, + async canUseChat() { + return { ok: true }; + }, + async getUserById() { + return { id: 'user-1' }; + }, + async resolveUserPolicies() { + return { unrestricted: true, policies: {} }; + }, + }, + }); + + await proxy.submitSessionReplyForUser( + 'user-1', + 'session-1', + 'request-goose-meta', + { + role: 'user', + content: [{ type: 'text', text: '帮我生成一个分享页面' }], + }, + ); + + assert.equal(replyBodies.length, 1); + assert.equal(replyBodies[0]?.user_message?.metadata?.userVisible, true); + assert.equal(replyBodies[0]?.user_message?.metadata?.agentVisible, true); + }); +}); + test('submitSessionReplyForUser passes current prompt to Memory V2 resolve before existing reply path', async () => { let resolveInput = null; await withFakeGoosedSession(async ({ apiTarget, workingDir }) => {