@adhdev/daemon-core 0.9.82-rc.8 → 0.9.82-rc.81

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.
Files changed (74) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  2. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
  4. package/dist/commands/router.d.ts +22 -0
  5. package/dist/config/mesh-config.d.ts +66 -1
  6. package/dist/git/git-commands.d.ts +1 -0
  7. package/dist/git/git-status.d.ts +5 -0
  8. package/dist/git/git-types.d.ts +10 -0
  9. package/dist/index.d.ts +13 -6
  10. package/dist/index.js +5074 -1177
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +5038 -1163
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/installer.d.ts +1 -4
  15. package/dist/launch.d.ts +1 -1
  16. package/dist/logging/async-batch-writer.d.ts +10 -0
  17. package/dist/mesh/beads-db.d.ts +18 -0
  18. package/dist/mesh/mesh-active-work.d.ts +60 -0
  19. package/dist/mesh/mesh-events.d.ts +29 -5
  20. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  21. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  22. package/dist/mesh/mesh-ledger.d.ts +38 -1
  23. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  24. package/dist/mesh/refine-config.d.ts +119 -0
  25. package/dist/providers/chat-message-normalization.d.ts +1 -0
  26. package/dist/providers/cli-provider-instance.d.ts +2 -1
  27. package/dist/repo-mesh-types.d.ts +39 -0
  28. package/dist/status/reporter.d.ts +2 -0
  29. package/package.json +3 -1
  30. package/src/boot/daemon-lifecycle.ts +1 -0
  31. package/src/cli-adapters/provider-cli-adapter.ts +91 -3
  32. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  33. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  34. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  35. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  36. package/src/cli-adapters/provider-cli-shared.ts +20 -10
  37. package/src/commands/chat-commands.ts +310 -12
  38. package/src/commands/cli-manager.ts +101 -0
  39. package/src/commands/handler.ts +8 -1
  40. package/src/commands/mesh-coordinator.ts +13 -143
  41. package/src/commands/router.ts +2435 -414
  42. package/src/config/chat-history.ts +9 -7
  43. package/src/config/mesh-config.ts +244 -1
  44. package/src/daemon/dev-cli-debug.ts +10 -1
  45. package/src/detection/ide-detector.ts +26 -16
  46. package/src/git/git-commands.ts +3 -3
  47. package/src/git/git-status.ts +97 -6
  48. package/src/git/git-summary.ts +3 -0
  49. package/src/git/git-types.ts +11 -0
  50. package/src/index.ts +31 -5
  51. package/src/installer.d.ts +1 -1
  52. package/src/installer.ts +8 -6
  53. package/src/launch.d.ts +1 -1
  54. package/src/launch.ts +37 -28
  55. package/src/logging/async-batch-writer.ts +55 -0
  56. package/src/logging/logger.ts +2 -1
  57. package/src/mesh/beads-db.ts +176 -0
  58. package/src/mesh/coordinator-prompt.ts +27 -7
  59. package/src/mesh/mesh-active-work.ts +243 -0
  60. package/src/mesh/mesh-events.ts +398 -46
  61. package/src/mesh/mesh-fast-forward.ts +430 -0
  62. package/src/mesh/mesh-host-ownership.ts +73 -0
  63. package/src/mesh/mesh-ledger.ts +138 -1
  64. package/src/mesh/mesh-work-queue.ts +199 -137
  65. package/src/mesh/refine-config.ts +306 -0
  66. package/src/providers/chat-message-normalization.ts +3 -1
  67. package/src/providers/cli-provider-instance.ts +91 -13
  68. package/src/providers/ide-provider-instance.ts +17 -3
  69. package/src/providers/provider-loader.ts +10 -4
  70. package/src/providers/read-chat-contract.ts +1 -1
  71. package/src/providers/version-archive.ts +38 -20
  72. package/src/repo-mesh-types.ts +43 -0
  73. package/src/status/reporter.ts +15 -0
  74. package/src/system/host-memory.ts +29 -12
