@adhdev/daemon-core 0.9.44 → 0.9.45

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/src/launch.ts CHANGED
@@ -24,6 +24,7 @@ import { detectIDEs } from './detection/ide-detector.js';
24
24
  import { IDEInfo } from './detection/ide-detector.js';
25
25
  import { ProviderLoader } from './providers/provider-loader.js';
26
26
  import type { ProviderModule } from './providers/contracts.js';
27
+ import { findMacAppProcessPids } from './launch/macos-app-process.js';
27
28
 
28
29
  // ─── Provider-based dynamic IDE infrastructure ────────────────
29
30
  // Reads cdpPorts, processNames from provider.js — only create provider.js to add new IDE
@@ -71,6 +72,37 @@ function escapeForAppleScript(value: string): string {
71
72
  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
72
73
  }
73
74
 
75
+ function getIdePathCandidates(ideId: string): string[] {
76
+ return getProviderLoader().getIdePathCandidates(ideId);
77
+ }
78
+
79
+ function getMacAppProcessPids(ideId: string): number[] {
80
+ const appPaths = getIdePathCandidates(ideId);
81
+ if (appPaths.length === 0) return [];
82
+ try {
83
+ const output = execSync('ps axww -o pid=,args=', {
84
+ encoding: 'utf-8',
85
+ timeout: 3000,
86
+ stdio: ['pipe', 'pipe', 'pipe'],
87
+ });
88
+ return findMacAppProcessPids(output, appPaths);
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+
94
+ function killMacAppPathProcesses(ideId: string, signal: NodeJS.Signals): boolean {
95
+ const pids = getMacAppProcessPids(ideId);
96
+ let signalled = false;
97
+ for (const pid of pids) {
98
+ try {
99
+ process.kill(pid, signal);
100
+ signalled = true;
101
+ } catch { }
102
+ }
103
+ return signalled;
104
+ }
105
+
74
106
  // ─── Helpers ────────────────────────────────────
75
107
 
76
108
  /** Find available port (primary → secondary → sequential after) */
@@ -137,6 +169,7 @@ export async function killIdeProcess(ideId: string): Promise<boolean> {
137
169
  } catch {
138
170
  try { execSync(`pkill -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
139
171
  }
172
+ killMacAppPathProcesses(ideId, 'SIGTERM');
140
173
  } else if (plat === 'win32' && winProcesses) {
141
174
  // Windows: taskkill for each process name
142
175
  for (const proc of winProcesses) {
@@ -164,6 +197,7 @@ export async function killIdeProcess(ideId: string): Promise<boolean> {
164
197
  // Force terminate retry
165
198
  if (plat === 'darwin' && appName) {
166
199
  try { execSync(`pkill -9 -x "${appName}" 2>/dev/null`, { timeout: 5000 }); } catch { }
200
+ killMacAppPathProcesses(ideId, 'SIGKILL');
167
201
  } else if (plat === 'win32' && winProcesses) {
168
202
  for (const proc of winProcesses) {
169
203
  try { execSync(`taskkill /IM "${proc}" /F 2>nul`); } catch { }
@@ -185,14 +219,16 @@ export function isIdeRunning(ideId: string): boolean {
185
219
  try {
186
220
  if (plat === 'darwin') {
187
221
  const appName = getMacAppIdentifiers()[ideId];
188
- if (!appName) return false;
222
+ if (!appName) return getMacAppProcessPids(ideId).length > 0;
189
223
  try {
190
224
  const result = execSync(`pgrep -x "${appName}" 2>/dev/null`, {
191
225
  encoding: 'utf-8',
192
226
  timeout: 3000,
193
227
  });
194
- return result.trim().length > 0;
195
- } catch {
228
+ if (result.trim().length > 0) return true;
229
+ } catch { }
230
+
231
+ try {
196
232
  const result = execSync(
197
233
  `osascript -e 'tell application "System Events" to count (every process whose name is "${escapeForAppleScript(appName)}")'`,
198
234
  {
@@ -201,8 +237,10 @@ export function isIdeRunning(ideId: string): boolean {
201
237
  stdio: ['pipe', 'pipe', 'pipe'],
202
238
  },
203
239
  );
204
- return Number.parseInt(result.trim() || '0', 10) > 0;
205
- }
240
+ if (Number.parseInt(result.trim() || '0', 10) > 0) return true;
241
+ } catch { }
242
+
243
+ return getMacAppProcessPids(ideId).length > 0;
206
244
  } else if (plat === 'win32') {
207
245
  const winProcesses = getWinProcessNames()[ideId];
208
246
  if (!winProcesses) return false;
@@ -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, readChatHistory, rebuildClaudeSavedHistoryFromNativeProject, rebuildHermesSavedHistoryFromCanonicalSession } from '../config/chat-history.js';
20
+ import { ChatHistoryWriter, isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistory, rebuildClaudeSavedHistoryFromNativeProject, rebuildCodexSavedHistoryFromNativeSession, rebuildHermesSavedHistoryFromCanonicalSession, resolveCodexSessionTranscriptPath } 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';
@@ -184,6 +184,8 @@ export class CliProviderInstance implements ProviderInstance {
184
184
  private lastCanonicalHermesWatchPath: string | undefined = undefined;
185
185
  private lastCanonicalClaudeRebuildMtimeMs = 0;
186
186
  private lastCanonicalClaudeCheckAt = 0;
187
+ private lastCanonicalCodexRebuildMtimeMs = 0;
188
+ private lastCanonicalCodexCheckAt = 0;
187
189
  private cachedSqliteDb: {
188
190
  prepare(sql: string): { get(...values: Array<string | number>): unknown };
189
191
  close(): void;
@@ -1047,6 +1049,26 @@ export class CliProviderInstance implements ProviderInstance {
1047
1049
  const canonicalHistory = this.provider.canonicalHistory;
1048
1050
  if (!canonicalHistory) return false;
1049
1051
 
1052
+ if (isNativeSourceCanonicalHistory(canonicalHistory)) {
1053
+ const restoredHistory = readProviderChatHistory(this.type, {
1054
+ canonicalHistory,
1055
+ historySessionId: this.providerSessionId,
1056
+ workspace: this.workingDir,
1057
+ offset: 0,
1058
+ limit: Number.MAX_SAFE_INTEGER,
1059
+ historyBehavior: this.provider.historyBehavior,
1060
+ });
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
+ }));
1069
+ return true;
1070
+ }
1071
+
1050
1072
  try {
1051
1073
  let rebuilt = false;
1052
1074
  if (canonicalHistory.format === 'hermes-json') {
@@ -1085,6 +1107,22 @@ export class CliProviderInstance implements ProviderInstance {
1085
1107
  if (transcriptMtime > 0 && transcriptMtime <= this.lastCanonicalClaudeRebuildMtimeMs) return true;
1086
1108
  rebuilt = rebuildClaudeSavedHistoryFromNativeProject(this.providerSessionId, this.workingDir);
1087
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();
1088
1126
  }
1089
1127
  if (!rebuilt) return false;
1090
1128
  const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId, 0, this.provider.historyBehavior);
@@ -1104,8 +1142,19 @@ export class CliProviderInstance implements ProviderInstance {
1104
1142
  private restorePersistedHistoryFromCurrentSession(): void {
1105
1143
  if (!this.providerSessionId) return;
1106
1144
  this.syncCanonicalSavedHistoryIfNeeded();
1107
- this.historyWriter.compactHistorySession(this.type, this.providerSessionId, this.provider.historyBehavior);
1108
- const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId, 0, this.provider.historyBehavior);
1145
+ const restoredHistory = isNativeSourceCanonicalHistory(this.provider.canonicalHistory)
1146
+ ? readProviderChatHistory(this.type, {
1147
+ canonicalHistory: this.provider.canonicalHistory,
1148
+ historySessionId: this.providerSessionId,
1149
+ workspace: this.workingDir,
1150
+ offset: 0,
1151
+ limit: Number.MAX_SAFE_INTEGER,
1152
+ historyBehavior: this.provider.historyBehavior,
1153
+ })
1154
+ : (() => {
1155
+ this.historyWriter.compactHistorySession(this.type, this.providerSessionId!, this.provider.historyBehavior);
1156
+ return readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId, 0, this.provider.historyBehavior);
1157
+ })();
1109
1158
  this.historyWriter.seedSessionHistory(
1110
1159
  this.type,
1111
1160
  restoredHistory.messages,
@@ -36,7 +36,10 @@ export interface ReadChatResult {
36
36
  controlValues?: Record<string, string | number | boolean>;
37
37
  /** Flexible always-visible metadata for compact/live surfaces. */
38
38
  summaryMetadata?: ProviderSummaryMetadata;
39
- /** Provider-driven UI effects derived from chat state */
39
+ /** Provider-owned transcript authority/coverage hints for daemon/dashboard sync. */
40
+ transcriptAuthority?: 'provider' | 'daemon';
41
+ coverage?: 'full' | 'tail' | 'current-turn';
42
+ /** Provider-driven UI effects derived from chat state */
40
43
  effects?: ProviderEffect[];
41
44
  }
42
45
 
@@ -609,13 +612,23 @@ export interface ProviderCanonicalHistoryConfig {
609
612
  * Native history format.
610
613
  * - 'hermes-json': single JSON file per session (~/.hermes/sessions/session_{{sessionId}}.json)
611
614
  * - 'claude-jsonl': JSONL transcript under ~/.claude/projects/
615
+ * - 'codex-jsonl': rollout JSONL transcript under ~/.codex/sessions/YYYY/MM/DD/
612
616
  */
613
- format: 'hermes-json' | 'claude-jsonl';
617
+ format: 'hermes-json' | 'claude-jsonl' | 'codex-jsonl';
614
618
  /**
615
619
  * Path to the native history file. Supports ~ and {{sessionId}} placeholder.
616
620
  * e.g. "~/.hermes/sessions/session_{{sessionId}}.json"
617
621
  */
618
622
  watchPath: string;
623
+ /**
624
+ * How ADHDev should use native history.
625
+ * - 'native-source': provider-native files are canonical; ADHDev reads them directly and keeps only in-memory/thin projections.
626
+ * - 'materialized-mirror': transitional compatibility mode; native files are rewritten into ~/.adhdev/history before read/list.
627
+ * - 'disabled': ignore native history and use ADHDev mirror only.
628
+ *
629
+ * Omitted mode defaults to 'native-source'.
630
+ */
631
+ mode?: 'native-source' | 'materialized-mirror' | 'disabled';
619
632
  }
620
633
 
621
634
  /**
@@ -154,6 +154,8 @@ export function validateReadChatResultPayload(raw: unknown, source = 'read_chat'
154
154
  if (raw.summaryMetadata !== undefined) normalized.summaryMetadata = raw.summaryMetadata as any
155
155
  if (Array.isArray(raw.effects)) normalized.effects = raw.effects as any
156
156
  if (typeof raw.providerSessionId === 'string') normalized.providerSessionId = raw.providerSessionId
157
+ if (raw.transcriptAuthority === 'provider' || raw.transcriptAuthority === 'daemon') normalized.transcriptAuthority = raw.transcriptAuthority
158
+ if (raw.coverage === 'full' || raw.coverage === 'tail' || raw.coverage === 'current-turn') normalized.coverage = raw.coverage
157
159
 
158
160
  return normalized
159
161
  }