Skip to content

Commit 929c88b

Browse files
committed
feat: persist chat continuity state
1 parent 6cd40b4 commit 929c88b

File tree

9 files changed

+436
-16
lines changed

9 files changed

+436
-16
lines changed

scripts/test-ai-service-commands.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ let currentCopilotModel = 'gpt-4o';
3333
let clearedVisual = false;
3434
let resetBrowser = false;
3535
let clearedSessionIntent = false;
36+
let clearedChatContinuity = false;
3637

3738
const sessionIntentState = {
3839
currentRepo: { repoName: 'copilot-liku-cli' },
@@ -41,12 +42,25 @@ const sessionIntentState = {
4142
explicitCorrections: [{ text: 'MUSE is a different repo, this is copilot-liku-cli.' }]
4243
};
4344

45+
const chatContinuityState = {
46+
activeGoal: 'Produce a confident synthesis of ticker LUNR in TradingView',
47+
currentSubgoal: 'Inspect the active TradingView chart',
48+
continuationReady: true,
49+
lastTurn: {
50+
actionSummary: 'focus_window -> screenshot',
51+
verificationStatus: 'verified'
52+
}
53+
};
54+
4455
const handler = createCommandHandler({
4556
aiProviders: { copilot: {}, openai: {}, anthropic: {}, ollama: {} },
4657
captureVisualContext: () => Promise.resolve({ type: 'system', message: 'captured' }),
4758
clearVisualContext: () => {
4859
clearedVisual = true;
4960
},
61+
clearChatContinuityState: () => {
62+
clearedChatContinuity = true;
63+
},
5064
exchangeForCopilotSession: () => Promise.resolve(),
5165
getCopilotModels: () => ([
5266
{
@@ -91,6 +105,7 @@ const handler = createCommandHandler({
91105
}
92106
]),
93107
getCurrentCopilotModel: () => currentCopilotModel,
108+
getChatContinuityState: () => chatContinuityState,
94109
getCurrentProvider: () => currentProvider,
95110
getSessionIntentState: () => sessionIntentState,
96111
getStatus: () => ({
@@ -170,7 +185,8 @@ test('clear command resets history and visual state', () => {
170185
assert.strictEqual(clearedVisual, true);
171186
assert.strictEqual(resetBrowser, true);
172187
assert.strictEqual(clearedSessionIntent, true);
173-
assert.ok(result.message.includes('session intent state'));
188+
assert.strictEqual(clearedChatContinuity, true);
189+
assert.ok(result.message.includes('chat continuity state'));
174190
});
175191

176192
test('state command reports current repo and forgone features', () => {
@@ -179,13 +195,18 @@ test('state command reports current repo and forgone features', () => {
179195
assert.ok(result.message.includes('Current repo: copilot-liku-cli'));
180196
assert.ok(result.message.includes('Downstream repo intent: muse-ai'));
181197
assert.ok(result.message.includes('Forgone features: terminal-liku ui'));
198+
assert.ok(result.message.includes('Active goal: Produce a confident synthesis of ticker LUNR in TradingView'));
199+
assert.ok(result.message.includes('Continuation ready: yes'));
182200
});
183201

184202
test('state clear command clears session intent state', () => {
185203
clearedSessionIntent = false;
204+
clearedChatContinuity = false;
186205
const result = handler.handleCommand('/state clear');
187206
assert.strictEqual(result.type, 'system');
188207
assert.strictEqual(clearedSessionIntent, true);
208+
assert.strictEqual(clearedChatContinuity, true);
209+
assert.ok(result.message.includes('chat continuity state'));
189210
});
190211

191212
test('model command uses normalized model keys', () => {

scripts/test-chat-inline-proof-evaluator.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ test('evaluator passes continuity-acknowledgement transcript', () => {
168168

169169
test('evaluator passes repo-boundary clarification transcript', () => {
170170
const transcript = [
171-
'Conversation, visual context, browser session state, and session intent state cleared.',
171+
'Conversation, visual context, browser session state, session intent state, and chat continuity state cleared.',
172172
'> MUSE is a different repo, this is copilot-liku-cli.',
173173
'[copilot:stub]',
174174
'Understood. MUSE is a different repo and this session is in copilot-liku-cli.',
@@ -201,7 +201,7 @@ test('evaluator fails repo-boundary clarification when it skips the switch step'
201201

202202
test('evaluator passes forgone-feature suppression transcript', () => {
203203
const transcript = [
204-
'Conversation, visual context, browser session state, and session intent state cleared.',
204+
'Conversation, visual context, browser session state, session intent state, and chat continuity state cleared.',
205205
'> I have forgone the implementation of: terminal-liku ui.',
206206
'[copilot:stub]',
207207
'Understood.',

scripts/test-message-builder-session-intent.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@ async function main() {
2222
});
2323

2424
const messages = await builder.buildMessages('hello', false, {
25-
sessionIntentContext: '- currentRepo: copilot-liku-cli\n- forgoneFeatures: terminal-liku ui'
25+
sessionIntentContext: '- currentRepo: copilot-liku-cli\n- forgoneFeatures: terminal-liku ui',
26+
chatContinuityContext: '- activeGoal: Produce a confident synthesis of ticker LUNR in TradingView\n- lastExecutedActions: focus_window -> screenshot\n- continuationReady: yes'
2627
});
2728

2829
const sessionMessage = messages.find((entry) => entry.role === 'system' && entry.content.includes('## Session Constraints'));
2930
assert(sessionMessage, 'session constraints section is injected');
3031
assert(sessionMessage.content.includes('terminal-liku ui'));
3132

33+
const continuityMessage = messages.find((entry) => entry.role === 'system' && entry.content.includes('## Recent Action Continuity'));
34+
assert(continuityMessage, 'chat continuity section is injected');
35+
assert(continuityMessage.content.includes('lastExecutedActions: focus_window -> screenshot'));
36+
3237
console.log('PASS message builder session intent');
3338
}
3439

scripts/test-session-intent-state.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const os = require('os');
66
const path = require('path');
77

88
const {
9+
formatChatContinuityContext,
10+
formatChatContinuitySummary,
911
createSessionIntentStateStore,
1012
formatSessionIntentContext,
1113
formatSessionIntentSummary
@@ -59,7 +61,19 @@ test('session intent formatters emit compact system and summary views', () => {
5961
currentRepo: { repoName: 'copilot-liku-cli', projectRoot: 'C:/dev/copilot-Liku-cli' },
6062
downstreamRepoIntent: { repoName: 'muse-ai' },
6163
forgoneFeatures: [{ feature: 'terminal-liku ui' }],
62-
explicitCorrections: [{ text: 'MUSE is a different repo, this is copilot-liku-cli.' }]
64+
explicitCorrections: [{ text: 'MUSE is a different repo, this is copilot-liku-cli.' }],
65+
chatContinuity: {
66+
activeGoal: 'Produce a confident synthesis of ticker LUNR in TradingView',
67+
currentSubgoal: 'Inspect the current chart state',
68+
continuationReady: true,
69+
degradedReason: null,
70+
lastTurn: {
71+
actionSummary: 'focus_window -> screenshot',
72+
executionStatus: 'succeeded',
73+
verificationStatus: 'verified',
74+
nextRecommendedStep: 'Continue from the latest chart evidence.'
75+
}
76+
}
6377
};
6478

6579
const context = formatSessionIntentContext(state);
@@ -70,4 +84,46 @@ test('session intent formatters emit compact system and summary views', () => {
7084
const summary = formatSessionIntentSummary(state);
7185
assert.ok(summary.includes('Current repo: copilot-liku-cli'));
7286
assert.ok(summary.includes('Forgone features: terminal-liku ui'));
87+
88+
const continuityContext = formatChatContinuityContext(state);
89+
assert.ok(continuityContext.includes('activeGoal: Produce a confident synthesis'));
90+
assert.ok(continuityContext.includes('lastExecutedActions: focus_window -> screenshot'));
91+
assert.ok(continuityContext.includes('continuationReady: yes'));
92+
93+
const continuitySummary = formatChatContinuitySummary(state);
94+
assert.ok(continuitySummary.includes('Active goal: Produce a confident synthesis'));
95+
assert.ok(continuitySummary.includes('Continuation ready: yes'));
96+
});
97+
98+
test('session intent store records and clears chat continuity state', () => {
99+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'liku-session-intent-'));
100+
const stateFile = path.join(tempDir, 'session-intent-state.json');
101+
const store = createSessionIntentStateStore({ stateFile });
102+
103+
const recorded = store.recordExecutedTurn({
104+
userMessage: 'help me make a confident synthesis of ticker LUNR in tradingview',
105+
executionIntent: 'help me make a confident synthesis of ticker LUNR in tradingview',
106+
committedSubgoal: 'Inspect the active TradingView chart',
107+
actionPlan: [{ type: 'focus_window' }, { type: 'screenshot' }],
108+
success: true,
109+
screenshotCaptured: true,
110+
observationEvidence: { captureMode: 'window', captureTrusted: true },
111+
verification: { status: 'verified' },
112+
nextRecommendedStep: 'Continue from the latest chart evidence.'
113+
}, {
114+
cwd: path.join(__dirname, '..')
115+
});
116+
117+
assert.strictEqual(recorded.chatContinuity.activeGoal, 'help me make a confident synthesis of ticker LUNR in tradingview');
118+
assert.strictEqual(recorded.chatContinuity.lastTurn.actionSummary, 'focus_window -> screenshot');
119+
assert.strictEqual(recorded.chatContinuity.continuationReady, true);
120+
121+
const reloaded = createSessionIntentStateStore({ stateFile }).getChatContinuity({ cwd: path.join(__dirname, '..') });
122+
assert.strictEqual(reloaded.currentSubgoal, 'Inspect the active TradingView chart');
123+
assert.strictEqual(reloaded.lastTurn.captureMode, 'window');
124+
125+
const cleared = store.clearChatContinuity({ cwd: path.join(__dirname, '..') });
126+
assert.strictEqual(cleared.chatContinuity.activeGoal, null);
127+
assert.strictEqual(cleared.chatContinuity.continuationReady, false);
128+
fs.rmSync(tempDir, { recursive: true, force: true });
73129
});

src/cli/commands/chat.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const readline = require('readline');
77
const { success, error, info, warn, highlight, dim, bold } = require('../util/output');
88
const systemAutomation = require('../../main/system-automation');
99
const preferences = require('../../main/preferences');
10+
const { recordChatContinuityTurn } = require('../../main/session-intent-state');
1011
const {
1112
getLogLevel: getUiAutomationLogLevel,
1213
resetLogSettings: resetUiAutomationLogSettings,
@@ -264,6 +265,67 @@ function buildForcedObservationAnswerPrompt(userMessage) {
264265
].join(' ');
265266
}
266267

268+
function inferContinuationVerificationStatus(execResult) {
269+
if (!execResult) return 'unknown';
270+
if (execResult.cancelled) return 'cancelled';
271+
if (execResult.success === false) return 'failed';
272+
if (execResult.postVerificationFailed) return 'unverified';
273+
if (execResult.postVerification?.verified) return 'verified';
274+
if (execResult.focusVerification?.verified) return 'verified';
275+
if (execResult.focusVerification?.applicable && !execResult.focusVerification?.verified) return 'unverified';
276+
return execResult.success ? 'not-applicable' : 'unknown';
277+
}
278+
279+
function inferNextRecommendedStep(execResult) {
280+
if (!execResult) return 'Continue from the last committed subgoal using the current app state.';
281+
if (execResult.cancelled) return 'Ask whether to retry the interrupted step or choose a different path.';
282+
if (execResult.success === false) return 'Review the failed step and gather fresh evidence before continuing.';
283+
if (execResult.postVerification?.needsFollowUp) return 'Continue with the detected follow-up flow for the current app state.';
284+
if (execResult.screenshotCaptured) return 'Continue from the latest visual evidence and current app state.';
285+
if (inferContinuationVerificationStatus(execResult) === 'unverified') return 'Gather fresh evidence before claiming the requested state change is complete.';
286+
return 'Continue from the current subgoal using the latest execution results.';
287+
}
288+
289+
function recordContinuityFromExecution(ai, actionData, execResult, details = {}) {
290+
try {
291+
const latestVisual = typeof ai?.getLatestVisualContext === 'function'
292+
? ai.getLatestVisualContext()
293+
: null;
294+
const captureMode = String(latestVisual?.scope || '').trim() || (execResult?.screenshotCaptured ? 'screen' : null);
295+
const captureTrusted = captureMode ? (captureMode === 'window' || captureMode === 'region') : null;
296+
const targetWindowHandle = Number(details.targetWindowHandle || execResult?.focusVerification?.expectedWindowHandle || 0) || null;
297+
recordChatContinuityTurn({
298+
recordedAt: new Date().toISOString(),
299+
userMessage: details.userMessage || '',
300+
executionIntent: details.executionIntent || details.userMessage || '',
301+
activeGoal: details.executionIntent || details.userMessage || '',
302+
committedSubgoal: actionData?.thought || details.executionIntent || details.userMessage || '',
303+
thought: actionData?.thought || '',
304+
actionPlan: Array.isArray(actionData?.actions) ? actionData.actions : [],
305+
success: !!execResult?.success,
306+
cancelled: !!execResult?.cancelled,
307+
postVerificationFailed: !!execResult?.postVerificationFailed,
308+
postVerification: execResult?.postVerification || null,
309+
focusVerification: execResult?.focusVerification || null,
310+
screenshotCaptured: !!execResult?.screenshotCaptured,
311+
executedCount: Array.isArray(actionData?.actions) ? actionData.actions.length : 0,
312+
targetWindowHandle,
313+
windowTitle: latestVisual?.windowTitle || null,
314+
observationEvidence: {
315+
captureMode,
316+
captureTrusted,
317+
windowHandle: Number(latestVisual?.windowHandle || 0) || targetWindowHandle || null
318+
},
319+
verification: {
320+
status: inferContinuationVerificationStatus(execResult)
321+
},
322+
nextRecommendedStep: inferNextRecommendedStep(execResult)
323+
}, { cwd: process.cwd() });
324+
} catch (continuityError) {
325+
warn(`Could not record chat continuity state: ${continuityError.message}`);
326+
}
327+
}
328+
267329
function shouldAutoCaptureObservationAfterActions(userMessage, actions, execResult) {
268330
if (!isLikelyObservationInput(userMessage)) return false;
269331
if (!Array.isArray(actions) || actions.length === 0) return false;
@@ -1136,6 +1198,14 @@ async function runChatLoop(ai, options) {
11361198
}
11371199
}
11381200

1201+
recordContinuityFromExecution(ai, actionData, execResult, {
1202+
userMessage: line,
1203+
executionIntent: effectiveUserMessage,
1204+
targetWindowHandle: actionData?.actions?.find((action) => action?.windowHandle || action?.targetWindowHandle)?.windowHandle
1205+
|| actionData?.actions?.find((action) => action?.windowHandle || action?.targetWindowHandle)?.targetWindowHandle
1206+
|| null
1207+
});
1208+
11391209
// ===== VISION AUTO-CONTINUATION =====
11401210
// If the AI requested a screenshot during its action sequence AND we captured it,
11411211
// automatically send a follow-up message so the AI can analyze the capture and
@@ -1252,6 +1322,14 @@ async function runChatLoop(ai, options) {
12521322
break;
12531323
}
12541324

1325+
recordContinuityFromExecution(ai, contActionData, contExecResult, {
1326+
userMessage: line,
1327+
executionIntent: effectiveUserMessage,
1328+
targetWindowHandle: contActionData?.actions?.find((action) => action?.windowHandle || action?.targetWindowHandle)?.windowHandle
1329+
|| contActionData?.actions?.find((action) => action?.windowHandle || action?.targetWindowHandle)?.targetWindowHandle
1330+
|| null
1331+
});
1332+
12551333
// If the continuation itself requested another screenshot, loop again
12561334
if (!contExecResult?.screenshotCaptured) break;
12571335
}

src/main/ai-service.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,15 @@ const {
7171
updateBrowserSessionState
7272
} = require('./ai-service/browser-session-state');
7373
const {
74+
clearChatContinuityState,
75+
formatChatContinuityContext,
7476
clearSessionIntentState,
7577
formatSessionIntentContext,
7678
formatSessionIntentSummary,
79+
getChatContinuityState,
7780
getSessionIntentState,
78-
ingestUserIntentState
81+
ingestUserIntentState,
82+
recordChatContinuityTurn
7983
} = require('./session-intent-state');
8084
const {
8185
clearSemanticDOMSnapshot,
@@ -349,8 +353,10 @@ const commandHandler = createCommandHandler({
349353
}
350354
},
351355
clearVisualContext,
356+
clearChatContinuityState,
352357
exchangeForCopilotSession,
353358
getCopilotModels,
359+
getChatContinuityState,
354360
getCurrentCopilotModel,
355361
getCurrentProvider,
356362
getStatus,
@@ -379,7 +385,17 @@ const commandHandler = createCommandHandler({
379385
* Build messages array for API call
380386
*/
381387
async function buildMessages(userMessage, includeVisual = false, options = {}) {
382-
return messageBuilder.buildMessages(userMessage, includeVisual, options);
388+
const mergedOptions = { ...(options || {}) };
389+
try {
390+
const sessionState = getSessionIntentState({ cwd: process.cwd() });
391+
if (!(typeof mergedOptions.sessionIntentContext === 'string' && mergedOptions.sessionIntentContext.trim())) {
392+
mergedOptions.sessionIntentContext = formatSessionIntentContext(sessionState) || '';
393+
}
394+
if (!(typeof mergedOptions.chatContinuityContext === 'string' && mergedOptions.chatContinuityContext.trim())) {
395+
mergedOptions.chatContinuityContext = formatChatContinuityContext(sessionState) || '';
396+
}
397+
} catch {}
398+
return messageBuilder.buildMessages(userMessage, includeVisual, mergedOptions);
383399
}
384400

385401
function getCopilotModelCapabilities(modelKey) {
@@ -1276,9 +1292,12 @@ async function sendMessage(userMessage, options = {}) {
12761292
}
12771293

12781294
let sessionIntentContextText = '';
1295+
let chatContinuityContextText = '';
12791296
try {
12801297
ingestUserIntentState(enhancedMessage, { cwd: process.cwd() });
1281-
sessionIntentContextText = formatSessionIntentContext(getSessionIntentState({ cwd: process.cwd() })) || '';
1298+
const sessionState = getSessionIntentState({ cwd: process.cwd() });
1299+
sessionIntentContextText = formatSessionIntentContext(sessionState) || '';
1300+
chatContinuityContextText = formatChatContinuityContext(sessionState) || '';
12821301
} catch (err) {
12831302
console.warn('[AI] Session intent state error (non-fatal):', err.message);
12841303
}
@@ -1288,7 +1307,8 @@ async function sendMessage(userMessage, options = {}) {
12881307
extraSystemMessages: baseExtraSystemMessages,
12891308
skillsContext: skillsContextText,
12901309
memoryContext: memoryContextText,
1291-
sessionIntentContext: sessionIntentContextText
1310+
sessionIntentContext: sessionIntentContextText,
1311+
chatContinuityContext: chatContinuityContextText
12921312
});
12931313

12941314
try {
@@ -1513,8 +1533,9 @@ function handleCommand(command) {
15131533
clearVisualContext();
15141534
resetBrowserSessionState();
15151535
clearSessionIntentState({ cwd: process.cwd() });
1536+
clearChatContinuityState({ cwd: process.cwd() });
15161537
historyStore.saveConversationHistory();
1517-
return { type: 'system', message: 'Conversation, visual context, browser session state, and session intent state cleared.' };
1538+
return { type: 'system', message: 'Conversation, visual context, browser session state, session intent state, and chat continuity state cleared.' };
15181539

15191540
case '/vision':
15201541
if (parts[1] === 'on') {
@@ -5408,8 +5429,11 @@ module.exports = {
54085429
// Cognitive layer (v0.0.15)
54095430
memoryStore,
54105431
skillRouter,
5432+
getChatContinuityState,
54115433
getSessionIntentState,
5434+
clearChatContinuityState,
54125435
ingestUserIntentState,
5436+
recordChatContinuityTurn,
54135437
// Session persistence (N4)
54145438
saveSessionNote,
54155439
// Cross-model reflection (N6)

0 commit comments

Comments
 (0)