@@ -761,6 +761,17 @@ export class ProviderCliAdapter implements CliAdapter {
761
761
  if (stableMs < 2000) return;
762
762
 
763
763
  const startupModal = this.runParseApproval(this.recentOutputBuffer);
764
+ const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
765
+ if (!startupModal && startupStatus !== 'idle') {
766
+ this.recordTrace('startup_settle_deferred', {
767
+ trigger,
768
+ startupStatus,
769
+ stableMs,
770
+ screenText: summarizeCliTraceText(screenText, 500),
771
+ });
772
+ this.scheduleStartupSettleCheck();
773
+ return;
774
+ }
764
775
  this.startupParseGate = false;
765
776
  if (this.startupSettleTimer) {
766
777
  clearTimeout(this.startupSettleTimer);
@@ -956,6 +967,38 @@ export class ProviderCliAdapter implements CliAdapter {
956
967
  return true;
957
968
  }
958
969
 
970
+ private clearParsedIdleResponseGuard(reason: string, parsedStatus: any): boolean {
971
+ const parsedRawStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status.trim() : '';
972
+ const parsedModal = parsedStatus?.activeModal ?? parsedStatus?.modal ?? null;
973
+ const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
974
+ if (
975
+ !this.isWaitingForResponse
976
+ || parsedRawStatus !== 'idle'
977
+ || !!parsedModal
978
+ || !!blockingModal
979
+ || !this.parsedStatusHasFinalAssistantMessage(parsedStatus)
980
+ ) {
981
+ return false;
982
+ }
983
+ this.clearAllTimers();
984
+ this.clearIdleFinishCandidate(reason);
985
+ this.responseBuffer = '';
986
+ this.isWaitingForResponse = false;
987
+ this.responseSettleIgnoreUntil = 0;
988
+ this.submitRetryUsed = false;
989
+ this.submitRetryPromptSnippet = '';
990
+ this.finishRetryCount = 0;
991
+ this.currentTurnScope = null;
992
+ this.activeModal = null;
993
+ this.setStatus('idle', reason);
994
+ this.recordTrace('parsed_idle_response_cleared', {
995
+ reason,
996
+ parsedStatus: parsedRawStatus,
997
+ parsedMessageCount: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages.length : 0,
998
+ });
999
+ return true;
1000
+ }
1001
+
959
1002
  private hasMeaningfulResponseBuffer(promptSnippet: string): boolean {
960
1003
  const raw = String(this.responseBuffer || '').trim();
961
1004
  if (!raw) return false;
@@ -1489,6 +1532,7 @@ export class ProviderCliAdapter implements CliAdapter {
1489
1532
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1490
1533
  recentOutputBuffer: this.recentOutputBuffer,
1491
1534
  terminalScreenText: parseScreenText,
1535
+ workingDir: this.workingDir,
1492
1536
  baseMessages: [],
1493
1537
  partialResponse: this.responseBuffer,
1494
1538
  isWaitingForResponse: this.isWaitingForResponse,
@@ -1552,6 +1596,15 @@ export class ProviderCliAdapter implements CliAdapter {
1552
1596
  return !!(startupModal || this.activeModal);
1553
1597
  }
1554
1598
 
1599
+ private parsedStatusHasFinalAssistantMessage(parsed: any): boolean {
1600
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
1601
+ const lastAssistant = [...messages].reverse().find((message: any) => {
1602
+ if (!message || message.role !== 'assistant') return false;
1603
+ return typeof message.content === 'string' && message.content.trim().length > 0;
1604
+ });
1605
+ return !!lastAssistant;
1606
+ }
1607
+
1555
1608
  private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
1556
1609
  if (this.parseErrorMessage) return 'error';
1557
1610
  if (this.hasActionableApproval(startupModal)) return 'waiting_approval';
@@ -1564,8 +1617,14 @@ export class ProviderCliAdapter implements CliAdapter {
1564
1617
  getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
1565
1618
  const allowParse = options.allowParse !== false;
1566
1619
  const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
1620
+ const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
1621
+ ? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
1622
+ : null;
1567
1623
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1568
1624
  let effectiveModal = startupModal || this.activeModal;
1625
+ if (startupDetectedStatus === 'waiting_approval') {
1626
+ effectiveStatus = 'waiting_approval';
1627
+ }
1569
1628
  if (allowParse && !startupModal && !effectiveModal) {
1570
1629
  const parsed = this.getFreshParsedStatusCache();
1571
1630
  const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
@@ -1575,6 +1634,18 @@ export class ProviderCliAdapter implements CliAdapter {
1575
1634
  if (parsed?.status === 'waiting_approval' && parsedModal) {
1576
1635
  effectiveStatus = 'waiting_approval';
1577
1636
  effectiveModal = parsedModal;
1637
+ } else if (
1638
+ effectiveStatus === 'idle'
1639
+ && parsed?.status === 'generating'
1640
+ && !this.parsedStatusHasFinalAssistantMessage(parsed)
1641
+ ) {
1642
+ effectiveStatus = 'generating';
1643
+ } else if (
1644
+ effectiveStatus === 'generating'
1645
+ && parsed?.status === 'idle'
1646
+ && this.parsedStatusHasFinalAssistantMessage(parsed)
1647
+ ) {
1648
+ effectiveStatus = 'idle';
1578
1649
  }
1579
1650
  }
1580
1651
  const bufferState = this.getBufferState();
@@ -1665,6 +1736,7 @@ export class ProviderCliAdapter implements CliAdapter {
1665
1736
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1666
1737
  recentOutputBuffer: this.recentOutputBuffer,
1667
1738
  terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
1739
+ workingDir: this.workingDir,
1668
1740
  baseMessages: [],
1669
1741
  partialResponse: this.responseBuffer,
1670
1742
  isWaitingForResponse: this.isWaitingForResponse,
@@ -1979,7 +2051,10 @@ export class ProviderCliAdapter implements CliAdapter {
1979
2051
  }
1980
2052
  }
1981
2053
  if (this.isWaitingForResponse && !allowInputDuringGeneration) {
1982
- if (!this.clearStaleIdleResponseGuard('send_message_guard')) {
2054
+ if (
2055
+ !this.clearStaleIdleResponseGuard('send_message_guard')
2056
+ && !this.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend)
2057
+ ) {
1983
2058
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1984
2059
  }
1985
2060
  }
@@ -2369,10 +2444,23 @@ export class ProviderCliAdapter implements CliAdapter {
2369
2444
  getDebugState(): Record<string, any> {
2370
2445
  const screenText = sanitizeTerminalText(this.terminalScreen.getText());
2371
2446
  const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2372
- const effectiveStatus = this.projectEffectiveStatus(startupModal);
2373
- const effectiveReady = this.ready || !!startupModal;
2447
+ const startupDetectedStatus = this.startupParseGate && !startupModal
2448
+ ? this.runDetectStatus(this.recentOutputBuffer || screenText)
2449
+ : null;
2450
+ const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
2374
2451
  const parsedDebugState = this.getParsedDebugState();
2375
2452
  const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
2453
+ let effectiveStatus = this.projectEffectiveStatus(startupModal);
2454
+ if (startupDetectedStatus === 'waiting_approval') {
2455
+ effectiveStatus = 'waiting_approval';
2456
+ }
2457
+ if (
2458
+ effectiveStatus === 'idle'
2459
+ && parsedDebugState?.status === 'generating'
2460
+ && !this.parsedStatusHasFinalAssistantMessage(parsedDebugState)
2461
+ ) {
2462
+ effectiveStatus = 'generating';
2463
+ }
2376
2464
  return {
2377
2465
  type: this.cliType,
2378
2466
  name: this.cliName,
@@ -15,6 +15,7 @@ export declare function buildCliParseInput(options: {
15
15
  accumulatedRawBuffer: string;
16
16
  recentOutputBuffer: string;
17
17
  terminalScreenText: string;
18
+ workingDir?: string;
18
19
  baseMessages: CliChatMessage[];
19
20
  partialResponse: string;
20
21
  isWaitingForResponse?: boolean;
@@ -35,6 +35,7 @@ export function buildCliParseInput(options: {
35
35
  accumulatedRawBuffer: string;
36
36
  recentOutputBuffer: string;
37
37
  terminalScreenText: string;
38
+ workingDir?: string;
38
39
  baseMessages: CliChatMessage[];
39
40
  partialResponse: string;
40
41
  isWaitingForResponse?: boolean;
@@ -46,6 +47,7 @@ export function buildCliParseInput(options: {
46
47
  accumulatedRawBuffer,
47
48
  recentOutputBuffer,
48
49
  terminalScreenText,
50
+ workingDir,
49
51
  baseMessages,
50
52
  partialResponse,
51
53
  isWaitingForResponse,
@@ -66,6 +68,8 @@ export function buildCliParseInput(options: {
66
68
  rawBuffer,
67
69
  recentBuffer,
68
70
  screenText,
71
+ workspace: workingDir,
72
+ workingDir,
69
73
  screen: buildCliScreenSnapshot(screenText),
70
74
  bufferScreen: buildCliScreenSnapshot(buffer),
71
75
  recentScreen: buildCliScreenSnapshot(recentBuffer),
@@ -36,7 +36,9 @@ export function resolveCliSpawnPlan(options: {
36
36
  : spawnConfig.command;
37
37
  const binaryPath = findBinary(configuredCommand);
38
38
  const isWin = os.platform() === 'win32';
39
- const allArgs = [...spawnConfig.args, ...extraArgs];
39
+ const allArgs = [...spawnConfig.args, ...extraArgs].map((arg) =>
40
+ typeof arg === 'string' ? arg.replace(/\{\{workingDir\}\}/g, workingDir) : arg,
41
+ );
40
42
 
41
43
  let shellCmd: string;
42
44
  let shellArgs: string[];
@@ -55,6 +55,8 @@ export interface CliScriptInput {
55
55
  rawBuffer: string;
56
56
  recentBuffer: string;
57
57
  screenText: string;
58
+ workspace?: string;
59
+ workingDir?: string;
58
60
  screen: CliScreenSnapshot;
59
61
  bufferScreen: CliScreenSnapshot;
60
62
  recentScreen: CliScreenSnapshot;
@@ -94,6 +94,8 @@ export interface CliScriptInput {
94
94
  rawBuffer: string;
95
95
  recentBuffer: string;
96
96
  screenText: string;
97
+ workspace?: string;
98
+ workingDir?: string;
97
99
  screen: CliScreenSnapshot;
98
100
  bufferScreen: CliScreenSnapshot;
99
101
  recentScreen: CliScreenSnapshot;
@@ -484,17 +486,25 @@ export function findBinary(name: string): string {
484
486
  return path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
485
487
  }
486
488
  const isWin = os.platform() === 'win32';
487
- try {
488
- const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
489
- return execSync(cmd, {
490
- encoding: 'utf-8',
491
- timeout: 5000,
492
- stdio: ['pipe', 'pipe', 'pipe'],
493
- ...(isWin ? { windowsHide: true } : {}),
494
- }).trim().split('\n')[0].trim();
495
- } catch {
496
- return isWin ? `${trimmed}.cmd` : trimmed;
489
+ const paths = (process.env.PATH || '').split(path.delimiter);
490
+ const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
491
+
492
+ for (const p of paths) {
493
+ if (!p) continue;
494
+ for (const ext of exes) {
495
+ const fullPath = path.join(p, trimmed + ext);
496
+ try {
497
+ const fs = require('fs');
498
+ if (fs.existsSync(fullPath)) {
499
+ const stat = fs.statSync(fullPath);
500
+ if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
501
+ return fullPath;
502
+ }
503
+ }
504
+ } catch { }
505
+ }
497
506
  }
507
+ return isWin ? `${trimmed}.cmd` : trimmed;
498
508
  }
499
509
 
500
510
  export function isScriptBinary(binaryPath: string): boolean {
@@ -24,6 +24,8 @@ import { filterUserFacingChatMessages, normalizeChatMessages } from '../provider
24
24
  const RECENT_SEND_WINDOW_MS = 1200;
25
25
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
26
26
  const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
27
+ const CLI_NATIVE_HISTORY_FRESH_MS = 5 * 60_000;
28
+ const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli']);
27
29
  const recentSendByTarget = new Map<string, number>();
28
30
 
29
31
  interface ApprovalSelectableInstance extends ProviderInstance {
@@ -151,7 +153,17 @@ function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
151
153
  const instance = h.ctx.instanceManager?.getInstance(targetSessionId);
152
154
  const state = instance?.getState?.();
153
155
  const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
154
- return providerSessionId || targetSessionId;
156
+ if (providerSessionId) return providerSessionId;
157
+
158
+ const currentSession = h.currentSession as any;
159
+ if (currentSession?.sessionId === targetSessionId) {
160
+ const currentProviderSessionId = typeof currentSession.providerSessionId === 'string'
161
+ ? currentSession.providerSessionId.trim()
162
+ : '';
163
+ if (currentProviderSessionId) return currentProviderSessionId;
164
+ }
165
+
166
+ return targetSessionId;
155
167
  }
156
168
 
157
169
  function getInteractionId(args: any): string | undefined {
@@ -221,6 +233,116 @@ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[]
221
233
  return normalizeChatMessages(messages);
222
234
  }
223
235
 
236
+ function getMessageNewestReceivedAt(messages: Array<{ receivedAt?: unknown; timestamp?: unknown }>): number {
237
+ let newest = 0;
238
+ for (const message of messages) {
239
+ const receivedAt = Number(message?.receivedAt ?? message?.timestamp ?? 0);
240
+ if (Number.isFinite(receivedAt) && receivedAt > newest) newest = receivedAt;
241
+ }
242
+ return newest;
243
+ }
244
+
245
+ function buildCliMessageSourceProvenance(args: {
246
+ selected: 'native-history' | 'pty-parser';
247
+ provider: string;
248
+ nativeHandle?: string;
249
+ fallbackReason?: string;
250
+ nativeSource?: string;
251
+ sourcePath?: string;
252
+ sourceMtimeMs?: number;
253
+ nativeMessages?: ChatMessage[];
254
+ ptyMessages?: ChatMessage[];
255
+ returnedMessages?: ChatMessage[];
256
+ safeMapping?: boolean;
257
+ freshEnough?: boolean;
258
+ ptyStatusApprovalOnly?: boolean;
259
+ }): Record<string, unknown> {
260
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
261
+ const sourceMtimeAgeMs = sourceMtimeMs > 0 ? Math.max(0, Date.now() - sourceMtimeMs) : undefined;
262
+ const nativeMessages = args.nativeMessages || [];
263
+ const ptyMessages = args.ptyMessages || [];
264
+ const returnedMessages = args.returnedMessages || [];
265
+ return {
266
+ selected: args.selected,
267
+ provider: args.provider,
268
+ providerType: args.provider,
269
+ ...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
270
+ ...(args.nativeHandle ? { nativeSessionId: args.nativeHandle } : {}),
271
+ ...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
272
+ ...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
273
+ ...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
274
+ ptyStatusApprovalOnly: args.ptyStatusApprovalOnly === true,
275
+ staleness: {
276
+ sourceMtimeMs: sourceMtimeMs || undefined,
277
+ sourceMtimeAgeMs,
278
+ nativeNewestMessageAt: getMessageNewestReceivedAt(nativeMessages),
279
+ ptyNewestMessageAt: getMessageNewestReceivedAt(ptyMessages),
280
+ freshEnough: args.freshEnough === true,
281
+ },
282
+ coverage: {
283
+ nativeMessageCount: nativeMessages.length,
284
+ ptyMessageCount: ptyMessages.length,
285
+ returnedMessageCount: returnedMessages.length,
286
+ safeMapping: args.safeMapping === true,
287
+ },
288
+ };
289
+ }
290
+
291
+ function buildNativeHistoryFallbackReason(args: {
292
+ providerType: string;
293
+ nativeSource?: string;
294
+ nativeMessageCount: number;
295
+ safeMapping: boolean;
296
+ freshEnough: boolean;
297
+ }): string {
298
+ if (!supportsCliNativeTranscript(args.providerType)) return 'provider_native_transcript_not_supported';
299
+ if (args.nativeSource === 'native-unavailable') return 'native_history_unavailable';
300
+ if (args.nativeSource && args.nativeSource !== 'provider-native') return `native_history_source_${args.nativeSource}`;
301
+ if (args.nativeMessageCount <= 0) return 'native_history_empty';
302
+ if (!args.safeMapping) return 'native_history_not_safely_mapped';
303
+ if (!args.freshEnough) return 'native_history_stale';
304
+ return 'native_history_not_selected';
305
+ }
306
+
307
+ function supportsCliNativeTranscript(providerType: string): boolean {
308
+ return CLI_NATIVE_TRANSCRIPT_PROVIDERS.has(providerType);
309
+ }
310
+
311
+ function hasSafeNativeHistoryMapping(args: {
312
+ historySessionId?: string;
313
+ providerSessionId?: string;
314
+ workspace?: string;
315
+ nativeMessages: ChatMessage[];
316
+ }): boolean {
317
+ const explicitSessionId = String(args.historySessionId || args.providerSessionId || '').trim();
318
+ if (explicitSessionId) {
319
+ const messageSessionIds = args.nativeMessages
320
+ .map((message: any) => typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '')
321
+ .filter(Boolean);
322
+ if (messageSessionIds.length === 0) return true;
323
+ return messageSessionIds.some((id) => id === explicitSessionId);
324
+ }
325
+ const workspace = String(args.workspace || '').trim();
326
+ if (!workspace) return false;
327
+ return args.nativeMessages.some((message: any) => String(message?.workspace || '').trim() === workspace);
328
+ }
329
+
330
+ function isNativeHistoryFreshEnough(args: {
331
+ sourceMtimeMs?: number;
332
+ nativeMessages: ChatMessage[];
333
+ ptyMessages: ChatMessage[];
334
+ }): boolean {
335
+ const nativeNewest = getMessageNewestReceivedAt(args.nativeMessages);
336
+ const ptyNewest = getMessageNewestReceivedAt(args.ptyMessages);
337
+ if (nativeNewest > 0 && nativeNewest >= ptyNewest) return true;
338
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
339
+ if (sourceMtimeMs > 0 && Date.now() - sourceMtimeMs <= CLI_NATIVE_HISTORY_FRESH_MS) return true;
340
+ return ptyNewest === 0 && nativeNewest > 0;
341
+ }
342
+
343
+ function shouldPreserveReadChatPayloadField(key: string): boolean {
344
+ return key === 'messageSource' || key === 'transcriptProvenance';
345
+ }
224
346
 
225
347
  function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
226
348
  const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
@@ -281,7 +403,7 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
281
403
  }
282
404
  switch (raw) {
283
405
  case 'starting':
284
- return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'generating';
406
+ return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'starting';
285
407
  case 'stopped':
286
408
  case 'disconnected':
287
409
  case 'not_monitored':
@@ -304,7 +426,18 @@ function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal:
304
426
  return true;
305
427
  }
306
428
 
307
- function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): string {
429
+ function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any, parsedMessages?: unknown[]): string {
430
+ const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
431
+ if (adapterRawStatus === 'starting'
432
+ && isGeneratingLikeStatus(parsedStatus)
433
+ && !hasNonEmptyModalButtons(activeModal)
434
+ && Array.isArray(parsedMessages)
435
+ && parsedMessages.length === 0
436
+ && Array.isArray(adapterStatus?.messages)
437
+ && adapterStatus.messages.length === 0
438
+ && !(typeof adapter.isProcessing === 'function' && adapter.isProcessing())) {
439
+ return 'starting';
440
+ }
308
441
  if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
309
442
  return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
310
443
  }
@@ -356,6 +489,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
356
489
  return {
357
490
  success: true,
358
491
  ...validatedPayload,
492
+ ...Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key))),
359
493
  messages: sync.messages,
360
494
  totalMessages: sync.totalMessages,
361
495
  ...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
@@ -584,6 +718,8 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
584
718
  adapterStatus: debugReadChat.adapterStatus,
585
719
  parsedStatus: debugReadChat.parsedStatus,
586
720
  returnedStatus: debugReadChat.returnedStatus,
721
+ selectedMessageSource: debugReadChat.selectedMessageSource,
722
+ messageSource: debugReadChat.messageSource,
587
723
  parsedMsgCount: debugReadChat.parsedMsgCount,
588
724
  returnedMsgCount: debugReadChat.returnedMsgCount,
589
725
  shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
@@ -646,6 +782,8 @@ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Pr
646
782
  providerSessionId: readResult.providerSessionId,
647
783
  transcriptAuthority: readResult.transcriptAuthority,
648
784
  coverage: readResult.coverage,
785
+ messageSource: readResult.messageSource,
786
+ transcriptProvenance: readResult.transcriptProvenance,
649
787
  activeModal: readResult.activeModal,
650
788
  messagesTail: Array.isArray(readResult.messages) ? readResult.messages.slice(-20) : [],
651
789
  debugReadChat: readResult.debugReadChat,
@@ -874,7 +1012,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
874
1012
  ? parsedRecord.coverage
875
1013
  : undefined;
876
1014
  const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
877
- const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus);
1015
+ const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus, parsedRecord.messages);
878
1016
  const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
879
1017
  const parsedMessages = finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus);
880
1018
  const returnedMessages = runtimeMessageMerger?.category === 'cli'
@@ -882,25 +1020,146 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
882
1020
  && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
883
1021
  ? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
884
1022
  : parsedMessages;
1023
+ const providerType = provider?.type || adapter.cliType;
1024
+ let selectedMessages = returnedMessages;
1025
+ let selectedTitle = title;
1026
+ let selectedProviderSessionId = providerSessionId;
1027
+ let selectedTranscriptAuthority = transcriptAuthority;
1028
+ let selectedCoverage = coverage;
1029
+ let messageSource = buildCliMessageSourceProvenance({
1030
+ selected: 'pty-parser',
1031
+ provider: adapter.cliType,
1032
+ fallbackReason: supportsCliNativeTranscript(providerType) ? 'native_history_not_checked' : 'provider_native_transcript_not_supported',
1033
+ ptyMessages: returnedMessages,
1034
+ returnedMessages,
1035
+ ptyStatusApprovalOnly: false,
1036
+ });
1037
+
1038
+ if (supportsCliNativeTranscript(providerType)) {
1039
+ const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
1040
+ const workspace = typeof args?.workspace === 'string'
1041
+ ? args.workspace
1042
+ : typeof (h.currentSession as any)?.workspace === 'string'
1043
+ ? (h.currentSession as any).workspace
1044
+ : typeof adapter.workingDir === 'string'
1045
+ ? adapter.workingDir
1046
+ : undefined;
1047
+ const nativeHistoryLimit = Math.max(
1048
+ normalizeReadChatTailLimit(args) || 0,
1049
+ returnedMessages.length,
1050
+ 200,
1051
+ );
1052
+ let nativeHistory: ReturnType<typeof readProviderChatHistory> | null = null;
1053
+ try {
1054
+ nativeHistory = readProviderChatHistory(agentStr, {
1055
+ canonicalHistory: provider?.canonicalHistory,
1056
+ historySessionId,
1057
+ workspace,
1058
+ offset: 0,
1059
+ limit: nativeHistoryLimit,
1060
+ excludeRecentCount: 0,
1061
+ historyBehavior: provider?.historyBehavior,
1062
+ scripts: provider?.scripts as any,
1063
+ });
1064
+ } catch (error: any) {
1065
+ const fallbackReason = `native_history_error:${error?.message || String(error)}`;
1066
+ messageSource = buildCliMessageSourceProvenance({
1067
+ selected: 'pty-parser',
1068
+ provider: adapter.cliType,
1069
+ fallbackReason,
1070
+ ptyMessages: returnedMessages,
1071
+ returnedMessages,
1072
+ ptyStatusApprovalOnly: false,
1073
+ });
1074
+ nativeHistory = null;
1075
+ }
1076
+
1077
+ if (nativeHistory) {
1078
+ const nativeMessages = Array.isArray((nativeHistory as any).messages)
1079
+ ? normalizeChatMessages((nativeHistory as any).messages as ChatMessage[])
1080
+ : [];
1081
+ const historyProviderSessionId = typeof (nativeHistory as any)?.providerSessionId === 'string'
1082
+ ? (nativeHistory as any).providerSessionId
1083
+ : historySessionId;
1084
+ const safeMapping = hasSafeNativeHistoryMapping({
1085
+ historySessionId,
1086
+ providerSessionId,
1087
+ workspace,
1088
+ nativeMessages,
1089
+ });
1090
+ const freshEnough = isNativeHistoryFreshEnough({
1091
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1092
+ nativeMessages,
1093
+ ptyMessages: returnedMessages,
1094
+ });
1095
+ if ((nativeHistory as any).source === 'provider-native' && nativeMessages.length > 0 && safeMapping && freshEnough) {
1096
+ selectedMessages = finalizeStreamingMessagesWhenIdle(nativeMessages, returnedStatus);
1097
+ selectedProviderSessionId = historyProviderSessionId || providerSessionId;
1098
+ selectedTranscriptAuthority = 'provider';
1099
+ selectedCoverage = (nativeHistory as any).hasMore ? 'tail' : 'full';
1100
+ messageSource = buildCliMessageSourceProvenance({
1101
+ selected: 'native-history',
1102
+ provider: adapter.cliType,
1103
+ nativeHandle: selectedProviderSessionId || historySessionId,
1104
+ nativeSource: (nativeHistory as any).source,
1105
+ sourcePath: (nativeHistory as any).sourcePath,
1106
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1107
+ nativeMessages,
1108
+ ptyMessages: returnedMessages,
1109
+ returnedMessages: selectedMessages,
1110
+ safeMapping,
1111
+ freshEnough,
1112
+ ptyStatusApprovalOnly: true,
1113
+ });
1114
+ } else {
1115
+ const fallbackReason = buildNativeHistoryFallbackReason({
1116
+ providerType,
1117
+ nativeSource: (nativeHistory as any).source,
1118
+ nativeMessageCount: nativeMessages.length,
1119
+ safeMapping,
1120
+ freshEnough,
1121
+ });
1122
+ messageSource = buildCliMessageSourceProvenance({
1123
+ selected: 'pty-parser',
1124
+ provider: adapter.cliType,
1125
+ nativeHandle: historyProviderSessionId || historySessionId,
1126
+ fallbackReason,
1127
+ nativeSource: (nativeHistory as any).source,
1128
+ sourcePath: (nativeHistory as any).sourcePath,
1129
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1130
+ nativeMessages,
1131
+ ptyMessages: returnedMessages,
1132
+ returnedMessages,
1133
+ safeMapping,
1134
+ freshEnough,
1135
+ ptyStatusApprovalOnly: false,
1136
+ });
1137
+ }
1138
+ }
1139
+ }
885
1140
  LOG.debug('Command', `[read_chat] cli-like parsed provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord.status || '')} parsedMsgCount=${parsedRecord.messages.length} returnedMsgCount=${returnedMessages.length}`);
886
1141
  return buildReadChatCommandResult({
887
- messages: returnedMessages,
1142
+ messages: selectedMessages,
888
1143
  status: returnedStatus,
889
1144
  activeModal,
1145
+ messageSource,
1146
+ transcriptProvenance: messageSource,
890
1147
  debugReadChat: {
891
1148
  provider: adapter.cliType,
892
1149
  targetSessionId: String(args?.targetSessionId || ''),
893
1150
  adapterStatus: String(adapterStatus.status || ''),
894
1151
  parsedStatus: String(parsedRecord.status || ''),
895
1152
  returnedStatus: String(returnedStatus || ''),
896
- shouldPreferAdapterMessages: false,
1153
+ selectedMessageSource: (messageSource as any).selected,
1154
+ messageSource,
1155
+ shouldPreferAdapterMessages: supportsCliNativeTranscript(providerType) && (messageSource as any).selected !== 'native-history',
897
1156
  parsedMsgCount: parsedRecord.messages.length,
898
- returnedMsgCount: returnedMessages.length,
1157
+ returnedMsgCount: selectedMessages.length,
899
1158
  },
900
- ...(title ? { title } : {}),
901
- ...(providerSessionId ? { providerSessionId } : {}),
902
- ...(transcriptAuthority ? { transcriptAuthority } : {}),
903
- ...(coverage ? { coverage } : {}),
1159
+ ...(selectedTitle ? { title: selectedTitle } : {}),
1160
+ ...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
1161
+ ...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
1162
+ ...(selectedCoverage ? { coverage: selectedCoverage } : {}),
904
1163
  }, args);
905
1164
  }
906
1165
  const historyLimit = normalizeReadChatTailLimit(args);
@@ -924,9 +1183,48 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
924
1183
  const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
925
1184
  ? (history as any).providerSessionId
926
1185
  : historySessionId;
1186
+ const historyMessages = Array.isArray((history as any)?.messages)
1187
+ ? normalizeChatMessages((history as any).messages as ChatMessage[])
1188
+ : [];
1189
+ const safeMapping = supportsCliNativeTranscript(agentStr)
1190
+ ? hasSafeNativeHistoryMapping({
1191
+ historySessionId,
1192
+ providerSessionId: historyProviderSessionId,
1193
+ workspace,
1194
+ nativeMessages: historyMessages,
1195
+ })
1196
+ : false;
1197
+ const nativeSelected = supportsCliNativeTranscript(agentStr)
1198
+ && (history as any).source === 'provider-native'
1199
+ && historyMessages.length > 0
1200
+ && safeMapping;
1201
+ const messageSource = buildCliMessageSourceProvenance({
1202
+ selected: nativeSelected ? 'native-history' : 'pty-parser',
1203
+ provider: agentStr,
1204
+ nativeHandle: historyProviderSessionId || historySessionId,
1205
+ fallbackReason: nativeSelected
1206
+ ? undefined
1207
+ : buildNativeHistoryFallbackReason({
1208
+ providerType: agentStr,
1209
+ nativeSource: (history as any).source,
1210
+ nativeMessageCount: historyMessages.length,
1211
+ safeMapping,
1212
+ freshEnough: true,
1213
+ }),
1214
+ nativeSource: (history as any).source,
1215
+ sourcePath: (history as any).sourcePath,
1216
+ sourceMtimeMs: (history as any).sourceMtimeMs,
1217
+ nativeMessages: historyMessages,
1218
+ returnedMessages: historyMessages,
1219
+ safeMapping,
1220
+ freshEnough: true,
1221
+ ptyStatusApprovalOnly: false,
1222
+ });
927
1223
  return buildReadChatCommandResult({
928
- messages: Array.isArray((history as any)?.messages) ? (history as any).messages : [],
1224
+ messages: historyMessages,
929
1225
  status: 'idle',
1226
+ messageSource,
1227
+ transcriptProvenance: messageSource,
930
1228
  ...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
931
1229
  ...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
932
1230
  ...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')