[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
351
infrastructure/ai/harness/contextManager.test.ts
Normal file
351
infrastructure/ai/harness/contextManager.test.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import type { ModelMessage } from 'ai';
|
||||
import { prepareTurnContext, prepareStepContext } from './contextManager.ts';
|
||||
import { TraceStore } from './traceStore.ts';
|
||||
import { ToolOutputStore } from './toolOutputStore.ts';
|
||||
import { createInitialCattyRuntimeContext } from './cattyRuntimeContext.ts';
|
||||
|
||||
test('normal turn and step preparation preserve exact user literals alongside noisy tool output', async () => {
|
||||
const literal = "printf '%s\\n' 'CANONICAL_GUI_" + 'x'.repeat(1200) + "_END'";
|
||||
const content = `Execute exactly: ${literal}\n\n\n\n${'keep this line\n'.repeat(5)}`;
|
||||
for (const userContent of [content, [{ type: 'text' as const, text: content }]]) {
|
||||
const messages: ModelMessage[] = [
|
||||
{ role: 'user', content: userContent },
|
||||
{ role: 'assistant', content: [{ type: 'tool-call', toolCallId: 'noise', toolName: 'terminal_execute', input: {} }] },
|
||||
{ role: 'tool', content: [{ type: 'tool-result', toolCallId: 'noise', toolName: 'terminal_execute', output: { type: 'text', value: 'log '.repeat(10000) } }] },
|
||||
];
|
||||
const turn = await prepareTurnContext({ messages, backend: 'catty', contextWindow: 1_300_000, trigger: 'pre-turn' });
|
||||
const step = await prepareStepContext({ messages: turn.messages, contextWindow: 1_300_000, stepNumber: 0 });
|
||||
for (const prepared of [turn, step]) {
|
||||
assert.deepEqual(prepared.messages.find(message => message.role === 'user')?.content, userContent);
|
||||
assert.ok(JSON.stringify(prepared.messages).length < JSON.stringify(messages).length);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('prepareTurnContext applies typed compression before LLM summarize threshold', async () => {
|
||||
const longOutput = 'line\n'.repeat(20_000);
|
||||
const messages: ModelMessage[] = [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Check nginx error logs on prod-web-01 and summarize failures.',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{
|
||||
type: 'tool-call',
|
||||
toolCallId: 'call-1',
|
||||
toolName: 'terminal_execute',
|
||||
input: { sessionId: 'sess-1', command: 'tail -n 500 /var/log/nginx/error.log' },
|
||||
}],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: [{
|
||||
type: 'tool-result',
|
||||
toolCallId: 'call-1',
|
||||
toolName: 'terminal_execute',
|
||||
output: { type: 'text', value: longOutput },
|
||||
}],
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Found repeated upstream timeout errors.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Fix only the upstream timeout issue, do not restart nginx yet.',
|
||||
},
|
||||
];
|
||||
|
||||
const traces: string[] = [];
|
||||
const prepared = await prepareTurnContext({
|
||||
messages,
|
||||
backend: 'catty',
|
||||
contextWindow: 128_000,
|
||||
trigger: 'pre-turn',
|
||||
sessionId: 'chat-1',
|
||||
onEvent: (event) => {
|
||||
if (event.type === 'compaction') traces.push(event.trace.trigger);
|
||||
},
|
||||
reinjection: {
|
||||
permissionMode: 'confirm',
|
||||
userGoal: 'Fix upstream timeout without restarting nginx.',
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(prepared.messages.length >= messages.length);
|
||||
const serialized = JSON.stringify(prepared.messages);
|
||||
assert.match(serialized, /Fix only the upstream timeout issue/);
|
||||
assert.match(serialized, /Permission mode: confirm/);
|
||||
assert.ok(serialized.length < JSON.stringify(messages).length);
|
||||
});
|
||||
|
||||
test('prepareTurnContext skips reinjection when no compaction occurred', async () => {
|
||||
const messages: ModelMessage[] = [
|
||||
{ role: 'user', content: 'List running containers on prod-web-01.' },
|
||||
{ role: 'assistant', content: 'I will check docker ps.' },
|
||||
];
|
||||
|
||||
const events: string[] = [];
|
||||
const prepared = await prepareTurnContext({
|
||||
messages,
|
||||
backend: 'catty',
|
||||
contextWindow: 128_000,
|
||||
trigger: 'pre-turn',
|
||||
sessionId: 'chat-no-compact',
|
||||
onEvent: (event) => {
|
||||
if (event.type === 'compaction') events.push(event.trace.trigger);
|
||||
},
|
||||
reinjection: {
|
||||
permissionMode: 'confirm',
|
||||
userGoal: 'List running containers on prod-web-01.',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(prepared.didAdjust, false);
|
||||
assert.equal(events.length, 0);
|
||||
const serialized = JSON.stringify(prepared.messages);
|
||||
assert.doesNotMatch(serialized, /Netcatty session context/);
|
||||
assert.doesNotMatch(serialized, /Permission mode: confirm/);
|
||||
});
|
||||
|
||||
test('prepareTurnContext force trigger retains recent user goal in replay', async () => {
|
||||
const messages: ModelMessage[] = Array.from({ length: 40 }, (_, index) => ({
|
||||
role: index % 2 === 0 ? 'user' : 'assistant',
|
||||
content: index === 38
|
||||
? 'SSH into db-01 and inspect /var/log/postgresql/postgresql.log for crash signatures.'
|
||||
: `filler message ${index}`,
|
||||
})) as ModelMessage[];
|
||||
|
||||
const prepared = await prepareTurnContext({
|
||||
messages,
|
||||
backend: 'catty',
|
||||
contextWindow: 128_000,
|
||||
trigger: 'force',
|
||||
force: true,
|
||||
sessionId: 'chat-2',
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(prepared.messages);
|
||||
assert.match(serialized, /SSH into db-01/);
|
||||
assert.match(serialized, /postgresql\.log/);
|
||||
});
|
||||
|
||||
test('TraceStore records compaction events for export', async () => {
|
||||
const store = new TraceStore();
|
||||
await prepareTurnContext({
|
||||
messages: [{ role: 'user', content: 'x'.repeat(500_000) }],
|
||||
backend: 'catty',
|
||||
contextWindow: 1000,
|
||||
trigger: 'force',
|
||||
force: true,
|
||||
sessionId: 'chat-3',
|
||||
onEvent: (event) => store.append(event),
|
||||
});
|
||||
|
||||
const exported = store.exportTrace('chat-3');
|
||||
assert.ok(exported.compactions.length >= 1);
|
||||
assert.equal(exported.compactions[0]?.trigger, 'force');
|
||||
});
|
||||
|
||||
test('TraceStore bounds compaction history independently from events', () => {
|
||||
const store = new TraceStore(20, 3);
|
||||
for (let index = 0; index < 10; index += 1) {
|
||||
store.append({
|
||||
id: `compaction-${index}`,
|
||||
type: 'compaction',
|
||||
sessionId: 'chat-bounded-compactions',
|
||||
trace: { trigger: 'force' },
|
||||
} as import('./types').AgentEvent);
|
||||
}
|
||||
assert.equal(store.getCompactions('chat-bounded-compactions').length, 3);
|
||||
});
|
||||
|
||||
test('prepareStepContext replaces prior step handle notices under v7 carry-forward semantics', async () => {
|
||||
const store = new ToolOutputStore();
|
||||
store.store({
|
||||
chatSessionId: 'chat-4',
|
||||
capabilityId: 'sftp.read',
|
||||
content: 'large payload',
|
||||
});
|
||||
const runtimeContext = createInitialCattyRuntimeContext({
|
||||
chatSessionId: 'chat-4',
|
||||
turnId: 'turn-1',
|
||||
permissionMode: 'confirm',
|
||||
scopeType: 'terminal',
|
||||
});
|
||||
const priorNotice: ModelMessage = {
|
||||
role: 'user',
|
||||
content: '[step 1] Tool output handles available: tool-output-old',
|
||||
};
|
||||
const prepared = await prepareStepContext({
|
||||
messages: [priorNotice, { role: 'user', content: 'continue' }],
|
||||
stepNumber: 2,
|
||||
sessionId: 'chat-4',
|
||||
chatSessionId: 'chat-4',
|
||||
toolOutputStore: store,
|
||||
runtimeContext,
|
||||
});
|
||||
|
||||
const notices = prepared.messages.filter(
|
||||
(message) => message.role === 'user'
|
||||
&& typeof message.content === 'string'
|
||||
&& message.content.includes('Tool output handles available'),
|
||||
);
|
||||
assert.equal(notices.length, 1);
|
||||
assert.match(String(notices[0]?.content), /\[step 2\]/);
|
||||
assert.doesNotMatch(String(notices[0]?.content), /tool-output-old/);
|
||||
});
|
||||
|
||||
test('prepareStepContext emits step compaction trace when over budget', async () => {
|
||||
const messages: ModelMessage[] = Array.from({ length: 30 }, (_, index) => ({
|
||||
role: index % 2 === 0 ? 'user' : 'assistant',
|
||||
content: 'payload '.repeat(2_000),
|
||||
})) as ModelMessage[];
|
||||
|
||||
const events: string[] = [];
|
||||
const prepared = await prepareStepContext({
|
||||
messages,
|
||||
stepNumber: 3,
|
||||
sessionId: 'chat-5',
|
||||
chatSessionId: 'chat-5',
|
||||
contextWindow: 4_000,
|
||||
reservedTokens: 500,
|
||||
maxOutputTokens: 512,
|
||||
providerId: 'anthropic',
|
||||
runtimeContext: createInitialCattyRuntimeContext({
|
||||
chatSessionId: 'chat-5',
|
||||
turnId: 'turn-2',
|
||||
permissionMode: 'confirm',
|
||||
scopeType: 'terminal',
|
||||
}),
|
||||
onEvent: (event) => {
|
||||
if (event.type === 'compaction') events.push(event.trace.trigger);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(prepared.didAdjust, true);
|
||||
assert.equal(prepared.trace?.trigger, 'step');
|
||||
assert.ok(events.includes('step'));
|
||||
});
|
||||
|
||||
test('prepareStepContext retains handle notice after step budget guard', async () => {
|
||||
const store = new ToolOutputStore();
|
||||
store.store({
|
||||
chatSessionId: 'chat-handle',
|
||||
capabilityId: 'sftp.read',
|
||||
content: 'large payload',
|
||||
});
|
||||
const messages: ModelMessage[] = Array.from({ length: 30 }, (_, index) => ({
|
||||
role: index % 2 === 0 ? 'user' : 'assistant',
|
||||
content: 'payload '.repeat(2_000),
|
||||
})) as ModelMessage[];
|
||||
|
||||
const prepared = await prepareStepContext({
|
||||
messages,
|
||||
stepNumber: 2,
|
||||
sessionId: 'chat-handle',
|
||||
chatSessionId: 'chat-handle',
|
||||
contextWindow: 4_000,
|
||||
reservedTokens: 500,
|
||||
maxOutputTokens: 512,
|
||||
toolOutputStore: store,
|
||||
runtimeContext: createInitialCattyRuntimeContext({
|
||||
chatSessionId: 'chat-handle',
|
||||
turnId: 'turn-handle',
|
||||
permissionMode: 'confirm',
|
||||
scopeType: 'terminal',
|
||||
}),
|
||||
});
|
||||
|
||||
const notices = prepared.messages.filter(
|
||||
(message) => message.role === 'user'
|
||||
&& typeof message.content === 'string'
|
||||
&& message.content.includes('Tool output handles available'),
|
||||
);
|
||||
assert.equal(notices.length, 1);
|
||||
assert.match(String(notices[0]?.content), /\[step 2\]/);
|
||||
});
|
||||
|
||||
test('prepareStepContext never leaves orphan results from a parallel tool batch', async () => {
|
||||
const toolCallIds = ['a', 'b', 'c', 'd', 'e', 'f'];
|
||||
const messages: ModelMessage[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: toolCallIds.map((toolCallId) => ({
|
||||
type: 'tool-call' as const,
|
||||
toolCallId,
|
||||
toolName: 'terminal_poll',
|
||||
input: { jobId: toolCallId },
|
||||
})),
|
||||
},
|
||||
...toolCallIds.map((toolCallId) => ({
|
||||
role: 'tool' as const,
|
||||
content: [{
|
||||
type: 'tool-result' as const,
|
||||
toolCallId,
|
||||
toolName: 'terminal_poll',
|
||||
output: { type: 'text' as const, value: `${toolCallId}:${'output '.repeat(4_000)}` },
|
||||
}],
|
||||
})),
|
||||
{ role: 'user', content: 'summarize the completed jobs' },
|
||||
];
|
||||
|
||||
const prepared = await prepareStepContext({
|
||||
messages,
|
||||
stepNumber: 4,
|
||||
sessionId: 'chat-parallel',
|
||||
chatSessionId: 'chat-parallel',
|
||||
contextWindow: 8_000,
|
||||
reservedTokens: 500,
|
||||
maxOutputTokens: 512,
|
||||
runtimeContext: createInitialCattyRuntimeContext({
|
||||
chatSessionId: 'chat-parallel',
|
||||
turnId: 'turn-parallel',
|
||||
permissionMode: 'confirm',
|
||||
scopeType: 'terminal',
|
||||
}),
|
||||
});
|
||||
|
||||
const knownCalls = new Set<string>();
|
||||
for (const message of prepared.messages) {
|
||||
if (message.role === 'assistant' && Array.isArray(message.content)) {
|
||||
for (const part of message.content as Array<{ type?: string; toolCallId?: string }>) {
|
||||
if (part.type === 'tool-call' && part.toolCallId) knownCalls.add(part.toolCallId);
|
||||
}
|
||||
}
|
||||
if (message.role === 'tool' && Array.isArray(message.content)) {
|
||||
for (const part of message.content as Array<{ type?: string; toolCallId?: string }>) {
|
||||
if (part.type === 'tool-result') assert.equal(knownCalls.has(part.toolCallId ?? ''), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('prepareTurnContext calls summarize when over dynamic threshold', async () => {
|
||||
let summarizeCalls = 0;
|
||||
const messages: ModelMessage[] = Array.from({ length: 24 }, (_, index) => ({
|
||||
role: index % 2 === 0 ? 'user' : 'assistant',
|
||||
content: 'history '.repeat(3_000),
|
||||
})) as ModelMessage[];
|
||||
|
||||
const prepared = await prepareTurnContext({
|
||||
messages,
|
||||
backend: 'catty',
|
||||
contextWindow: 8_000,
|
||||
maxOutputTokens: 512,
|
||||
trigger: 'pre-turn',
|
||||
sessionId: 'chat-6',
|
||||
providerId: 'openai',
|
||||
summarize: async () => {
|
||||
summarizeCalls += 1;
|
||||
return 'summary of earlier work';
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(summarizeCalls, 1);
|
||||
assert.equal(prepared.trace?.didLlmSummarize, true);
|
||||
assert.match(JSON.stringify(prepared.messages), /summary of earlier work/);
|
||||
});
|
||||
Reference in New Issue
Block a user