@adhdev/daemon-core 0.9.82-rc.121 → 0.9.82-rc.122
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-adapter-types.d.ts +4 -1
- package/dist/cli-adapters/provider-cli-adapter.d.ts +4 -1
- package/dist/index.js +268 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +268 -33
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +28 -0
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/package.json +1 -1
- package/src/cli-adapter-types.ts +2 -1
- package/src/cli-adapters/provider-cli-adapter.ts +20 -1
- package/src/commands/chat-commands.ts +12 -2
- package/src/commands/cli-manager.ts +9 -1
- package/src/mesh/mesh-events.ts +220 -15
- package/src/providers/cli-provider-instance.ts +42 -3
|
@@ -27,6 +27,18 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
27
27
|
forwarded: number;
|
|
28
28
|
suppressed: boolean;
|
|
29
29
|
intentionalCleanupStop: boolean;
|
|
30
|
+
terminalLedgerEvidence?: undefined;
|
|
31
|
+
terminalLedgerKind?: undefined;
|
|
32
|
+
duplicateRefineTerminalEvent?: undefined;
|
|
33
|
+
duplicateCompletion?: undefined;
|
|
34
|
+
error?: undefined;
|
|
35
|
+
} | {
|
|
36
|
+
success: boolean;
|
|
37
|
+
forwarded: number;
|
|
38
|
+
suppressed: boolean;
|
|
39
|
+
terminalLedgerEvidence: boolean;
|
|
40
|
+
terminalLedgerKind: unknown;
|
|
41
|
+
intentionalCleanupStop?: undefined;
|
|
30
42
|
duplicateRefineTerminalEvent?: undefined;
|
|
31
43
|
duplicateCompletion?: undefined;
|
|
32
44
|
error?: undefined;
|
|
@@ -36,14 +48,28 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
36
48
|
suppressed: boolean;
|
|
37
49
|
duplicateRefineTerminalEvent: boolean;
|
|
38
50
|
intentionalCleanupStop?: undefined;
|
|
51
|
+
terminalLedgerEvidence?: undefined;
|
|
52
|
+
terminalLedgerKind?: undefined;
|
|
39
53
|
duplicateCompletion?: undefined;
|
|
40
54
|
error?: undefined;
|
|
55
|
+
} | {
|
|
56
|
+
success: boolean;
|
|
57
|
+
forwarded: number;
|
|
58
|
+
suppressed: boolean;
|
|
59
|
+
duplicateCompletion: boolean;
|
|
60
|
+
terminalLedgerEvidence: boolean;
|
|
61
|
+
intentionalCleanupStop?: undefined;
|
|
62
|
+
terminalLedgerKind?: undefined;
|
|
63
|
+
duplicateRefineTerminalEvent?: undefined;
|
|
64
|
+
error?: undefined;
|
|
41
65
|
} | {
|
|
42
66
|
success: boolean;
|
|
43
67
|
forwarded: number;
|
|
44
68
|
suppressed: boolean;
|
|
45
69
|
duplicateCompletion: boolean;
|
|
46
70
|
intentionalCleanupStop?: undefined;
|
|
71
|
+
terminalLedgerEvidence?: undefined;
|
|
72
|
+
terminalLedgerKind?: undefined;
|
|
47
73
|
duplicateRefineTerminalEvent?: undefined;
|
|
48
74
|
error?: undefined;
|
|
49
75
|
} | {
|
|
@@ -51,6 +77,8 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
51
77
|
forwarded: number;
|
|
52
78
|
suppressed?: undefined;
|
|
53
79
|
intentionalCleanupStop?: undefined;
|
|
80
|
+
terminalLedgerEvidence?: undefined;
|
|
81
|
+
terminalLedgerKind?: undefined;
|
|
54
82
|
duplicateRefineTerminalEvent?: undefined;
|
|
55
83
|
duplicateCompletion?: undefined;
|
|
56
84
|
error?: undefined;
|
|
@@ -48,6 +48,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
48
48
|
private lastApprovalEventAt;
|
|
49
49
|
private autoApproveBusy;
|
|
50
50
|
private autoApproveBusyTimer;
|
|
51
|
+
private lastAutoApprovalSignature;
|
|
51
52
|
private controlValues;
|
|
52
53
|
private summaryMetadata;
|
|
53
54
|
private appliedEffectKeys;
|
package/package.json
CHANGED
package/src/cli-adapter-types.ts
CHANGED
|
@@ -38,7 +38,8 @@ export interface CliAdapter {
|
|
|
38
38
|
workingDir: string;
|
|
39
39
|
_acpInstance?: AcpAdapterHandle;
|
|
40
40
|
spawn(): Promise<void>;
|
|
41
|
-
sendMessage(text: string): Promise<void>;
|
|
41
|
+
sendMessage(text: string, options?: { force?: boolean }): Promise<void>;
|
|
42
|
+
forceSendMessage?(text: string): Promise<void>;
|
|
42
43
|
getStatus(): CliAdapterStatus;
|
|
43
44
|
getScriptParsedStatus?(): unknown;
|
|
44
45
|
getDebugSnapshot?(): unknown;
|
|
@@ -2210,10 +2210,29 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
2210
2210
|
), 50);
|
|
2211
2211
|
}
|
|
2212
2212
|
|
|
2213
|
-
async sendMessage(text: string): Promise<void> {
|
|
2213
|
+
async sendMessage(text: string, options: { force?: boolean } = {}): Promise<void> {
|
|
2214
|
+
if (options.force === true) {
|
|
2215
|
+
await this.forceSendMessage(text);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2214
2218
|
await this.sendMessageNow(text, true);
|
|
2215
2219
|
}
|
|
2216
2220
|
|
|
2221
|
+
async forceSendMessage(text: string): Promise<void> {
|
|
2222
|
+
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
2223
|
+
const content = String(text || '');
|
|
2224
|
+
if (!content.trim()) return;
|
|
2225
|
+
this.recordTrace('force_send_message', {
|
|
2226
|
+
text: summarizeCliTraceText(content, 500),
|
|
2227
|
+
status: this.currentStatus,
|
|
2228
|
+
isWaitingForResponse: this.isWaitingForResponse,
|
|
2229
|
+
queueLength: this.pendingOutboundQueue.length,
|
|
2230
|
+
});
|
|
2231
|
+
LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.currentStatus}`);
|
|
2232
|
+
await this.writeToPty(content + this.sendKey);
|
|
2233
|
+
this.onStatusChange?.();
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2217
2236
|
private enqueuePendingOutboundMessage(text: string, reason: string): void {
|
|
2218
2237
|
const content = String(text || '');
|
|
2219
2238
|
const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
|
|
@@ -1881,8 +1881,18 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1881
1881
|
assertTextOnlyInput(provider, input);
|
|
1882
1882
|
if (!text) return { success: false, error: 'text required for PTY send' };
|
|
1883
1883
|
await waitOnceForFreshHermesCliStart(adapter, _log);
|
|
1884
|
-
|
|
1885
|
-
|
|
1884
|
+
const forceSend = args?.force === true || args?.forceSend === true;
|
|
1885
|
+
if (forceSend && typeof adapter.forceSendMessage === 'function') {
|
|
1886
|
+
await adapter.forceSendMessage(text);
|
|
1887
|
+
} else if (forceSend) {
|
|
1888
|
+
await adapter.sendMessage(text, { force: true });
|
|
1889
|
+
} else {
|
|
1890
|
+
await adapter.sendMessage(text);
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
..._logSendSuccess(`${transport}-adapter`, adapter.cliType),
|
|
1894
|
+
...(forceSend ? { forceSent: true } : {}),
|
|
1895
|
+
};
|
|
1886
1896
|
} catch (e: any) {
|
|
1887
1897
|
return { success: false, error: `${transport} send failed: ${e.message}` };
|
|
1888
1898
|
}
|
|
@@ -1206,11 +1206,19 @@ export class DaemonCliManager {
|
|
|
1206
1206
|
}
|
|
1207
1207
|
const message = input.textFallback;
|
|
1208
1208
|
if (!message) throw new Error('message required for send_chat');
|
|
1209
|
-
|
|
1209
|
+
const forceSend = args?.force === true || args?.forceSend === true;
|
|
1210
|
+
if (forceSend && typeof (adapter as any).forceSendMessage === 'function') {
|
|
1211
|
+
await (adapter as any).forceSendMessage(message);
|
|
1212
|
+
} else if (forceSend) {
|
|
1213
|
+
await adapter.sendMessage(message, { force: true });
|
|
1214
|
+
} else {
|
|
1215
|
+
await adapter.sendMessage(message);
|
|
1216
|
+
}
|
|
1210
1217
|
return {
|
|
1211
1218
|
success: true,
|
|
1212
1219
|
status: BUSY_AGENT_STATUSES.has(currentStatus) ? currentStatus : 'generating',
|
|
1213
1220
|
...(BUSY_AGENT_STATUSES.has(currentStatus) ? { queued: true, queuedReason: 'agent_runtime_busy' } : {}),
|
|
1221
|
+
...(forceSend ? { forceSent: true, queued: false } : {}),
|
|
1214
1222
|
};
|
|
1215
1223
|
} else if (action === 'clear_history') {
|
|
1216
1224
|
if (typeof adapter.clearHistory === 'function') adapter.clearHistory();
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -77,7 +77,7 @@ function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEve
|
|
|
77
77
|
if (!REFINE_TERMINAL_EVENTS.has(event.event)) return false;
|
|
78
78
|
const jobId = readRefineJobId(event);
|
|
79
79
|
if (!jobId) return false;
|
|
80
|
-
return
|
|
80
|
+
return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) =>
|
|
81
81
|
pending.event === event.event && readRefineJobId(pending) === jobId,
|
|
82
82
|
);
|
|
83
83
|
}
|
|
@@ -104,7 +104,7 @@ function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): strin
|
|
|
104
104
|
function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
105
105
|
const fingerprint = buildPendingEventFingerprint(event);
|
|
106
106
|
if (!fingerprint.trim()) return false;
|
|
107
|
-
return
|
|
107
|
+
return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
function getPendingEventsPath(meshId: string): string {
|
|
@@ -112,6 +112,88 @@ function getPendingEventsPath(meshId: string): string {
|
|
|
112
112
|
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function readPendingMeshCoordinatorEventsFromDisk(meshId?: string): PendingMeshCoordinatorEvent[] {
|
|
116
|
+
if (!meshId) return [];
|
|
117
|
+
const path = getPendingEventsPath(meshId);
|
|
118
|
+
if (!existsSync(path)) return [];
|
|
119
|
+
try {
|
|
120
|
+
const raw = readFileSync(path, 'utf-8');
|
|
121
|
+
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
122
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
123
|
+
});
|
|
124
|
+
} catch { return []; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
128
|
+
const acceptedJobIds = new Set(
|
|
129
|
+
pending
|
|
130
|
+
.filter(event => event.event === 'refine:accepted')
|
|
131
|
+
.map(event => readRefineJobId(event))
|
|
132
|
+
.filter(Boolean),
|
|
133
|
+
);
|
|
134
|
+
if (acceptedJobIds.size === 0) return [];
|
|
135
|
+
const existingTerminalJobIds = new Set(
|
|
136
|
+
pending
|
|
137
|
+
.filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
|
|
138
|
+
.map(event => `${event.event}:${readRefineJobId(event)}`)
|
|
139
|
+
.filter(value => !value.endsWith(':')),
|
|
140
|
+
);
|
|
141
|
+
const backfilled: PendingMeshCoordinatorEvent[] = [];
|
|
142
|
+
const entries = readLedgerEntries(meshId);
|
|
143
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
144
|
+
const entry = entries[i];
|
|
145
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
|
|
146
|
+
const payload = readRecord(entry.payload);
|
|
147
|
+
if (payload?.source !== 'refine_mesh_node_async_job') continue;
|
|
148
|
+
const refineJob = readRecord(payload.refineJob);
|
|
149
|
+
const jobId = readNonEmptyString(refineJob?.jobId);
|
|
150
|
+
if (!jobId || !acceptedJobIds.has(jobId)) continue;
|
|
151
|
+
const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
|
|
152
|
+
if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
|
|
153
|
+
existingTerminalJobIds.add(`${eventName}:${jobId}`);
|
|
154
|
+
const result = readRecord(payload.result);
|
|
155
|
+
const metadataEvent = {
|
|
156
|
+
source: 'refine_mesh_node_async_job',
|
|
157
|
+
jobId,
|
|
158
|
+
interactionId: readNonEmptyString(refineJob?.interactionId),
|
|
159
|
+
meshId,
|
|
160
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
161
|
+
targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
|
|
162
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
163
|
+
status: eventName === 'refine:completed' ? 'completed' : 'failed',
|
|
164
|
+
startedAt: readNonEmptyString(refineJob?.startedAt),
|
|
165
|
+
completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
|
|
166
|
+
retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
|
|
167
|
+
...(result ? { result } : {}),
|
|
168
|
+
};
|
|
169
|
+
backfilled.push({
|
|
170
|
+
event: eventName,
|
|
171
|
+
meshId,
|
|
172
|
+
nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
|
|
173
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
174
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
175
|
+
metadataEvent,
|
|
176
|
+
coordinatorMessage: buildMeshSystemMessage({
|
|
177
|
+
event: eventName,
|
|
178
|
+
nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
|
|
179
|
+
metadataEvent,
|
|
180
|
+
}),
|
|
181
|
+
queuedAt: Date.now(),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return backfilled.reverse();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
188
|
+
const backfilled = refineTerminalEventFromLedger(meshId, events);
|
|
189
|
+
if (backfilled.length === 0) return events;
|
|
190
|
+
const terminalJobIds = new Set(backfilled.map(event => readRefineJobId(event)).filter(Boolean));
|
|
191
|
+
return [
|
|
192
|
+
...events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event)))),
|
|
193
|
+
...backfilled,
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
115
197
|
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
116
198
|
try {
|
|
117
199
|
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
@@ -136,25 +218,16 @@ export function drainPendingMeshCoordinatorEvents(meshId?: string): PendingMeshC
|
|
|
136
218
|
const path = getPendingEventsPath(meshId);
|
|
137
219
|
if (!existsSync(path)) return [];
|
|
138
220
|
try {
|
|
139
|
-
const
|
|
221
|
+
const parsed = readPendingMeshCoordinatorEventsFromDisk(meshId);
|
|
140
222
|
try { unlinkSync(path); } catch { /* concurrent drain already removed it */ }
|
|
141
|
-
return
|
|
142
|
-
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
143
|
-
});
|
|
223
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, parsed);
|
|
144
224
|
} catch { return []; }
|
|
145
225
|
}
|
|
146
226
|
|
|
147
227
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
148
228
|
export function getPendingMeshCoordinatorEvents(meshId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
149
229
|
if (!meshId) return [];
|
|
150
|
-
|
|
151
|
-
if (!existsSync(path)) return [];
|
|
152
|
-
try {
|
|
153
|
-
const raw = readFileSync(path, 'utf-8');
|
|
154
|
-
return raw.split('\n').filter(Boolean).flatMap(line => {
|
|
155
|
-
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
156
|
-
});
|
|
157
|
-
} catch { return []; }
|
|
230
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, readPendingMeshCoordinatorEventsFromDisk(meshId));
|
|
158
231
|
}
|
|
159
232
|
|
|
160
233
|
/** Explicitly clear all pending coordinator events for a mesh. */
|
|
@@ -338,6 +411,80 @@ function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metad
|
|
|
338
411
|
return false;
|
|
339
412
|
}
|
|
340
413
|
|
|
414
|
+
function findRecentTerminalLedgerEvidence(args: {
|
|
415
|
+
meshId: string;
|
|
416
|
+
sessionId?: string;
|
|
417
|
+
nodeId?: string;
|
|
418
|
+
}): { kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
|
|
419
|
+
if (!args.sessionId && !args.nodeId) return null;
|
|
420
|
+
const entries = readLedgerEntries(args.meshId);
|
|
421
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
422
|
+
const entry = entries[i];
|
|
423
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
424
|
+
if (args.sessionId && entry.sessionId === args.sessionId) {
|
|
425
|
+
return { kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
426
|
+
}
|
|
427
|
+
if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
|
|
428
|
+
return { kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function buildLongGeneratingCompletionReconciliation(args: {
|
|
435
|
+
meshId: string;
|
|
436
|
+
nodeId?: string;
|
|
437
|
+
nodeLabel: string;
|
|
438
|
+
metadataEvent: Record<string, unknown>;
|
|
439
|
+
sourceInstanceId?: string;
|
|
440
|
+
}): Record<string, unknown> | null {
|
|
441
|
+
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
442
|
+
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
443
|
+
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
444
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
445
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
446
|
+
const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
|
|
447
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
|
|
448
|
+
const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
|
|
449
|
+
const explicitCompletionEvidence = Boolean(
|
|
450
|
+
finalSummary
|
|
451
|
+
|| workerResult
|
|
452
|
+
|| completionDiagnostic?.finalAssistantPresent === true
|
|
453
|
+
|| status === 'idle'
|
|
454
|
+
|| status === 'ready'
|
|
455
|
+
|| status === 'completed',
|
|
456
|
+
);
|
|
457
|
+
if (explicitCompletionEvidence) {
|
|
458
|
+
return {
|
|
459
|
+
...args.metadataEvent,
|
|
460
|
+
targetSessionId: sessionId,
|
|
461
|
+
providerType,
|
|
462
|
+
providerSessionId,
|
|
463
|
+
finalSummary,
|
|
464
|
+
source: 'long_generating_reconciliation',
|
|
465
|
+
reconciledFromEvent: 'monitor:long_generating',
|
|
466
|
+
timestamp: args.metadataEvent.timestamp ?? Date.now(),
|
|
467
|
+
completionDiagnostic: {
|
|
468
|
+
...(completionDiagnostic || {}),
|
|
469
|
+
reconciliationReason: 'provider_completion_evidence',
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const terminal = findRecentTerminalLedgerEvidence({
|
|
475
|
+
meshId: args.meshId,
|
|
476
|
+
sessionId: sessionId || undefined,
|
|
477
|
+
nodeId: nodeId || undefined,
|
|
478
|
+
});
|
|
479
|
+
if (!terminal) return null;
|
|
480
|
+
return {
|
|
481
|
+
...args.metadataEvent,
|
|
482
|
+
source: 'long_generating_terminal_ledger_suppression',
|
|
483
|
+
terminalLedgerKind: terminal.kind,
|
|
484
|
+
terminalLedgerAt: terminal.timestamp,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
341
488
|
|
|
342
489
|
export function tryAssignQueueTask(
|
|
343
490
|
components: DaemonComponents,
|
|
@@ -474,6 +621,10 @@ function nodeHasActiveAssignment(meshId: string, nodeId: string): boolean {
|
|
|
474
621
|
return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedNodeId === nodeId);
|
|
475
622
|
}
|
|
476
623
|
|
|
624
|
+
function sessionHasActiveAssignment(meshId: string, sessionId: string): boolean {
|
|
625
|
+
return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedSessionId === sessionId);
|
|
626
|
+
}
|
|
627
|
+
|
|
477
628
|
function liveSessionCountForNode(components: DaemonComponents, meshId: string, nodeId: string): number {
|
|
478
629
|
return components.instanceManager.getByCategory('cli').filter((inst: any) => {
|
|
479
630
|
const state = inst.getState();
|
|
@@ -739,6 +890,9 @@ function buildMeshSystemMessage(args: {
|
|
|
739
890
|
}): string {
|
|
740
891
|
const metadata = formatCompletionMetadata(args.metadataEvent);
|
|
741
892
|
if (args.event === 'agent:generating_completed') {
|
|
893
|
+
if (args.metadataEvent.source === 'long_generating_reconciliation') {
|
|
894
|
+
return `[System] ${args.nodeLabel} already has completion evidence${metadata}. The long-generating monitor reconciled the terminal handoff and marked the session complete; wait for the queued completion event/status refresh before doing any manual transcript check.`;
|
|
895
|
+
}
|
|
742
896
|
return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. This completion came from the agent status event path; use mesh_read_chat once to review its final progress, but do not poll repeatedly.`;
|
|
743
897
|
}
|
|
744
898
|
if (args.event === 'agent:waiting_approval') {
|
|
@@ -770,7 +924,7 @@ function buildMeshSystemMessage(args: {
|
|
|
770
924
|
return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
|
|
771
925
|
}
|
|
772
926
|
if (args.event === 'monitor:long_generating') {
|
|
773
|
-
return `[System] ${args.nodeLabel}
|
|
927
|
+
return `[System] ${args.nodeLabel} is still reported as generating after a long interval${metadata}. Wait for pendingCoordinatorEvents or a completion/status event; if the user explicitly asks for status, make one bounded status check and then wait again.`;
|
|
774
928
|
}
|
|
775
929
|
if (args.event === 'refine:accepted') {
|
|
776
930
|
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
@@ -865,6 +1019,34 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
865
1019
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
866
1020
|
}
|
|
867
1021
|
|
|
1022
|
+
if (args.event === 'monitor:long_generating') {
|
|
1023
|
+
const reconciledCompletion = buildLongGeneratingCompletionReconciliation({
|
|
1024
|
+
meshId: args.meshId,
|
|
1025
|
+
nodeId: args.nodeId,
|
|
1026
|
+
nodeLabel: args.nodeLabel,
|
|
1027
|
+
metadataEvent: args.metadataEvent,
|
|
1028
|
+
sourceInstanceId: args.sourceInstanceId,
|
|
1029
|
+
});
|
|
1030
|
+
if (reconciledCompletion?.source === 'long_generating_reconciliation') {
|
|
1031
|
+
LOG.info('MeshEvents', `Reconciled long-generating monitor to completion for session ${eventSessionId || '(unknown session)'}`);
|
|
1032
|
+
return injectMeshSystemMessage(components, {
|
|
1033
|
+
...args,
|
|
1034
|
+
event: 'agent:generating_completed',
|
|
1035
|
+
metadataEvent: reconciledCompletion,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
if (reconciledCompletion?.source === 'long_generating_terminal_ledger_suppression') {
|
|
1039
|
+
LOG.info('MeshEvents', `Suppressed long-generating monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
|
|
1040
|
+
return {
|
|
1041
|
+
success: true,
|
|
1042
|
+
forwarded: 0,
|
|
1043
|
+
suppressed: true,
|
|
1044
|
+
terminalLedgerEvidence: true,
|
|
1045
|
+
terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
868
1050
|
if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
|
|
869
1051
|
LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
|
|
870
1052
|
return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
|
|
@@ -872,6 +1054,25 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
872
1054
|
|
|
873
1055
|
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
874
1056
|
if (args.event === 'agent:generating_completed' && eventSessionId) {
|
|
1057
|
+
const terminal = findRecentTerminalLedgerEvidence({
|
|
1058
|
+
meshId: args.meshId,
|
|
1059
|
+
sessionId: eventSessionId,
|
|
1060
|
+
nodeId: eventNodeId || undefined,
|
|
1061
|
+
});
|
|
1062
|
+
if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
|
|
1063
|
+
const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
|
|
1064
|
+
const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
|
|
1065
|
+
const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
1066
|
+
const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
|
|
1067
|
+
if (
|
|
1068
|
+
(terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
|
|
1069
|
+
|| (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
|
|
1070
|
+
|| args.metadataEvent.source === 'long_generating_reconciliation'
|
|
1071
|
+
) {
|
|
1072
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1073
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
875
1076
|
const duplicateCompletion = isDuplicateMeshCompletionEvent({
|
|
876
1077
|
meshId: args.meshId,
|
|
877
1078
|
event: args.event,
|
|
@@ -1186,6 +1387,10 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
1186
1387
|
completedAt: readNonEmptyString(payload.completedAt),
|
|
1187
1388
|
retryOfJobId: readNonEmptyString(payload.retryOfJobId),
|
|
1188
1389
|
...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
|
|
1390
|
+
...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
|
|
1391
|
+
...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
|
|
1392
|
+
...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
|
|
1393
|
+
...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
|
|
1189
1394
|
...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
|
|
1190
1395
|
intentional: payload.intentional === true,
|
|
1191
1396
|
intentionalStop: payload.intentionalStop === true,
|
|
@@ -329,6 +329,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
329
329
|
private lastApprovalEventAt = 0;
|
|
330
330
|
private autoApproveBusy = false;
|
|
331
331
|
private autoApproveBusyTimer: NodeJS.Timeout | null = null;
|
|
332
|
+
private lastAutoApprovalSignature = '';
|
|
332
333
|
private controlValues: Record<string, string | number | boolean> = {};
|
|
333
334
|
private summaryMetadata: unknown = undefined;
|
|
334
335
|
private appliedEffectKeys = new Set<string>();
|
|
@@ -982,15 +983,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
982
983
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
983
984
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
984
985
|
// snapshots would write stray keys into the input once the modal dismisses.
|
|
985
|
-
|
|
986
|
+
// However, Claude Code can present a second approval immediately after the
|
|
987
|
+
// first. Resolve a changed modal signature even while the previous write is
|
|
988
|
+
// still inside the short busy window.
|
|
989
|
+
if (!autoApproveActive) {
|
|
990
|
+
this.lastAutoApprovalSignature = '';
|
|
991
|
+
return autoApproveActive;
|
|
992
|
+
}
|
|
993
|
+
const modal = adapterStatus.activeModal;
|
|
994
|
+
const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
|
|
995
|
+
const signature = [
|
|
996
|
+
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
997
|
+
Array.isArray(modal?.buttons) ? modal.buttons.join('|') : '',
|
|
998
|
+
buttonIndex,
|
|
999
|
+
].join('::');
|
|
1000
|
+
if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
|
|
986
1001
|
this.autoApproveBusy = true;
|
|
1002
|
+
this.lastAutoApprovalSignature = signature;
|
|
987
1003
|
if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
|
|
988
1004
|
this.autoApproveBusyTimer = setTimeout(() => {
|
|
989
1005
|
this.autoApproveBusy = false;
|
|
990
1006
|
this.autoApproveBusyTimer = null;
|
|
1007
|
+
this.lastAutoApprovalSignature = '';
|
|
991
1008
|
}, 2000);
|
|
992
|
-
const modal = adapterStatus.activeModal;
|
|
993
|
-
const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
|
|
994
1009
|
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
995
1010
|
setTimeout(() => {
|
|
996
1011
|
this.adapter.resolveModal(buttonIndex);
|
|
@@ -1130,7 +1145,31 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1130
1145
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
1131
1146
|
const agentKey = `${this.type}:cli`;
|
|
1132
1147
|
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
|
|
1148
|
+
const monitorParsedStatus: any = parsedStatus;
|
|
1133
1149
|
for (const me of monitorEvents) {
|
|
1150
|
+
if (
|
|
1151
|
+
me.type === 'monitor:long_generating'
|
|
1152
|
+
&& this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
|
|
1153
|
+
&& !this.hasAdapterPendingResponse()
|
|
1154
|
+
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
1155
|
+
) {
|
|
1156
|
+
this.pushEvent({
|
|
1157
|
+
event: 'agent:generating_completed',
|
|
1158
|
+
chatTitle,
|
|
1159
|
+
duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
|
|
1160
|
+
timestamp: me.timestamp,
|
|
1161
|
+
finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
|
|
1162
|
+
completionDiagnostic: {
|
|
1163
|
+
providerType: this.type,
|
|
1164
|
+
sessionId: this.instanceId,
|
|
1165
|
+
providerSessionId: this.providerSessionId || null,
|
|
1166
|
+
reconciliationReason: 'long_generating_monitor_final_summary',
|
|
1167
|
+
finalAssistantPresent: true,
|
|
1168
|
+
},
|
|
1169
|
+
});
|
|
1170
|
+
this.generatingStartedAt = 0;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1134
1173
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
1135
1174
|
}
|
|
1136
1175
|
}
|