@adhdev/daemon-core 0.9.36 → 0.9.38

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.9.36",
3
+ "version": "0.9.38",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.36",
3
+ "version": "0.9.38",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1675,11 +1675,12 @@ export class ProviderCliAdapter implements CliAdapter {
1675
1675
 
1676
1676
  // ─── Public API (CliAdapter) ───────────────────
1677
1677
 
1678
- getStatus(): CliSessionStatus {
1679
- const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
1678
+ getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
1679
+ const allowParse = options.allowParse !== false;
1680
+ const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
1680
1681
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1681
1682
  let effectiveModal = startupModal || this.activeModal;
1682
- if (!startupModal && !effectiveModal && typeof this.cliScripts?.parseOutput === 'function') {
1683
+ if (allowParse && !startupModal && !effectiveModal && typeof this.cliScripts?.parseOutput === 'function') {
1683
1684
  let parsed = this.getFreshParsedStatusCache();
1684
1685
  if (!parsed && effectiveStatus !== 'idle') {
1685
1686
  const now = Date.now();
@@ -31,37 +31,115 @@ export function hydrateCliParsedMessages(
31
31
  ): any[] {
32
32
  const { committedMessages, scope, lastOutputAt } = options;
33
33
  const referenceMessages = [...committedMessages];
34
- const referenceComparables = referenceMessages.map((message) => normalizeComparableMessageContent(message?.content || ''));
34
+ const referenceComparables: Array<string | undefined> = new Array(referenceMessages.length);
35
35
  const usedReferenceIndexes = new Set<number>();
36
36
  const now = options.now ?? Date.now();
37
+ let exactReferenceIndexesByKey: Map<string, number[]> | null = null;
38
+ const exactReferenceCursorByKey = new Map<string, number>();
39
+
40
+ const hasFiniteTimestamp = (message: any): message is { timestamp: number } => (
41
+ typeof message?.timestamp === 'number' && Number.isFinite(message.timestamp)
42
+ );
43
+
44
+ const getReferenceComparable = (index: number): string => {
45
+ if (typeof referenceComparables[index] === 'string') return referenceComparables[index] || '';
46
+ const comparable = normalizeComparableMessageContent(referenceMessages[index]?.content || '');
47
+ referenceComparables[index] = comparable;
48
+ return comparable;
49
+ };
50
+
51
+ const messagesShareStableIdentity = (parsed: any, reference: any): boolean => {
52
+ if (!parsed || !reference) return false;
53
+ const parsedId = typeof parsed.id === 'string' ? parsed.id.trim() : '';
54
+ const referenceId = typeof reference.id === 'string' ? reference.id.trim() : '';
55
+ if (parsedId && referenceId && parsedId === referenceId) return true;
56
+ return typeof parsed.index === 'number'
57
+ && Number.isFinite(parsed.index)
58
+ && typeof reference.index === 'number'
59
+ && Number.isFinite(reference.index)
60
+ && parsed.index === reference.index;
61
+ };
62
+
63
+ const exactReferenceKey = (role: 'user' | 'assistant', comparable: string): string => `${role}\u0000${comparable}`;
64
+
65
+ const ensureExactReferenceIndex = (): Map<string, number[]> => {
66
+ if (exactReferenceIndexesByKey) return exactReferenceIndexesByKey;
67
+ const byKey = new Map<string, number[]>();
68
+ for (let i = 0; i < referenceMessages.length; i++) {
69
+ const candidate = referenceMessages[i];
70
+ if (!candidate || (candidate.role !== 'user' && candidate.role !== 'assistant') || !hasFiniteTimestamp(candidate)) continue;
71
+ const comparable = getReferenceComparable(i);
72
+ if (!comparable) continue;
73
+ const key = exactReferenceKey(candidate.role, comparable);
74
+ const indexes = byKey.get(key);
75
+ if (indexes) {
76
+ indexes.push(i);
77
+ } else {
78
+ byKey.set(key, [i]);
79
+ }
80
+ }
81
+ exactReferenceIndexesByKey = byKey;
82
+ return byKey;
83
+ };
84
+
85
+ const takeExactReferenceTimestamp = (role: 'user' | 'assistant', normalizedContent: string): number | undefined => {
86
+ const key = exactReferenceKey(role, normalizedContent);
87
+ const indexes = ensureExactReferenceIndex().get(key);
88
+ if (!indexes) return undefined;
89
+ let cursor = exactReferenceCursorByKey.get(key) || 0;
90
+ while (cursor < indexes.length) {
91
+ const candidateIndex = indexes[cursor];
92
+ cursor += 1;
93
+ if (usedReferenceIndexes.has(candidateIndex)) continue;
94
+ const candidate = referenceMessages[candidateIndex];
95
+ if (!candidate || candidate.role !== role || !hasFiniteTimestamp(candidate)) continue;
96
+ usedReferenceIndexes.add(candidateIndex);
97
+ exactReferenceCursorByKey.set(key, cursor);
98
+ return candidate.timestamp;
99
+ }
100
+ exactReferenceCursorByKey.set(key, cursor);
101
+ return undefined;
102
+ };
103
+
104
+ const findReferenceTimestamp = (message: any, role: 'user' | 'assistant', content: string, parsedIndex: number): number | undefined => {
105
+ const sameIndex = referenceMessages[parsedIndex];
106
+ if (
107
+ sameIndex
108
+ && !usedReferenceIndexes.has(parsedIndex)
109
+ && sameIndex.role === role
110
+ && hasFiniteTimestamp(sameIndex)
111
+ && messagesShareStableIdentity(message, sameIndex)
112
+ ) {
113
+ usedReferenceIndexes.add(parsedIndex);
114
+ return sameIndex.timestamp;
115
+ }
37
116
 
38
- const findReferenceTimestamp = (role: 'user' | 'assistant', content: string, parsedIndex: number): number | undefined => {
39
117
  const normalizedContent = normalizeComparableMessageContent(content);
40
118
  if (!normalizedContent) return undefined;
41
119
 
42
- const sameIndex = referenceMessages[parsedIndex];
43
120
  if (
44
121
  sameIndex
45
122
  && !usedReferenceIndexes.has(parsedIndex)
46
123
  && sameIndex.role === role
47
- && referenceComparables[parsedIndex] === normalizedContent
48
- && typeof sameIndex.timestamp === 'number'
49
- && Number.isFinite(sameIndex.timestamp)
124
+ && getReferenceComparable(parsedIndex) === normalizedContent
125
+ && hasFiniteTimestamp(sameIndex)
50
126
  ) {
51
127
  usedReferenceIndexes.add(parsedIndex);
52
128
  return sameIndex.timestamp;
53
129
  }
54
130
 
131
+ const exactTimestamp = takeExactReferenceTimestamp(role, normalizedContent);
132
+ if (typeof exactTimestamp === 'number') return exactTimestamp;
133
+
55
134
  for (let i = 0; i < referenceMessages.length; i++) {
56
135
  if (usedReferenceIndexes.has(i)) continue;
57
136
  const candidate = referenceMessages[i];
58
137
  if (!candidate || candidate.role !== role) continue;
59
- const candidateContent = referenceComparables[i];
138
+ const candidateContent = getReferenceComparable(i);
60
139
  if (!candidateContent) continue;
61
- const exactMatch = candidateContent === normalizedContent;
62
140
  const fuzzyMatch = candidateContent.includes(normalizedContent) || normalizedContent.includes(candidateContent);
63
- if (!exactMatch && !fuzzyMatch) continue;
64
- if (typeof candidate.timestamp === 'number' && Number.isFinite(candidate.timestamp)) {
141
+ if (!fuzzyMatch) continue;
142
+ if (hasFiniteTimestamp(candidate)) {
65
143
  usedReferenceIndexes.add(i);
66
144
  return candidate.timestamp;
67
145
  }
@@ -78,7 +156,7 @@ export function hydrateCliParsedMessages(
78
156
  const parsedTimestamp = typeof message.timestamp === 'number' && Number.isFinite(message.timestamp)
79
157
  ? message.timestamp
80
158
  : undefined;
81
- const referenceTimestamp = parsedTimestamp ?? findReferenceTimestamp(role, content, index);
159
+ const referenceTimestamp = parsedTimestamp ?? findReferenceTimestamp(message, role, content, index);
82
160
  const fallbackTimestamp = role === 'user'
83
161
  ? (scope?.startedAt || now)
84
162
  : (lastOutputAt || scope?.startedAt || now);
@@ -48,6 +48,13 @@ function buildPersistableCliHistorySignature(message: PersistableCliHistoryMessa
48
48
  ].join('|');
49
49
  }
50
50
 
51
+ function hasSamePersistableCliHistoryIdentity(a: PersistableCliHistoryMessage, b: PersistableCliHistoryMessage): boolean {
52
+ return String(a?.role || '') === String(b?.role || '')
53
+ && String(a?.kind || '') === String(b?.kind || '')
54
+ && String(a?.senderName || '') === String(b?.senderName || '')
55
+ && String(a?.content || '') === String(b?.content || '');
56
+ }
57
+
51
58
  export function buildIncrementalHistoryAppendMessages(
52
59
  previousMessages: PersistableCliHistoryMessage[],
53
60
  currentMessages: PersistableCliHistoryMessage[],
@@ -55,20 +62,32 @@ export function buildIncrementalHistoryAppendMessages(
55
62
  if (!Array.isArray(currentMessages) || currentMessages.length === 0) return [];
56
63
  if (!Array.isArray(previousMessages) || previousMessages.length === 0) return currentMessages;
57
64
 
58
- const previousSignatures = previousMessages.map(buildPersistableCliHistorySignature);
59
- const currentSignatures = currentMessages.map(buildPersistableCliHistorySignature);
60
-
65
+ const comparableLength = Math.min(previousMessages.length, currentMessages.length);
61
66
  let sharedPrefixLength = 0;
62
67
  while (
63
- sharedPrefixLength < previousSignatures.length
64
- && sharedPrefixLength < currentSignatures.length
65
- && previousSignatures[sharedPrefixLength] === currentSignatures[sharedPrefixLength]
68
+ sharedPrefixLength < comparableLength
69
+ && hasSamePersistableCliHistoryIdentity(previousMessages[sharedPrefixLength], currentMessages[sharedPrefixLength])
70
+ ) {
71
+ sharedPrefixLength += 1;
72
+ }
73
+
74
+ if (sharedPrefixLength === currentMessages.length) return [];
75
+ if (sharedPrefixLength === previousMessages.length) return currentMessages.slice(sharedPrefixLength);
76
+
77
+ // Rare fallback: preserve the older whitespace-normalized behavior only when
78
+ // the cheap identity check detects a changed prefix. Recomputing normalized
79
+ // signatures for the full transcript on every idle status poll was a CPU
80
+ // hot path for long CLI sessions.
81
+ while (
82
+ sharedPrefixLength < comparableLength
83
+ && buildPersistableCliHistorySignature(previousMessages[sharedPrefixLength])
84
+ === buildPersistableCliHistorySignature(currentMessages[sharedPrefixLength])
66
85
  ) {
67
86
  sharedPrefixLength += 1;
68
87
  }
69
88
 
70
- if (sharedPrefixLength === currentSignatures.length) return [];
71
- if (sharedPrefixLength === previousSignatures.length) return currentMessages.slice(sharedPrefixLength);
89
+ if (sharedPrefixLength === currentMessages.length) return [];
90
+ if (sharedPrefixLength === previousMessages.length) return currentMessages.slice(sharedPrefixLength);
72
91
  return currentMessages;
73
92
  }
74
93
 
@@ -418,13 +437,15 @@ export class CliProviderInstance implements ProviderInstance {
418
437
  }));
419
438
  if (!canonicalBackedHistory && !shouldSkipReplayPersist && normalizedMessagesToSave.length > 0) {
420
439
  const incrementalMessages = buildIncrementalHistoryAppendMessages(this.lastPersistedHistoryMessages, normalizedMessagesToSave);
421
- this.historyWriter.appendNewMessages(
422
- this.type,
423
- incrementalMessages,
424
- parsedStatus?.title || dirName,
425
- this.instanceId,
426
- this.providerSessionId,
427
- );
440
+ if (incrementalMessages.length > 0) {
441
+ this.historyWriter.appendNewMessages(
442
+ this.type,
443
+ incrementalMessages,
444
+ parsedStatus?.title || dirName,
445
+ this.instanceId,
446
+ this.providerSessionId,
447
+ );
448
+ }
428
449
  }
429
450
  if (!canonicalBackedHistory) {
430
451
  this.lastPersistedHistoryMessages = normalizedMessagesToSave;
@@ -494,7 +515,7 @@ export class CliProviderInstance implements ProviderInstance {
494
515
  }
495
516
 
496
517
  getHotChatSessionState(): HotChatSessionState {
497
- const adapterStatus = this.adapter.getStatus();
518
+ const adapterStatus = this.adapter.getStatus({ allowParse: false });
498
519
  const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
499
520
  const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
500
521
  const runtime = this.adapter.getRuntimeMetadata();