@adhdev/daemon-core 0.9.3 → 0.9.5

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.
@@ -260,37 +260,12 @@ export class CliProviderInstance implements ProviderInstance {
260
260
 
261
261
  async onTick(): Promise<void> {
262
262
  if (this.providerSessionId) return;
263
- if (this.type === 'hermes-cli' && this.launchMode === 'new') return;
263
+ if (this.provider.resume?.skipProbeOnNewSession && this.launchMode === 'new') return;
264
264
 
265
- let probedSessionId: string | null = null;
266
-
267
- // Prefer declarative probe from provider.json schema
268
265
  const probeConfig = this.provider.sessionProbe;
269
- if (probeConfig) {
270
- probedSessionId = this.probeSessionIdFromConfig(probeConfig);
271
- } else {
272
- // Legacy hardcoded probes (backward compat until providers migrate)
273
- if (this.type === 'opencode-cli') {
274
- probedSessionId = this.probeSessionIdFromConfig({
275
- dbPath: '~/.local/share/opencode/opencode.db',
276
- query: 'select id from session where directory in ({dirs}) and time_created >= ? and time_archived is null order by time_updated desc limit 1',
277
- timestampFormat: 'unix_ms',
278
- });
279
- } else if (this.type === 'codex-cli') {
280
- probedSessionId = this.probeSessionIdFromConfig({
281
- dbPath: '~/.codex/state_5.sqlite',
282
- query: 'select id from threads where cwd in ({dirs}) and updated_at >= ? and archived = 0 order by updated_at desc limit 1',
283
- timestampFormat: 'unix_s',
284
- });
285
- } else if (this.type === 'goose-cli') {
286
- probedSessionId = this.probeSessionIdFromConfig({
287
- dbPath: '~/.local/share/goose/sessions/sessions.db',
288
- query: 'select id from sessions where working_dir in ({dirs}) and created_at >= ? order by updated_at desc limit 1',
289
- timestampFormat: 'iso',
290
- });
291
- }
292
- }
266
+ if (!probeConfig) return;
293
267
 
268
+ const probedSessionId = this.probeSessionIdFromConfig(probeConfig);
294
269
  if (probedSessionId) {
295
270
  this.promoteProviderSessionId(probedSessionId);
296
271
  }
@@ -355,7 +330,7 @@ export class CliProviderInstance implements ProviderInstance {
355
330
  ? 'error'
356
331
  : (autoApproveActive ? 'generating' : adapterStatus.status);
357
332
  const parsedProviderSessionId = normalizeProviderSessionId(
358
- this.type,
333
+ this.provider,
359
334
  typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
360
335
  );
361
336
  if (parsedProviderSessionId) {
@@ -684,7 +659,7 @@ export class CliProviderInstance implements ProviderInstance {
684
659
  if (!data || typeof data !== 'object') return;
685
660
 
686
661
  const patchedProviderSessionId = normalizeProviderSessionId(
687
- this.type,
662
+ this.provider,
688
663
  typeof data.providerSessionId === 'string' ? data.providerSessionId : '',
689
664
  );
690
665
  if (patchedProviderSessionId) {
@@ -960,53 +935,43 @@ export class CliProviderInstance implements ProviderInstance {
960
935
 
961
936
  private syncCanonicalSavedHistoryIfNeeded(): boolean {
962
937
  if (!this.providerSessionId) return false;
963
- if (this.type === 'hermes-cli') {
964
- try {
965
- const canonicalPath = path.join(os.homedir(), '.hermes', 'sessions', `session_${this.providerSessionId}.json`);
966
- if (!fs.existsSync(canonicalPath)) return false;
967
- const stat = fs.statSync(canonicalPath);
938
+ const canonicalHistory = this.provider.canonicalHistory;
939
+ if (!canonicalHistory) return false;
940
+
941
+ try {
942
+ let rebuilt = false;
943
+ if (canonicalHistory.format === 'hermes-json') {
944
+ const watchPath = canonicalHistory.watchPath
945
+ .replace(/^~/, os.homedir())
946
+ .replace('{{sessionId}}', this.providerSessionId);
947
+ if (!fs.existsSync(watchPath)) return false;
948
+ const stat = fs.statSync(watchPath);
968
949
  if (stat.mtimeMs <= this.lastCanonicalHermesSyncMtimeMs) return true;
969
- const rebuilt = rebuildHermesSavedHistoryFromCanonicalSession(this.providerSessionId);
970
- if (!rebuilt) return false;
971
- this.lastCanonicalHermesSyncMtimeMs = stat.mtimeMs;
972
- const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId);
973
- this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
974
- role: message.role,
975
- content: message.content,
976
- kind: message.kind,
977
- senderName: message.senderName,
978
- receivedAt: message.receivedAt,
979
- }));
980
- return true;
981
- } catch {
982
- return false;
983
- }
984
- }
985
- if (this.type === 'claude-cli') {
986
- try {
987
- const rebuilt = rebuildClaudeSavedHistoryFromNativeProject(this.providerSessionId, this.workingDir);
988
- if (!rebuilt) return false;
989
- const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId);
990
- this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
991
- role: message.role,
992
- content: message.content,
993
- kind: message.kind,
994
- senderName: message.senderName,
995
- receivedAt: message.receivedAt,
996
- }));
997
- return true;
998
- } catch {
999
- return false;
950
+ rebuilt = rebuildHermesSavedHistoryFromCanonicalSession(this.providerSessionId);
951
+ if (rebuilt) this.lastCanonicalHermesSyncMtimeMs = stat.mtimeMs;
952
+ } else if (canonicalHistory.format === 'claude-jsonl') {
953
+ rebuilt = rebuildClaudeSavedHistoryFromNativeProject(this.providerSessionId, this.workingDir);
1000
954
  }
955
+ if (!rebuilt) return false;
956
+ const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId, 0, this.provider.historyBehavior);
957
+ this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
958
+ role: message.role,
959
+ content: message.content,
960
+ kind: message.kind,
961
+ senderName: message.senderName,
962
+ receivedAt: message.receivedAt,
963
+ }));
964
+ return true;
965
+ } catch {
966
+ return false;
1001
967
  }
