@adhdev/daemon-core 0.9.82-rc.10 → 0.9.82-rc.100
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.
- package/dist/cli-adapters/provider-cli-adapter.d.ts +10 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +10 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.js +5733 -1216
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5699 -1203
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +60 -0
- package/dist/mesh/mesh-events.d.ts +26 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/refine-config.d.ts +176 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/repo-mesh-types.d.ts +46 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +1 -0
- package/src/cli-adapters/provider-cli-adapter.ts +255 -14
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +28 -10
- package/src/commands/chat-commands.ts +570 -20
- package/src/commands/cli-manager.ts +129 -1
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +2820 -437
- package/src/config/chat-history.ts +9 -7
- package/src/config/mesh-config.ts +245 -1
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/index.ts +30 -4
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +176 -0
- package/src/mesh/coordinator-prompt.ts +31 -8
- package/src/mesh/mesh-active-work.ts +255 -0
- package/src/mesh/mesh-events.ts +389 -47
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +138 -1
- package/src/mesh/mesh-work-queue.ts +199 -137
- package/src/mesh/refine-config.ts +356 -0
- package/src/providers/chat-message-normalization.ts +7 -12
- package/src/providers/cli-provider-instance.ts +93 -14
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +51 -0
- package/src/status/reporter.ts +15 -0
- 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.
|
|
3
|
+
"version": "0.9.82-rc.100",
|
|
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;
|
|
@@ -1372,6 +1426,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1372
1426
|
this.activeModal = null;
|
|
1373
1427
|
this.setStatus('idle', 'response_finished');
|
|
1374
1428
|
this.onStatusChange?.();
|
|
1429
|
+
this.schedulePendingOutboundFlush();
|
|
1375
1430
|
}
|
|
1376
1431
|
|
|
1377
1432
|
private maybeCommitVisibleIdleTranscript(session: ParsedSession, parsedMessages: CliChatMessage[]): boolean {
|
|
@@ -1402,6 +1457,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1402
1457
|
this.activeModal = null;
|
|
1403
1458
|
this.setStatus('idle', 'script_idle_commit');
|
|
1404
1459
|
this.onStatusChange?.();
|
|
1460
|
+
this.schedulePendingOutboundFlush();
|
|
1405
1461
|
this.recordTrace('script_idle_commit', {
|
|
1406
1462
|
messageCount: parsedMessages.length,
|
|
1407
1463
|
lastAssistant: summarizeCliTraceText(visibleAssistant.content, 320),
|
|
@@ -1489,6 +1545,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1489
1545
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1490
1546
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1491
1547
|
terminalScreenText: parseScreenText,
|
|
1548
|
+
workingDir: this.workingDir,
|
|
1492
1549
|
baseMessages: [],
|
|
1493
1550
|
partialResponse: this.responseBuffer,
|
|
1494
1551
|
isWaitingForResponse: this.isWaitingForResponse,
|
|
@@ -1552,6 +1609,15 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1552
1609
|
return !!(startupModal || this.activeModal);
|
|
1553
1610
|
}
|
|
1554
1611
|
|
|
1612
|
+
private parsedStatusHasFinalAssistantMessage(parsed: any): boolean {
|
|
1613
|
+
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1614
|
+
const lastAssistant = [...messages].reverse().find((message: any) => {
|
|
1615
|
+
if (!message || message.role !== 'assistant') return false;
|
|
1616
|
+
return typeof message.content === 'string' && message.content.trim().length > 0;
|
|
1617
|
+
});
|
|
1618
|
+
return !!lastAssistant;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1555
1621
|
private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
|
|
1556
1622
|
if (this.parseErrorMessage) return 'error';
|
|
1557
1623
|
if (this.hasActionableApproval(startupModal)) return 'waiting_approval';
|
|
@@ -1564,8 +1630,16 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1564
1630
|
getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
|
|
1565
1631
|
const allowParse = options.allowParse !== false;
|
|
1566
1632
|
const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
1633
|
+
const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
|
|
1634
|
+
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
1635
|
+
: null;
|
|
1567
1636
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
1568
1637
|
let effectiveModal = startupModal || this.activeModal;
|
|
1638
|
+
if (startupDetectedStatus === 'waiting_approval') {
|
|
1639
|
+
effectiveStatus = 'waiting_approval';
|
|
1640
|
+
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
1641
|
+
effectiveStatus = 'idle';
|
|
1642
|
+
}
|
|
1569
1643
|
if (allowParse && !startupModal && !effectiveModal) {
|
|
1570
1644
|
const parsed = this.getFreshParsedStatusCache();
|
|
1571
1645
|
const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
|
|
@@ -1575,6 +1649,18 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1575
1649
|
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
1576
1650
|
effectiveStatus = 'waiting_approval';
|
|
1577
1651
|
effectiveModal = parsedModal;
|
|
1652
|
+
} else if (
|
|
1653
|
+
effectiveStatus === 'idle'
|
|
1654
|
+
&& parsed?.status === 'generating'
|
|
1655
|
+
&& !this.parsedStatusHasFinalAssistantMessage(parsed)
|
|
1656
|
+
) {
|
|
1657
|
+
effectiveStatus = 'generating';
|
|
1658
|
+
} else if (
|
|
1659
|
+
effectiveStatus === 'generating'
|
|
1660
|
+
&& parsed?.status === 'idle'
|
|
1661
|
+
&& this.parsedStatusHasFinalAssistantMessage(parsed)
|
|
1662
|
+
) {
|
|
1663
|
+
effectiveStatus = 'idle';
|
|
1578
1664
|
}
|
|
1579
1665
|
}
|
|
1580
1666
|
const bufferState = this.getBufferState();
|
|
@@ -1583,6 +1669,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1583
1669
|
messages: [],
|
|
1584
1670
|
workingDir: this.workingDir,
|
|
1585
1671
|
activeModal: effectiveModal,
|
|
1672
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
1673
|
+
pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
|
|
1674
|
+
id: message.id,
|
|
1675
|
+
role: message.role,
|
|
1676
|
+
content: message.content,
|
|
1677
|
+
queuedAt: message.queuedAt,
|
|
1678
|
+
source: message.source,
|
|
1679
|
+
})),
|
|
1586
1680
|
errorMessage: this.parseErrorMessage || undefined,
|
|
1587
1681
|
errorReason: this.parseErrorMessage ? 'parse_error' : undefined,
|
|
1588
1682
|
...(bufferState ? { bufferState } : {}),
|
|
@@ -1600,7 +1694,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1600
1694
|
const cached = this.parsedStatusCache;
|
|
1601
1695
|
const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
|
|
1602
1696
|
if (
|
|
1603
|
-
|
|
1697
|
+
!this.providerOwnsTranscript()
|
|
1698
|
+
&& cached
|
|
1604
1699
|
&& cached.responseBuffer === this.responseBuffer
|
|
1605
1700
|
&& cached.currentTurnScope === this.currentTurnScope
|
|
1606
1701
|
&& cached.recentOutputBuffer === this.recentOutputBuffer
|
|
@@ -1665,6 +1760,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1665
1760
|
accumulatedRawBuffer: this.accumulatedRawBuffer,
|
|
1666
1761
|
recentOutputBuffer: this.recentOutputBuffer,
|
|
1667
1762
|
terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
|
|
1763
|
+
workingDir: this.workingDir,
|
|
1668
1764
|
baseMessages: [],
|
|
1669
1765
|
partialResponse: this.responseBuffer,
|
|
1670
1766
|
isWaitingForResponse: this.isWaitingForResponse,
|
|
@@ -1925,6 +2021,104 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1925
2021
|
}
|
|
1926
2022
|
|
|
1927
2023
|
async sendMessage(text: string): Promise<void> {
|
|
2024
|
+
await this.sendMessageNow(text, true);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
private enqueuePendingOutboundMessage(text: string, reason: string): void {
|
|
2028
|
+
const content = String(text || '');
|
|
2029
|
+
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
2030
|
+
if (duplicate) {
|
|
2031
|
+
this.recordTrace('send_message_queued_duplicate_suppressed', {
|
|
2032
|
+
reason,
|
|
2033
|
+
queueLength: this.pendingOutboundQueue.length,
|
|
2034
|
+
text: summarizeCliTraceText(content, 500),
|
|
2035
|
+
});
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const queuedAt = Date.now();
|
|
2039
|
+
const message: PendingOutboundMessage = {
|
|
2040
|
+
id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
|
|
2041
|
+
role: 'user',
|
|
2042
|
+
content,
|
|
2043
|
+
queuedAt,
|
|
2044
|
+
source: 'sendMessage',
|
|
2045
|
+
};
|
|
2046
|
+
this.pendingOutboundQueue.push(message);
|
|
2047
|
+
this.recordTrace('send_message_queued', {
|
|
2048
|
+
reason,
|
|
2049
|
+
queueLength: this.pendingOutboundQueue.length,
|
|
2050
|
+
queuedAt,
|
|
2051
|
+
text: summarizeCliTraceText(content, 500),
|
|
2052
|
+
});
|
|
2053
|
+
LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
|
|
2054
|
+
this.onStatusChange?.();
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
|
|
2058
|
+
if (this.provider.allowInputDuringGeneration === true) return null;
|
|
2059
|
+
if (this.hasActionableApproval()) return null;
|
|
2060
|
+
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
2061
|
+
? String(parsedStatusBeforeSend.status)
|
|
2062
|
+
: '';
|
|
2063
|
+
if (parsedSessionStatus === 'idle' && this.parsedStatusHasFinalAssistantMessage(parsedStatusBeforeSend)) return null;
|
|
2064
|
+
if (this.currentStatus === 'generating') return 'current_status_generating';
|
|
2065
|
+
if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating') {
|
|
2066
|
+
const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
|
|
2067
|
+
const parsedHasActionableModal = Boolean(
|
|
2068
|
+
parsedModal
|
|
2069
|
+
&& Array.isArray(parsedModal.buttons)
|
|
2070
|
+
&& parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
|
|
2071
|
+
);
|
|
2072
|
+
const terminalLooksIdle = this.currentStatus === 'idle'
|
|
2073
|
+
&& this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
2074
|
+
&& !this.isWaitingForResponse
|
|
2075
|
+
&& !this.currentTurnScope
|
|
2076
|
+
&& !this.hasActionableApproval()
|
|
2077
|
+
&& !parsedHasActionableModal;
|
|
2078
|
+
return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
|
|
2079
|
+
}
|
|
2080
|
+
if (this.isWaitingForResponse && this.currentTurnScope) return 'active_turn_in_progress';
|
|
2081
|
+
return null;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
private schedulePendingOutboundFlush(delayMs = 0): void {
|
|
2085
|
+
if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
|
|
2086
|
+
this.pendingOutboundFlushTimer = setTimeout(() => {
|
|
2087
|
+
this.pendingOutboundFlushTimer = null;
|
|
2088
|
+
void this.flushPendingOutboundQueue();
|
|
2089
|
+
}, Math.max(0, delayMs));
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
private async flushPendingOutboundQueue(): Promise<void> {
|
|
2093
|
+
if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
|
|
2094
|
+
if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) return;
|
|
2095
|
+
this.pendingOutboundFlushInFlight = true;
|
|
2096
|
+
try {
|
|
2097
|
+
while (this.pendingOutboundQueue.length > 0) {
|
|
2098
|
+
if (this.currentStatus !== 'idle' || this.isWaitingForResponse || this.hasActionableApproval()) break;
|
|
2099
|
+
const next = this.pendingOutboundQueue[0];
|
|
2100
|
+
this.recordTrace('send_message_queue_flush', {
|
|
2101
|
+
id: next.id,
|
|
2102
|
+
queuedAt: next.queuedAt,
|
|
2103
|
+
queueLength: this.pendingOutboundQueue.length,
|
|
2104
|
+
text: summarizeCliTraceText(next.content, 500),
|
|
2105
|
+
});
|
|
2106
|
+
try {
|
|
2107
|
+
await this.sendMessageNow(next.content, false);
|
|
2108
|
+
this.pendingOutboundQueue.shift();
|
|
2109
|
+
this.onStatusChange?.();
|
|
2110
|
+
} catch (error: any) {
|
|
2111
|
+
LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
|
|
2112
|
+
this.schedulePendingOutboundFlush(1000);
|
|
2113
|
+
break;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
} finally {
|
|
2117
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
|
|
1928
2122
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
1929
2123
|
const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
|
|
1930
2124
|
const allowInterventionPrompt = allowInputDuringGeneration
|
|
@@ -1937,27 +2131,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1937
2131
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1938
2132
|
}
|
|
1939
2133
|
}
|
|
2134
|
+
const parsedStatusBeforeSend = !allowInputDuringGeneration
|
|
2135
|
+
? (() => {
|
|
2136
|
+
try {
|
|
2137
|
+
return this.getScriptParsedStatus?.() || null;
|
|
2138
|
+
} catch {
|
|
2139
|
+
return null;
|
|
2140
|
+
}
|
|
2141
|
+
})()
|
|
2142
|
+
: null;
|
|
2143
|
+
const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
|
|
2144
|
+
if (allowQueue && queueReason) {
|
|
2145
|
+
this.enqueuePendingOutboundMessage(text, queueReason);
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
1940
2148
|
if (!allowInterventionPrompt) {
|
|
1941
2149
|
await this.waitForInteractivePrompt();
|
|
1942
2150
|
}
|
|
1943
2151
|
if (!this.ready) {
|
|
1944
2152
|
this.resolveStartupState('send_precheck');
|
|
1945
|
-
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle'
|
|
2153
|
+
if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
|
|
1946
2154
|
this.ready = true;
|
|
1947
2155
|
this.startupParseGate = false;
|
|
2156
|
+
this.setStatus('idle', 'send_message_idle_prompt_recovery');
|
|
1948
2157
|
LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
|
|
1949
2158
|
}
|
|
1950
2159
|
}
|
|
1951
2160
|
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
2161
|
const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
|
|
1962
2162
|
? String(parsedStatusBeforeSend.status)
|
|
1963
2163
|
: '';
|
|
@@ -1975,11 +2175,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1975
2175
|
&& !this.hasActionableApproval()
|
|
1976
2176
|
&& !parsedHasActionableModal;
|
|
1977
2177
|
if (!terminalLooksIdle) {
|
|
2178
|
+
if (allowQueue) {
|
|
2179
|
+
this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
1978
2182
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1979
2183
|
}
|
|
1980
2184
|
}
|
|
1981
2185
|
if (this.isWaitingForResponse && !allowInputDuringGeneration) {
|
|
1982
|
-
if (
|
|
2186
|
+
if (
|
|
2187
|
+
!this.clearStaleIdleResponseGuard('send_message_guard')
|
|
2188
|
+
&& !this.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend)
|
|
2189
|
+
) {
|
|
2190
|
+
if (allowQueue) {
|
|
2191
|
+
this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
1983
2194
|
throw new Error(`${this.cliName} is still processing the previous prompt`);
|
|
1984
2195
|
}
|
|
1985
2196
|
}
|
|
@@ -2230,6 +2441,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2230
2441
|
this.pendingTerminalQueryTail = '';
|
|
2231
2442
|
this.ptyOutputChunks = [];
|
|
2232
2443
|
this.finishRetryCount = 0;
|
|
2444
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2445
|
+
this.pendingOutboundQueue = [];
|
|
2446
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2233
2447
|
if (this.ptyProcess) {
|
|
2234
2448
|
this.ptyProcess.write('\x03');
|
|
2235
2449
|
setTimeout(() => {
|
|
@@ -2251,6 +2465,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2251
2465
|
this.pendingTerminalQueryTail = '';
|
|
2252
2466
|
this.ptyOutputChunks = [];
|
|
2253
2467
|
this.finishRetryCount = 0;
|
|
2468
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2469
|
+
this.pendingOutboundQueue = [];
|
|
2470
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2254
2471
|
if (this.ptyProcess) {
|
|
2255
2472
|
try {
|
|
2256
2473
|
if (typeof this.ptyProcess.detach === 'function') {
|
|
@@ -2281,6 +2498,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2281
2498
|
this.ptyOutputChunks = [];
|
|
2282
2499
|
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
2283
2500
|
this.finishRetryCount = 0;
|
|
2501
|
+
if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
|
|
2502
|
+
this.pendingOutboundQueue = [];
|
|
2503
|
+
this.pendingOutboundFlushInFlight = false;
|
|
2284
2504
|
this.resetTerminalScreen();
|
|
2285
2505
|
this.ptyProcess?.clearBuffer?.();
|
|
2286
2506
|
this.onStatusChange?.();
|
|
@@ -2369,10 +2589,23 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2369
2589
|
getDebugState(): Record<string, any> {
|
|
2370
2590
|
const screenText = sanitizeTerminalText(this.terminalScreen.getText());
|
|
2371
2591
|
const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
|
|
2372
|
-
const
|
|
2373
|
-
|
|
2592
|
+
const startupDetectedStatus = this.startupParseGate && !startupModal
|
|
2593
|
+
? this.runDetectStatus(this.recentOutputBuffer || screenText)
|
|
2594
|
+
: null;
|
|
2595
|
+
const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
|
|
2374
2596
|
const parsedDebugState = this.getParsedDebugState();
|
|
2375
2597
|
const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
|
|
2598
|
+
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
2599
|
+
if (startupDetectedStatus === 'waiting_approval') {
|
|
2600
|
+
effectiveStatus = 'waiting_approval';
|
|
2601
|
+
}
|
|
2602
|
+
if (
|
|
2603
|
+
effectiveStatus === 'idle'
|
|
2604
|
+
&& parsedDebugState?.status === 'generating'
|
|
2605
|
+
&& !this.parsedStatusHasFinalAssistantMessage(parsedDebugState)
|
|
2606
|
+
) {
|
|
2607
|
+
effectiveStatus = 'generating';
|
|
2608
|
+
}
|
|
2376
2609
|
return {
|
|
2377
2610
|
type: this.cliType,
|
|
2378
2611
|
name: this.cliName,
|
|
@@ -2407,6 +2640,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2407
2640
|
rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
|
|
2408
2641
|
sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
|
|
2409
2642
|
responseBuffer: this.responseBuffer.slice(-1000),
|
|
2643
|
+
pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
|
|
2644
|
+
id: message.id,
|
|
2645
|
+
role: message.role,
|
|
2646
|
+
content: message.content,
|
|
2647
|
+
queuedAt: message.queuedAt,
|
|
2648
|
+
source: message.source,
|
|
2649
|
+
})),
|
|
2650
|
+
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
2410
2651
|
lastOutputAt: this.lastOutputAt,
|
|
2411
2652
|
lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
|
|
2412
2653
|
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;
|
|
@@ -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[];
|