mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-19 18:14:54 +00:00
Keep the Main turn state when a reminder or skill notice arrives
The live chat handler treats any frame without a task_id, other than a terminal_incident, as the end of the unscoped Main turn and clears its direct activities and pending submissions, so "Thinking..." or "Sending..." vanished until the next /api/state snapshot (up to 20 s). Reminder and /notify rows are exactly such frames (role system, empty task_id) and arrive in the middle of a conversation. Decide by the frame's typed kind: chat_activity.unkeyedFrameEndsTurn says an unkeyed terminal_incident, reminder or skill_notice row concludes nothing; every other unkeyed final still clears the turn. No text matching. chat.js only swaps its inline check for the predicate, so the module stays under its byte cap (199,325 of 200,000). Tests: through the real header reducer, a reminder, skill notice or incident frame keeps "Thinking..." while an ordinary unkeyed reply or another System row still ends it, and a reply quoting a reminder's words ends it too; the source pin now names the predicate in the cleanup branch.
This commit is contained in:
parent
a35a7790c7
commit
1f25016db5
3 changed files with 29 additions and 4 deletions
|
|
@ -126,6 +126,7 @@ import {
|
|||
partitionLocalEchoJournal,
|
||||
projectCollapsedActivity,
|
||||
positiveTaskTerminalFact,
|
||||
unkeyedFrameEndsTurn,
|
||||
projectIdFromTask,
|
||||
rawTimestampEpoch,
|
||||
stampNodeTimestamp,
|
||||
|
|
@ -3714,8 +3715,8 @@ export function createChatInstance({
|
|||
if (finished?.clientMessageId) {
|
||||
pendingSubmissions.delete(finished.clientMessageId);
|
||||
}
|
||||
} else if (msg.system_type !== 'terminal_incident') {
|
||||
// Unkeyed finals clear unscoped state; incidents are informational.
|
||||
} else if (unkeyedFrameEndsTurn(msg)) {
|
||||
// Unkeyed finals clear unscoped state; incidents, notes and skill notices do not.
|
||||
activeDirectActivities.clear();
|
||||
pendingSubmissions.clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1056,6 +1056,15 @@ export function positiveTaskTerminalFact(row) {
|
|||
&& TERMINAL_SUBAGENT_EVENTS.has(String(row.subagent_event || '').toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an unkeyed live row ends the unscoped Main turn, by its typed kind. An
|
||||
* incident report, a note Ouroboros left for this moment and a skill's notice arrive
|
||||
* in the middle of a conversation and conclude nothing.
|
||||
*/
|
||||
export function unkeyedFrameEndsTurn(row) {
|
||||
return !['terminal_incident', 'reminder', 'skill_notice'].includes(String(row?.system_type || ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Single status reducer for the chat header (owner decisions 2A/5A; managed
|
||||
* activities added by the project-continuity contract). Priority: disconnected
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
createStateSnapshotSequencer,
|
||||
isForegroundLiveCard,
|
||||
routingAnnotationText,
|
||||
unkeyedFrameEndsTurn,
|
||||
} from '../modules/chat_activity.js';
|
||||
|
||||
const chatSource = readFileSync(new URL('../modules/chat.js', import.meta.url), 'utf8');
|
||||
|
|
@ -26,7 +27,7 @@ test('review-only owner anchors do not advertise foreground task activity', () =
|
|||
test('unkeyed terminal incidents do not clear unrelated live turns', () => {
|
||||
// The cleanup guard also requires positive terminal evidence. Keep this
|
||||
// source contract resilient to the guard's explicit conjunction while
|
||||
// still checking the incident carve-out below it.
|
||||
// still checking the carve-out below it (the typed kinds that end nothing).
|
||||
const cleanupStart = chatSource.indexOf('if (!finalizing && (!explicitTaskId || typedTerminal))');
|
||||
const finalCleanup = chatSource.slice(
|
||||
cleanupStart,
|
||||
|
|
@ -34,13 +35,27 @@ test('unkeyed terminal incidents do not clear unrelated live turns', () => {
|
|||
);
|
||||
assert.match(
|
||||
finalCleanup,
|
||||
/} else if \(msg\.system_type !== 'terminal_incident'\) \{[^}]*?activeDirectActivities\.clear\(\);[^}]*?pendingSubmissions\.clear\(\);\s*}\s*}/,
|
||||
/} else if \(unkeyedFrameEndsTurn\(msg\)\) \{[^}]*?activeDirectActivities\.clear\(\);[^}]*?pendingSubmissions\.clear\(\);\s*}\s*}/,
|
||||
);
|
||||
// Ordinary unkeyed finals retain their existing global cleanup semantics.
|
||||
assert.match(finalCleanup, /activeDirectActivities\.clear\(\);/);
|
||||
assert.match(finalCleanup, /pendingSubmissions\.clear\(\);/);
|
||||
});
|
||||
|
||||
test('a note or skill notice arriving mid-turn keeps Thinking/Sending; an ordinary unkeyed final still ends it', () => {
|
||||
// What the live handler does with the unscoped turn for each frame (the branch pinned above).
|
||||
const turn = { isConnected: true, hasActiveLiveCard: false, activeDirectCount: 1, pendingSubmissionsCount: 1 };
|
||||
const after = (frame) => computeDerivedChatStatus(unkeyedFrameEndsTurn(frame)
|
||||
? { ...turn, activeDirectCount: 0, pendingSubmissionsCount: 0 } : turn).text;
|
||||
for (const kind of ['reminder', 'skill_notice', 'terminal_incident']) {
|
||||
assert.equal(after({ role: 'system', system_type: kind, task_id: '', content: 'Call mother' }), 'Thinking...', kind);
|
||||
}
|
||||
assert.equal(after({ role: 'assistant', task_id: '', content: 'Done.' }), 'Online');
|
||||
assert.equal(after({ role: 'system', system_type: 'startup_notice', task_id: '' }), 'Online');
|
||||
// The kind decides, never the words: a reply that quotes a reminder still ends the turn.
|
||||
assert.equal(after({ role: 'assistant', task_id: '', content: 'Reminder · Ouroboros · written Oct 3 14:05' }), 'Online');
|
||||
});
|
||||
|
||||
test('routing receipts display the event-time label while keeping raw target metadata', () => {
|
||||
const annotation = {
|
||||
action: 'steer_task',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue