@adhdev/daemon-core 0.9.35 → 0.9.37

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.35",
3
+ "version": "0.9.37",
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.35",
3
+ "version": "0.9.37",
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",
@@ -369,7 +369,17 @@ export class ProviderCliAdapter implements CliAdapter {
369
369
  return baseMessages.slice(-ProviderCliAdapter.PARSE_MESSAGE_TAIL_LIMIT);
370
370
  }
371
371
 
372
+ private messagesShareStableIdentity(left: any, right: any): boolean {
373
+ if (left === right) return true;
374
+ if (!left || !right) return false;
375
+ if ((left.role || '') !== (right.role || '')) return false;
376
+ if (left.id && right.id && String(left.id) === String(right.id)) return true;
377
+ if (typeof left.index === 'number' && typeof right.index === 'number' && left.index === right.index) return true;
378
+ return false;
379
+ }
380
+
372
381
  private messagesComparable(left: any, right: any): boolean {
382
+ if (this.messagesShareStableIdentity(left, right)) return true;
373
383
  if (!left || !right) return false;
374
384
  if ((left.role || '') !== (right.role || '')) return false;
375
385
  const leftText = normalizeComparableTranscriptText(left.content);
@@ -1665,11 +1675,12 @@ export class ProviderCliAdapter implements CliAdapter {
1665
1675
 
1666
1676
  // ─── Public API (CliAdapter) ───────────────────
1667
1677
 
1668
- getStatus(): CliSessionStatus {
1669
- 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;
1670
1681
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1671
1682
  let effectiveModal = startupModal || this.activeModal;
1672
- if (!startupModal && !effectiveModal && typeof this.cliScripts?.parseOutput === 'function') {
1683
+ if (allowParse && !startupModal && !effectiveModal && typeof this.cliScripts?.parseOutput === 'function') {
1673
1684
  let parsed = this.getFreshParsedStatusCache();
1674
1685
  if (!parsed && effectiveStatus !== 'idle') {
1675
1686
  const now = Date.now();
@@ -1723,6 +1734,82 @@ export class ProviderCliAdapter implements CliAdapter {
1723
1734
  this.syncMessageViews();
1724
1735
  }
1725
1736
 
1737
+ private getSharedCommittedPrefixLength(parsedMessages: any[]): number {
1738
+ const committedMessages = this.committedMessages;
1739
+ const max = Math.min(parsedMessages.length, committedMessages.length);
1740
+ let index = 0;
1741
+ while (index < max && this.messagesShareStableIdentity(parsedMessages[index], committedMessages[index])) {
1742
+ index += 1;
1743
+ }
1744
+ return index;
1745
+ }
1746
+
1747
+ private hydrateCommittedPrefixForParsedStatus(parsedMessages: any[]): any[] | null {
1748
+ const sharedPrefixLength = this.getSharedCommittedPrefixLength(parsedMessages);
1749
+ if (sharedPrefixLength !== this.committedMessages.length) return null;
1750
+
1751
+ const committedHydratedMessages = this.committedMessages.map((message, index) => {
1752
+ const timestamp = typeof message.timestamp === 'number' && Number.isFinite(message.timestamp)
1753
+ ? message.timestamp
1754
+ : (this.lastOutputAt || this.currentTurnScope?.startedAt || Date.now());
1755
+ const contentValue = message.content;
1756
+ return {
1757
+ role: message.role,
1758
+ content: typeof contentValue === 'string' ? contentValue : String(contentValue || ''),
1759
+ timestamp,
1760
+ receivedAt: typeof message.receivedAt === 'number' && Number.isFinite(message.receivedAt)
1761
+ ? message.receivedAt
1762
+ : timestamp,
1763
+ kind: message.kind,
1764
+ id: message.id || `msg_${index}`,
1765
+ index: typeof message.index === 'number' ? message.index : index,
1766
+ meta: message.meta,
1767
+ senderName: message.senderName,
1768
+ };
1769
+ });
1770
+ const extraMessages = parsedMessages.slice(sharedPrefixLength);
1771
+ if (extraMessages.length === 0) return committedHydratedMessages;
1772
+
1773
+ const extraHydratedMessages = hydrateCliParsedMessages(extraMessages, {
1774
+ committedMessages: [],
1775
+ scope: this.currentTurnScope,
1776
+ lastOutputAt: this.lastOutputAt,
1777
+ }).map((message, offset) => ({
1778
+ ...message,
1779
+ id: message.id || `msg_${sharedPrefixLength + offset}`,
1780
+ index: typeof message.index === 'number' ? message.index : sharedPrefixLength + offset,
1781
+ }));
1782
+ return [...committedHydratedMessages, ...extraHydratedMessages];
1783
+ }
1784
+
1785
+ private hydrateParsedMessagesForStatus(parsedMessages: any[]): any[] {
1786
+ return this.hydrateCommittedPrefixForParsedStatus(parsedMessages)
1787
+ || hydrateCliParsedMessages(parsedMessages, {
1788
+ committedMessages: this.committedMessages,
1789
+ scope: this.currentTurnScope,
1790
+ lastOutputAt: this.lastOutputAt,
1791
+ });
1792
+ }
1793
+
1794
+ private buildCommittedChatMessages(): any[] {
1795
+ return this.committedMessages.map((message, index) => {
1796
+ const contentValue = message.content;
1797
+ return buildChatMessage({
1798
+ role: message.role,
1799
+ content: typeof contentValue === 'string' ? contentValue : String(contentValue || ''),
1800
+ timestamp: message.timestamp,
1801
+ kind: message.kind,
1802
+ meta: message.meta,
1803
+ senderName: message.senderName,
1804
+ id: message.id || `msg_${index}`,
1805
+ index: typeof message.index === 'number' ? message.index : index,
1806
+ receivedAt: typeof message.receivedAt === 'number'
1807
+ ? message.receivedAt
1808
+ : message.timestamp,
1809
+ });
1810
+ });
1811
+ }
1812
+
1726
1813
  /**
1727
1814
  * Script-based full parse — returns ReadChatResult.
1728
1815
  * Called by command handler / dashboard for rich content rendering.
@@ -1765,7 +1852,7 @@ export class ProviderCliAdapter implements CliAdapter {
1765
1852
  this.onStatusChange?.();
1766
1853
  }
1767
1854
  }
1768
- if (parsed && Array.isArray(parsed.messages)) {
1855
+ if (parsed && Array.isArray(parsed.messages) && this.provider.allowInputDuringGeneration === true) {
1769
1856
  const hydratedForCommit = normalizeCliParsedMessages(parsed.messages, {
1770
1857
  committedMessages: this.committedMessages,
1771
1858
  scope: this.currentTurnScope,
@@ -1787,25 +1874,13 @@ export class ProviderCliAdapter implements CliAdapter {
1787
1874
  && this.currentStatus === 'idle';
1788
1875
  let result: any;
1789
1876
  if (parsed && Array.isArray(parsed.messages)) {
1790
- const parsedHydratedMessages = hydrateCliParsedMessages(parsed.messages, {
1791
- committedMessages: this.committedMessages,
1792
- scope: this.currentTurnScope,
1793
- lastOutputAt: this.lastOutputAt,
1794
- });
1795
- const committedHydratedMessages = this.committedMessages.map((message, index) => buildChatMessage({
1796
- ...message,
1797
- id: message.id || `msg_${index}`,
1798
- index: typeof message.index === 'number' ? message.index : index,
1799
- receivedAt: typeof message.receivedAt === 'number'
1800
- ? message.receivedAt
1801
- : message.timestamp,
1802
- }));
1877
+ const parsedHydratedMessages = this.hydrateParsedMessagesForStatus(parsed.messages);
1803
1878
  const parsedLastAssistant = [...parsedHydratedMessages].reverse().find((message) => message.role === 'assistant' && typeof message.content === 'string' && message.content.trim());
1804
1879
  const shouldAdoptParsedIdleReplay =
1805
1880
  !this.currentTurnScope
1806
1881
  && !this.activeModal
1807
1882
  && !!parsedLastAssistant
1808
- && parsedTranscriptIsRicherThanCommitted(parsedHydratedMessages, committedHydratedMessages)
1883
+ && parsedTranscriptIsRicherThanCommitted(parsedHydratedMessages, this.committedMessages)
1809
1884
  && (
1810
1885
  this.currentStatus === 'idle'
1811
1886
  || (
@@ -1816,11 +1891,23 @@ export class ProviderCliAdapter implements CliAdapter {
1816
1891
  )
1817
1892
  );
1818
1893
  if (shouldAdoptParsedIdleReplay) {
1819
- this.committedMessages = normalizeCliParsedMessages(parsed.messages, {
1820
- committedMessages: this.committedMessages,
1821
- scope: this.currentTurnScope,
1822
- lastOutputAt: this.lastOutputAt,
1823
- });
1894
+ this.committedMessages = this.getSharedCommittedPrefixLength(parsed.messages) === this.committedMessages.length
1895
+ ? parsedHydratedMessages.map((message) => ({
1896
+ role: message.role,
1897
+ content: typeof message.content === 'string' ? message.content : String(message.content || ''),
1898
+ timestamp: message.timestamp,
1899
+ receivedAt: message.receivedAt,
1900
+ kind: message.kind,
1901
+ id: message.id,
1902
+ index: message.index,
1903
+ meta: message.meta,
1904
+ senderName: message.senderName,
1905
+ }))
1906
+ : normalizeCliParsedMessages(parsed.messages, {
1907
+ committedMessages: this.committedMessages,
1908
+ scope: this.currentTurnScope,
1909
+ lastOutputAt: this.lastOutputAt,
1910
+ });
1824
1911
  this.syncMessageViews();
1825
1912
  if (this.currentStatus !== 'idle' || this.isWaitingForResponse) {
1826
1913
  this.responseBuffer = '';
@@ -1835,25 +1922,15 @@ export class ProviderCliAdapter implements CliAdapter {
1835
1922
  this.onStatusChange?.();
1836
1923
  }
1837
1924
  }
1838
- const effectiveCommittedHydratedMessages = shouldAdoptParsedIdleReplay
1839
- ? this.committedMessages.map((message, index) => buildChatMessage({
1840
- ...message,
1841
- id: message.id || `msg_${index}`,
1842
- index: typeof message.index === 'number' ? message.index : index,
1843
- receivedAt: typeof message.receivedAt === 'number'
1844
- ? message.receivedAt
1845
- : message.timestamp,
1846
- }))
1847
- : committedHydratedMessages;
1848
1925
  const shouldPreferCommittedHistoryReplay =
1849
1926
  !this.currentTurnScope
1850
1927
  && !this.activeModal
1851
- && effectiveCommittedHydratedMessages.length > parsedHydratedMessages.length;
1928
+ && this.committedMessages.length > parsedHydratedMessages.length;
1852
1929
  const shouldPreferCommittedIdleReplay =
1853
1930
  shouldPreferCommittedMessages
1854
1931
  && !shouldAdoptParsedIdleReplay;
1855
1932
  const hydratedMessages = (shouldPreferCommittedIdleReplay || shouldPreferCommittedHistoryReplay)
1856
- ? effectiveCommittedHydratedMessages
1933
+ ? this.buildCommittedChatMessages()
1857
1934
  : parsedHydratedMessages;
1858
1935
  result = {
1859
1936
  id: parsed.id || 'cli_session',
@@ -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);
@@ -494,7 +494,7 @@ export class CliProviderInstance implements ProviderInstance {
494
494
  }
495
495
 
496
496
  getHotChatSessionState(): HotChatSessionState {
497
- const adapterStatus = this.adapter.getStatus();
497
+ const adapterStatus = this.adapter.getStatus({ allowParse: false });
498
498
  const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
499
499
  const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
500
500
  const runtime = this.adapter.getRuntimeMetadata();