@adhdev/daemon-core 0.9.82-rc.10 → 0.9.82-rc.101

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 (67) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +13 -0
  2. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +10 -0
  4. package/dist/commands/router.d.ts +22 -0
  5. package/dist/config/mesh-config.d.ts +66 -1
  6. package/dist/index.d.ts +12 -5
  7. package/dist/index.js +5793 -1219
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +5759 -1206
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/installer.d.ts +1 -4
  12. package/dist/launch.d.ts +1 -1
  13. package/dist/logging/async-batch-writer.d.ts +10 -0
  14. package/dist/mesh/beads-db.d.ts +18 -0
  15. package/dist/mesh/mesh-active-work.d.ts +60 -0
  16. package/dist/mesh/mesh-events.d.ts +26 -5
  17. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  18. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  19. package/dist/mesh/mesh-ledger.d.ts +38 -1
  20. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  21. package/dist/mesh/refine-config.d.ts +176 -0
  22. package/dist/providers/chat-message-normalization.d.ts +1 -0
  23. package/dist/providers/cli-provider-instance.d.ts +2 -1
  24. package/dist/repo-mesh-types.d.ts +46 -0
  25. package/dist/status/reporter.d.ts +2 -0
  26. package/package.json +3 -1
  27. package/src/boot/daemon-lifecycle.ts +1 -0
  28. package/src/cli-adapters/provider-cli-adapter.ts +319 -14
  29. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  30. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  31. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  32. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  33. package/src/cli-adapters/provider-cli-shared.ts +28 -10
  34. package/src/commands/chat-commands.ts +570 -20
  35. package/src/commands/cli-manager.ts +129 -1
  36. package/src/commands/handler.ts +8 -1
  37. package/src/commands/mesh-coordinator.ts +13 -143
  38. package/src/commands/router.ts +2820 -437
  39. package/src/config/chat-history.ts +9 -7
  40. package/src/config/mesh-config.ts +245 -1
  41. package/src/daemon/dev-cli-debug.ts +10 -1
  42. package/src/detection/ide-detector.ts +26 -16
  43. package/src/index.ts +30 -4
  44. package/src/installer.d.ts +1 -1
  45. package/src/installer.ts +8 -6
  46. package/src/launch.d.ts +1 -1
  47. package/src/launch.ts +37 -28
  48. package/src/logging/async-batch-writer.ts +55 -0
  49. package/src/logging/logger.ts +2 -1
  50. package/src/mesh/beads-db.ts +176 -0
  51. package/src/mesh/coordinator-prompt.ts +31 -8
  52. package/src/mesh/mesh-active-work.ts +255 -0
  53. package/src/mesh/mesh-events.ts +389 -47
  54. package/src/mesh/mesh-fast-forward.ts +430 -0
  55. package/src/mesh/mesh-host-ownership.ts +73 -0
  56. package/src/mesh/mesh-ledger.ts +138 -1
  57. package/src/mesh/mesh-work-queue.ts +199 -137
  58. package/src/mesh/refine-config.ts +356 -0
  59. package/src/providers/chat-message-normalization.ts +7 -12
  60. package/src/providers/cli-provider-instance.ts +102 -17
  61. package/src/providers/ide-provider-instance.ts +17 -3
  62. package/src/providers/provider-loader.ts +10 -4
  63. package/src/providers/read-chat-contract.ts +1 -1
  64. package/src/providers/version-archive.ts +38 -20
  65. package/src/repo-mesh-types.ts +51 -0
  66. package/src/status/reporter.ts +15 -0
  67. package/src/system/host-memory.ts +29 -12
@@ -19,10 +19,37 @@ export interface RepoMesh {
19
19
  defaultBranch?: string;
20
20
  policy: RepoMeshPolicy;
21
21
  coordinator: RepoMeshCoordinatorConfig;
22
+ meshHost?: RepoMeshHostMetadata;
22
23
  projectContext: ProjectContextSnapshot;
23
24
  nodes: RepoMeshNode[];
24
25
  status: 'active' | 'archived' | 'deleted';
25
26
  }
