feat(context): wire headroom proxy and injection budget shadow paths
Add fail-open headroom provider routing for goosed sync, context-budget planning with shadow telemetry events, and fix portal-resume smoke to skip force_deep_reasoning by default so session round-trip checks stay fast. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -52,6 +52,8 @@ import {
|
||||
} from './chat-task-intent-config.mjs';
|
||||
import { extractAgentRunExperience } from './experience-extractor.mjs';
|
||||
import { executeRainPipeline, isRainModeMessage } from './rain-service/index.mjs';
|
||||
import { buildContextBudgetResolvedEvent, resolveContextBudgetMode } from './context-budget.mjs';
|
||||
import { buildHeadroomRunObservation, resolveHeadroomMode } from './memind-headroom-policy.mjs';
|
||||
|
||||
const DEFAULT_RUN_RETRY_DELAYS_MS = [1500, 5000, 15000];
|
||||
const TERMINAL_STATUSES = new Set(['succeeded', 'failed']);
|
||||
@@ -1946,6 +1948,11 @@ export function createAgentRunGateway({
|
||||
sessionId: row.agent_session_id ?? null,
|
||||
});
|
||||
await appendEvent(runId, 'intent_routed', routing);
|
||||
if (resolveHeadroomMode() !== 'off') {
|
||||
await appendEvent(runId, 'headroom_context_observed', buildHeadroomRunObservation({
|
||||
skillId: routing?.suggestedSkill ?? routing?.suggested_skill ?? null,
|
||||
}));
|
||||
}
|
||||
if (routingDecision === CHAT_INTENT_ROUTE.AGENT && chatIntentRouter?.applyAgentOrchestration) {
|
||||
const grantedSkills = await resolveGrantedSkills(row.user_id);
|
||||
let goalContext = null;
|
||||
@@ -1997,11 +2004,32 @@ export function createAgentRunGateway({
|
||||
}
|
||||
}
|
||||
if (!rainHandoff) {
|
||||
let preparedContext = null;
|
||||
if (
|
||||
resolveContextBudgetMode() !== 'off' &&
|
||||
chatIntentRouter?.prepareAgentOrchestrationContext
|
||||
) {
|
||||
preparedContext = chatIntentRouter.prepareAgentOrchestrationContext(
|
||||
userMessage,
|
||||
routing,
|
||||
{
|
||||
grantedSkills,
|
||||
memoryContext: agentMemoryContext,
|
||||
goalContext,
|
||||
runtimeContext,
|
||||
},
|
||||
);
|
||||
const budgetEvent = buildContextBudgetResolvedEvent(preparedContext.budgetPlan);
|
||||
if (budgetEvent) {
|
||||
await appendEvent(runId, 'context_budget_resolved', budgetEvent);
|
||||
}
|
||||
}
|
||||
userMessage = chatIntentRouter.applyAgentOrchestration(userMessage, routing, {
|
||||
grantedSkills,
|
||||
memoryContext: agentMemoryContext,
|
||||
goalContext,
|
||||
runtimeContext,
|
||||
preparedContext,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+45
-6
@@ -9,6 +9,7 @@ import {
|
||||
isProductCampaignIntent,
|
||||
PAGE_DATA_COLLECT_SKILL_NAME,
|
||||
} from './chat-skills.mjs';
|
||||
import { applyContextBudgetToOrchestration } from './context-budget.mjs';
|
||||
import { isDirectChatSessionId } from './direct-chat-service.mjs';
|
||||
import {
|
||||
memoryLimitForIntervention,
|
||||
@@ -1175,20 +1176,56 @@ export function buildAgentOrchestrationAgentText({
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function applyAgentOrchestrationToUserMessage(
|
||||
export function prepareAgentOrchestrationContext(
|
||||
userMessage,
|
||||
classification,
|
||||
{ grantedSkills = [], memoryContext = null, goalContext = null, runtimeContext = null } = {},
|
||||
) {
|
||||
const displayText = messageDisplayText(userMessage);
|
||||
const skillPrompt = resolveSkillPrompt(classification?.suggestedSkill, grantedSkills, displayText);
|
||||
const agentText = buildAgentOrchestrationAgentText({
|
||||
displayText,
|
||||
classification,
|
||||
skillPrompt,
|
||||
const budgeted = applyContextBudgetToOrchestration({
|
||||
memoryContext,
|
||||
goalContext,
|
||||
runtimeContext,
|
||||
skillPrompt,
|
||||
userTask: displayText,
|
||||
});
|
||||
return {
|
||||
displayText,
|
||||
classification,
|
||||
skillPrompt: budgeted.skillPrompt,
|
||||
memoryContext: budgeted.memoryContext,
|
||||
goalContext: budgeted.goalContext,
|
||||
runtimeContext: budgeted.runtimeContext,
|
||||
budgetPlan: budgeted.plan,
|
||||
budgetApplied: budgeted.applied,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyAgentOrchestrationToUserMessage(
|
||||
userMessage,
|
||||
classification,
|
||||
{
|
||||
grantedSkills = [],
|
||||
memoryContext = null,
|
||||
goalContext = null,
|
||||
runtimeContext = null,
|
||||
preparedContext = null,
|
||||
} = {},
|
||||
) {
|
||||
const prepared = preparedContext ?? prepareAgentOrchestrationContext(userMessage, classification, {
|
||||
grantedSkills,
|
||||
memoryContext,
|
||||
goalContext,
|
||||
runtimeContext,
|
||||
});
|
||||
const agentText = buildAgentOrchestrationAgentText({
|
||||
displayText: prepared.displayText,
|
||||
classification: prepared.classification,
|
||||
skillPrompt: prepared.skillPrompt,
|
||||
memoryContext: prepared.memoryContext,
|
||||
goalContext: prepared.goalContext,
|
||||
runtimeContext: prepared.runtimeContext,
|
||||
});
|
||||
const content = Array.isArray(userMessage?.content)
|
||||
? userMessage.content.map((item, index) => {
|
||||
@@ -1200,7 +1237,7 @@ export function applyAgentOrchestrationToUserMessage(
|
||||
: [{ type: 'text', text: agentText }];
|
||||
const metadata = {
|
||||
...(userMessage?.metadata ?? {}),
|
||||
displayText: displayText || undefined,
|
||||
displayText: prepared.displayText || undefined,
|
||||
chatIntentRoute: CHAT_INTENT_ROUTE.AGENT,
|
||||
chatIntentSource: classification?.source ?? null,
|
||||
userVisible: userMessage?.metadata?.userVisible ?? true,
|
||||
@@ -1901,6 +1938,7 @@ export function createChatIntentRouter(options = {}) {
|
||||
classifySessionAction,
|
||||
resolveAgentMemoryContext,
|
||||
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
|
||||
prepareAgentOrchestrationContext,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2005,5 +2043,6 @@ export function createManagedChatIntentRouter({
|
||||
},
|
||||
|
||||
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
|
||||
prepareAgentOrchestrationContext,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export const CONTEXT_BUDGET_MODES = Object.freeze(['off', 'shadow', 'active']);
|
||||
export const DEFAULT_CONTEXT_BUDGET_MAX_CHARS = 12_000;
|
||||
|
||||
const SOURCE_PRIORITY = Object.freeze({
|
||||
user_task: 100,
|
||||
skill_prompt: 90,
|
||||
goal: 85,
|
||||
temporal: 75,
|
||||
memory: 65,
|
||||
harness: 50,
|
||||
});
|
||||
|
||||
const HARNESS_TITLE_PRIORITY = Object.freeze({
|
||||
'TKMind 用户空间沙箱': 95,
|
||||
'TKMind 当前时间基准': 94,
|
||||
'TKMind 代码委托策略': 88,
|
||||
'TKMind 日程写入规则': 82,
|
||||
'TKMind 定时自动任务规则': 82,
|
||||
'TKMind 长期记忆规则': 58,
|
||||
'TKMind 用户偏好画像': 57,
|
||||
'TKMind 已沉淀用户记忆': 56,
|
||||
});
|
||||
|
||||
function normalizeMode(value, fallback = 'off') {
|
||||
const raw = String(value ?? fallback).trim().toLowerCase();
|
||||
return CONTEXT_BUDGET_MODES.includes(raw) ? raw : fallback;
|
||||
}
|
||||
|
||||
export function resolveContextBudgetMode(env = process.env) {
|
||||
return normalizeMode(env.MEMIND_CONTEXT_BUDGET_MODE, 'off');
|
||||
}
|
||||
|
||||
export function resolveContextBudgetMaxChars(env = process.env) {
|
||||
const raw = Number(env.MEMIND_CONTEXT_BUDGET_MAX_CHARS ?? DEFAULT_CONTEXT_BUDGET_MAX_CHARS);
|
||||
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_CONTEXT_BUDGET_MAX_CHARS;
|
||||
}
|
||||
|
||||
export function fingerprintContent(text) {
|
||||
const normalized = String(text ?? '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!normalized) return null;
|
||||
return crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
function blockPriority(block) {
|
||||
const source = String(block?.source ?? '').trim();
|
||||
const base = SOURCE_PRIORITY[source] ?? 40;
|
||||
if (source !== 'harness') return base;
|
||||
const title = String(block?.title ?? block?.label ?? '').trim();
|
||||
return HARNESS_TITLE_PRIORITY[title] ?? base;
|
||||
}
|
||||
|
||||
function blockChars(block) {
|
||||
return String(block?.text ?? block?.content ?? '').length;
|
||||
}
|
||||
|
||||
function normalizeMemoryBlock(item) {
|
||||
const label = String(item?.label ?? item?.title ?? '').trim();
|
||||
const text = String(item?.text ?? item?.content ?? item?.summary ?? '').trim();
|
||||
if (!text) return null;
|
||||
return {
|
||||
source: 'memory',
|
||||
id: String(item?.id ?? item?.memoryId ?? label ?? text.slice(0, 32)),
|
||||
label: label || null,
|
||||
text,
|
||||
fingerprint: fingerprintContent(text),
|
||||
priority: SOURCE_PRIORITY.memory,
|
||||
chars: text.length,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHarnessBlock(entry) {
|
||||
const title = String(entry?.title ?? '').trim();
|
||||
const text = String(entry?.content ?? entry?.text ?? '').trim();
|
||||
if (!text) return null;
|
||||
return {
|
||||
source: 'harness',
|
||||
id: title || text.slice(0, 32),
|
||||
title: title || null,
|
||||
text,
|
||||
fingerprint: fingerprintContent(text),
|
||||
priority: blockPriority({ source: 'harness', title }),
|
||||
chars: text.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildContextBudgetBlocks({
|
||||
memories = [],
|
||||
temporalText = '',
|
||||
goalEnvelope = '',
|
||||
harnessEntries = [],
|
||||
skillPrompt = '',
|
||||
userTask = '',
|
||||
} = {}) {
|
||||
const blocks = [];
|
||||
|
||||
const task = String(userTask ?? '').trim();
|
||||
if (task) {
|
||||
blocks.push({
|
||||
source: 'user_task',
|
||||
id: 'user_task',
|
||||
text: task,
|
||||
fingerprint: fingerprintContent(task),
|
||||
priority: SOURCE_PRIORITY.user_task,
|
||||
chars: task.length,
|
||||
});
|
||||
}
|
||||
|
||||
const skill = String(skillPrompt ?? '').trim();
|
||||
if (skill) {
|
||||
blocks.push({
|
||||
source: 'skill_prompt',
|
||||
id: 'skill_prompt',
|
||||
text: skill,
|
||||
fingerprint: fingerprintContent(skill),
|
||||
priority: SOURCE_PRIORITY.skill_prompt,
|
||||
chars: skill.length,
|
||||
});
|
||||
}
|
||||
|
||||
const goal = String(goalEnvelope ?? '').trim();
|
||||
if (goal) {
|
||||
blocks.push({
|
||||
source: 'goal',
|
||||
id: 'goal',
|
||||
text: goal,
|
||||
fingerprint: fingerprintContent(goal),
|
||||
priority: SOURCE_PRIORITY.goal,
|
||||
chars: goal.length,
|
||||
});
|
||||
}
|
||||
|
||||
const temporal = String(temporalText ?? '').trim();
|
||||
if (temporal) {
|
||||
blocks.push({
|
||||
source: 'temporal',
|
||||
id: 'temporal',
|
||||
text: temporal,
|
||||
fingerprint: fingerprintContent(temporal),
|
||||
priority: SOURCE_PRIORITY.temporal,
|
||||
chars: temporal.length,
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of Array.isArray(memories) ? memories : []) {
|
||||
const block = normalizeMemoryBlock(item);
|
||||
if (block) blocks.push(block);
|
||||
}
|
||||
|
||||
for (const entry of Array.isArray(harnessEntries) ? harnessEntries : []) {
|
||||
const block = normalizeHarnessBlock(entry);
|
||||
if (block) blocks.push(block);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
export function planContextBudget(input = {}, { maxChars, env = process.env } = {}) {
|
||||
const mode = resolveContextBudgetMode(env);
|
||||
const budget = Number.isFinite(maxChars) ? maxChars : resolveContextBudgetMaxChars(env);
|
||||
const blocks = buildContextBudgetBlocks(input);
|
||||
const seenFingerprints = new Set();
|
||||
const kept = [];
|
||||
const droppedItems = [];
|
||||
const duplicateItems = [];
|
||||
|
||||
const sorted = [...blocks].sort((a, b) => {
|
||||
const priorityDelta = blockPriority(b) - blockPriority(a);
|
||||
if (priorityDelta !== 0) return priorityDelta;
|
||||
return blockChars(a) - blockChars(b);
|
||||
});
|
||||
|
||||
let totalChars = 0;
|
||||
for (const block of sorted) {
|
||||
if (block.source === 'user_task') {
|
||||
kept.push(block);
|
||||
totalChars += block.chars;
|
||||
if (block.fingerprint) seenFingerprints.add(block.fingerprint);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (block.fingerprint && seenFingerprints.has(block.fingerprint)) {
|
||||
duplicateItems.push({
|
||||
source: block.source,
|
||||
id: block.id,
|
||||
reason: 'duplicate_fingerprint',
|
||||
fingerprint: block.fingerprint,
|
||||
chars: block.chars,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (totalChars + block.chars > budget) {
|
||||
droppedItems.push({
|
||||
source: block.source,
|
||||
id: block.id,
|
||||
reason: 'over_budget',
|
||||
fingerprint: block.fingerprint ?? null,
|
||||
chars: block.chars,
|
||||
priority: blockPriority(block),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
kept.push(block);
|
||||
totalChars += block.chars;
|
||||
if (block.fingerprint) seenFingerprints.add(block.fingerprint);
|
||||
}
|
||||
|
||||
const fingerprints = [...seenFingerprints];
|
||||
return {
|
||||
mode,
|
||||
budget,
|
||||
blocks: kept,
|
||||
droppedItems,
|
||||
duplicateItems,
|
||||
totalChars,
|
||||
fingerprints,
|
||||
inputBlockCount: blocks.length,
|
||||
keptBlockCount: kept.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyContextBudgetToOrchestration({
|
||||
memoryContext = null,
|
||||
goalContext = null,
|
||||
runtimeContext = null,
|
||||
skillPrompt = '',
|
||||
userTask = '',
|
||||
plan = null,
|
||||
env = process.env,
|
||||
} = {}) {
|
||||
const mode = resolveContextBudgetMode(env);
|
||||
if (mode === 'off') {
|
||||
return {
|
||||
memoryContext,
|
||||
goalContext,
|
||||
runtimeContext,
|
||||
skillPrompt,
|
||||
plan: null,
|
||||
applied: false,
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedPlan = plan ?? planContextBudget({
|
||||
memories: memoryContext?.injectionEnabled ? memoryContext.memories : [],
|
||||
temporalText: runtimeContext?.injectionEnabled ? runtimeContext.injectionText : '',
|
||||
goalEnvelope: goalContext?.injectionEnabled ? goalContext.envelope : '',
|
||||
skillPrompt,
|
||||
userTask,
|
||||
}, { env });
|
||||
|
||||
if (mode === 'shadow') {
|
||||
return {
|
||||
memoryContext,
|
||||
goalContext,
|
||||
runtimeContext,
|
||||
skillPrompt,
|
||||
plan: resolvedPlan,
|
||||
applied: false,
|
||||
};
|
||||
}
|
||||
|
||||
const keptBySource = {
|
||||
memory: new Set(),
|
||||
goal: false,
|
||||
temporal: false,
|
||||
skill_prompt: false,
|
||||
};
|
||||
|
||||
for (const block of resolvedPlan.blocks) {
|
||||
if (block.source === 'memory') keptBySource.memory.add(block.id);
|
||||
if (block.source === 'goal') keptBySource.goal = true;
|
||||
if (block.source === 'temporal') keptBySource.temporal = true;
|
||||
if (block.source === 'skill_prompt') keptBySource.skill_prompt = true;
|
||||
}
|
||||
|
||||
const nextMemoryContext = memoryContext?.injectionEnabled
|
||||
? {
|
||||
...memoryContext,
|
||||
memories: (Array.isArray(memoryContext.memories) ? memoryContext.memories : [])
|
||||
.filter((item, index) => {
|
||||
const label = String(item?.label ?? item?.title ?? '').trim();
|
||||
const text = String(item?.text ?? item?.content ?? item?.summary ?? '').trim();
|
||||
const id = String(item?.id ?? item?.memoryId ?? label ?? (text.slice(0, 32) || `memory-${index}`));
|
||||
return keptBySource.memory.has(id);
|
||||
}),
|
||||
}
|
||||
: memoryContext;
|
||||
|
||||
const nextGoalContext = goalContext?.injectionEnabled && !keptBySource.goal
|
||||
? { ...goalContext, injectionEnabled: false, envelope: '' }
|
||||
: goalContext;
|
||||
|
||||
const nextRuntimeContext = runtimeContext?.injectionEnabled && !keptBySource.temporal
|
||||
? { ...runtimeContext, injectionEnabled: false, injectionText: '' }
|
||||
: runtimeContext;
|
||||
|
||||
const nextSkillPrompt = keptBySource.skill_prompt ? skillPrompt : '';
|
||||
|
||||
return {
|
||||
memoryContext: nextMemoryContext,
|
||||
goalContext: nextGoalContext,
|
||||
runtimeContext: nextRuntimeContext,
|
||||
skillPrompt: nextSkillPrompt,
|
||||
plan: resolvedPlan,
|
||||
applied: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyContextBudgetToHarnessEntries(entries = [], { plan = null, env = process.env } = {}) {
|
||||
const mode = resolveContextBudgetMode(env);
|
||||
if (mode !== 'active') {
|
||||
return {
|
||||
entries,
|
||||
plan: plan ?? (mode === 'shadow'
|
||||
? planContextBudget({ harnessEntries: entries }, { env })
|
||||
: null),
|
||||
applied: false,
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedPlan = plan ?? planContextBudget({ harnessEntries: entries }, { env });
|
||||
const keptIds = new Set(
|
||||
resolvedPlan.blocks
|
||||
.filter((block) => block.source === 'harness')
|
||||
.map((block) => block.id),
|
||||
);
|
||||
|
||||
return {
|
||||
entries: entries.filter((entry) => {
|
||||
const title = String(entry?.title ?? '').trim();
|
||||
const text = String(entry?.content ?? entry?.text ?? '').trim();
|
||||
const id = title || text.slice(0, 32);
|
||||
return keptIds.has(id);
|
||||
}),
|
||||
plan: resolvedPlan,
|
||||
applied: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildContextBudgetResolvedEvent(plan) {
|
||||
if (!plan) return null;
|
||||
return {
|
||||
mode: plan.mode,
|
||||
budget: plan.budget,
|
||||
totalChars: plan.totalChars,
|
||||
inputBlockCount: plan.inputBlockCount,
|
||||
keptBlockCount: plan.keptBlockCount,
|
||||
droppedCount: plan.droppedItems.length,
|
||||
duplicateCount: plan.duplicateItems.length,
|
||||
droppedItems: plan.droppedItems,
|
||||
duplicateItems: plan.duplicateItems,
|
||||
fingerprints: plan.fingerprints,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
applyContextBudgetToHarnessEntries,
|
||||
applyContextBudgetToOrchestration,
|
||||
buildContextBudgetBlocks,
|
||||
fingerprintContent,
|
||||
planContextBudget,
|
||||
resolveContextBudgetMode,
|
||||
} from './context-budget.mjs';
|
||||
|
||||
test('resolveContextBudgetMode defaults to off', () => {
|
||||
assert.equal(resolveContextBudgetMode({}), 'off');
|
||||
assert.equal(resolveContextBudgetMode({ MEMIND_CONTEXT_BUDGET_MODE: 'shadow' }), 'shadow');
|
||||
assert.equal(resolveContextBudgetMode({ MEMIND_CONTEXT_BUDGET_MODE: 'bogus' }), 'off');
|
||||
});
|
||||
|
||||
test('fingerprintContent normalizes whitespace and case', () => {
|
||||
assert.equal(
|
||||
fingerprintContent(' Hello World '),
|
||||
fingerprintContent('hello world'),
|
||||
);
|
||||
});
|
||||
|
||||
test('planContextBudget dedupes duplicate fingerprints across sources', () => {
|
||||
const duplicate = '用户偏好:喜欢简洁回答';
|
||||
const plan = planContextBudget({
|
||||
memories: [{ label: '偏好', text: duplicate }],
|
||||
harnessEntries: [{ title: 'TKMind 用户偏好画像', content: duplicate }],
|
||||
userTask: '帮我总结今天的工作',
|
||||
}, {
|
||||
maxChars: 10_000,
|
||||
env: { MEMIND_CONTEXT_BUDGET_MODE: 'shadow' },
|
||||
});
|
||||
|
||||
assert.equal(plan.duplicateItems.length, 1);
|
||||
assert.equal(plan.keptBlockCount, plan.inputBlockCount - 1);
|
||||
});
|
||||
|
||||
test('planContextBudget drops lower-priority blocks when over budget', () => {
|
||||
const plan = planContextBudget({
|
||||
userTask: 'task',
|
||||
skillPrompt: 'skill',
|
||||
goalEnvelope: 'goal',
|
||||
temporalText: 'temporal',
|
||||
memories: [{ label: 'm1', text: 'memory-one' }],
|
||||
harnessEntries: [{ title: 'TKMind 已沉淀用户记忆', content: 'stored-memory' }],
|
||||
}, {
|
||||
maxChars: 20,
|
||||
env: { MEMIND_CONTEXT_BUDGET_MODE: 'active' },
|
||||
});
|
||||
|
||||
assert.ok(plan.droppedItems.length > 0);
|
||||
assert.ok(plan.blocks.some((block) => block.source === 'user_task'));
|
||||
assert.ok(plan.droppedItems.every((item) => item.source !== 'user_task'));
|
||||
});
|
||||
|
||||
test('applyContextBudgetToOrchestration is no-op in shadow mode', () => {
|
||||
const memoryContext = {
|
||||
injectionEnabled: true,
|
||||
memories: [{ label: 'a', text: 'alpha' }, { label: 'b', text: 'beta' }],
|
||||
};
|
||||
const applied = applyContextBudgetToOrchestration({
|
||||
memoryContext,
|
||||
userTask: 'do work',
|
||||
env: { MEMIND_CONTEXT_BUDGET_MODE: 'shadow', MEMIND_CONTEXT_BUDGET_MAX_CHARS: '10' },
|
||||
});
|
||||
|
||||
assert.equal(applied.applied, false);
|
||||
assert.equal(applied.memoryContext.memories.length, 2);
|
||||
assert.ok(applied.plan);
|
||||
});
|
||||
|
||||
test('applyContextBudgetToHarnessEntries filters in active mode', () => {
|
||||
const entries = [
|
||||
{ title: 'TKMind 当前时间基准', content: 'time anchor' },
|
||||
{ title: 'TKMind 已沉淀用户记忆', content: 'long '.repeat(200) },
|
||||
];
|
||||
const applied = applyContextBudgetToHarnessEntries(entries, {
|
||||
env: {
|
||||
MEMIND_CONTEXT_BUDGET_MODE: 'active',
|
||||
MEMIND_CONTEXT_BUDGET_MAX_CHARS: '40',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(applied.applied, true);
|
||||
assert.ok(applied.entries.length < entries.length);
|
||||
assert.ok(applied.entries.some((entry) => entry.title === 'TKMind 当前时间基准'));
|
||||
});
|
||||
|
||||
test('buildContextBudgetBlocks preserves source metadata', () => {
|
||||
const blocks = buildContextBudgetBlocks({
|
||||
userTask: 'hello',
|
||||
harnessEntries: [{ title: 'TKMind 用户空间沙箱', content: 'sandbox rules' }],
|
||||
});
|
||||
assert.ok(blocks.some((block) => block.source === 'user_task'));
|
||||
assert.ok(blocks.some((block) => block.source === 'harness' && block.priority >= 90));
|
||||
});
|
||||
@@ -23,3 +23,13 @@ MEMORY_CANDIDATE_ENABLED=0
|
||||
|
||||
# Goose v1.49 local session DB (isolated from production memind_sessions)
|
||||
# GOOSE_SESSION_DB_URL=postgresql://john@127.0.0.1:5432/goose_sessions_v149_dev
|
||||
|
||||
# Headroom context compression (Phase 1, local loopback only)
|
||||
# MEMIND_HEADROOM_MODE=off
|
||||
# MEMIND_HEADROOM_PROXY_BASE_URL=http://127.0.0.1:8787/v1
|
||||
# MEMIND_HEADROOM_UPSTREAM_BASE_URL=http://127.0.0.1:18036/v1
|
||||
# HEADROOM_OUTPUT_SHAPER=0
|
||||
|
||||
# Context injection budget (Phase 2, default off — does not open memory injection)
|
||||
# MEMIND_CONTEXT_BUDGET_MODE=off
|
||||
# MEMIND_CONTEXT_BUDGET_MAX_CHARS=12000
|
||||
|
||||
+26
-1
@@ -20,6 +20,11 @@ import {
|
||||
buildCursorExecutorLaunchPlan,
|
||||
cursorExecutorEnabled,
|
||||
} from './cursor-agent-launch.mjs';
|
||||
import {
|
||||
probeHeadroomProxyReachable,
|
||||
resolveGoosedApiUrlWithHeadroom,
|
||||
resolveHeadroomMode,
|
||||
} from './memind-headroom-policy.mjs';
|
||||
|
||||
export const MEMIND_DEEPSEEK_NO_THINK_PROVIDER_ID = 'custom_memind_deepseek_no_think';
|
||||
|
||||
@@ -885,13 +890,33 @@ async function syncDeepseekNoThinkProfileToGoosed(apiTarget, apiSecret, profile,
|
||||
...(Array.isArray(profile.models) ? profile.models : []),
|
||||
].filter(Boolean).map((item) => String(item).trim()).filter(Boolean)),
|
||||
];
|
||||
const compatProxyBaseUrl = resolveDeepseekNoThinkProxyBaseUrl();
|
||||
const headroomMode = resolveHeadroomMode();
|
||||
const headroomReachable = headroomMode === 'off'
|
||||
? false
|
||||
: await probeHeadroomProxyReachable({ fetchImpl });
|
||||
const headroomRouting = resolveGoosedApiUrlWithHeadroom({
|
||||
apiUrl: compatProxyBaseUrl,
|
||||
mode: headroomMode,
|
||||
headroomReachable,
|
||||
eligible: true,
|
||||
});
|
||||
if (headroomMode !== 'off') {
|
||||
console.log('[llm-providers] headroom provider routing', {
|
||||
mode: headroomRouting.mode,
|
||||
routed: headroomRouting.routed,
|
||||
fallback: headroomRouting.fallback ?? null,
|
||||
wouldRouteTo: headroomRouting.wouldRouteTo ?? null,
|
||||
apiUrl: headroomRouting.apiUrl,
|
||||
});
|
||||
}
|
||||
const goosedProviderId = await upsertCustomProviderOnGoosed(
|
||||
apiTarget,
|
||||
apiSecret,
|
||||
{
|
||||
name: 'memind_deepseek_no_think',
|
||||
goosedProviderId: MEMIND_DEEPSEEK_NO_THINK_PROVIDER_ID,
|
||||
apiUrl: resolveDeepseekNoThinkProxyBaseUrl(),
|
||||
apiUrl: headroomRouting.apiUrl,
|
||||
apiKey: profile.apiKey,
|
||||
models,
|
||||
defaultModel: profile.defaultModel,
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
resolveDeepseekNoThinkProxyBaseUrl,
|
||||
} from './deepseek-no-think-proxy.mjs';
|
||||
|
||||
export const DEFAULT_HEADROOM_PROXY_PORT = 8787;
|
||||
export const MEMIND_HEADROOM_MODES = Object.freeze(['off', 'shadow', 'active']);
|
||||
|
||||
export const DEFAULT_HEADROOM_EXCLUDED_SKILLS = Object.freeze([
|
||||
'static-page-publish',
|
||||
'page-data-collect',
|
||||
'aider-development',
|
||||
]);
|
||||
|
||||
function normalizeMode(value, fallback = 'off') {
|
||||
const raw = String(value ?? fallback).trim().toLowerCase();
|
||||
return MEMIND_HEADROOM_MODES.includes(raw) ? raw : fallback;
|
||||
}
|
||||
|
||||
export function resolveHeadroomMode(env = process.env) {
|
||||
return normalizeMode(env.MEMIND_HEADROOM_MODE, 'off');
|
||||
}
|
||||
|
||||
export function resolveHeadroomProxyPort(env = process.env) {
|
||||
const port = Number(env.MEMIND_HEADROOM_PROXY_PORT ?? DEFAULT_HEADROOM_PROXY_PORT);
|
||||
return Number.isFinite(port) && port > 0 ? port : DEFAULT_HEADROOM_PROXY_PORT;
|
||||
}
|
||||
|
||||
export function resolveHeadroomProxyBaseUrl(env = process.env) {
|
||||
const explicit = String(env.MEMIND_HEADROOM_PROXY_BASE_URL ?? '').trim();
|
||||
if (explicit) return explicit.replace(/\/$/, '');
|
||||
const host = String(env.MEMIND_HEADROOM_PROXY_HOST ?? '127.0.0.1').trim() || '127.0.0.1';
|
||||
return `http://${host}:${resolveHeadroomProxyPort(env)}/v1`;
|
||||
}
|
||||
|
||||
export function resolveHeadroomUpstreamBaseUrl(env = process.env) {
|
||||
const explicit = String(env.MEMIND_HEADROOM_UPSTREAM_BASE_URL ?? '').trim();
|
||||
if (explicit) return explicit.replace(/\/$/, '');
|
||||
return resolveDeepseekNoThinkProxyBaseUrl(env);
|
||||
}
|
||||
|
||||
export function parseHeadroomExcludedSkills(env = process.env) {
|
||||
const raw = String(env.MEMIND_HEADROOM_EXCLUDED_SKILLS ?? '').trim();
|
||||
if (!raw) return [...DEFAULT_HEADROOM_EXCLUDED_SKILLS];
|
||||
return raw.split(',').map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
export function isHeadroomEligibleForSkill(skillId, env = process.env) {
|
||||
if (resolveHeadroomMode(env) === 'off') return false;
|
||||
const skill = String(skillId ?? '').trim();
|
||||
if (!skill) return true;
|
||||
return !parseHeadroomExcludedSkills(env).includes(skill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether goosed should talk to headroom instead of the direct compat proxy.
|
||||
* fail-open: active mode falls back to the original apiUrl when headroom is unreachable.
|
||||
*/
|
||||
export function resolveGoosedApiUrlWithHeadroom({
|
||||
apiUrl,
|
||||
mode = resolveHeadroomMode(),
|
||||
headroomReachable = false,
|
||||
eligible = true,
|
||||
env = process.env,
|
||||
} = {}) {
|
||||
const normalizedMode = normalizeMode(mode, 'off');
|
||||
const original = String(apiUrl ?? '').trim();
|
||||
if (!original || normalizedMode === 'off' || !eligible) {
|
||||
return {
|
||||
apiUrl: original,
|
||||
mode: normalizedMode,
|
||||
routed: false,
|
||||
eligible,
|
||||
};
|
||||
}
|
||||
|
||||
const headroomBase = resolveHeadroomProxyBaseUrl(env);
|
||||
if (normalizedMode === 'shadow') {
|
||||
return {
|
||||
apiUrl: original,
|
||||
mode: 'shadow',
|
||||
routed: false,
|
||||
eligible: true,
|
||||
wouldRouteTo: headroomBase,
|
||||
upstreamBaseUrl: resolveHeadroomUpstreamBaseUrl(env),
|
||||
};
|
||||
}
|
||||
|
||||
if (!headroomReachable) {
|
||||
return {
|
||||
apiUrl: original,
|
||||
mode: 'active',
|
||||
routed: false,
|
||||
eligible: true,
|
||||
fallback: 'headroom_unreachable',
|
||||
wouldRouteTo: headroomBase,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
apiUrl: headroomBase,
|
||||
mode: 'active',
|
||||
routed: true,
|
||||
eligible: true,
|
||||
upstreamBaseUrl: resolveHeadroomUpstreamBaseUrl(env),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildHeadroomRunObservation({ skillId, env = process.env } = {}) {
|
||||
const mode = resolveHeadroomMode(env);
|
||||
const eligible = isHeadroomEligibleForSkill(skillId, env);
|
||||
return {
|
||||
mode,
|
||||
eligible,
|
||||
excludedSkills: parseHeadroomExcludedSkills(env),
|
||||
proxyBaseUrl: mode !== 'off' ? resolveHeadroomProxyBaseUrl(env) : null,
|
||||
upstreamBaseUrl: mode !== 'off' ? resolveHeadroomUpstreamBaseUrl(env) : null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function probeHeadroomProxyReachable({
|
||||
baseUrl,
|
||||
fetchImpl = fetch,
|
||||
timeoutMs = 1500,
|
||||
env = process.env,
|
||||
} = {}) {
|
||||
const root = String(baseUrl ?? resolveHeadroomProxyBaseUrl(env))
|
||||
.trim()
|
||||
.replace(/\/v1\/?$/i, '');
|
||||
if (!root) return false;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetchImpl(`${root}/v1/models`, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
headers: { Authorization: 'Bearer headroom-probe' },
|
||||
});
|
||||
// Proxy alive when it responds, even with auth/upstream errors.
|
||||
return response.status > 0;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
buildHeadroomRunObservation,
|
||||
isHeadroomEligibleForSkill,
|
||||
resolveGoosedApiUrlWithHeadroom,
|
||||
resolveHeadroomMode,
|
||||
resolveHeadroomProxyBaseUrl,
|
||||
} from './memind-headroom-policy.mjs';
|
||||
|
||||
test('resolveHeadroomMode defaults to off and rejects unknown values', () => {
|
||||
assert.equal(resolveHeadroomMode({}), 'off');
|
||||
assert.equal(resolveHeadroomMode({ MEMIND_HEADROOM_MODE: 'shadow' }), 'shadow');
|
||||
assert.equal(resolveHeadroomMode({ MEMIND_HEADROOM_MODE: 'bogus' }), 'off');
|
||||
});
|
||||
|
||||
test('resolveHeadroomProxyBaseUrl honors explicit override', () => {
|
||||
assert.equal(
|
||||
resolveHeadroomProxyBaseUrl({ MEMIND_HEADROOM_PROXY_BASE_URL: 'http://127.0.0.1:9999/v1' }),
|
||||
'http://127.0.0.1:9999/v1',
|
||||
);
|
||||
});
|
||||
|
||||
test('isHeadroomEligibleForSkill excludes page and code skills by default', () => {
|
||||
assert.equal(isHeadroomEligibleForSkill('web', { MEMIND_HEADROOM_MODE: 'shadow' }), true);
|
||||
assert.equal(
|
||||
isHeadroomEligibleForSkill('static-page-publish', { MEMIND_HEADROOM_MODE: 'shadow' }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isHeadroomEligibleForSkill('page-data-collect', { MEMIND_HEADROOM_MODE: 'active' }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveGoosedApiUrlWithHeadroom is fail-open in active mode', () => {
|
||||
const original = 'http://127.0.0.1:18036/v1';
|
||||
const shadow = resolveGoosedApiUrlWithHeadroom({
|
||||
apiUrl: original,
|
||||
mode: 'shadow',
|
||||
headroomReachable: false,
|
||||
eligible: true,
|
||||
env: { MEMIND_HEADROOM_PROXY_BASE_URL: 'http://127.0.0.1:8787/v1' },
|
||||
});
|
||||
assert.equal(shadow.apiUrl, original);
|
||||
assert.equal(shadow.routed, false);
|
||||
assert.equal(shadow.wouldRouteTo, 'http://127.0.0.1:8787/v1');
|
||||
|
||||
const activeFallback = resolveGoosedApiUrlWithHeadroom({
|
||||
apiUrl: original,
|
||||
mode: 'active',
|
||||
headroomReachable: false,
|
||||
eligible: true,
|
||||
env: { MEMIND_HEADROOM_PROXY_BASE_URL: 'http://127.0.0.1:8787/v1' },
|
||||
});
|
||||
assert.equal(activeFallback.apiUrl, original);
|
||||
assert.equal(activeFallback.fallback, 'headroom_unreachable');
|
||||
|
||||
const active = resolveGoosedApiUrlWithHeadroom({
|
||||
apiUrl: original,
|
||||
mode: 'active',
|
||||
headroomReachable: true,
|
||||
eligible: true,
|
||||
env: { MEMIND_HEADROOM_PROXY_BASE_URL: 'http://127.0.0.1:8787/v1' },
|
||||
});
|
||||
assert.equal(active.apiUrl, 'http://127.0.0.1:8787/v1');
|
||||
assert.equal(active.routed, true);
|
||||
});
|
||||
|
||||
test('buildHeadroomRunObservation reports skill eligibility', () => {
|
||||
const observation = buildHeadroomRunObservation({
|
||||
skillId: 'static-page-publish',
|
||||
env: { MEMIND_HEADROOM_MODE: 'shadow' },
|
||||
});
|
||||
assert.equal(observation.mode, 'shadow');
|
||||
assert.equal(observation.eligible, false);
|
||||
assert.equal(observation.proxyBaseUrl, 'http://127.0.0.1:8787/v1');
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { prepareGooseV149CheckEnv } from './goose-v149-canary.mjs';
|
||||
import { waitForAgentRunWorkerIdle } from './goose-v149-worker-idle.mjs';
|
||||
import {
|
||||
createReporter,
|
||||
loginViaApi,
|
||||
@@ -47,7 +48,8 @@ async function createGoosedAgentRun(baseUrl, cookie, { sessionId, message }) {
|
||||
body: JSON.stringify({
|
||||
request_id: requestId,
|
||||
session_id: sessionId,
|
||||
force_deep_reasoning: process.env.GOOSE_V149_PORTAL_RESUME_FORCE_DEEP !== '0',
|
||||
// Resume smoke validates session round-trip, not deep-reasoning latency.
|
||||
force_deep_reasoning: process.env.GOOSE_V149_PORTAL_RESUME_FORCE_DEEP === '1',
|
||||
user_message: {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'user',
|
||||
@@ -104,6 +106,10 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForAgentRunWorkerIdle(root, process.env, {
|
||||
logPrefix: '[goose-v149-portal-resume]',
|
||||
});
|
||||
|
||||
const reporter = createReporter();
|
||||
const username = process.env.RELEASE_GATE_SCENARIO_USERNAME ?? 'john';
|
||||
const password =
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Local headroom proxy smoke (loopback only).
|
||||
* Requires: headroom proxy running with upstream pointing at DeepSeek compat proxy.
|
||||
*
|
||||
* Example:
|
||||
* OPENAI_BASE_URL=http://127.0.0.1:18036/v1 HEADROOM_OUTPUT_SHAPER=0 headroom proxy --port 8787
|
||||
*/
|
||||
import {
|
||||
buildHeadroomRunObservation,
|
||||
probeHeadroomProxyReachable,
|
||||
resolveHeadroomMode,
|
||||
resolveHeadroomProxyBaseUrl,
|
||||
resolveHeadroomUpstreamBaseUrl,
|
||||
} from '../memind-headroom-policy.mjs';
|
||||
|
||||
const mode = resolveHeadroomMode(process.env);
|
||||
if (mode === 'off') {
|
||||
console.log('HEADROOM_LOCAL_SKIP: MEMIND_HEADROOM_MODE=off');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const reachable = await probeHeadroomProxyReachable();
|
||||
const observation = buildHeadroomRunObservation({ skillId: 'web' });
|
||||
|
||||
console.log('HEADROOM_LOCAL_PROBE:');
|
||||
console.log(` mode=${mode}`);
|
||||
console.log(` proxy=${resolveHeadroomProxyBaseUrl()}`);
|
||||
console.log(` upstream=${resolveHeadroomUpstreamBaseUrl()}`);
|
||||
console.log(` reachable=${reachable}`);
|
||||
console.log(` observation=${JSON.stringify(observation)}`);
|
||||
|
||||
if (!reachable) {
|
||||
console.error('HEADROOM_LOCAL_FAIL: proxy unreachable');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('HEADROOM_LOCAL_OK');
|
||||
+12
-1
@@ -1,5 +1,6 @@
|
||||
import path from 'node:path';
|
||||
import { developerToolsFromPolicy } from './capabilities.mjs';
|
||||
import { applyContextBudgetToHarnessEntries } from './context-budget.mjs';
|
||||
import { buildSessionMemoryEntries } from './user-memory-profile.mjs';
|
||||
import { buildSandboxSessionConstraints } from './user-publish.mjs';
|
||||
|
||||
@@ -307,13 +308,23 @@ export async function reconcileAgentSession(
|
||||
})
|
||||
: null;
|
||||
|
||||
const memoryEntries = buildSessionMemoryEntries({
|
||||
const rawMemoryEntries = buildSessionMemoryEntries({
|
||||
workingDir,
|
||||
sessionPolicy,
|
||||
sandboxConstraints: sandboxText,
|
||||
userContext,
|
||||
userMemories,
|
||||
});
|
||||
const { entries: memoryEntries, plan: harnessBudgetPlan } = applyContextBudgetToHarnessEntries(
|
||||
rawMemoryEntries,
|
||||
);
|
||||
if (harnessBudgetPlan?.mode === 'shadow' && harnessBudgetPlan.duplicateItems.length > 0) {
|
||||
console.log('[session-reconcile] context budget shadow', {
|
||||
duplicateCount: harnessBudgetPlan.duplicateItems.length,
|
||||
droppedCount: harnessBudgetPlan.droppedItems.length,
|
||||
keptBlockCount: harnessBudgetPlan.keptBlockCount,
|
||||
});
|
||||
}
|
||||
|
||||
if (memoryEntries.length > 0) {
|
||||
for (const entry of memoryEntries) {
|
||||
|
||||
Reference in New Issue
Block a user