@adhdev/daemon-core 0.9.45 → 0.9.47
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/config/chat-history.d.ts +6 -4
- package/dist/index.js +191 -713
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +191 -713
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +2 -7
- package/dist/providers/contracts.d.ts +23 -14
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/commands/chat-commands.ts +1 -0
- package/src/commands/router.ts +5 -1
- package/src/config/chat-history.ts +161 -680
- package/src/providers/cli-provider-instance.ts +32 -70
- package/src/providers/contracts.ts +24 -14
- package/src/providers/provider-schema.ts +40 -0
|
@@ -17,7 +17,7 @@ import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
|
17
17
|
import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
|
|
18
18
|
import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
19
19
|
import { StatusMonitor } from './status-monitor.js';
|
|
20
|
-
import { ChatHistoryWriter, isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistory
|
|
20
|
+
import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderNativeHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
|
|
21
21
|
import { LOG } from '../logging/logger.js';
|
|
22
22
|
import type { ChatMessage } from '../types.js';
|
|
23
23
|
import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
|
|
@@ -179,13 +179,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
179
179
|
private historyWriter: ChatHistoryWriter;
|
|
180
180
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
181
181
|
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
182
|
-
private
|
|
183
|
-
private
|
|
184
|
-
private lastCanonicalHermesWatchPath: string | undefined = undefined;
|
|
185
|
-
private lastCanonicalClaudeRebuildMtimeMs = 0;
|
|
186
|
-
private lastCanonicalClaudeCheckAt = 0;
|
|
187
|
-
private lastCanonicalCodexRebuildMtimeMs = 0;
|
|
188
|
-
private lastCanonicalCodexCheckAt = 0;
|
|
182
|
+
private lastNativeSourceCanonicalCheckAt = 0;
|
|
183
|
+
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
189
184
|
private cachedSqliteDb: {
|
|
190
185
|
prepare(sql: string): { get(...values: Array<string | number>): unknown };
|
|
191
186
|
close(): void;
|
|
@@ -1050,6 +1045,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1050
1045
|
if (!canonicalHistory) return false;
|
|
1051
1046
|
|
|
1052
1047
|
if (isNativeSourceCanonicalHistory(canonicalHistory)) {
|
|
1048
|
+
const cacheKey = [this.type, this.providerSessionId, this.workingDir].join('\0');
|
|
1049
|
+
const now = Date.now();
|
|
1050
|
+
if (cacheKey === this.lastNativeSourceCanonicalCacheKey && now - this.lastNativeSourceCanonicalCheckAt < 2_000) {
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
this.lastNativeSourceCanonicalCacheKey = cacheKey;
|
|
1054
|
+
this.lastNativeSourceCanonicalCheckAt = now;
|
|
1055
|
+
|
|
1053
1056
|
const restoredHistory = readProviderChatHistory(this.type, {
|
|
1054
1057
|
canonicalHistory,
|
|
1055
1058
|
historySessionId: this.providerSessionId,
|
|
@@ -1057,74 +1060,32 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1057
1060
|
offset: 0,
|
|
1058
1061
|
limit: Number.MAX_SAFE_INTEGER,
|
|
1059
1062
|
historyBehavior: this.provider.historyBehavior,
|
|
1063
|
+
scripts: this.provider.scripts as any,
|
|
1060
1064
|
});
|
|
1061
|
-
if (restoredHistory.source
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1065
|
+
if (restoredHistory.source === 'provider-native') {
|
|
1066
|
+
this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
|
|
1067
|
+
role: message.role,
|
|
1068
|
+
content: message.content,
|
|
1069
|
+
kind: message.kind,
|
|
1070
|
+
senderName: message.senderName,
|
|
1071
|
+
receivedAt: message.receivedAt,
|
|
1072
|
+
}));
|
|
1073
|
+
}
|
|
1069
1074
|
return true;
|
|
1070
1075
|
}
|
|
1071
1076
|
|
|
1072
1077
|
try {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
if (!fs.existsSync(watchPath)) return false;
|
|
1084
|
-
} else if (this.lastCanonicalHermesSyncMtimeMs === 0) {
|
|
1085
|
-
// First check: file existence not yet confirmed, must verify
|
|
1086
|
-
if (!fs.existsSync(watchPath)) return false;
|
|
1087
|
-
}
|
|
1088
|
-
const stat = fs.statSync(watchPath);
|
|
1089
|
-
if (stat.mtimeMs <= this.lastCanonicalHermesSyncMtimeMs) return true;
|
|
1090
|
-
rebuilt = rebuildHermesSavedHistoryFromCanonicalSession(this.providerSessionId);
|
|
1091
|
-
if (rebuilt) this.lastCanonicalHermesSyncMtimeMs = stat.mtimeMs;
|
|
1092
|
-
} else if (canonicalHistory.format === 'claude-jsonl') {
|
|
1093
|
-
// Throttle: only check for changes at most once per 2s
|
|
1094
|
-
const now = Date.now();
|
|
1095
|
-
if (now - this.lastCanonicalClaudeCheckAt < 2_000 && this.lastCanonicalClaudeRebuildMtimeMs !== 0) {
|
|
1096
|
-
return true;
|
|
1097
|
-
}
|
|
1098
|
-
this.lastCanonicalClaudeCheckAt = now;
|
|
1099
|
-
// Only rebuild if the transcript file has changed since last rebuild
|
|
1100
|
-
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
1101
|
-
const workspaceSegment = typeof this.workingDir === 'string'
|
|
1102
|
-
? this.workingDir.replace(/[\\/]/g, '-').replace(/^-+/, '')
|
|
1103
|
-
: '';
|
|
1104
|
-
const transcriptFile = path.join(claudeProjectsDir, workspaceSegment, `${this.providerSessionId}.jsonl`);
|
|
1105
|
-
let transcriptMtime = 0;
|
|
1106
|
-
try { transcriptMtime = fs.statSync(transcriptFile).mtimeMs; } catch { /* not found yet */ }
|
|
1107
|
-
if (transcriptMtime > 0 && transcriptMtime <= this.lastCanonicalClaudeRebuildMtimeMs) return true;
|
|
1108
|
-
rebuilt = rebuildClaudeSavedHistoryFromNativeProject(this.providerSessionId, this.workingDir);
|
|
1109
|
-
if (rebuilt) this.lastCanonicalClaudeRebuildMtimeMs = transcriptMtime || Date.now();
|
|
1110
|
-
} else if (canonicalHistory.format === 'codex-jsonl') {
|
|
1111
|
-
// Codex stores rollout transcripts under ~/.codex/sessions/YYYY/MM/DD/.
|
|
1112
|
-
// Resolving requires a recursive lookup, so throttle the probe like Claude.
|
|
1113
|
-
const now = Date.now();
|
|
1114
|
-
if (now - this.lastCanonicalCodexCheckAt < 2_000 && this.lastCanonicalCodexRebuildMtimeMs !== 0) {
|
|
1115
|
-
return true;
|
|
1116
|
-
}
|
|
1117
|
-
this.lastCanonicalCodexCheckAt = now;
|
|
1118
|
-
const transcriptFile = resolveCodexSessionTranscriptPath(this.providerSessionId, this.workingDir);
|
|
1119
|
-
let transcriptMtime = 0;
|
|
1120
|
-
if (transcriptFile) {
|
|
1121
|
-
try { transcriptMtime = fs.statSync(transcriptFile).mtimeMs; } catch { /* not found yet */ }
|
|
1122
|
-
}
|
|
1123
|
-
if (transcriptMtime > 0 && transcriptMtime <= this.lastCanonicalCodexRebuildMtimeMs) return true;
|
|
1124
|
-
rebuilt = rebuildCodexSavedHistoryFromNativeSession(this.providerSessionId, this.workingDir);
|
|
1125
|
-
if (rebuilt) this.lastCanonicalCodexRebuildMtimeMs = transcriptMtime || Date.now();
|
|
1078
|
+
const cacheKey = [this.type, this.providerSessionId, this.workingDir, canonicalHistory.mode || 'materialized-mirror'].join('\0');
|
|
1079
|
+
const now = Date.now();
|
|
1080
|
+
if (cacheKey === this.lastNativeSourceCanonicalCacheKey && now - this.lastNativeSourceCanonicalCheckAt < 2_000) {
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
this.lastNativeSourceCanonicalCacheKey = cacheKey;
|
|
1084
|
+
this.lastNativeSourceCanonicalCheckAt = now;
|
|
1085
|
+
|
|
1086
|
+
if (!materializeProviderNativeHistory(this.type, canonicalHistory, this.providerSessionId, this.workingDir, this.provider.scripts as any)) {
|
|
1087
|
+
return false;
|
|
1126
1088
|
}
|
|
1127
|
-
if (!rebuilt) return false;
|
|
1128
1089
|
const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId, 0, this.provider.historyBehavior);
|
|
1129
1090
|
this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
|
|
1130
1091
|
role: message.role,
|
|
@@ -1150,6 +1111,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1150
1111
|
offset: 0,
|
|
1151
1112
|
limit: Number.MAX_SAFE_INTEGER,
|
|
1152
1113
|
historyBehavior: this.provider.historyBehavior,
|
|
1114
|
+
scripts: this.provider.scripts as any,
|
|
1153
1115
|
})
|
|
1154
1116
|
: (() => {
|
|
1155
1117
|
this.historyWriter.compactHistorySession(this.type, this.providerSessionId!, this.provider.historyBehavior);
|
|
@@ -602,24 +602,34 @@ export interface ProviderHistoryBehavior {
|
|
|
602
602
|
requireStrictSessionIdFormat?: boolean;
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Provider-owned native history script names.
|
|
607
|
+
*
|
|
608
|
+
* These functions live in the provider's versioned CLI script bundle, not in
|
|
609
|
+
* daemon-core. They let each provider own native transcript file discovery and
|
|
610
|
+
* parsing while daemon-core only validates/pages the normalized result.
|
|
611
|
+
*/
|
|
612
|
+
export interface ProviderCanonicalHistoryScriptsConfig {
|
|
613
|
+
/** Reads one native session. Default: 'readNativeHistory'. */
|
|
614
|
+
readSession?: string;
|
|
615
|
+
/** Lists native sessions with summary metadata. Default: 'listNativeHistory'. */
|
|
616
|
+
listSessions?: string;
|
|
617
|
+
}
|
|
618
|
+
|
|
605
619
|
/**
|
|
606
620
|
* Canonical history sync config — for providers that maintain their own native history files.
|
|
607
|
-
*
|
|
608
|
-
*
|
|
621
|
+
*
|
|
622
|
+
* Preferred mode is provider-owned scripts via `scripts`. `format` is now an
|
|
623
|
+
* opaque provider label retained for diagnostics/backward compatibility; daemon
|
|
624
|
+
* live paths must not branch on provider-specific format values.
|
|
609
625
|
*/
|
|
610
626
|
export interface ProviderCanonicalHistoryConfig {
|
|
611
|
-
/**
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
format: 'hermes-json' | 'claude-jsonl' | 'codex-jsonl';
|
|
618
|
-
/**
|
|
619
|
-
* Path to the native history file. Supports ~ and {{sessionId}} placeholder.
|
|
620
|
-
* e.g. "~/.hermes/sessions/session_{{sessionId}}.json"
|
|
621
|
-
*/
|
|
622
|
-
watchPath: string;
|
|
627
|
+
/** Opaque provider-owned history format label. */
|
|
628
|
+
format?: string;
|
|
629
|
+
/** Optional native history glob/template for diagnostics only. */
|
|
630
|
+
watchPath?: string;
|
|
631
|
+
/** Provider-owned script entry points for native transcript list/read. */
|
|
632
|
+
scripts?: ProviderCanonicalHistoryScriptsConfig;
|
|
623
633
|
/**
|
|
624
634
|
* How ADHDev should use native history.
|
|
625
635
|
* - 'native-source': provider-native files are canonical; ADHDev reads them directly and keeps only in-memory/thin projections.
|
|
@@ -123,6 +123,7 @@ export function validateProviderDefinition(raw: unknown): ProviderValidationResu
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
validateCapabilities(provider as unknown as ProviderModule, controls, errors)
|
|
126
|
+
validateCanonicalHistory(provider.canonicalHistory, errors)
|
|
126
127
|
|
|
127
128
|
for (const control of controls) {
|
|
128
129
|
validateControl(control as ProviderControlDef, errors)
|
|
@@ -192,6 +193,45 @@ function validateCapabilities(provider: ProviderModule, controls: ProviderContro
|
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
|
|
196
|
+
function validateCanonicalHistory(raw: unknown, errors: string[]): void {
|
|
197
|
+
if (raw === undefined) return
|
|
198
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
199
|
+
errors.push('canonicalHistory must be an object')
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const canonicalHistory = raw as Record<string, unknown>
|
|
204
|
+
const format = canonicalHistory.format
|
|
205
|
+
if (format !== undefined && (typeof format !== 'string' || !format.trim())) {
|
|
206
|
+
errors.push('canonicalHistory.format must be a non-empty string when provided')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const watchPath = canonicalHistory.watchPath
|
|
210
|
+
if (watchPath !== undefined && (typeof watchPath !== 'string' || !watchPath.trim())) {
|
|
211
|
+
errors.push('canonicalHistory.watchPath must be a non-empty string when provided')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const mode = canonicalHistory.mode
|
|
215
|
+
if (mode !== undefined && !['native-source', 'materialized-mirror', 'disabled'].includes(String(mode))) {
|
|
216
|
+
errors.push('canonicalHistory.mode must be one of: native-source, materialized-mirror, disabled')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const scripts = canonicalHistory.scripts
|
|
220
|
+
if (scripts === undefined) return
|
|
221
|
+
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
|
|
222
|
+
errors.push('canonicalHistory.scripts must be an object')
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const scriptConfig = scripts as Record<string, unknown>
|
|
227
|
+
for (const key of ['readSession', 'listSessions']) {
|
|
228
|
+
const value = scriptConfig[key]
|
|
229
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
230
|
+
errors.push(`canonicalHistory.scripts.${key} must be a non-empty string`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
195
235
|
function validateControl(control: ProviderControlDef, errors: string[]): void {
|
|
196
236
|
if (!control || typeof control !== 'object') {
|
|
197
237
|
errors.push('controls: each control must be an object')
|