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 <cursoragent@cursor.com>
This commit is contained in:
@@ -406,6 +406,8 @@ export function applyAgentOrchestrationToUserMessage(userMessage, classification
|
|||||||
displayText: displayText || undefined,
|
displayText: displayText || undefined,
|
||||||
chatIntentRoute: CHAT_INTENT_ROUTE.AGENT,
|
chatIntentRoute: CHAT_INTENT_ROUTE.AGENT,
|
||||||
chatIntentSource: classification?.source ?? null,
|
chatIntentSource: classification?.source ?? null,
|
||||||
|
userVisible: userMessage?.metadata?.userVisible ?? true,
|
||||||
|
agentVisible: userMessage?.metadata?.agentVisible ?? true,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
...userMessage,
|
...userMessage,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -16,6 +16,7 @@ import { buildCurrentTimeAgentPrefix, buildTaskRoutingAgentText } from './user-m
|
|||||||
import { reconcileAgentSession } from './session-reconcile.mjs';
|
import { reconcileAgentSession } from './session-reconcile.mjs';
|
||||||
import { createImgproxySigner } from './imgproxy-signer.mjs';
|
import { createImgproxySigner } from './imgproxy-signer.mjs';
|
||||||
import { isDirectChatSessionId } from './direct-chat-service.mjs';
|
import { isDirectChatSessionId } from './direct-chat-service.mjs';
|
||||||
|
import { ensureGooseUserMessageMetadata } from './goose-message.mjs';
|
||||||
import {
|
import {
|
||||||
memoryLimitForIntervention,
|
memoryLimitForIntervention,
|
||||||
resolveMemoryInterventionMode,
|
resolveMemoryInterventionMode,
|
||||||
@@ -1375,6 +1376,10 @@ export function createTkmindProxy({
|
|||||||
await userAuth.getAgentSessionPolicy(userId),
|
await userAuth.getAgentSessionPolicy(userId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
body = {
|
||||||
|
...body,
|
||||||
|
user_message: ensureGooseUserMessageMetadata(body.user_message),
|
||||||
|
};
|
||||||
|
|
||||||
const target = await resolveTarget(sessionId);
|
const target = await resolveTarget(sessionId);
|
||||||
const upstream = await apiFetch(
|
const upstream = await apiFetch(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { createMemoryV2 } from './memory-v2.mjs';
|
|||||||
async function withFakeGoosedSession(handler) {
|
async function withFakeGoosedSession(handler) {
|
||||||
const workingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memind-memory-v2-'));
|
const workingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memind-memory-v2-'));
|
||||||
const harnessEntries = [];
|
const harnessEntries = [];
|
||||||
|
const replyBodies = [];
|
||||||
let server;
|
let server;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -52,6 +53,7 @@ async function withFakeGoosedSession(handler) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (req.method === 'POST' && req.url === '/sessions/session-1/reply') {
|
if (req.method === 'POST' && req.url === '/sessions/session-1/reply') {
|
||||||
|
replyBodies.push(body);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ ok: true }));
|
res.end(JSON.stringify({ ok: true }));
|
||||||
return;
|
return;
|
||||||
@@ -77,6 +79,7 @@ async function withFakeGoosedSession(handler) {
|
|||||||
apiTarget: `http://127.0.0.1:${port}`,
|
apiTarget: `http://127.0.0.1:${port}`,
|
||||||
workingDir,
|
workingDir,
|
||||||
harnessEntries,
|
harnessEntries,
|
||||||
|
replyBodies,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (server?.listening) {
|
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 () => {
|
test('submitSessionReplyForUser passes current prompt to Memory V2 resolve before existing reply path', async () => {
|
||||||
let resolveInput = null;
|
let resolveInput = null;
|
||||||
await withFakeGoosedSession(async ({ apiTarget, workingDir }) => {
|
await withFakeGoosedSession(async ({ apiTarget, workingDir }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user