@adhdev/daemon-core 0.9.82-rc.80 → 0.9.82-rc.82
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/index.js +150 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +150 -18
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +9 -0
- package/package.json +1 -1
- package/src/commands/chat-commands.ts +76 -13
- package/src/commands/cli-manager.ts +39 -1
- package/src/mesh/mesh-active-work.ts +25 -4
- package/src/mesh/mesh-events.ts +57 -0
- package/src/providers/read-chat-contract.ts +1 -1
|
@@ -33,6 +33,13 @@ export interface MeshActiveWorkSummary {
|
|
|
33
33
|
sourceCounts: Record<MeshActiveWorkSource, number>;
|
|
34
34
|
statusCounts: Record<MeshActiveWorkStatus, number>;
|
|
35
35
|
staleDirectCount: number;
|
|
36
|
+
/**
|
|
37
|
+
* When staleDirectCount > 0, this note clarifies that stale direct records are
|
|
38
|
+
* historical/recovery evidence — orphaned ledger entries whose original node or session
|
|
39
|
+
* is no longer present in the live mesh. They are NOT active or unresolved work items.
|
|
40
|
+
* The active queue (queue source) is the authoritative source for pending/assigned work.
|
|
41
|
+
*/
|
|
42
|
+
staleDirectNote?: string;
|
|
36
43
|
}
|
|
37
44
|
export interface BuildMeshActiveWorkOptions {
|
|
38
45
|
meshId: string;
|
|
@@ -47,5 +54,7 @@ export declare function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRec
|
|
|
47
54
|
export declare function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): {
|
|
48
55
|
activeWork: MeshActiveWorkRecord[];
|
|
49
56
|
staleDirectWork: MeshActiveWorkRecord[];
|
|
57
|
+
staleDirectWorkNote?: string;
|
|
58
|
+
terminalDirectWork: MeshActiveWorkRecord[];
|
|
50
59
|
summary: MeshActiveWorkSummary;
|
|
51
60
|
};
|
package/package.json
CHANGED
|
@@ -24,7 +24,8 @@ import { filterUserFacingChatMessages, normalizeChatMessages } from '../provider
|
|
|
24
24
|
const RECENT_SEND_WINDOW_MS = 1200;
|
|
25
25
|
export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
|
|
26
26
|
const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
|
|
27
|
-
const
|
|
27
|
+
const CLI_NATIVE_HISTORY_FRESH_MS = 5 * 60_000;
|
|
28
|
+
const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli']);
|
|
28
29
|
const recentSendByTarget = new Map<string, number>();
|
|
29
30
|
|
|
30
31
|
interface ApprovalSelectableInstance extends ProviderInstance {
|
|
@@ -152,7 +153,17 @@ function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
|
|
|
152
153
|
const instance = h.ctx.instanceManager?.getInstance(targetSessionId);
|
|
153
154
|
const state = instance?.getState?.();
|
|
154
155
|
const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
|
|
155
|
-
|
|
156
|
+
if (providerSessionId) return providerSessionId;
|
|
157
|
+
|
|
158
|
+
const currentSession = h.currentSession as any;
|
|
159
|
+
if (currentSession?.sessionId === targetSessionId) {
|
|
160
|
+
const currentProviderSessionId = typeof currentSession.providerSessionId === 'string'
|
|
161
|
+
? currentSession.providerSessionId.trim()
|
|
162
|
+
: '';
|
|
163
|
+
if (currentProviderSessionId) return currentProviderSessionId;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return targetSessionId;
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
function getInteractionId(args: any): string | undefined {
|
|
@@ -254,7 +265,9 @@ function buildCliMessageSourceProvenance(args: {
|
|
|
254
265
|
return {
|
|
255
266
|
selected: args.selected,
|
|
256
267
|
provider: args.provider,
|
|
268
|
+
providerType: args.provider,
|
|
257
269
|
...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
|
|
270
|
+
...(args.nativeHandle ? { nativeSessionId: args.nativeHandle } : {}),
|
|
258
271
|
...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
|
|
259
272
|
...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
|
|
260
273
|
...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
|
|
@@ -282,7 +295,7 @@ function buildNativeHistoryFallbackReason(args: {
|
|
|
282
295
|
safeMapping: boolean;
|
|
283
296
|
freshEnough: boolean;
|
|
284
297
|
}): string {
|
|
285
|
-
if (args.providerType
|
|
298
|
+
if (!supportsCliNativeTranscript(args.providerType)) return 'provider_native_transcript_not_supported';
|
|
286
299
|
if (args.nativeSource === 'native-unavailable') return 'native_history_unavailable';
|
|
287
300
|
if (args.nativeSource && args.nativeSource !== 'provider-native') return `native_history_source_${args.nativeSource}`;
|
|
288
301
|
if (args.nativeMessageCount <= 0) return 'native_history_empty';
|
|
@@ -291,8 +304,8 @@ function buildNativeHistoryFallbackReason(args: {
|
|
|
291
304
|
return 'native_history_not_selected';
|
|
292
305
|
}
|
|
293
306
|
|
|
294
|
-
function
|
|
295
|
-
return providerType
|
|
307
|
+
function supportsCliNativeTranscript(providerType: string): boolean {
|
|
308
|
+
return CLI_NATIVE_TRANSCRIPT_PROVIDERS.has(providerType);
|
|
296
309
|
}
|
|
297
310
|
|
|
298
311
|
function hasSafeNativeHistoryMapping(args: {
|
|
@@ -323,7 +336,7 @@ function isNativeHistoryFreshEnough(args: {
|
|
|
323
336
|
const ptyNewest = getMessageNewestReceivedAt(args.ptyMessages);
|
|
324
337
|
if (nativeNewest > 0 && nativeNewest >= ptyNewest) return true;
|
|
325
338
|
const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
|
|
326
|
-
if (sourceMtimeMs > 0 && Date.now() - sourceMtimeMs <=
|
|
339
|
+
if (sourceMtimeMs > 0 && Date.now() - sourceMtimeMs <= CLI_NATIVE_HISTORY_FRESH_MS) return true;
|
|
327
340
|
return ptyNewest === 0 && nativeNewest > 0;
|
|
328
341
|
}
|
|
329
342
|
|
|
@@ -390,7 +403,7 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
|
|
|
390
403
|
}
|
|
391
404
|
switch (raw) {
|
|
392
405
|
case 'starting':
|
|
393
|
-
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : '
|
|
406
|
+
return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'starting';
|
|
394
407
|
case 'stopped':
|
|
395
408
|
case 'disconnected':
|
|
396
409
|
case 'not_monitored':
|
|
@@ -413,7 +426,18 @@ function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal:
|
|
|
413
426
|
return true;
|
|
414
427
|
}
|
|
415
428
|
|
|
416
|
-
function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): string {
|
|
429
|
+
function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any, parsedMessages?: unknown[]): string {
|
|
430
|
+
const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
431
|
+
if (adapterRawStatus === 'starting'
|
|
432
|
+
&& isGeneratingLikeStatus(parsedStatus)
|
|
433
|
+
&& !hasNonEmptyModalButtons(activeModal)
|
|
434
|
+
&& Array.isArray(parsedMessages)
|
|
435
|
+
&& parsedMessages.length === 0
|
|
436
|
+
&& Array.isArray(adapterStatus?.messages)
|
|
437
|
+
&& adapterStatus.messages.length === 0
|
|
438
|
+
&& !(typeof adapter.isProcessing === 'function' && adapter.isProcessing())) {
|
|
439
|
+
return 'starting';
|
|
440
|
+
}
|
|
417
441
|
if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
|
|
418
442
|
return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
|
|
419
443
|
}
|
|
@@ -988,7 +1012,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
988
1012
|
? parsedRecord.coverage
|
|
989
1013
|
: undefined;
|
|
990
1014
|
const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
|
|
991
|
-
const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus);
|
|
1015
|
+
const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus, parsedRecord.messages);
|
|
992
1016
|
const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
|
|
993
1017
|
const parsedMessages = finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus);
|
|
994
1018
|
const returnedMessages = runtimeMessageMerger?.category === 'cli'
|
|
@@ -1005,13 +1029,13 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1005
1029
|
let messageSource = buildCliMessageSourceProvenance({
|
|
1006
1030
|
selected: 'pty-parser',
|
|
1007
1031
|
provider: adapter.cliType,
|
|
1008
|
-
fallbackReason:
|
|
1032
|
+
fallbackReason: supportsCliNativeTranscript(providerType) ? 'native_history_not_checked' : 'provider_native_transcript_not_supported',
|
|
1009
1033
|
ptyMessages: returnedMessages,
|
|
1010
1034
|
returnedMessages,
|
|
1011
1035
|
ptyStatusApprovalOnly: false,
|
|
1012
1036
|
});
|
|
1013
1037
|
|
|
1014
|
-
if (
|
|
1038
|
+
if (supportsCliNativeTranscript(providerType)) {
|
|
1015
1039
|
const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
|
|
1016
1040
|
const workspace = typeof args?.workspace === 'string'
|
|
1017
1041
|
? args.workspace
|
|
@@ -1128,7 +1152,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1128
1152
|
returnedStatus: String(returnedStatus || ''),
|
|
1129
1153
|
selectedMessageSource: (messageSource as any).selected,
|
|
1130
1154
|
messageSource,
|
|
1131
|
-
shouldPreferAdapterMessages: (messageSource as any).selected !== 'native-history',
|
|
1155
|
+
shouldPreferAdapterMessages: supportsCliNativeTranscript(providerType) && (messageSource as any).selected !== 'native-history',
|
|
1132
1156
|
parsedMsgCount: parsedRecord.messages.length,
|
|
1133
1157
|
returnedMsgCount: selectedMessages.length,
|
|
1134
1158
|
},
|
|
@@ -1159,9 +1183,48 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1159
1183
|
const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
|
|
1160
1184
|
? (history as any).providerSessionId
|
|
1161
1185
|
: historySessionId;
|
|
1186
|
+
const historyMessages = Array.isArray((history as any)?.messages)
|
|
1187
|
+
? normalizeChatMessages((history as any).messages as ChatMessage[])
|
|
1188
|
+
: [];
|
|
1189
|
+
const safeMapping = supportsCliNativeTranscript(agentStr)
|
|
1190
|
+
? hasSafeNativeHistoryMapping({
|
|
1191
|
+
historySessionId,
|
|
1192
|
+
providerSessionId: historyProviderSessionId,
|
|
1193
|
+
workspace,
|
|
1194
|
+
nativeMessages: historyMessages,
|
|
1195
|
+
})
|
|
1196
|
+
: false;
|
|
1197
|
+
const nativeSelected = supportsCliNativeTranscript(agentStr)
|
|
1198
|
+
&& (history as any).source === 'provider-native'
|
|
1199
|
+
&& historyMessages.length > 0
|
|
1200
|
+
&& safeMapping;
|
|
1201
|
+
const messageSource = buildCliMessageSourceProvenance({
|
|
1202
|
+
selected: nativeSelected ? 'native-history' : 'pty-parser',
|
|
1203
|
+
provider: agentStr,
|
|
1204
|
+
nativeHandle: historyProviderSessionId || historySessionId,
|
|
1205
|
+
fallbackReason: nativeSelected
|
|
1206
|
+
? undefined
|
|
1207
|
+
: buildNativeHistoryFallbackReason({
|
|
1208
|
+
providerType: agentStr,
|
|
1209
|
+
nativeSource: (history as any).source,
|
|
1210
|
+
nativeMessageCount: historyMessages.length,
|
|
1211
|
+
safeMapping,
|
|
1212
|
+
freshEnough: true,
|
|
1213
|
+
}),
|
|
1214
|
+
nativeSource: (history as any).source,
|
|
1215
|
+
sourcePath: (history as any).sourcePath,
|
|
1216
|
+
sourceMtimeMs: (history as any).sourceMtimeMs,
|
|
1217
|
+
nativeMessages: historyMessages,
|
|
1218
|
+
returnedMessages: historyMessages,
|
|
1219
|
+
safeMapping,
|
|
1220
|
+
freshEnough: true,
|
|
1221
|
+
ptyStatusApprovalOnly: false,
|
|
1222
|
+
});
|
|
1162
1223
|
return buildReadChatCommandResult({
|
|
1163
|
-
messages:
|
|
1224
|
+
messages: historyMessages,
|
|
1164
1225
|
status: 'idle',
|
|
1226
|
+
messageSource,
|
|
1227
|
+
transcriptProvenance: messageSource,
|
|
1165
1228
|
...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
|
|
1166
1229
|
...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
|
|
1167
1230
|
...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')
|
|
@@ -81,6 +81,7 @@ export interface CliManagerDeps {
|
|
|
81
81
|
type CommandResult = { success: boolean;[key: string]: unknown };
|
|
82
82
|
|
|
83
83
|
const BUSY_AGENT_STATUSES = new Set(['generating', 'running', 'streaming', 'starting', 'busy', 'waiting', 'waiting_approval', 'long_generating']);
|
|
84
|
+
const ZERO_MESSAGE_STARTING_SEND_WAIT_MS = 2_000;
|
|
84
85
|
|
|
85
86
|
function normalizeAgentStatus(value: unknown): string {
|
|
86
87
|
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
@@ -104,6 +105,24 @@ function hasAdapterPendingResponse(adapter: any): boolean {
|
|
|
104
105
|
return false;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
function countMessages(value: unknown): number {
|
|
109
|
+
return Array.isArray(value) ? value.length : 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasZeroMessageStartingLaunch(adapter: any): boolean {
|
|
113
|
+
const adapterStatus = adapter?.getStatus?.({ allowParse: false }) ?? adapter?.getStatus?.() ?? {};
|
|
114
|
+
const parsedStatus = typeof adapter?.getScriptParsedStatus === 'function'
|
|
115
|
+
? adapter.getScriptParsedStatus()
|
|
116
|
+
: {};
|
|
117
|
+
const adapterRawStatus = normalizeAgentStatus(adapterStatus?.status);
|
|
118
|
+
const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
|
|
119
|
+
if (adapterRawStatus !== 'starting') return false;
|
|
120
|
+
if (parsedRawStatus && parsedRawStatus !== 'starting' && parsedRawStatus !== 'generating') return false;
|
|
121
|
+
if (hasNonEmptyModalButtons(adapterStatus?.activeModal ?? adapterStatus?.modal ?? parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
122
|
+
if (countMessages(adapterStatus?.messages) > 0 || countMessages(parsedStatus?.messages) > 0) return false;
|
|
123
|
+
return !hasAdapterPendingResponse(adapter);
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
function shouldSuppressStaleParsedBusyStatus(adapterStatus: string, parsedStatus: any, adapter: any): boolean {
|
|
108
127
|
const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
|
|
109
128
|
if (!BUSY_AGENT_STATUSES.has(parsedRawStatus)) return false;
|
|
@@ -130,6 +149,20 @@ function getEffectiveAgentSendStatus(adapter: any): string {
|
|
|
130
149
|
return adapterStatus;
|
|
131
150
|
}
|
|
132
151
|
|
|
152
|
+
async function waitForZeroMessageStartingLaunch(adapter: any): Promise<boolean> {
|
|
153
|
+
try {
|
|
154
|
+
if (!hasZeroMessageStartingLaunch(adapter)) return false;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
await new Promise(resolve => setTimeout(resolve, ZERO_MESSAGE_STARTING_SEND_WAIT_MS));
|
|
159
|
+
try {
|
|
160
|
+
return hasZeroMessageStartingLaunch(adapter);
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
133
166
|
export interface CliTransportFactoryParams {
|
|
134
167
|
runtimeId: string;
|
|
135
168
|
providerType: string;
|
|
@@ -1123,7 +1156,12 @@ export class DaemonCliManager {
|
|
|
1123
1156
|
const { adapter, key } = found;
|
|
1124
1157
|
|
|
1125
1158
|
if (action === 'send_chat') {
|
|
1126
|
-
|
|
1159
|
+
let currentStatus = getEffectiveAgentSendStatus(adapter);
|
|
1160
|
+
if (currentStatus === 'starting' && await waitForZeroMessageStartingLaunch(adapter)) {
|
|
1161
|
+
currentStatus = 'idle';
|
|
1162
|
+
} else if (currentStatus === 'starting') {
|
|
1163
|
+
currentStatus = getEffectiveAgentSendStatus(adapter);
|
|
1164
|
+
}
|
|
1127
1165
|
if (BUSY_AGENT_STATUSES.has(currentStatus)) {
|
|
1128
1166
|
return {
|
|
1129
1167
|
success: false,
|
|
@@ -36,6 +36,13 @@ export interface MeshActiveWorkSummary {
|
|
|
36
36
|
sourceCounts: Record<MeshActiveWorkSource, number>;
|
|
37
37
|
statusCounts: Record<MeshActiveWorkStatus, number>;
|
|
38
38
|
staleDirectCount: number;
|
|
39
|
+
/**
|
|
40
|
+
* When staleDirectCount > 0, this note clarifies that stale direct records are
|
|
41
|
+
* historical/recovery evidence — orphaned ledger entries whose original node or session
|
|
42
|
+
* is no longer present in the live mesh. They are NOT active or unresolved work items.
|
|
43
|
+
* The active queue (queue source) is the authoritative source for pending/assigned work.
|
|
44
|
+
*/
|
|
45
|
+
staleDirectNote?: string;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
export interface BuildMeshActiveWorkOptions {
|
|
@@ -134,6 +141,7 @@ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]):
|
|
|
134
141
|
sourceCounts[item.source] += 1;
|
|
135
142
|
statusCounts[item.status] += 1;
|
|
136
143
|
}
|
|
144
|
+
const staleDirectCount = activeWork.filter(item => item.source === 'direct' && item.staleReason).length;
|
|
137
145
|
return {
|
|
138
146
|
totalActiveCount: activeWork.length,
|
|
139
147
|
queueActiveCount: sourceCounts.queue,
|
|
@@ -144,14 +152,17 @@ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]):
|
|
|
144
152
|
idleCount: statusCounts.idle,
|
|
145
153
|
sourceCounts,
|
|
146
154
|
statusCounts,
|
|
147
|
-
staleDirectCount
|
|
155
|
+
staleDirectCount,
|
|
156
|
+
...(staleDirectCount > 0 ? { staleDirectNote: 'Stale direct records are orphaned ledger entries whose node/session no longer exists. They are historical recovery evidence only — not active or unresolved work. The queue (source: queue) is authoritative for pending/assigned tasks.' } : {}),
|
|
148
157
|
};
|
|
149
158
|
}
|
|
150
159
|
|
|
151
|
-
|
|
160
|
+
|
|
161
|
+
export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeWork: MeshActiveWorkRecord[]; staleDirectWork: MeshActiveWorkRecord[]; staleDirectWorkNote?: string; terminalDirectWork: MeshActiveWorkRecord[]; summary: MeshActiveWorkSummary } {
|
|
152
162
|
const now = opts.now ?? Date.now();
|
|
153
163
|
const records: MeshActiveWorkRecord[] = [];
|
|
154
164
|
const staleDirectWork: MeshActiveWorkRecord[] = [];
|
|
165
|
+
const terminalDirectWork: MeshActiveWorkRecord[] = [];
|
|
155
166
|
|
|
156
167
|
for (const task of opts.queue || []) {
|
|
157
168
|
if (task.status !== 'pending' && task.status !== 'assigned') continue;
|
|
@@ -184,7 +195,6 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
184
195
|
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
185
196
|
const status = terminalStatus || live.status || 'assigned';
|
|
186
197
|
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
187
|
-
if (terminalRow && opts.includeTerminalDirect !== true) continue;
|
|
188
198
|
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
189
199
|
const { title, summary } = summarizeMessage(message);
|
|
190
200
|
const record: MeshActiveWorkRecord = {
|
|
@@ -207,6 +217,10 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
207
217
|
terminalAt: terminal?.timestamp,
|
|
208
218
|
staleReason: live.staleReason,
|
|
209
219
|
};
|
|
220
|
+
if (terminalRow) {
|
|
221
|
+
terminalDirectWork.push(record);
|
|
222
|
+
if (opts.includeTerminalDirect !== true) continue;
|
|
223
|
+
}
|
|
210
224
|
if (live.staleReason && !terminalRow) {
|
|
211
225
|
staleDirectWork.push(record);
|
|
212
226
|
continue;
|
|
@@ -216,7 +230,14 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
216
230
|
|
|
217
231
|
records.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
218
232
|
staleDirectWork.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
233
|
+
terminalDirectWork.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
219
234
|
const summary = buildMeshActiveWorkSummary(records);
|
|
220
235
|
summary.staleDirectCount = staleDirectWork.length;
|
|
221
|
-
|
|
236
|
+
const staleDirectWorkNote = staleDirectWork.length > 0
|
|
237
|
+
? 'These are orphaned ledger entries whose original node or session no longer exists in the live mesh. They are historical/recovery evidence only — not active or unresolved work. Do not treat staleDirectCount as a status mismatch; use the queue (source: queue) as authoritative for pending/assigned tasks.'
|
|
238
|
+
: undefined;
|
|
239
|
+
if (staleDirectWorkNote) {
|
|
240
|
+
summary.staleDirectNote = staleDirectWorkNote;
|
|
241
|
+
}
|
|
242
|
+
return { activeWork: records, staleDirectWork, staleDirectWorkNote, terminalDirectWork, summary };
|
|
222
243
|
}
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -82,6 +82,31 @@ function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEve
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string {
|
|
86
|
+
const metadata = readRecord(event.metadataEvent) || {};
|
|
87
|
+
const sessionId = resolveEventSessionId(metadata);
|
|
88
|
+
const providerSessionId = readNonEmptyString(metadata.providerSessionId);
|
|
89
|
+
const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
|
|
90
|
+
const jobId = readRefineJobId(event);
|
|
91
|
+
const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
|
|
92
|
+
return [
|
|
93
|
+
event.meshId,
|
|
94
|
+
event.event,
|
|
95
|
+
event.nodeId || '',
|
|
96
|
+
sessionId || '',
|
|
97
|
+
providerSessionId || '',
|
|
98
|
+
taskId || '',
|
|
99
|
+
jobId || '',
|
|
100
|
+
timestamp || '',
|
|
101
|
+
].join('::');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
105
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
106
|
+
if (!fingerprint.trim()) return false;
|
|
107
|
+
return getPendingMeshCoordinatorEvents(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
function getPendingEventsPath(meshId: string): string {
|
|
86
111
|
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
87
112
|
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
@@ -93,6 +118,10 @@ export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEv
|
|
|
93
118
|
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
|
|
94
119
|
return true;
|
|
95
120
|
}
|
|
121
|
+
if (hasPendingCoordinatorEventDuplicate(event)) {
|
|
122
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
96
125
|
appendFileSync(getPendingEventsPath(event.meshId), JSON.stringify(event) + '\n', 'utf-8');
|
|
97
126
|
return true;
|
|
98
127
|
} catch (e: any) {
|
|
@@ -1071,6 +1100,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1071
1100
|
return true;
|
|
1072
1101
|
});
|
|
1073
1102
|
|
|
1103
|
+
// Refine terminal events (refine:completed, refine:failed) are coordinator-delivered
|
|
1104
|
+
// synchronously; only buffer them for MCP when no CLI coordinator is present.
|
|
1105
|
+
// Agent runtime events (agent:*) use dual delivery so both CLI and MCP coordinators
|
|
1106
|
+
// receive them regardless of whether a live CLI coordinator session is active.
|
|
1107
|
+
const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
|
|
1108
|
+
|
|
1074
1109
|
if (coordinatorInstances.length === 0) {
|
|
1075
1110
|
// No CLI coordinator session found — buffer for MCP-based coordinators.
|
|
1076
1111
|
if (queuePendingMeshCoordinatorEvent({
|
|
@@ -1091,6 +1126,28 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1091
1126
|
return { success: true, forwarded: 0 };
|
|
1092
1127
|
}
|
|
1093
1128
|
|
|
1129
|
+
// CLI coordinator is present. For non-refine events, also buffer for MCP coordinators
|
|
1130
|
+
// that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
|
|
1131
|
+
// forwarded directly only — they must not accumulate in the pending queue when a live
|
|
1132
|
+
// coordinator already received them.
|
|
1133
|
+
if (!isRefineTerminalEvent) {
|
|
1134
|
+
if (queuePendingMeshCoordinatorEvent({
|
|
1135
|
+
event: args.event,
|
|
1136
|
+
meshId: args.meshId,
|
|
1137
|
+
nodeLabel: args.nodeLabel,
|
|
1138
|
+
nodeId: args.nodeId || undefined,
|
|
1139
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
1140
|
+
metadataEvent: {
|
|
1141
|
+
...args.metadataEvent,
|
|
1142
|
+
...(recoveryContext ? { recoveryContext } : {}),
|
|
1143
|
+
},
|
|
1144
|
+
coordinatorMessage: messageText,
|
|
1145
|
+
queuedAt: Date.now(),
|
|
1146
|
+
})) {
|
|
1147
|
+
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1094
1151
|
for (const coord of coordinatorInstances) {
|
|
1095
1152
|
const coordState = coord.getState();
|
|
1096
1153
|
LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}`);
|
|
@@ -2,7 +2,7 @@ import type { MessagePart, ModalInfo, ReadChatResult } from './contracts.js'
|
|
|
2
2
|
import { normalizeMessageParts } from './contracts.js'
|
|
3
3
|
import type { ChatBubbleState, ChatMessage } from '../types.js'
|
|
4
4
|
|
|
5
|
-
const VALID_STATUSES = ['idle', 'generating', 'waiting_approval', 'error', 'panel_hidden', 'streaming', 'long_generating'] as const
|
|
5
|
+
const VALID_STATUSES = ['idle', 'generating', 'waiting_approval', 'error', 'panel_hidden', 'starting', 'streaming', 'long_generating'] as const
|
|
6
6
|
const VALID_ROLES = ['user', 'assistant', 'system', 'human'] as const
|
|
7
7
|
const VALID_BUBBLE_STATES = ['draft', 'streaming', 'final', 'removed'] as const
|
|
8
8
|
const VALID_TURN_STATUSES = ['open', 'waiting_approval', 'complete', 'error'] as const
|