@adhdev/daemon-core 0.9.45 → 0.9.46

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.
@@ -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, rebuildClaudeSavedHistoryFromNativeProject, rebuildCodexSavedHistoryFromNativeSession, rebuildHermesSavedHistoryFromCanonicalSession, resolveCodexSessionTranscriptPath } from '../config/chat-history.js';
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 lastCanonicalHermesSyncMtimeMs = 0;
183
- private lastCanonicalHermesExistCheckAt = 0;
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 !== 'provider-native') return false;
1062
- this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
1063
- role: message.role,
1064
- content: message.content,
1065
- kind: message.kind,
1066
- senderName: message.senderName,
1067
- receivedAt: message.receivedAt,
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
- let rebuilt = false;
1074
- if (canonicalHistory.format === 'hermes-json') {
1075
- const watchPath = canonicalHistory.watchPath
1076
- .replace(/^~/, os.homedir())
1077
- .replace('{{sessionId}}', this.providerSessionId);
1078
- // Throttle existsSync: check file existence at most once per 2s
1079
- const now = Date.now();
1080
- if (watchPath !== this.lastCanonicalHermesWatchPath || now - this.lastCanonicalHermesExistCheckAt >= 2_000) {
1081
- this.lastCanonicalHermesWatchPath = watchPath;
1082
- this.lastCanonicalHermesExistCheckAt = now;
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
- * When set, daemon syncs from the provider's native format into the ADHDev JSONL store.
608
- * Replaces hardcoded hermes-cli / claude-cli checks in cli-provider-instance.ts.
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
- * Native history format.
613
- * - 'hermes-json': single JSON file per session (~/.hermes/sessions/session_{{sessionId}}.json)
614
- * - 'claude-jsonl': JSONL transcript under ~/.claude/projects/
615
- * - 'codex-jsonl': rollout JSONL transcript under ~/.codex/sessions/YYYY/MM/DD/
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')