fix: complete goal resume and comment input follow-ups (#3361)
This commit is contained in:
committed by
GitHub
parent
f3f844463b
commit
7c9fdd1ee5
@@ -124,7 +124,10 @@ before touching the filesystem). Rationale: metadata rides every
|
||||
The consecutive state is derived from the loaded message history, not
|
||||
persisted, using `info.time.created` chronology rather than message IDs.
|
||||
Summary messages are not agent turns; an ordinary completed assistant
|
||||
turn naturally breaks the consecutive condition;
|
||||
turn naturally breaks the consecutive condition. Explicit Resume grants
|
||||
one new recovery attempt over the same transcript; the continuation
|
||||
consumes that permission, so another truncation blocks again. Resume
|
||||
does not bypass assistant errors or the token budget;
|
||||
- otherwise, small-model audit of the objective + the last assistant turn
|
||||
only — no conversation history and no continuation prompts
|
||||
(`restrictToPreferredProvider`, session's own provider/model preferred):
|
||||
|
||||
@@ -707,7 +707,7 @@ export const createSessionGoalRuntime = ({
|
||||
// A second consecutive completed, non-summary length-truncated turn is a
|
||||
// bounded recovery failure. Derive this from the loaded transcript rather
|
||||
// than persisting another goal counter.
|
||||
if (lengthTail && hasRepeatedLengthTail(messages, lastAssistant, goal.createdAt)) {
|
||||
if (lengthTail && goal.statusReason !== 'resumed' && hasRepeatedLengthTail(messages, lastAssistant, goal.createdAt)) {
|
||||
await settleGoal({
|
||||
sessionId, directory, goal, status: 'blocked', statusReason: 'repeated output truncation', tokensUsed, tokensBaseline, tokensCommitted, lastAccountedMessageID,
|
||||
});
|
||||
|
||||
@@ -77,7 +77,10 @@ const createRuntimeHarness = ({ messages, messageFactory, goalOverrides = {}, ma
|
||||
const fetchImpl = vi.fn(async (input, init = {}) => {
|
||||
const pathname = requestPath(input);
|
||||
requests.push({ pathname, method: init.method ?? 'GET', body: init.body });
|
||||
if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') return jsonResponse(activeSession);
|
||||
if (pathname === `/session/${SESSION_ID}` && init.method === 'PATCH') {
|
||||
activeSession.metadata = JSON.parse(init.body).metadata;
|
||||
return jsonResponse(activeSession);
|
||||
}
|
||||
if (pathname === `/session/${SESSION_ID}`) return jsonResponse(activeSession);
|
||||
if (pathname === '/session/status') return jsonResponse({});
|
||||
if (pathname === `/session/${SESSION_ID}/children`) return jsonResponse([]);
|
||||
@@ -98,7 +101,7 @@ const createRuntimeHarness = ({ messages, messageFactory, goalOverrides = {}, ma
|
||||
idleQuietMs: 10,
|
||||
maxAutoTurns,
|
||||
});
|
||||
return { runtime, requests, service };
|
||||
return { runtime, requests, service, activeSession };
|
||||
};
|
||||
|
||||
const runIdleTick = async (runtime) => {
|
||||
@@ -420,6 +423,45 @@ describe('session goal live activity gate', () => {
|
||||
runtime.stop();
|
||||
});
|
||||
|
||||
it('allows one explicit Resume after repeated truncation, then blocks another cutoff', async () => {
|
||||
const first = assistantMessage('first', { finish: 'length', time: { created: 10, completed: 11 } });
|
||||
const second = assistantMessage('second', { finish: 'length', time: { created: 20, completed: 21 } });
|
||||
const messages = [first, second];
|
||||
const { runtime, requests, activeSession } = createRuntimeHarness({ messages });
|
||||
try {
|
||||
await runIdleTick(runtime);
|
||||
expect(lastPatchedGoal(requests).status).toBe('blocked');
|
||||
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(0);
|
||||
Object.assign(activeSession.metadata.openchamber.goal, { status: 'active', statusReason: 'resumed', turnsUsed: 0 });
|
||||
await runIdleTick(runtime);
|
||||
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'active', statusReason: '', turnsUsed: 1 });
|
||||
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
|
||||
messages.push(assistantMessage('third', { finish: 'length', time: { created: 30, completed: 31 } }));
|
||||
await runIdleTick(runtime);
|
||||
expect(lastPatchedGoal(requests)).toMatchObject({ status: 'blocked', statusReason: 'repeated output truncation' });
|
||||
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(1);
|
||||
} finally {
|
||||
runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ tokenBudget: 1, error: undefined, expectedStatus: 'budgetLimited' },
|
||||
{ tokenBudget: null, error: { name: 'APIError' }, expectedStatus: 'blocked' },
|
||||
])('keeps $expectedStatus protection on explicit Resume', async ({ tokenBudget, error, expectedStatus }) => {
|
||||
const { runtime, requests } = createRuntimeHarness({
|
||||
goalOverrides: { statusReason: 'resumed', tokenBudget },
|
||||
messages: [assistantMessage('resumed', { finish: 'length', error })],
|
||||
});
|
||||
try {
|
||||
await runIdleTick(runtime);
|
||||
expect(lastPatchedGoal(requests).status).toBe(expectedStatus);
|
||||
expect(requests.filter((request) => request.pathname.endsWith('/prompt_async'))).toHaveLength(0);
|
||||
} finally {
|
||||
runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it('continues after a truncated agent turn followed by a length-finished summary', async () => {
|
||||
const firstLength = assistantMessage('agent-length', { finish: 'length', time: { created: 10, completed: 11 } });
|
||||
const summary = assistantMessage('summary', {
|
||||
|
||||
Reference in New Issue
Block a user