27
+ export type RepoMeshDaemonRole = 'host' | 'member';
28
+ export interface RepoMeshHostPairingMetadata {
29
+ status: 'not_configured' | 'pairing' | 'paired' | 'rejected' | 'revoked';
30
+ tokenId?: string;
31
+ joinedAt?: string;
32
+ lastPairedAt?: string;
33
+ lastRejectedAt?: string;
34
+ expiresAt?: string;
35
+ }
36
+ export interface RepoMeshHostMetadata {
37
+ /** Local daemon role for this mesh. Missing metadata defaults to host for standalone compatibility. */
38
+ role: RepoMeshDaemonRole;
39
+ /** Daemon that owns mesh truth/status/git/queue/session/ledger/coordinator ownership. */
40
+ hostDaemonId?: string;
41
+ /** Mesh node that represents the host daemon, when known. */
42
+ hostNodeId?: string;
43
+ /** Future standalone manual pairing endpoint entered by member daemons. */
44
+ hostAddress?: string;
45
+ /** Redacted pairing state only; raw join tokens must not be persisted here. */
46
+ pairing?: RepoMeshHostPairingMetadata;
47
+ }
48
+ export interface RepoMeshHostStatus extends RepoMeshHostMetadata {
49
+ canOwnCoordinator: boolean;
50
+ canOwnQueue: boolean;
51
+ defaulted: boolean;
52
+ }
26
53
  export interface RepoMeshNode {
27
54
  id: string;
28
55
  daemonId: string;
@@ -37,6 +64,7 @@ export interface RepoMeshNode {
37
64
  effectiveCapabilities: RepoMeshNodeCapabilities;
38
65
  policy: RepoMeshNodePolicy;
39
66
  health: RepoMeshNodeHealth;
67
+ role?: RepoMeshDaemonRole;
40
68
  status: 'enabled' | 'disabled' | 'removed';
41
69
  }
42
70
  export type RepoMeshNodeHealth = 'online' | 'offline' | 'degraded' | 'dirty' | 'wrong_branch' | 'unknown';
@@ -46,6 +74,13 @@ export interface RepoMeshPolicy {
46
74
  requirePreTaskCheckpoint: boolean;
47
75
  requirePostTaskCheckpoint: boolean;
48
76
  requireApprovalForPush: boolean;
77
+ /**
78
+ * Narrow Refinery opt-in: when validation and patch-equivalence have passed,
79
+ * allow Refinery to publish submodule gitlink commits to each submodule's
80
+ * configured remote main branch with a non-force push, then verify reachability.
81
+ * Defaults to false; root branch pushes/merges are not affected.
82
+ */
83
+ allowAutoPublishSubmoduleMainCommits?: boolean;
49
84
  requireApprovalForDestructiveGit: boolean;
50
85
  dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
51
86
  maxParallelTasks: number;
@@ -185,6 +220,7 @@ export interface LocalMeshEntry {
185
220
  defaultBranch?: string;
186
221
  policy: RepoMeshPolicy;
187
222
  coordinator: RepoMeshCoordinatorConfig;
223
+ meshHost?: RepoMeshHostMetadata;
188
224
  nodes: LocalMeshNodeEntry[];
189
225
  createdAt: string;
190
226
  updatedAt: string;
@@ -206,12 +242,15 @@ export interface LocalMeshNodeEntry {
206
242
  clonedFromNodeId?: string;
207
243
  /** Optional associated/external repos configured as node metadata. */
208
244
  relatedRepos?: RepoMeshRelatedRepo[];
245
+ role?: RepoMeshDaemonRole;
209
246
  }
210
247
  export interface RepoMeshStatus {
211
248
  meshId: string;
212
249
  meshName: string;
213
250
  repoIdentity: string;
251
+ defaultBranch?: string;
214
252
  refreshedAt: string;
253
+ meshHost?: RepoMeshHostStatus;
215
254
  nodes: RepoMeshNodeStatus[];
216
255
  queue?: RepoMeshQueueStatus;
217
256
  ledger?: RepoMeshLedgerStatus;
@@ -248,11 +287,18 @@ export interface RepoMeshNodeStatus {
248
287
  repoRoot?: string;
249
288
  daemonId?: string;
250
289
  machineId?: string;
290
+ role?: RepoMeshDaemonRole;
251
291
  machineStatus?: string;
252
292
  isLocalWorktree?: boolean;
253
293
  worktreeBranch?: string;
254
294
  health: RepoMeshNodeHealth;
255
295
  git?: GitRepoStatus;
296
+ /**
297
+ * True when the selected coordinator has evidence that a peer git probe is still
298
+ * in flight or just timed out during initial mesh handshake, so callers should
299
+ * treat missing git data as pending instead of authoritative absence.
300
+ */
301
+ gitProbePending?: boolean;
256
302
  providers: string[];
257
303
  activeSessions: string[];
258
304
  activeSessionDetails?: RepoMeshSessionStatus[];
@@ -46,6 +46,8 @@ export declare class DaemonStatusReporter {
46
46
  private lastStatusSentAt;
47
47
  private statusPendingThrottle;
48
48
  private lastP2PStatusHash;
49
+ private lastP2PStatusSentAt;
50
+ private p2pDebounceTimer;
49
51
  private lastServerStatusHash;
50
52
  private lastStatusSummary;
51
53
  private statusTimer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.10",
3
+ "version": "0.9.82-rc.101",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,6 +49,7 @@
49
49
  "@adhdev/session-host-core": "*",
50
50
  "@agentclientprotocol/sdk": "^0.16.1",
51
51
  "@xterm/xterm": "^6.0.0",
52
+ "better-sqlite3": "^12.10.0",
52
53
  "chalk": "^5.3.0",
53
54
  "chokidar": "^4.0.3",
54
55
  "conf": "^13.0.0",
@@ -60,6 +61,7 @@
60
61
  "@adhdev/ghostty-vt-node": "*"
61
62
  },
62
63
  "devDependencies": {
64
+ "@types/better-sqlite3": "^7.6.13",
63
65
  "@types/js-yaml": "^4.0.9",
64
66
  "@types/node": "^22.0.0",
65
67
  "@types/ws": "^8.18.1",
@@ -309,6 +309,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
309
309
  statusInstanceId: config.statusInstanceId,
310
310
  statusVersion: config.statusVersion,
311
311
  getMeshPeerConnectionStatus: config.getMeshPeerConnectionStatus,
312
+ dispatchMeshCommand: config.dispatchMeshCommand,
312
313
  getCdpLogFn: config.getCdpLogFn || ((ideType: string) => LOG.forComponent(`CDP:${ideType}`).asLogFn()),
313
314
  });
314
315
 
@@ -110,6 +110,14 @@ interface SendMessageCompletion {
110
110
  rejectOnce: (error: unknown) => void;
111
111
  }
112
112
 
113
+ interface PendingOutboundMessage {
114
+ id: string;
115
+ role: 'user';
116
+ content: string;
117
+ queuedAt: number;
118
+ source: 'sendMessage';
119
+ }
120
+
113
121
  export function appendBoundedText(current: string, chunk: string, maxChars: number): string {
114
122
  if (!chunk) return current.length <= maxChars ? current : current.slice(-maxChars);
115
123
  if (maxChars <= 0) return '';
@@ -186,6 +194,9 @@ export class ProviderCliAdapter implements CliAdapter {
186
194
  private idleFinishCandidate: IdleFinishCandidate | null = null;
187
195
  private finishRetryTimer: NodeJS.Timeout | null = null;
188
196
  private finishRetryCount = 0;
197
+ private pendingOutboundQueue: PendingOutboundMessage[] = [];
198
+ private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
199
+ private pendingOutboundFlushInFlight = false;
189
200
 
190
201
  // Resize redraw suppression
191
202
  private resizeSuppressUntil: number = 0;
@@ -761,6 +772,17 @@ export class ProviderCliAdapter implements CliAdapter {
761
772
  if (stableMs < 2000) return;
762
773
 
763
774
  const startupModal = this.runParseApproval(this.recentOutputBuffer);
775
+ const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
776
+ if (!startupModal && startupStatus !== 'idle') {
777
+ this.recordTrace('startup_settle_deferred', {
778
+ trigger,
779
+ startupStatus,
780
+ stableMs,
781
+ screenText: summarizeCliTraceText(screenText, 500),
782
+ });
783
+ this.scheduleStartupSettleCheck();
784
+ return;
785
+ }
764
786
  this.startupParseGate = false;
765
787
  if (this.startupSettleTimer) {
766
788
  clearTimeout(this.startupSettleTimer);
@@ -956,6 +978,38 @@ export class ProviderCliAdapter implements CliAdapter {
956
978
  return true;
957
979
  }
958
980
 
981
+ private clearParsedIdleResponseGuard(reason: string, parsedStatus: any): boolean {
982
+ const parsedRawStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status.trim() : '';
983
+ const parsedModal = parsedStatus?.activeModal ?? parsedStatus?.modal ?? null;
984
+ const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
985
+ if (
986
+ !this.isWaitingForResponse
987
+ || parsedRawStatus !== 'idle'
988
+ || !!parsedModal
989
+ || !!blockingModal
990
+ || !this.parsedStatusHasFinalAssistantMessage(parsedStatus)
991
+ ) {
992
+ return false;
993
+ }
994
+ this.clearAllTimers();
995
+ this.clearIdleFinishCandidate(reason);
996
+ this.responseBuffer = '';
997
+ this.isWaitingForResponse = false;
998
+ this.responseSettleIgnoreUntil = 0;
999
+ this.submitRetryUsed = false;
1000
+ this.submitRetryPromptSnippet = '';
1001
+ this.finishRetryCount = 0;
1002
+ this.currentTurnScope = null;
1003
+ this.activeModal = null;
1004
+ this.setStatus('idle', reason);
1005
+ this.recordTrace('parsed_idle_response_cleared', {
1006
+ reason,
1007
+ parsedStatus: parsedRawStatus,
1008
+ parsedMessageCount: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages.length : 0,
1009
+ });
1010
+ return true;
1011
+ }
1012
+
959
1013
  private hasMeaningfulResponseBuffer(promptSnippet: string): boolean {
960
1014
  const raw = String(this.responseBuffer || '').trim();
961
1015
  if (!raw) return false;
@@ -1318,6 +1372,11 @@ export class ProviderCliAdapter implements CliAdapter {
1318
1372
  this.idleTimeout = setTimeout(() => {
1319
1373
  if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1320
1374
  if (this.shouldDeferIdleTimeoutFinish()) return;
1375
+ const parsed = this.runParseSession();
1376
+ if (this.shouldKeepCodexTurnOpenForFinish(parsed)) {
1377
+ this.rescheduleCodexFinishCheck('codex_idle_timeout_not_final');
1378
+ return;
1379
+ }
1321
1380
  this.clearIdleFinishCandidate('idle_timeout_finish');
1322
1381
  this.finishResponse();
1323
1382
  }
@@ -1327,6 +1386,11 @@ export class ProviderCliAdapter implements CliAdapter {
1327
1386
  private finishResponse(): void {
1328
1387
  if (this.submitPendingUntil > Date.now()) return;
1329
1388
  if (this.responseSettleIgnoreUntil > Date.now()) return;
1389
+ const parsedBeforeFinish = this.runParseSession();
1390
+ if (this.shouldKeepCodexTurnOpenForFinish(parsedBeforeFinish)) {
1391
+ this.rescheduleCodexFinishCheck('codex_finish_not_final');
1392
+ return;
1393
+ }
1330
1394
  this.clearIdleFinishCandidate('finish_response_enter');
1331
1395
  this.recordTrace('finish_response', {
1332
1396
  ...buildCliTraceParseSnapshot({
@@ -1372,6 +1436,7 @@ export class ProviderCliAdapter implements CliAdapter {
1372
1436
  this.activeModal = null;
1373
1437
  this.setStatus('idle', 'response_finished');
1374
1438
  this.onStatusChange?.();
1439
+ this.schedulePendingOutboundFlush();
1375
1440
  }
1376
1441
 
1377
1442
  private maybeCommitVisibleIdleTranscript(session: ParsedSession, parsedMessages: CliChatMessage[]): boolean {
@@ -1402,6 +1467,7 @@ export class ProviderCliAdapter implements CliAdapter {
1402
1467
  this.activeModal = null;
1403
1468
  this.setStatus('idle', 'script_idle_commit');
1404
1469
  this.onStatusChange?.();
1470
+ this.schedulePendingOutboundFlush();
1405
1471
  this.recordTrace('script_idle_commit', {
1406
1472
  messageCount: parsedMessages.length,
1407
1473
  lastAssistant: summarizeCliTraceText(visibleAssistant.content, 320),
@@ -1489,6 +1555,7 @@ export class ProviderCliAdapter implements CliAdapter {
1489
1555
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1490
1556
  recentOutputBuffer: this.recentOutputBuffer,
1491
1557
  terminalScreenText: parseScreenText,
1558
+ workingDir: this.workingDir,
1492
1559
  baseMessages: [],
1493
1560
  partialResponse: this.responseBuffer,
1494
1561
  isWaitingForResponse: this.isWaitingForResponse,
@@ -1552,6 +1619,58 @@ export class ProviderCliAdapter implements CliAdapter {
1552
1619
  return !!(startupModal || this.activeModal);
1553
1620
  }
1554
1621
 
1622
+ private parsedStatusHasFinalAssistantMessage(parsed: any): boolean {
1623
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
1624
+ const lastAssistant = [...messages].reverse().find((message: any) => {
1625
+ if (!message || message.role !== 'assistant') return false;
1626
+ return typeof message.content === 'string' && message.content.trim().length > 0;
1627
+ });
1628
+ return !!lastAssistant;
1629
+ }
1630
+
1631
+ private parsedStatusHasFinalStandardAssistantMessage(parsed: any): boolean {
1632
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
1633
+ const lastAssistant = [...messages].reverse().find((message: any) => {
1634
+ if (!message || message.role !== 'assistant') return false;
1635
+ return typeof message.content === 'string' && message.content.trim().length > 0;
1636
+ });
1637
+ if (!lastAssistant) return false;
1638
+ const kind = typeof lastAssistant.kind === 'string' && lastAssistant.kind.trim()
1639
+ ? lastAssistant.kind.trim()
1640
+ : 'standard';
1641
+ return kind === 'standard' && lastAssistant.meta?.streaming !== true;
1642
+ }
1643
+
1644
+ private shouldKeepCodexTurnOpenForFinish(parsed: any): boolean {
1645
+ if (this.cliType !== 'codex-cli') return false;
1646
+ if (!this.isWaitingForResponse || !this.currentTurnScope || this.hasActionableApproval()) return false;
1647
+ const parsedStatus = typeof parsed?.status === 'string' ? parsed.status.trim() : '';
1648
+ if (parsedStatus !== 'idle') return true;
1649
+ if (parsed?.activeModal || parsed?.modal) return true;
1650
+ return !this.parsedStatusHasFinalStandardAssistantMessage(parsed);
1651
+ }
1652
+
1653
+ private rescheduleCodexFinishCheck(reason: string): void {
1654
+ this.clearIdleFinishCandidate(reason);
1655
+ this.setStatus('generating', reason);
1656
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1657
+ this.idleTimeout = setTimeout(() => {
1658
+ if (!this.isWaitingForResponse || this.hasActionableApproval()) return;
1659
+ this.settledBuffer = this.recentOutputBuffer;
1660
+ this.evaluateSettled();
1661
+ }, this.getIdleFinishConfirmMs());
1662
+ this.recordTrace('codex_finish_deferred', {
1663
+ reason,
1664
+ ...buildCliTraceParseSnapshot({
1665
+ accumulatedBuffer: this.accumulatedBuffer,
1666
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1667
+ responseBuffer: this.responseBuffer,
1668
+ partialResponse: this.responseBuffer,
1669
+ scope: this.currentTurnScope,
1670
+ }),
1671
+ });
1672
+ }
1673
+
1555
1674
  private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
1556
1675
  if (this.parseErrorMessage) return 'error';
1557
1676
  if (this.hasActionableApproval(startupModal)) return 'waiting_approval';
@@ -1564,8 +1683,16 @@ export class ProviderCliAdapter implements CliAdapter {
1564
1683
  getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
1565
1684
  const allowParse = options.allowParse !== false;
1566
1685
  const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
1686
+ const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
1687
+ ? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
1688
+ : null;
1567
1689
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1568
1690
  let effectiveModal = startupModal || this.activeModal;
1691
+ if (startupDetectedStatus === 'waiting_approval') {
1692
+ effectiveStatus = 'waiting_approval';
1693
+ } else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
1694
+ effectiveStatus = 'idle';
1695
+ }
1569
1696
  if (allowParse && !startupModal && !effectiveModal) {
1570
1697
  const parsed = this.getFreshParsedStatusCache();
1571
1698
  const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
@@ -1575,6 +1702,18 @@ export class ProviderCliAdapter implements CliAdapter {
1575
1702
  if (parsed?.status === 'waiting_approval' && parsedModal) {
1576
1703
  effectiveStatus = 'waiting_approval';
1577
1704
  effectiveModal = parsedModal;
1705
+ } else if (
1706
+ effectiveStatus === 'idle'
1707
+ && parsed?.status === 'generating'
1708
+ && !this.parsedStatusHasFinalAssistantMessage(parsed)
1709
+ ) {
1710
+ effectiveStatus = 'generating';
1711
+ } else if (
1712
+ effectiveStatus === 'generating'
1713
+ && parsed?.status === 'idle'
1714
+ && this.parsedStatusHasFinalAssistantMessage(parsed)
1715
+ ) {
1716
+ effectiveStatus = 'idle';
1578
1717
  }
1579
1718
  }
1580
1719
  const bufferState = this.getBufferState();
@@ -1583,6 +1722,14 @@ export class ProviderCliAdapter implements CliAdapter {
1583
1722
  messages: [],
1584
1723
  workingDir: this.workingDir,
1585
1724
  activeModal: effectiveModal,
1725
+ pendingOutboundCount: this.pendingOutboundQueue.length,
1726
+ pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
1727
+ id: message.id,
1728
+ role: message.role,
1729
+ content: message.content,
1730
+ queuedAt: message.queuedAt,
1731
+ source: message.source,
1732
+ })),
1586
1733
  errorMessage: this.parseErrorMessage || undefined,
1587
1734
  errorReason: this.parseErrorMessage ? 'parse_error' : undefined,
1588
1735
  ...(bufferState ? { bufferState } : {}),
@@ -1600,7 +1747,8 @@ export class ProviderCliAdapter implements CliAdapter {
1600
1747
  const cached = this.parsedStatusCache;
1601
1748
  const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
1602
1749
  if (
1603
- cached
1750
+ !this.providerOwnsTranscript()
1751
+ && cached
1604
1752
  && cached.responseBuffer === this.responseBuffer
1605
1753
  && cached.currentTurnScope === this.currentTurnScope
1606
1754
  && cached.recentOutputBuffer === this.recentOutputBuffer
@@ -1631,6 +1779,12 @@ export class ProviderCliAdapter implements CliAdapter {
1631
1779
  }),
1632
1780
  activeModal,
1633
1781
  providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
1782
+ errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
1783
+ ? (parsed as any).errorMessage.trim()
1784
+ : undefined,
1785
+ errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
1786
+ ? (parsed as any).errorReason.trim()
1787
+ : undefined,
1634
1788
  ...(bufferState ? { bufferState } : {}),
1635
1789
  ...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
1636
1790
  ? { transcriptAuthority: (parsed as any).transcriptAuthority }
@@ -1665,6 +1819,7 @@ export class ProviderCliAdapter implements CliAdapter {
1665
1819
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1666
1820
  recentOutputBuffer: this.recentOutputBuffer,
1667
1821
  terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
1822
+ workingDir: this.workingDir,
1668
1823
  baseMessages: [],
1669
1824
  partialResponse: this.responseBuffer,
1670
1825
  isWaitingForResponse: this.isWaitingForResponse,
@@ -1925,6 +2080,104 @@ export class ProviderCliAdapter implements CliAdapter {
1925
2080
  }
1926
2081
 
1927
2082
  async sendMessage(text: string): Promise<void> {
2083
+ await this.sendMessageNow(text, true);
2084
+ }
2085
+
2086
+ private enqueuePendingOutboundMessage(text: string, reason: string): void {
2087
+ const content = String(text || '');
2088
+ const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
2089
+ if (duplicate) {
2090
+ this.recordTrace('send_message_queued_duplicate_suppressed', {
2091
+ reason,
2092
+ queueLength: this.pendingOutboundQueue.length,
2093
+ text: summarizeCliTraceText(content, 500),
2094
+ });
2095
+ return;
2096
+ }
2097
+ const queuedAt = Date.now();
2098
+ const message: PendingOutboundMessage = {
2099
+ id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
2100
+ role: 'user',
2101
+ content,
2102
+ queuedAt,
2103
+ source: 'sendMessage',
2104
+ };
2105
+ this.pendingOutboundQueue.push(message);
2106
+ this.recordTrace('send_message_queued', {
2107
+ reason,
2108
+ queueLength: this.pendingOutboundQueue.length,
2109
+ queuedAt,
2110
+ text: summarizeCliTraceText(content, 500),
2111
+ });
2112
+ LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
2113
+ this.onStatusChange?.();
2114
+ }
2115
+
2116
+ private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
2117
+ if (this.provider.allowInputDuringGeneration === true) return null;
2118
+ if (this.hasActionableApproval()) return null;
2119
+ const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
2120
+ ? String(parsedStatusBeforeSend.status)
2121
+ : '';
2122
+ if (parsedSessionStatus === 'idle' && this.parsedStatusHasFinalAssistantMessage(parsedStatusBeforeSend)) return null;
2123
+ if (this.currentStatus === 'generating') return 'current_status_generating';
2124
+ if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating') {
2125
+ const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
2126
+ const parsedHasActionableModal = Boolean(
2127
+ parsedModal
2128
+ && Array.isArray(parsedModal.buttons)
2129
+ && parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
2130
+ );
2131
+ const terminalLooksIdle = this.currentStatus === 'idle'
2132
+ && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
2133
+ && !this.isWaitingForResponse
2134
+ && !this.currentTurnScope
2135
+ && !this.hasActionableApproval()
2136
+ && !parsedHasActionableModal;
2137
+ return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
2138
+ }
2139
+ if (this.isWaitingForResponse && this.currentTurnScope) return 'active_turn_in_progress';
2140
+ return null;
2141
+ }
2142
+
2143
+ private schedulePendingOutboundFlush(delayMs = 0): void {
2144
+ if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
2145
+ this.pendingOutboundFlushTimer = setTimeout(() => {
2146
+ this.pendingOutboundFlushTimer = null;
2147
+ void this.flushPendingOutboundQueue();
2148
+ }, Math.max(0, delayMs));
2149
+ }
2150
+
2151
+ private async flushPendingOutboundQueue(): Promise<void> {
2152
+ if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
2153
+ if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) return;
2154
+ this.pendingOutboundFlushInFlight = true;
2155
+ try {
2156
+ while (this.pendingOutboundQueue.length > 0) {
2157
+ if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) break;
2158
+ const next = this.pendingOutboundQueue[0];
2159
+ this.recordTrace('send_message_queue_flush', {
2160
+ id: next.id,
2161
+ queuedAt: next.queuedAt,
2162
+ queueLength: this.pendingOutboundQueue.length,
2163
+ text: summarizeCliTraceText(next.content, 500),
2164
+ });
2165
+ try {
2166
+ await this.sendMessageNow(next.content, false);
2167
+ this.pendingOutboundQueue.shift();
2168
+ this.onStatusChange?.();
2169
+ } catch (error: any) {
2170
+ LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
2171
+ this.schedulePendingOutboundFlush(1000);
2172
+ break;
2173
+ }
2174
+ }
2175
+ } finally {
2176
+ this.pendingOutboundFlushInFlight = false;
2177
+ }
2178
+ }
2179
+
2180
+ private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
1928
2181
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1929
2182
  const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
1930
2183
  const allowInterventionPrompt = allowInputDuringGeneration
@@ -1937,27 +2190,33 @@ export class ProviderCliAdapter implements CliAdapter {
1937
2190
  await new Promise(resolve => setTimeout(resolve, 50));
1938
2191
  }
1939
2192
  }
2193
+ const parsedStatusBeforeSend = !allowInputDuringGeneration
2194
+ ? (() => {
2195
+ try {
2196
+ return this.getScriptParsedStatus?.() || null;
2197
+ } catch {
2198
+ return null;
2199
+ }
2200
+ })()
2201
+ : null;
2202
+ const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
2203
+ if (allowQueue && queueReason) {
2204
+ this.enqueuePendingOutboundMessage(text, queueReason);
2205
+ return;
2206
+ }
1940
2207
  if (!allowInterventionPrompt) {
1941
2208
  await this.waitForInteractivePrompt();
1942
2209
  }
1943
2210
  if (!this.ready) {
1944
2211
  this.resolveStartupState('send_precheck');
1945
- if (this.runDetectStatus(this.recentOutputBuffer) === 'idle' && this.currentStatus === 'idle') {
2212
+ if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
1946
2213
  this.ready = true;
1947
2214
  this.startupParseGate = false;
2215
+ this.setStatus('idle', 'send_message_idle_prompt_recovery');
1948
2216
  LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
1949
2217
  }
1950
2218
  }
1951
2219
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
1952
- const parsedStatusBeforeSend = !allowInputDuringGeneration
1953
- ? (() => {
1954
- try {
1955
- return this.getScriptParsedStatus?.() || null;
1956
- } catch {
1957
- return null;
1958
- }
1959
- })()
1960
- : null;
1961
2220
  const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
1962
2221
  ? String(parsedStatusBeforeSend.status)
1963
2222
  : '';
@@ -1975,11 +2234,22 @@ export class ProviderCliAdapter implements CliAdapter {
1975
2234
  && !this.hasActionableApproval()
1976
2235
  && !parsedHasActionableModal;
1977
2236
  if (!terminalLooksIdle) {
2237
+ if (allowQueue) {
2238
+ this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
2239
+ return;
2240
+ }
1978
2241
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1979
2242
  }
1980
2243
  }
1981
2244
  if (this.isWaitingForResponse && !allowInputDuringGeneration) {
1982
- if (!this.clearStaleIdleResponseGuard('send_message_guard')) {
2245
+ if (
2246
+ !this.clearStaleIdleResponseGuard('send_message_guard')
2247
+ && !this.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend)
2248
+ ) {
2249
+ if (allowQueue) {
2250
+ this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
2251
+ return;
2252
+ }
1983
2253
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1984
2254
  }
1985
2255
  }
@@ -2230,6 +2500,9 @@ export class ProviderCliAdapter implements CliAdapter {
2230
2500
  this.pendingTerminalQueryTail = '';
2231
2501
  this.ptyOutputChunks = [];
2232
2502
  this.finishRetryCount = 0;
2503
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2504
+ this.pendingOutboundQueue = [];
2505
+ this.pendingOutboundFlushInFlight = false;
2233
2506
  if (this.ptyProcess) {
2234
2507
  this.ptyProcess.write('\x03');
2235
2508
  setTimeout(() => {
@@ -2251,6 +2524,9 @@ export class ProviderCliAdapter implements CliAdapter {
2251
2524
  this.pendingTerminalQueryTail = '';
2252
2525
  this.ptyOutputChunks = [];
2253
2526
  this.finishRetryCount = 0;
2527
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2528
+ this.pendingOutboundQueue = [];
2529
+ this.pendingOutboundFlushInFlight = false;
2254
2530
  if (this.ptyProcess) {
2255
2531
  try {
2256
2532
  if (typeof this.ptyProcess.detach === 'function') {
@@ -2281,6 +2557,9 @@ export class ProviderCliAdapter implements CliAdapter {
2281
2557
  this.ptyOutputChunks = [];
2282
2558
  if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
2283
2559
  this.finishRetryCount = 0;
2560
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2561
+ this.pendingOutboundQueue = [];
2562
+ this.pendingOutboundFlushInFlight = false;
2284
2563
  this.resetTerminalScreen();
2285
2564
  this.ptyProcess?.clearBuffer?.();
2286
2565
  this.onStatusChange?.();
@@ -2369,10 +2648,26 @@ export class ProviderCliAdapter implements CliAdapter {
2369
2648
  getDebugState(): Record<string, any> {
2370
2649
  const screenText = sanitizeTerminalText(this.terminalScreen.getText());
2371
2650
  const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2372
- const effectiveStatus = this.projectEffectiveStatus(startupModal);
2373
- const effectiveReady = this.ready || !!startupModal;
2651
+ const startupDetectedStatus = this.startupParseGate && !startupModal
2652
+ ? this.runDetectStatus(this.recentOutputBuffer || screenText)
2653
+ : null;
2654
+ const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
2374
2655
  const parsedDebugState = this.getParsedDebugState();
2375
2656
  const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
2657
+ let effectiveStatus = this.projectEffectiveStatus(startupModal);
2658
+ if (parsedDebugState?.status === 'error') {
2659
+ effectiveStatus = 'error';
2660
+ }
2661
+ if (startupDetectedStatus === 'waiting_approval') {
2662
+ effectiveStatus = 'waiting_approval';
2663
+ }
2664
+ if (
2665
+ effectiveStatus === 'idle'
2666
+ && parsedDebugState?.status === 'generating'
2667
+ && !this.parsedStatusHasFinalAssistantMessage(parsedDebugState)
2668
+ ) {
2669
+ effectiveStatus = 'generating';
2670
+ }
2376
2671
  return {
2377
2672
  type: this.cliType,
2378
2673
  name: this.cliName,
@@ -2394,6 +2689,8 @@ export class ProviderCliAdapter implements CliAdapter {
2394
2689
  providerSessionId: parsedDebugState.providerSessionId,
2395
2690
  transcriptAuthority: parsedDebugState.transcriptAuthority,
2396
2691
  coverage: parsedDebugState.coverage,
2692
+ errorMessage: parsedDebugState.errorMessage,
2693
+ errorReason: parsedDebugState.errorReason,
2397
2694
  activeModal: parsedDebugState.activeModal,
2398
2695
  messageCount: parsedMessages.length,
2399
2696
  } : null,
@@ -2407,6 +2704,14 @@ export class ProviderCliAdapter implements CliAdapter {
2407
2704
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
2408
2705
  sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
2409
2706
  responseBuffer: this.responseBuffer.slice(-1000),
2707
+ pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
2708
+ id: message.id,
2709
+ role: message.role,
2710
+ content: message.content,
2711
+ queuedAt: message.queuedAt,
2712
+ source: message.source,
2713
+ })),
2714
+ pendingOutboundCount: this.pendingOutboundQueue.length,
2410
2715
  lastOutputAt: this.lastOutputAt,
2411
2716
  lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2412
2717
  lastScreenChangeAt: this.lastScreenChangeAt,
@@ -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;