@adhdev/daemon-core 0.9.44 → 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.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +3 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +6 -0
- package/dist/config/chat-history.d.ts +34 -11
- package/dist/index.js +410 -310
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +410 -310
- package/dist/index.mjs.map +1 -1
- package/dist/launch/macos-app-process.d.ts +2 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -5
- package/dist/providers/contracts.d.ts +33 -11
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +25 -3
- package/src/cli-adapters/provider-cli-shared.ts +6 -0
- package/src/commands/chat-commands.ts +28 -3
- package/src/commands/router.ts +13 -3
- package/src/config/chat-history.ts +282 -284
- package/src/launch/macos-app-process.ts +37 -0
- package/src/launch.ts +43 -5
- package/src/providers/cli-provider-instance.ts +57 -46
- package/src/providers/contracts.ts +35 -12
- package/src/providers/provider-schema.ts +40 -0
- package/src/providers/read-chat-contract.ts +2 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function normalizeMacAppPath(appPath: string): string | null {
|
|
2
|
+
const trimmed = String(appPath || '').trim()
|
|
3
|
+
if (!trimmed) return null
|
|
4
|
+
return trimmed.replace(/\/+$/, '')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parsePsLine(line: string): { pid: number; args: string } | null {
|
|
8
|
+
const match = line.match(/^\s*(\d+)\s+(.+)$/)
|
|
9
|
+
if (!match) return null
|
|
10
|
+
const pid = Number.parseInt(match[1], 10)
|
|
11
|
+
if (!Number.isFinite(pid)) return null
|
|
12
|
+
return { pid, args: match[2] }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isMacAppProcessArgs(args: string, appPath: string): boolean {
|
|
16
|
+
const normalized = normalizeMacAppPath(appPath)
|
|
17
|
+
if (!normalized) return false
|
|
18
|
+
return String(args || '').startsWith(`${normalized}/`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function findMacAppProcessPids(psOutput: string, appPaths: readonly string[]): number[] {
|
|
22
|
+
const normalizedPaths = appPaths
|
|
23
|
+
.map(normalizeMacAppPath)
|
|
24
|
+
.filter((value): value is string => !!value)
|
|
25
|
+
|
|
26
|
+
if (normalizedPaths.length === 0) return []
|
|
27
|
+
|
|
28
|
+
const pids: number[] = []
|
|
29
|
+
for (const line of String(psOutput || '').split(/\r?\n/)) {
|
|
30
|
+
const parsed = parsePsLine(line)
|
|
31
|
+
if (!parsed) continue
|
|
32
|
+
if (normalizedPaths.some(appPath => isMacAppProcessArgs(parsed.args, appPath))) {
|
|
33
|
+
pids.push(parsed.pid)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return pids
|
|
37
|
+
}
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,11 +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;
|
|
182
|
+
private lastNativeSourceCanonicalCheckAt = 0;
|
|
183
|
+
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
187
184
|
private cachedSqliteDb: {
|
|
188
185
|
prepare(sql: string): { get(...values: Array<string | number>): unknown };
|
|
189
186
|
close(): void;
|
|
@@ -1047,46 +1044,48 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1047
1044
|
const canonicalHistory = this.provider.canonicalHistory;
|
|
1048
1045
|
if (!canonicalHistory) return false;
|
|
1049
1046
|
|
|
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
|
+
|
|
1056
|
+
const restoredHistory = readProviderChatHistory(this.type, {
|
|
1057
|
+
canonicalHistory,
|
|
1058
|
+
historySessionId: this.providerSessionId,
|
|
1059
|
+
workspace: this.workingDir,
|
|
1060
|
+
offset: 0,
|
|
1061
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
1062
|
+
historyBehavior: this.provider.historyBehavior,
|
|
1063
|
+
scripts: this.provider.scripts as any,
|
|
1064
|
+
});
|
|
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
|
+
}
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1050
1077
|
try {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (!fs.existsSync(watchPath)) return false;
|
|
1062
|
-
} else if (this.lastCanonicalHermesSyncMtimeMs === 0) {
|
|
1063
|
-
// First check: file existence not yet confirmed, must verify
|
|
1064
|
-
if (!fs.existsSync(watchPath)) return false;
|
|
1065
|
-
}
|
|
1066
|
-
const stat = fs.statSync(watchPath);
|
|
1067
|
-
if (stat.mtimeMs <= this.lastCanonicalHermesSyncMtimeMs) return true;
|
|
1068
|
-
rebuilt = rebuildHermesSavedHistoryFromCanonicalSession(this.providerSessionId);
|
|
1069
|
-
if (rebuilt) this.lastCanonicalHermesSyncMtimeMs = stat.mtimeMs;
|
|
1070
|
-
} else if (canonicalHistory.format === 'claude-jsonl') {
|
|
1071
|
-
// Throttle: only check for changes at most once per 2s
|
|
1072
|
-
const now = Date.now();
|
|
1073
|
-
if (now - this.lastCanonicalClaudeCheckAt < 2_000 && this.lastCanonicalClaudeRebuildMtimeMs !== 0) {
|
|
1074
|
-
return true;
|
|
1075
|
-
}
|
|
1076
|
-
this.lastCanonicalClaudeCheckAt = now;
|
|
1077
|
-
// Only rebuild if the transcript file has changed since last rebuild
|
|
1078
|
-
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
1079
|
-
const workspaceSegment = typeof this.workingDir === 'string'
|
|
1080
|
-
? this.workingDir.replace(/[\\/]/g, '-').replace(/^-+/, '')
|
|
1081
|
-
: '';
|
|
1082
|
-
const transcriptFile = path.join(claudeProjectsDir, workspaceSegment, `${this.providerSessionId}.jsonl`);
|
|
1083
|
-
let transcriptMtime = 0;
|
|
1084
|
-
try { transcriptMtime = fs.statSync(transcriptFile).mtimeMs; } catch { /* not found yet */ }
|
|
1085
|
-
if (transcriptMtime > 0 && transcriptMtime <= this.lastCanonicalClaudeRebuildMtimeMs) return true;
|
|
1086
|
-
rebuilt = rebuildClaudeSavedHistoryFromNativeProject(this.providerSessionId, this.workingDir);
|
|
1087
|
-
if (rebuilt) this.lastCanonicalClaudeRebuildMtimeMs = 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;
|
|
1088
1088
|
}
|
|
1089
|
-
if (!rebuilt) return false;
|
|
1090
1089
|
const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId, 0, this.provider.historyBehavior);
|
|
1091
1090
|
this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
|
|
1092
1091
|
role: message.role,
|
|
@@ -1104,8 +1103,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1104
1103
|
private restorePersistedHistoryFromCurrentSession(): void {
|
|
1105
1104
|
if (!this.providerSessionId) return;
|
|
1106
1105
|
this.syncCanonicalSavedHistoryIfNeeded();
|
|
1107
|
-
|
|
1108
|
-
|
|
1106
|
+
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.canonicalHistory)
|
|
1107
|
+
? readProviderChatHistory(this.type, {
|
|
1108
|
+
canonicalHistory: this.provider.canonicalHistory,
|
|
1109
|
+
historySessionId: this.providerSessionId,
|
|
1110
|
+
workspace: this.workingDir,
|
|
1111
|
+
offset: 0,
|
|
1112
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
1113
|
+
historyBehavior: this.provider.historyBehavior,
|
|
1114
|
+
scripts: this.provider.scripts as any,
|
|
1115
|
+
})
|
|
1116
|
+
: (() => {
|
|
1117
|
+
this.historyWriter.compactHistorySession(this.type, this.providerSessionId!, this.provider.historyBehavior);
|
|
1118
|
+
return readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId, 0, this.provider.historyBehavior);
|
|
1119
|
+
})();
|
|
1109
1120
|
this.historyWriter.seedSessionHistory(
|
|
1110
1121
|
this.type,
|
|
1111
1122
|
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-
|
|
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
|
|
|
@@ -599,23 +602,43 @@ export interface ProviderHistoryBehavior {
|
|
|
599
602
|
requireStrictSessionIdFormat?: boolean;
|
|
600
603
|
}
|
|
601
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
|
+
|
|
602
619
|
/**
|
|
603
620
|
* Canonical history sync config — for providers that maintain their own native history files.
|
|
604
|
-
*
|
|
605
|
-
*
|
|
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.
|
|
606
625
|
*/
|
|
607
626
|
export interface ProviderCanonicalHistoryConfig {
|
|
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;
|
|
608
633
|
/**
|
|
609
|
-
*
|
|
610
|
-
* - '
|
|
611
|
-
* - '
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
* Path to the native history file. Supports ~ and {{sessionId}} placeholder.
|
|
616
|
-
* e.g. "~/.hermes/sessions/session_{{sessionId}}.json"
|
|
634
|
+
* How ADHDev should use native history.
|
|
635
|
+
* - 'native-source': provider-native files are canonical; ADHDev reads them directly and keeps only in-memory/thin projections.
|
|
636
|
+
* - 'materialized-mirror': transitional compatibility mode; native files are rewritten into ~/.adhdev/history before read/list.
|
|
637
|
+
* - 'disabled': ignore native history and use ADHDev mirror only.
|
|
638
|
+
*
|
|
639
|
+
* Omitted mode defaults to 'native-source'.
|
|
617
640
|
*/
|
|
618
|
-
|
|
641
|
+
mode?: 'native-source' | 'materialized-mirror' | 'disabled';
|
|
619
642
|
}
|
|
620
643
|
|
|
621
644
|
/**
|
|
@@ -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')
|
|
@@ -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
|
}
|