1002
- return false;
1003
968
  }
1004
969
 
1005
970
  private restorePersistedHistoryFromCurrentSession(): void {
1006
971
  if (!this.providerSessionId) return;
1007
972
  this.syncCanonicalSavedHistoryIfNeeded();
1008
- this.historyWriter.compactHistorySession(this.type, this.providerSessionId);
1009
- const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId);
973
+ this.historyWriter.compactHistorySession(this.type, this.providerSessionId, this.provider.historyBehavior);
974
+ const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId, 0, this.provider.historyBehavior);
1010
975
  this.historyWriter.seedSessionHistory(
1011
976
  this.type,
1012
977
  restoredHistory.messages,
@@ -418,6 +418,13 @@ export interface ProviderModule {
418
418
  extensionIdPattern_flags?: string;
419
419
  compatibility?: ProviderCompatibilityEntry[];
420
420
  defaultScriptDir?: string;
421
+ /**
422
+ * Scripts that can run at the IDE main-page level (not just inside the extension webview session frame).
423
+ * Default: ['listModes', 'setMode', 'listModels', 'setModel'].
424
+ * Add extra scripts here if the provider supports them at the IDE level (e.g. 'setModelGui').
425
+ * Replaces hardcoded claude-code-vscode special-case in stream-commands.ts.
426
+ */
427
+ ideLevelScripts?: string[];
421
428
 
422
429
  // ─── CLI category only ───
423
430
  binary?: string;
@@ -426,6 +433,8 @@ export interface ProviderModule {
426
433
  args?: string[];
427
434
  shell?: boolean;
428
435
  env?: Record<string, string>;
436
+ /** Auto-implement spawn config — controls how this provider is invoked for autonomous script generation */
437
+ autoImpl?: ProviderAutoImplSpawnConfig;
429
438
  };
430
439
  /** Delay before submitting typed CLI input (provider-specific TUI tuning) */
431
440
  sendDelayMs?: number;
@@ -451,6 +460,24 @@ export interface ProviderModule {
451
460
  allowInputDuringGeneration?: boolean;
452
461
  /** Approval button priority hints used when auto-approve must pick a positive action */
453
462
  approvalPositiveHints?: string[];
463
+ /**
464
+ * Regex pattern (as string) that a valid provider session ID must match.
465
+ * If set and the ID doesn't match, it is rejected (treated as invalid).
466
+ * Replaces hardcoded HERMES_SESSION_ID_RE / CLAUDE_SESSION_ID_RE checks.
467
+ */
468
+ sessionIdPattern?: string;
469
+ /** History behavior config — controls message filtering and collapse during replay */
470
+ historyBehavior?: ProviderHistoryBehavior;
471
+ /**
472
+ * Canonical history sync config — for providers that maintain native history files.
473
+ * When set, daemon syncs from native format into ADHDev JSONL store on each tick.
474
+ */
475
+ canonicalHistory?: ProviderCanonicalHistoryConfig;
476
+ /**
477
+ * Auto-fix verification profile — provider-specific test expectations for `provider fix`.
478
+ * If not set, provider fix runs without pre/post verification.
479
+ */
480
+ autoFixProfile?: ProviderAutoFixProfile;
454
481
 
455
482
  // ─── CDP scripts (ide/extension category) ───
456
483
  scripts?: ProviderScripts;
@@ -538,6 +565,94 @@ export interface ProviderResumeCapability {
538
565
  resumeSessionArgs?: string[];
539
566
  newSessionArgs?: string[];
540
567
  sessionIdFormat?: 'uuid' | 'string';
568
+ /** Skip session ID probing when launchMode is 'new' — for providers that manage their own session IDs on new sessions */
569
+ skipProbeOnNewSession?: boolean;
570
+ /**
571
+ * Subcommands that carry a session ID as their next positional argument.
572
+ * e.g. ['resume', 'fork'] for codex-cli (codex resume <id> / codex fork <id>).
573
+ * Replaces the hardcoded readCodexResumeSessionId check in cli-manager.ts.
574
+ */
575
+ sessionIdFromSubcommand?: string[];
576
+ /**
577
+ * When --session-id is present without an explicit resume flag, treat as 'new' rather than 'resume'.
578
+ * e.g. goose-cli passes --session-id on new sessions but requires --resume/-r to actually resume.
579
+ * Replaces the hardcoded goose-cli check in cli-manager.ts.
580
+ */
581
+ sessionIdIsNewByDefault?: boolean;
582
+ }
583
+
584
+ /**
585
+ * History behavior config — controls how history messages are processed for this provider.
586
+ * Replaces hardcoded agentType checks in chat-history.ts.
587
+ */
588
+ export interface ProviderHistoryBehavior {
589
+ /** Collapse consecutive assistant turns during history replay (e.g. codex-cli shows replayed intermediate turns) */
590
+ collapseConsecutiveAssistantTurns?: boolean;
591
+ /** Regex patterns (as strings) to filter out from assistant messages — e.g. CLI starter prompt suggestions */
592
+ filterAssistantPatterns?: string[];
593
+ /** If true, session ID must match sessionIdPattern exactly — reject and return '' if it doesn't match */
594
+ requireStrictSessionIdFormat?: boolean;
595
+ }
596
+
597
+ /**
598
+ * Canonical history sync config — for providers that maintain their own native history files.
599
+ * When set, daemon syncs from the provider's native format into the ADHDev JSONL store.
600
+ * Replaces hardcoded hermes-cli / claude-cli checks in cli-provider-instance.ts.
601
+ */
602
+ export interface ProviderCanonicalHistoryConfig {
603
+ /**
604
+ * Native history format.
605
+ * - 'hermes-json': single JSON file per session (~/.hermes/sessions/session_{{sessionId}}.json)
606
+ * - 'claude-jsonl': JSONL transcript under ~/.claude/projects/
607
+ */
608
+ format: 'hermes-json' | 'claude-jsonl';
609
+ /**
610
+ * Path to the native history file. Supports ~ and {{sessionId}} placeholder.
611
+ * e.g. "~/.hermes/sessions/session_{{sessionId}}.json"
612
+ */
613
+ watchPath: string;
614
+ }
615
+
616
+ /**
617
+ * Auto-implement spawn config — controls how the provider is spawned for autonomous AI-driven
618
+ * provider script implementation (dev-auto-implement.ts).
619
+ * Replaces hardcoded per-command branching.
620
+ */
621
+ export interface ProviderAutoImplSpawnConfig {
622
+ /**
623
+ * How the meta-prompt is passed to the agent.
624
+ * - 'flag': passed via a CLI flag (e.g. `claude -p "..."`)
625
+ * - 'stdin': piped via stdin (generic fallback)
626
+ * - 'subcommand': prepended as a subcommand (e.g. `codex exec "..."`)
627
+ */
628
+ promptMode: 'flag' | 'stdin' | 'subcommand';
629
+ /** CLI flag used to pass the prompt (promptMode: 'flag') — e.g. '-p' */
630
+ promptFlag?: string;
631
+ /** Subcommand prepended before the prompt (promptMode: 'subcommand') — e.g. 'exec' */
632
+ subcommand?: string;
633
+ /** Extra args appended in auto-impl mode — e.g. ['--dangerously-skip-permissions'] */
634
+ extraArgs?: string[];
635
+ /** Custom meta-prompt template; use {{promptFile}} placeholder. If omitted, generic prompt is used. */
636
+ metaPrompt?: string;
637
+ /**
638
+ * If true, schedule an auto-stop timer when the agent output goes quiet during verification.
639
+ * Replaces the hardcoded `command !== 'codex'` check in dev-auto-implement.ts.
640
+ */
641
+ autoStopOnQuiet?: boolean;
642
+ }
643
+
644
+ /**
645
+ * Auto-fix verification profile — provider-specific test expectations for `provider fix`.
646
+ * Replaces the hardcoded CLI_AUTO_FIX_VERIFICATION_PROFILES record in provider-commands.ts.
647
+ */
648
+ export interface ProviderAutoFixProfile {
649
+ fixtureName: string;
650
+ description: string;
651
+ inspectFields?: string[];
652
+ focusAreas?: string[];
653
+ lastAssistantMustContainAny?: string[];
654
+ lastAssistantMustNotContainAny?: string[];
655
+ timeoutMs?: number;
541
656
  }
542
657
 
543
658
  /**
@@ -32,6 +32,11 @@ const KNOWN_PROVIDER_FIELDS = new Set<string>([
32
32
  'resume',
33
33
  'sessionProbe',
34
34
  'approvalPositiveHints',
35
+ 'sessionIdPattern',
36
+ 'historyBehavior',
37
+ 'canonicalHistory',
38
+ 'autoFixProfile',
39
+ 'ideLevelScripts',
35
40
  'scripts',
36
41
  'vscodeCommands',
37
42
  'inputMethod',
@@ -1,26 +1,28 @@
1
- const HERMES_SESSION_ID_RE = /^\d{8}_\d{6}_[a-z0-9]+$/i
2
- const CLAUDE_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
1
+ import type { ProviderModule } from './contracts.js'
3
2
 
4
- export function normalizeProviderSessionId(providerType: string | undefined, providerSessionId: string | null | undefined): string {
5
- const normalizedProviderType = typeof providerType === 'string' ? providerType.trim() : ''
3
+ /**
4
+ * Normalize and validate a provider session ID using the declarative `sessionIdPattern`
5
+ * from the provider's ProviderModule definition.
6
+ */
7
+ export function normalizeProviderSessionId(
8
+ provider: ProviderModule | undefined,
9
+ providerSessionId: string | null | undefined,
10
+ ): string {
6
11
  const normalizedId = typeof providerSessionId === 'string' ? providerSessionId.trim() : ''
7
12
  if (!normalizedId) return ''
8
13
 
9
14
  const lowered = normalizedId.toLowerCase()
10
15
  if (lowered === 'undefined' || lowered === 'null') return ''
11
16
 
12
- if (normalizedProviderType === 'hermes-cli' && !HERMES_SESSION_ID_RE.test(normalizedId)) {
13
- return ''
14
- }
15
- if (normalizedProviderType === 'claude-cli' && !CLAUDE_SESSION_ID_RE.test(normalizedId)) {
16
- return ''
17
+ const sessionIdPattern = provider?.sessionIdPattern
18
+ if (sessionIdPattern) {
19
+ try {
20
+ const re = new RegExp(sessionIdPattern, 'i')
21
+ if (!re.test(normalizedId)) return ''
22
+ } catch {
23
+ // Invalid regex in provider.json — skip validation
24
+ }
17
25
  }
18
26
 
19
27
  return normalizedId
20
28
  }
21
-
22
- export function isLegacyVolatileSessionReadKey(key: string | null | undefined): boolean {
23
- const normalizedKey = typeof key === 'string' ? key.trim() : ''
24
- if (!normalizedKey) return false
25
- return normalizedKey.startsWith('provider:codex:vscode-webview://')
26
- }