@adhdev/daemon-core 0.8.83 → 0.8.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +3 -0
- package/dist/cli-adapters/session-host-transport.d.ts +2 -1
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.js +297 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +297 -24
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +10 -0
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +252 -21
- package/src/cli-adapters/session-host-transport.ts +9 -1
- package/src/commands/chat-commands.ts +35 -2
- package/src/config/chat-history.ts +137 -0
- package/src/providers/cli-provider-instance.ts +68 -3
- package/src/session-host/startup-restore-policy.js +2 -0
- package/src/session-host/startup-restore-policy.ts +2 -0
|
@@ -8,6 +8,14 @@ import { type ProviderModule } from './contracts.js';
|
|
|
8
8
|
import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
|
|
9
9
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
10
10
|
import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
11
|
+
type PersistableCliHistoryMessage = {
|
|
12
|
+
role: string;
|
|
13
|
+
content: string;
|
|
14
|
+
kind?: string;
|
|
15
|
+
senderName?: string;
|
|
16
|
+
receivedAt?: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function buildIncrementalHistoryAppendMessages(previousMessages: PersistableCliHistoryMessage[], currentMessages: PersistableCliHistoryMessage[]): PersistableCliHistoryMessage[];
|
|
11
19
|
export declare function getForcedNewSessionScriptName(provider: ProviderModule | undefined, launchMode: 'new' | 'resume' | 'manual'): string | null;
|
|
12
20
|
export declare function waitForCliAdapterReady(adapter: {
|
|
13
21
|
isReady?: () => boolean;
|
|
@@ -39,6 +47,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
39
47
|
private appliedEffectKeys;
|
|
40
48
|
private historyWriter;
|
|
41
49
|
private runtimeMessages;
|
|
50
|
+
private lastPersistedHistoryMessages;
|
|
42
51
|
readonly instanceId: string;
|
|
43
52
|
private suppressIdleHistoryReplay;
|
|
44
53
|
private errorMessage;
|
|
@@ -99,3 +108,4 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
99
108
|
private buildSqlPlaceholderList;
|
|
100
109
|
private querySqliteText;
|
|
101
110
|
}
|
|
111
|
+
export {};
|
package/package.json
CHANGED
|
@@ -853,6 +853,58 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
853
853
|
);
|
|
854
854
|
}
|
|
855
855
|
|
|
856
|
+
private clearStaleIdleResponseGuard(reason: string): boolean {
|
|
857
|
+
const screenText = this.terminalScreen.getText() || '';
|
|
858
|
+
const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
|
|
859
|
+
const blockingModal = this.activeModal || this.getStartupConfirmationModal(screenText);
|
|
860
|
+
if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !visibleIdlePrompt || !!blockingModal) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
864
|
+
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
865
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
866
|
+
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
867
|
+
this.clearIdleFinishCandidate(reason);
|
|
868
|
+
this.responseBuffer = '';
|
|
869
|
+
this.isWaitingForResponse = false;
|
|
870
|
+
this.responseSettleIgnoreUntil = 0;
|
|
871
|
+
this.submitRetryUsed = false;
|
|
872
|
+
this.submitRetryPromptSnippet = '';
|
|
873
|
+
this.finishRetryCount = 0;
|
|
874
|
+
this.currentTurnScope = null;
|
|
875
|
+
this.activeModal = null;
|
|
876
|
+
this.recordTrace('stale_idle_response_cleared', {
|
|
877
|
+
reason,
|
|
878
|
+
screenText: summarizeCliTraceText(screenText, 240),
|
|
879
|
+
});
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private hasMeaningfulResponseBuffer(promptSnippet: string): boolean {
|
|
884
|
+
const raw = String(this.responseBuffer || '').trim();
|
|
885
|
+
if (!raw) return false;
|
|
886
|
+
const normalizedPrompt = compactPromptText(promptSnippet);
|
|
887
|
+
if (!normalizedPrompt) return true;
|
|
888
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
889
|
+
if (!normalizedBuffer) return false;
|
|
890
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
891
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
892
|
+
const remainder = normalizedBuffer
|
|
893
|
+
.slice(normalizedPrompt.length)
|
|
894
|
+
.replace(/[─═\-]+/g, '')
|
|
895
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
896
|
+
.replace(/accepteditson\([^)]*\)/gi, '')
|
|
897
|
+
.replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
|
|
898
|
+
.replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
|
|
899
|
+
.replace(/esctointerrupt/gi, '')
|
|
900
|
+
.replace(/❯/g, '')
|
|
901
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
902
|
+
.trim();
|
|
903
|
+
return remainder.length > 0;
|
|
904
|
+
}
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
|
|
856
908
|
private evaluateSettled(): void {
|
|
857
909
|
const now = Date.now();
|
|
858
910
|
if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
|
|
@@ -888,7 +940,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
888
940
|
lastOutputAt: this.lastOutputAt,
|
|
889
941
|
})
|
|
890
942
|
: [];
|
|
943
|
+
if (this.maybeCommitVisibleIdleTranscript(parsedTranscript)) {
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
891
946
|
const lastParsedAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
|
|
947
|
+
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || '');
|
|
892
948
|
this.recordTrace('settled', {
|
|
893
949
|
tail: summarizeCliTraceText(tail, 500),
|
|
894
950
|
screenText: summarizeCliTraceText(screenText, 1200),
|
|
@@ -906,6 +962,32 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
906
962
|
scope: this.currentTurnScope,
|
|
907
963
|
}),
|
|
908
964
|
});
|
|
965
|
+
if (
|
|
966
|
+
this.currentTurnScope
|
|
967
|
+
&& !lastParsedAssistant
|
|
968
|
+
&& !this.submitRetryUsed
|
|
969
|
+
&& this.ptyProcess
|
|
970
|
+
&& this.currentStatus !== 'waiting_approval'
|
|
971
|
+
&& promptLikelyVisible(screenText, normalizedPromptSnippet)
|
|
972
|
+
&& !this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)
|
|
973
|
+
) {
|
|
974
|
+
this.submitRetryUsed = true;
|
|
975
|
+
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
976
|
+
LOG.info('CLI', `[${this.cliType}] Retrying submit key from settled parser (no assistant yet)`);
|
|
977
|
+
this.recordTrace('submit_write', {
|
|
978
|
+
mode: 'settled_retry',
|
|
979
|
+
sendKey: this.sendKey,
|
|
980
|
+
screenText: summarizeCliTraceText(screenText, 500),
|
|
981
|
+
});
|
|
982
|
+
this.ptyProcess.write(this.sendKey);
|
|
983
|
+
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
984
|
+
this.settleTimer = setTimeout(() => {
|
|
985
|
+
this.settleTimer = null;
|
|
986
|
+
this.settledBuffer = this.recentOutputBuffer;
|
|
987
|
+
this.evaluateSettled();
|
|
988
|
+
}, this.timeouts.outputSettle + 150);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
909
991
|
if (this.currentTurnScope && !lastParsedAssistant) {
|
|
910
992
|
LOG.info(
|
|
911
993
|
'CLI',
|
|
@@ -956,11 +1038,20 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
956
1038
|
|
|
957
1039
|
const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
|
|
958
1040
|
const statusActivityHoldMs = this.getStatusActivityHoldMs();
|
|
1041
|
+
const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
|
|
1042
|
+
const visibleAssistantCandidate = this.looksLikeVisibleAssistantCandidate(screenText);
|
|
1043
|
+
if (this.currentTurnScope && this.cliType === 'claude-cli') {
|
|
1044
|
+
LOG.info(
|
|
1045
|
+
'CLI',
|
|
1046
|
+
`[${this.cliType}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} scriptStatus=${String(scriptStatus || '')} parsedStatus=${String(parsedTranscript?.status || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify(summarizeCliTraceText(lastParsedAssistant?.content || '', 120)).slice(0, 160)} visibleIdlePrompt=${String(visibleIdlePrompt)} visibleAssistantCandidate=${String(visibleAssistantCandidate)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 160)).slice(0, 220)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 160)).slice(0, 220)}`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
959
1049
|
const shouldHoldGenerating =
|
|
960
1050
|
scriptStatus === 'idle'
|
|
961
1051
|
&& this.isWaitingForResponse
|
|
962
1052
|
&& !modal
|
|
963
|
-
&& recentInteractiveActivity
|
|
1053
|
+
&& recentInteractiveActivity
|
|
1054
|
+
&& !(visibleIdlePrompt && visibleAssistantCandidate);
|
|
964
1055
|
|
|
965
1056
|
if (shouldHoldGenerating) {
|
|
966
1057
|
this.clearIdleFinishCandidate('hold_generating_recent_activity');
|
|
@@ -1202,6 +1293,70 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1202
1293
|
this.onStatusChange?.();
|
|
1203
1294
|
}
|
|
1204
1295
|
|
|
1296
|
+
private maybeCommitVisibleIdleTranscript(
|
|
1297
|
+
parsed: any,
|
|
1298
|
+
options?: { requireVisibleAssistantCandidate?: boolean; screenText?: string },
|
|
1299
|
+
): boolean {
|
|
1300
|
+
const allowImmediateScriptIdleCommit = this.provider.allowInputDuringGeneration === true;
|
|
1301
|
+
if (!allowImmediateScriptIdleCommit) return false;
|
|
1302
|
+
if (
|
|
1303
|
+
!parsed
|
|
1304
|
+
|| !Array.isArray(parsed.messages)
|
|
1305
|
+
|| parsed.status !== 'idle'
|
|
1306
|
+
|| !this.isWaitingForResponse
|
|
1307
|
+
|| !this.currentTurnScope
|
|
1308
|
+
|| this.activeModal
|
|
1309
|
+
|| parsed.activeModal
|
|
1310
|
+
) {
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (options?.requireVisibleAssistantCandidate) {
|
|
1315
|
+
const candidateText = options.screenText || this.terminalScreen.getText() || '';
|
|
1316
|
+
if (!this.looksLikeVisibleAssistantCandidate(candidateText)) {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const hydratedForIdleCommit = normalizeCliParsedMessages(parsed.messages, {
|
|
1322
|
+
committedMessages: this.committedMessages,
|
|
1323
|
+
scope: this.currentTurnScope,
|
|
1324
|
+
lastOutputAt: this.lastOutputAt,
|
|
1325
|
+
});
|
|
1326
|
+
const visibleAssistant = [...hydratedForIdleCommit].reverse().find((message) => message.role === 'assistant' && message.content.trim());
|
|
1327
|
+
if (!visibleAssistant) return false;
|
|
1328
|
+
|
|
1329
|
+
this.committedMessages = hydratedForIdleCommit;
|
|
1330
|
+
const promptForTrim = this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages);
|
|
1331
|
+
if (promptForTrim) {
|
|
1332
|
+
const lastAssistantForTrim = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
|
|
1333
|
+
if (lastAssistantForTrim) {
|
|
1334
|
+
lastAssistantForTrim.content = trimPromptEchoPrefix(lastAssistantForTrim.content, promptForTrim);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
1338
|
+
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
1339
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1340
|
+
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
1341
|
+
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
1342
|
+
this.syncMessageViews();
|
|
1343
|
+
this.responseBuffer = '';
|
|
1344
|
+
this.isWaitingForResponse = false;
|
|
1345
|
+
this.responseSettleIgnoreUntil = 0;
|
|
1346
|
+
this.submitRetryUsed = false;
|
|
1347
|
+
this.submitRetryPromptSnippet = '';
|
|
1348
|
+
this.finishRetryCount = 0;
|
|
1349
|
+
this.currentTurnScope = null;
|
|
1350
|
+
this.activeModal = null;
|
|
1351
|
+
this.setStatus('idle', 'script_idle_commit');
|
|
1352
|
+
this.onStatusChange?.();
|
|
1353
|
+
this.recordTrace('script_idle_commit', {
|
|
1354
|
+
messageCount: this.committedMessages.length,
|
|
1355
|
+
lastAssistant: summarizeCliTraceText(visibleAssistant.content, 320),
|
|
1356
|
+
});
|
|
1357
|
+
return true;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1205
1360
|
private commitCurrentTranscript(): { hasAssistant: boolean; assistantContent: string } {
|
|
1206
1361
|
const parsed = this.parseCurrentTranscript(
|
|
1207
1362
|
this.committedMessages,
|
|
@@ -1223,6 +1378,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1223
1378
|
}
|
|
1224
1379
|
this.syncMessageViews();
|
|
1225
1380
|
const lastAssistant = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
|
|
1381
|
+
if (this.currentTurnScope) {
|
|
1382
|
+
LOG.info(
|
|
1383
|
+
'CLI',
|
|
1384
|
+
`[${this.cliType}] commitCurrentTranscript committedMessages=${this.committedMessages.length} finalLastAssistant=${JSON.stringify(summarizeCliTraceText(lastAssistant?.content || '', 220)).slice(0, 260)}`
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1226
1387
|
this.recordTrace('commit_transcript', {
|
|
1227
1388
|
parsedStatus: parsed.status || null,
|
|
1228
1389
|
messageCount: this.committedMessages.length,
|
|
@@ -1242,17 +1403,25 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1242
1403
|
`[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
|
|
1243
1404
|
);
|
|
1244
1405
|
}
|
|
1406
|
+
const hasAssistant = !!lastAssistant;
|
|
1245
1407
|
return {
|
|
1246
|
-
hasAssistant
|
|
1408
|
+
hasAssistant,
|
|
1247
1409
|
assistantContent: lastAssistant?.content || '',
|
|
1248
1410
|
};
|
|
1249
1411
|
}
|
|
1412
|
+
if (this.currentTurnScope) {
|
|
1413
|
+
LOG.info(
|
|
1414
|
+
'CLI',
|
|
1415
|
+
`[${this.cliType}] commitCurrentTranscript parsed.messages=none responseBufferLen=${this.responseBuffer.length} accumulatedBufferLen=${this.accumulatedBuffer.length} parsedStatus=${parsed?.status || '-'} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1250
1418
|
return {
|
|
1251
1419
|
hasAssistant: false,
|
|
1252
1420
|
assistantContent: '',
|
|
1253
1421
|
};
|
|
1254
1422
|
}
|
|
1255
1423
|
|
|
1424
|
+
|
|
1256
1425
|
// ─── Script Execution ──────────────────────────
|
|
1257
1426
|
|
|
1258
1427
|
private runDetectStatus(text: string): string | null {
|
|
@@ -1359,26 +1528,35 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1359
1528
|
this.currentTurnScope,
|
|
1360
1529
|
screenText,
|
|
1361
1530
|
);
|
|
1531
|
+
if (this.maybeCommitVisibleIdleTranscript(parsed)) {
|
|
1532
|
+
return this.getScriptParsedStatus();
|
|
1533
|
+
}
|
|
1362
1534
|
const shouldPreferCommittedMessages =
|
|
1363
1535
|
!this.currentTurnScope
|
|
1364
|
-
&& this.
|
|
1365
|
-
&&
|
|
1536
|
+
&& !this.activeModal
|
|
1537
|
+
&& this.currentStatus === 'idle';
|
|
1366
1538
|
let result: any;
|
|
1367
1539
|
if (parsed && Array.isArray(parsed.messages)) {
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
:
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1540
|
+
const parsedHydratedMessages = hydrateCliParsedMessages(parsed.messages, {
|
|
1541
|
+
committedMessages: this.committedMessages,
|
|
1542
|
+
scope: this.currentTurnScope,
|
|
1543
|
+
lastOutputAt: this.lastOutputAt,
|
|
1544
|
+
});
|
|
1545
|
+
const committedHydratedMessages = this.committedMessages.map((message, index) => buildChatMessage({
|
|
1546
|
+
...message,
|
|
1547
|
+
id: message.id || `msg_${index}`,
|
|
1548
|
+
index: typeof message.index === 'number' ? message.index : index,
|
|
1549
|
+
receivedAt: typeof message.receivedAt === 'number'
|
|
1550
|
+
? message.receivedAt
|
|
1551
|
+
: message.timestamp,
|
|
1552
|
+
}));
|
|
1553
|
+
const shouldPreferCommittedHistoryReplay =
|
|
1554
|
+
!this.currentTurnScope
|
|
1555
|
+
&& !this.activeModal
|
|
1556
|
+
&& committedHydratedMessages.length > parsedHydratedMessages.length;
|
|
1557
|
+
const hydratedMessages = (shouldPreferCommittedMessages || shouldPreferCommittedHistoryReplay)
|
|
1558
|
+
? committedHydratedMessages
|
|
1559
|
+
: parsedHydratedMessages;
|
|
1382
1560
|
result = {
|
|
1383
1561
|
id: parsed.id || 'cli_session',
|
|
1384
1562
|
status: parsed.status || this.currentStatus,
|
|
@@ -1405,6 +1583,32 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1405
1583
|
};
|
|
1406
1584
|
}
|
|
1407
1585
|
|
|
1586
|
+
const hasVisibleAssistantMessage = Array.isArray(result?.messages)
|
|
1587
|
+
&& result.messages.some((message: any) => message?.role === 'assistant' && typeof message?.content === 'string' && message.content.trim());
|
|
1588
|
+
const shouldClampStaleGeneratingToIdle =
|
|
1589
|
+
result?.status === 'generating'
|
|
1590
|
+
&& this.currentStatus === 'idle'
|
|
1591
|
+
&& !this.currentTurnScope
|
|
1592
|
+
&& !result?.activeModal
|
|
1593
|
+
&& hasVisibleAssistantMessage;
|
|
1594
|
+
if (shouldClampStaleGeneratingToIdle) {
|
|
1595
|
+
result = {
|
|
1596
|
+
...result,
|
|
1597
|
+
status: 'idle',
|
|
1598
|
+
messages: Array.isArray(result.messages)
|
|
1599
|
+
? result.messages.map((message: any) => {
|
|
1600
|
+
if (message?.role !== 'assistant' || !message?.meta?.streaming) return message;
|
|
1601
|
+
const nextMeta = { ...(message.meta || {}) };
|
|
1602
|
+
delete nextMeta.streaming;
|
|
1603
|
+
return {
|
|
1604
|
+
...message,
|
|
1605
|
+
...(Object.keys(nextMeta).length > 0 ? { meta: nextMeta } : { meta: undefined }),
|
|
1606
|
+
};
|
|
1607
|
+
})
|
|
1608
|
+
: result.messages,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1408
1612
|
this.parsedStatusCache = {
|
|
1409
1613
|
committedMessagesRef: this.committedMessages,
|
|
1410
1614
|
responseBuffer: this.responseBuffer,
|
|
@@ -1541,9 +1745,36 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1541
1745
|
}
|
|
1542
1746
|
}
|
|
1543
1747
|
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
|
|
1544
|
-
|
|
1748
|
+
const parsedStatusBeforeSend = !allowInputDuringGeneration
|
|
1749
|
+
? (() => {
|
|
1750
|
+
try {
|
|
1751
|
+
return this.getScriptParsedStatus?.() || null;
|
|
1752
|
+
} catch {
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1755
|
+
})()
|
|
1756
|
+
: null;
|
|
1757
|
+
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1758
|
+
? String(parsedStatusBeforeSend.status)
|
|
1759
|
+
: '';
|
|
1760
|
+
const parsedMessagesBeforeSend = Array.isArray(parsedStatusBeforeSend?.messages)
|
|
1761
|
+
? parsedStatusBeforeSend.messages.filter((message: any) => message && (message.role === 'user' || message.role === 'assistant'))
|
|
1762
|
+
: [];
|
|
1763
|
+
const shouldCommitParsedIdleBeforeSend = !allowInputDuringGeneration
|
|
1764
|
+
&& parsedSessionStatus === 'idle'
|
|
1765
|
+
&& parsedMessagesBeforeSend.length > this.committedMessages.length
|
|
1766
|
+
&& parsedMessagesBeforeSend.some((message: any) => message?.role === 'assistant' && typeof message?.content === 'string' && message.content.trim());
|
|
1767
|
+
if (shouldCommitParsedIdleBeforeSend) {
|
|
1768
|
+
this.commitCurrentTranscript();
|
|
1769
|
+
}
|
|
1770
|
+
if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating')) {
|
|
1545
1771
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1546
1772
|
}
|
|
1773
|
+
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1774
|
+
if (!this.clearStaleIdleResponseGuard('send_message_guard')) {
|
|
1775
|
+
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1547
1778
|
const blockingModal = this.activeModal || this.getStartupConfirmationModal(this.terminalScreen.getText() || '');
|
|
1548
1779
|
if (blockingModal || this.currentStatus === 'waiting_approval') {
|
|
1549
1780
|
throw new Error(`${this.cliName} is awaiting confirmation before it can accept a prompt`);
|
|
@@ -1622,7 +1853,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1622
1853
|
this.submitRetryTimer = null;
|
|
1623
1854
|
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
|
|
1624
1855
|
if (this.currentStatus === 'waiting_approval') return;
|
|
1625
|
-
if (
|
|
1856
|
+
if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return;
|
|
1626
1857
|
const screenText = this.terminalScreen.getText();
|
|
1627
1858
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
|
|
1628
1859
|
if (/Esc to interrupt|Do you want to proceed|This command requires approval|Allow Codex to|Approve and run now|Always approve this session|Running…|Running\.\.\./i.test(screenText)) return;
|
|
@@ -1660,7 +1891,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1660
1891
|
this.submitRetryTimer = null;
|
|
1661
1892
|
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
|
|
1662
1893
|
if (this.currentStatus === 'waiting_approval') return;
|
|
1663
|
-
if (
|
|
1894
|
+
if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return;
|
|
1664
1895
|
const screenText = this.terminalScreen.getText();
|
|
1665
1896
|
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
|
|
1666
1897
|
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
|
|
@@ -27,6 +27,14 @@ interface SessionHostRuntimeOptions extends SessionHostPtyTransportFactoryOption
|
|
|
27
27
|
spawnOptions: PtySpawnOptions;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function shouldResumeAttachedSession(record: SessionHostRecord | null | undefined): boolean {
|
|
31
|
+
if (!record) return false;
|
|
32
|
+
if (record.lifecycle === 'interrupted') return true;
|
|
33
|
+
if (record.lifecycle !== 'stopped') return false;
|
|
34
|
+
if (record.meta?.restoredFromStorage === true) return true;
|
|
35
|
+
return typeof record.meta?.runtimeRecoveryState === 'string' && String(record.meta.runtimeRecoveryState).trim().length > 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
31
39
|
readonly ready: Promise<void>;
|
|
32
40
|
readonly terminalQueriesHandled = true;
|
|
@@ -215,7 +223,7 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
|
215
223
|
const existingRecord = existingRecords.success && existingRecords.result
|
|
216
224
|
? existingRecords.result.find((item) => item.sessionId === this.options.runtimeId) || null
|
|
217
225
|
: null;
|
|
218
|
-
if (existingRecord
|
|
226
|
+
if (shouldResumeAttachedSession(existingRecord)) {
|
|
219
227
|
const resumeResponse = await this.client.request<SessionHostRecord>({
|
|
220
228
|
type: 'resume_session',
|
|
221
229
|
payload: {
|
|
@@ -340,6 +340,9 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
|
|
|
340
340
|
|
|
341
341
|
function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
|
|
342
342
|
let validatedPayload: Record<string, any>;
|
|
343
|
+
const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
|
|
344
|
+
? payload.debugReadChat
|
|
345
|
+
: undefined;
|
|
343
346
|
try {
|
|
344
347
|
validatedPayload = validateReadChatResultPayload({
|
|
345
348
|
...payload,
|
|
@@ -361,6 +364,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
|
|
|
361
364
|
replaceFrom: 0,
|
|
362
365
|
totalMessages: messages.length,
|
|
363
366
|
lastMessageSignature,
|
|
367
|
+
...(debugReadChat ? { debugReadChat } : {}),
|
|
364
368
|
};
|
|
365
369
|
}
|
|
366
370
|
const sync = computeReadChatSync(messages, cursor);
|
|
@@ -372,6 +376,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
|
|
|
372
376
|
replaceFrom: sync.replaceFrom,
|
|
373
377
|
totalMessages: sync.totalMessages,
|
|
374
378
|
lastMessageSignature: sync.lastMessageSignature,
|
|
379
|
+
...(debugReadChat ? { debugReadChat } : {}),
|
|
375
380
|
};
|
|
376
381
|
}
|
|
377
382
|
|
|
@@ -480,16 +485,44 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
480
485
|
const parsedRecord = parsedStatus && typeof parsedStatus === 'object'
|
|
481
486
|
? parsedStatus as Record<string, any>
|
|
482
487
|
: null;
|
|
483
|
-
const
|
|
488
|
+
const adapterStatus = adapter.getStatus();
|
|
489
|
+
const shouldPreferAdapterMessages =
|
|
490
|
+
Array.isArray(adapterStatus.messages)
|
|
491
|
+
&& adapterStatus.messages.length > 0
|
|
492
|
+
&& Array.isArray(parsedRecord?.messages)
|
|
493
|
+
&& adapterStatus.messages.length > parsedRecord.messages.length;
|
|
494
|
+
const status = parsedRecord
|
|
495
|
+
? {
|
|
496
|
+
...parsedRecord,
|
|
497
|
+
messages: shouldPreferAdapterMessages ? adapterStatus.messages : parsedRecord.messages,
|
|
498
|
+
status: adapterStatus.status !== 'idle'
|
|
499
|
+
? adapterStatus.status
|
|
500
|
+
: (parsedRecord.status || adapterStatus.status),
|
|
501
|
+
activeModal: parsedRecord.activeModal || adapterStatus.activeModal,
|
|
502
|
+
}
|
|
503
|
+
: adapterStatus;
|
|
504
|
+
|
|
484
505
|
const title = typeof parsedRecord?.title === 'string' ? parsedRecord.title : undefined;
|
|
485
506
|
const providerSessionId = typeof parsedRecord?.providerSessionId === 'string'
|
|
486
507
|
? parsedRecord.providerSessionId
|
|
487
508
|
: undefined;
|
|
488
509
|
if (status) {
|
|
510
|
+
LOG.info('Command', `[read_chat] cli-like resolved provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord?.status || '')} shouldPreferAdapterMessages=${String(shouldPreferAdapterMessages)} adapterMsgCount=${Array.isArray(adapterStatus.messages) ? adapterStatus.messages.length : 0} parsedMsgCount=${Array.isArray(parsedRecord?.messages) ? parsedRecord.messages.length : 0} returnedMsgCount=${Array.isArray((status as any).messages) ? (status as any).messages.length : 0}`);
|
|
489
511
|
return buildReadChatCommandResult({
|
|
490
|
-
messages: status.messages || [],
|
|
512
|
+
messages: (status as any).messages || [],
|
|
491
513
|
status: status.status,
|
|
492
514
|
activeModal: status.activeModal,
|
|
515
|
+
debugReadChat: {
|
|
516
|
+
provider: adapter.cliType,
|
|
517
|
+
targetSessionId: String(args?.targetSessionId || ''),
|
|
518
|
+
adapterStatus: String(adapterStatus.status || ''),
|
|
519
|
+
parsedStatus: String(parsedRecord?.status || ''),
|
|
520
|
+
returnedStatus: String(status.status || ''),
|
|
521
|
+
shouldPreferAdapterMessages,
|
|
522
|
+
adapterMsgCount: Array.isArray(adapterStatus.messages) ? adapterStatus.messages.length : 0,
|
|
523
|
+
parsedMsgCount: Array.isArray(parsedRecord?.messages) ? parsedRecord.messages.length : 0,
|
|
524
|
+
returnedMsgCount: Array.isArray((status as any).messages) ? (status as any).messages.length : 0,
|
|
525
|
+
},
|
|
493
526
|
...(title ? { title } : {}),
|
|
494
527
|
...(providerSessionId ? { providerSessionId } : {}),
|
|
495
528
|
}, args);
|
|
@@ -1274,3 +1274,140 @@ export function listSavedHistorySessions(
|
|
|
1274
1274
|
return { sessions: [], hasMore: false };
|
|
1275
1275
|
}
|
|
1276
1276
|
}
|
|
1277
|
+
|
|
1278
|
+
function normalizeCanonicalHermesMessageContent(content: unknown): string {
|
|
1279
|
+
if (typeof content === 'string') return content.trim();
|
|
1280
|
+
if (content == null) return '';
|
|
1281
|
+
try {
|
|
1282
|
+
return JSON.stringify(content).trim();
|
|
1283
|
+
} catch {
|
|
1284
|
+
return String(content).trim();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function extractCanonicalHermesMessageTimestamp(message: Record<string, unknown>, fallbackTs: number): number {
|
|
1289
|
+
const numericTimestamp = Number(message.receivedAt || message.timestamp || message.ts || 0);
|
|
1290
|
+
if (Number.isFinite(numericTimestamp) && numericTimestamp > 0) return numericTimestamp;
|
|
1291
|
+
const stringTimestamp = typeof message.ts === 'string'
|
|
1292
|
+
? Date.parse(message.ts)
|
|
1293
|
+
: (typeof message.timestamp === 'string' ? Date.parse(message.timestamp) : NaN);
|
|
1294
|
+
if (Number.isFinite(stringTimestamp) && stringTimestamp > 0) return stringTimestamp;
|
|
1295
|
+
return fallbackTs;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function readExistingHermesSessionStartRecord(historySessionId: string): HistoryMessage | null {
|
|
1299
|
+
try {
|
|
1300
|
+
const dir = path.join(HISTORY_DIR, 'hermes-cli');
|
|
1301
|
+
if (!fs.existsSync(dir)) return null;
|
|
1302
|
+
const files = listHistoryFiles(dir, historySessionId).sort();
|
|
1303
|
+
for (const file of files) {
|
|
1304
|
+
const lines = fs.readFileSync(path.join(dir, file), 'utf-8').split('\n').filter(Boolean);
|
|
1305
|
+
for (const line of lines) {
|
|
1306
|
+
try {
|
|
1307
|
+
const parsed = JSON.parse(line) as HistoryMessage;
|
|
1308
|
+
if (parsed.historySessionId !== historySessionId) continue;
|
|
1309
|
+
if (parsed.kind === 'session_start' && parsed.role === 'system') {
|
|
1310
|
+
return parsed;
|
|
1311
|
+
}
|
|
1312
|
+
} catch {
|
|
1313
|
+
// Ignore malformed lines while probing for the original session_start marker.
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return null;
|
|
1318
|
+
} catch {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export function rebuildHermesSavedHistoryFromCanonicalSession(historySessionId: string): boolean {
|
|
1324
|
+
const normalizedSessionId = normalizeSavedHistorySessionId('hermes-cli', historySessionId);
|
|
1325
|
+
if (!normalizedSessionId) return false;
|
|
1326
|
+
|
|
1327
|
+
try {
|
|
1328
|
+
const sessionFilePath = path.join(os.homedir(), '.hermes', 'sessions', `session_${normalizedSessionId}.json`);
|
|
1329
|
+
if (!fs.existsSync(sessionFilePath)) return false;
|
|
1330
|
+
const raw = JSON.parse(fs.readFileSync(sessionFilePath, 'utf-8')) as {
|
|
1331
|
+
session_start?: string;
|
|
1332
|
+
last_updated?: string;
|
|
1333
|
+
messages?: Array<Record<string, unknown>>;
|
|
1334
|
+
};
|
|
1335
|
+
const canonicalMessages = Array.isArray(raw.messages) ? raw.messages : [];
|
|
1336
|
+
const dir = path.join(HISTORY_DIR, 'hermes-cli');
|
|
1337
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1338
|
+
const existingSessionStart = readExistingHermesSessionStartRecord(normalizedSessionId);
|
|
1339
|
+
const records: HistoryMessage[] = [];
|
|
1340
|
+
if (existingSessionStart) {
|
|
1341
|
+
records.push({
|
|
1342
|
+
...existingSessionStart,
|
|
1343
|
+
historySessionId: normalizedSessionId,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
let fallbackTs = Date.parse(raw.session_start || raw.last_updated || '') || Date.now();
|
|
1348
|
+
for (const message of canonicalMessages) {
|
|
1349
|
+
const role = String(message.role || '').trim();
|
|
1350
|
+
const content = normalizeCanonicalHermesMessageContent(message.content);
|
|
1351
|
+
if (!content) continue;
|
|
1352
|
+
const receivedAt = extractCanonicalHermesMessageTimestamp(message, fallbackTs);
|
|
1353
|
+
fallbackTs = receivedAt + 1;
|
|
1354
|
+
|
|
1355
|
+
if (role === 'user') {
|
|
1356
|
+
records.push({
|
|
1357
|
+
ts: new Date(receivedAt).toISOString(),
|
|
1358
|
+
receivedAt,
|
|
1359
|
+
role: 'user',
|
|
1360
|
+
content,
|
|
1361
|
+
kind: 'standard',
|
|
1362
|
+
agent: 'hermes-cli',
|
|
1363
|
+
historySessionId: normalizedSessionId,
|
|
1364
|
+
});
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (role === 'assistant') {
|
|
1369
|
+
records.push({
|
|
1370
|
+
ts: new Date(receivedAt).toISOString(),
|
|
1371
|
+
receivedAt,
|
|
1372
|
+
role: 'assistant',
|
|
1373
|
+
content,
|
|
1374
|
+
kind: 'standard',
|
|
1375
|
+
agent: 'hermes-cli',
|
|
1376
|
+
historySessionId: normalizedSessionId,
|
|
1377
|
+
});
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (role === 'tool') {
|
|
1382
|
+
records.push({
|
|
1383
|
+
ts: new Date(receivedAt).toISOString(),
|
|
1384
|
+
receivedAt,
|
|
1385
|
+
role: 'assistant',
|
|
1386
|
+
content,
|
|
1387
|
+
kind: 'tool',
|
|
1388
|
+
senderName: 'Tool',
|
|
1389
|
+
agent: 'hermes-cli',
|
|
1390
|
+
historySessionId: normalizedSessionId,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
if (records.length === 0) return false;
|
|
1396
|
+
|
|
1397
|
+
const prefix = `${normalizedSessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}_`;
|
|
1398
|
+
for (const file of fs.readdirSync(dir)) {
|
|
1399
|
+
if (file.startsWith(prefix) && file.endsWith('.jsonl')) {
|
|
1400
|
+
fs.unlinkSync(path.join(dir, file));
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const targetDate = new Date(records[records.length - 1].receivedAt || Date.now()).toISOString().slice(0, 10);
|
|
1405
|
+
const filePath = path.join(dir, `${prefix}${targetDate}.jsonl`);
|
|
1406
|
+
fs.writeFileSync(filePath, `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, 'utf-8');
|
|
1407
|
+
invalidatePersistedSavedHistoryIndex('hermes-cli', dir);
|
|
1408
|
+
savedHistorySessionCache.delete('hermes-cli');
|
|
1409
|
+
return true;
|
|
1410
|
+
} catch {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
}
|