@adhdev/daemon-core 0.9.82-rc.70 → 0.9.82-rc.72
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 +285 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +286 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/chat-commands.ts +242 -7
- package/src/commands/cli-manager.ts +19 -0
- package/src/commands/router.ts +80 -13
- package/src/mesh/coordinator-prompt.ts +2 -1
- package/src/providers/cli-provider-instance.ts +3 -1
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ 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 CODEX_NATIVE_HISTORY_FRESH_MS = 5 * 60_000;
|
|
27
28
|
const recentSendByTarget = new Map<string, number>();
|
|
28
29
|
|
|
29
30
|
interface ApprovalSelectableInstance extends ProviderInstance {
|
|
@@ -221,6 +222,114 @@ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[]
|
|
|
221
222
|
return normalizeChatMessages(messages);
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
function getMessageNewestReceivedAt(messages: Array<{ receivedAt?: unknown; timestamp?: unknown }>): number {
|
|
226
|
+
let newest = 0;
|
|
227
|
+
for (const message of messages) {
|
|
228
|
+
const receivedAt = Number(message?.receivedAt ?? message?.timestamp ?? 0);
|
|
229
|
+
if (Number.isFinite(receivedAt) && receivedAt > newest) newest = receivedAt;
|
|
230
|
+
}
|
|
231
|
+
return newest;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildCliMessageSourceProvenance(args: {
|
|
235
|
+
selected: 'native-history' | 'pty-parser';
|
|
236
|
+
provider: string;
|
|
237
|
+
nativeHandle?: string;
|
|
238
|
+
fallbackReason?: string;
|
|
239
|
+
nativeSource?: string;
|
|
240
|
+
sourcePath?: string;
|
|
241
|
+
sourceMtimeMs?: number;
|
|
242
|
+
nativeMessages?: ChatMessage[];
|
|
243
|
+
ptyMessages?: ChatMessage[];
|
|
244
|
+
returnedMessages?: ChatMessage[];
|
|
245
|
+
safeMapping?: boolean;
|
|
246
|
+
freshEnough?: boolean;
|
|
247
|
+
ptyStatusApprovalOnly?: boolean;
|
|
248
|
+
}): Record<string, unknown> {
|
|
249
|
+
const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
|
|
250
|
+
const sourceMtimeAgeMs = sourceMtimeMs > 0 ? Math.max(0, Date.now() - sourceMtimeMs) : undefined;
|
|
251
|
+
const nativeMessages = args.nativeMessages || [];
|
|
252
|
+
const ptyMessages = args.ptyMessages || [];
|
|
253
|
+
const returnedMessages = args.returnedMessages || [];
|
|
254
|
+
return {
|
|
255
|
+
selected: args.selected,
|
|
256
|
+
provider: args.provider,
|
|
257
|
+
...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
|
|
258
|
+
...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
|
|
259
|
+
...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
|
|
260
|
+
...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
|
|
261
|
+
ptyStatusApprovalOnly: args.ptyStatusApprovalOnly === true,
|
|
262
|
+
staleness: {
|
|
263
|
+
sourceMtimeMs: sourceMtimeMs || undefined,
|
|
264
|
+
sourceMtimeAgeMs,
|
|
265
|
+
nativeNewestMessageAt: getMessageNewestReceivedAt(nativeMessages),
|
|
266
|
+
ptyNewestMessageAt: getMessageNewestReceivedAt(ptyMessages),
|
|
267
|
+
freshEnough: args.freshEnough === true,
|
|
268
|
+
},
|
|
269
|
+
coverage: {
|
|
270
|
+
nativeMessageCount: nativeMessages.length,
|
|
271
|
+
ptyMessageCount: ptyMessages.length,
|
|
272
|
+
returnedMessageCount: returnedMessages.length,
|
|
273
|
+
safeMapping: args.safeMapping === true,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildNativeHistoryFallbackReason(args: {
|
|
279
|
+
providerType: string;
|
|
280
|
+
nativeSource?: string;
|
|
281
|
+
nativeMessageCount: number;
|
|
282
|
+
safeMapping: boolean;
|
|
283
|
+
freshEnough: boolean;
|
|
284
|
+
}): string {
|
|
285
|
+
if (args.providerType !== 'codex-cli') return 'provider_not_codex_cli';
|
|
286
|
+
if (args.nativeSource === 'native-unavailable') return 'native_history_unavailable';
|
|
287
|
+
if (args.nativeSource && args.nativeSource !== 'provider-native') return `native_history_source_${args.nativeSource}`;
|
|
288
|
+
if (args.nativeMessageCount <= 0) return 'native_history_empty';
|
|
289
|
+
if (!args.safeMapping) return 'native_history_not_safely_mapped';
|
|
290
|
+
if (!args.freshEnough) return 'native_history_stale';
|
|
291
|
+
return 'native_history_not_selected';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isCodexCliProvider(providerType: string): boolean {
|
|
295
|
+
return providerType === 'codex-cli';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function hasSafeNativeHistoryMapping(args: {
|
|
299
|
+
historySessionId?: string;
|
|
300
|
+
providerSessionId?: string;
|
|
301
|
+
workspace?: string;
|
|
302
|
+
nativeMessages: ChatMessage[];
|
|
303
|
+
}): boolean {
|
|
304
|
+
const explicitSessionId = String(args.historySessionId || args.providerSessionId || '').trim();
|
|
305
|
+
if (explicitSessionId) {
|
|
306
|
+
const messageSessionIds = args.nativeMessages
|
|
307
|
+
.map((message: any) => typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '')
|
|
308
|
+
.filter(Boolean);
|
|
309
|
+
if (messageSessionIds.length === 0) return true;
|
|
310
|
+
return messageSessionIds.some((id) => id === explicitSessionId);
|
|
311
|
+
}
|
|
312
|
+
const workspace = String(args.workspace || '').trim();
|
|
313
|
+
if (!workspace) return false;
|
|
314
|
+
return args.nativeMessages.some((message: any) => String(message?.workspace || '').trim() === workspace);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isNativeHistoryFreshEnough(args: {
|
|
318
|
+
sourceMtimeMs?: number;
|
|
319
|
+
nativeMessages: ChatMessage[];
|
|
320
|
+
ptyMessages: ChatMessage[];
|
|
321
|
+
}): boolean {
|
|
322
|
+
const nativeNewest = getMessageNewestReceivedAt(args.nativeMessages);
|
|
323
|
+
const ptyNewest = getMessageNewestReceivedAt(args.ptyMessages);
|
|
324
|
+
if (nativeNewest > 0 && nativeNewest >= ptyNewest) return true;
|
|
325
|
+
const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
|
|
326
|
+
if (sourceMtimeMs > 0 && Date.now() - sourceMtimeMs <= CODEX_NATIVE_HISTORY_FRESH_MS) return true;
|
|
327
|
+
return ptyNewest === 0 && nativeNewest > 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function shouldPreserveReadChatPayloadField(key: string): boolean {
|
|
331
|
+
return key === 'messageSource' || key === 'transcriptProvenance';
|
|
332
|
+
}
|
|
224
333
|
|
|
225
334
|
function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
|
|
226
335
|
const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
|
|
@@ -356,6 +465,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
|
|
|
356
465
|
return {
|
|
357
466
|
success: true,
|
|
358
467
|
...validatedPayload,
|
|
468
|
+
...Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key))),
|
|
359
469
|
messages: sync.messages,
|
|
360
470
|
totalMessages: sync.totalMessages,
|
|
361
471
|
...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
|
|
@@ -584,6 +694,8 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
|
|
|
584
694
|
adapterStatus: debugReadChat.adapterStatus,
|
|
585
695
|
parsedStatus: debugReadChat.parsedStatus,
|
|
586
696
|
returnedStatus: debugReadChat.returnedStatus,
|
|
697
|
+
selectedMessageSource: debugReadChat.selectedMessageSource,
|
|
698
|
+
messageSource: debugReadChat.messageSource,
|
|
587
699
|
parsedMsgCount: debugReadChat.parsedMsgCount,
|
|
588
700
|
returnedMsgCount: debugReadChat.returnedMsgCount,
|
|
589
701
|
shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
|
|
@@ -646,6 +758,8 @@ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Pr
|
|
|
646
758
|
providerSessionId: readResult.providerSessionId,
|
|
647
759
|
transcriptAuthority: readResult.transcriptAuthority,
|
|
648
760
|
coverage: readResult.coverage,
|
|
761
|
+
messageSource: readResult.messageSource,
|
|
762
|
+
transcriptProvenance: readResult.transcriptProvenance,
|
|
649
763
|
activeModal: readResult.activeModal,
|
|
650
764
|
messagesTail: Array.isArray(readResult.messages) ? readResult.messages.slice(-20) : [],
|
|
651
765
|
debugReadChat: readResult.debugReadChat,
|
|
@@ -882,25 +996,146 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
882
996
|
&& typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
|
|
883
997
|
? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
|
|
884
998
|
: parsedMessages;
|
|
999
|
+
const providerType = provider?.type || adapter.cliType;
|
|
1000
|
+
let selectedMessages = returnedMessages;
|
|
1001
|
+
let selectedTitle = title;
|
|
1002
|
+
let selectedProviderSessionId = providerSessionId;
|
|
1003
|
+
let selectedTranscriptAuthority = transcriptAuthority;
|
|
1004
|
+
let selectedCoverage = coverage;
|
|
1005
|
+
let messageSource = buildCliMessageSourceProvenance({
|
|
1006
|
+
selected: 'pty-parser',
|
|
1007
|
+
provider: adapter.cliType,
|
|
1008
|
+
fallbackReason: isCodexCliProvider(providerType) ? 'native_history_not_checked' : 'provider_not_codex_cli',
|
|
1009
|
+
ptyMessages: returnedMessages,
|
|
1010
|
+
returnedMessages,
|
|
1011
|
+
ptyStatusApprovalOnly: false,
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
if (isCodexCliProvider(providerType)) {
|
|
1015
|
+
const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
|
|
1016
|
+
const workspace = typeof args?.workspace === 'string'
|
|
1017
|
+
? args.workspace
|
|
1018
|
+
: typeof (h.currentSession as any)?.workspace === 'string'
|
|
1019
|
+
? (h.currentSession as any).workspace
|
|
1020
|
+
: typeof adapter.workingDir === 'string'
|
|
1021
|
+
? adapter.workingDir
|
|
1022
|
+
: undefined;
|
|
1023
|
+
const nativeHistoryLimit = Math.max(
|
|
1024
|
+
normalizeReadChatTailLimit(args) || 0,
|
|
1025
|
+
returnedMessages.length,
|
|
1026
|
+
200,
|
|
1027
|
+
);
|
|
1028
|
+
let nativeHistory: ReturnType<typeof readProviderChatHistory> | null = null;
|
|
1029
|
+
try {
|
|
1030
|
+
nativeHistory = readProviderChatHistory(agentStr, {
|
|
1031
|
+
canonicalHistory: provider?.canonicalHistory,
|
|
1032
|
+
historySessionId,
|
|
1033
|
+
workspace,
|
|
1034
|
+
offset: 0,
|
|
1035
|
+
limit: nativeHistoryLimit,
|
|
1036
|
+
excludeRecentCount: 0,
|
|
1037
|
+
historyBehavior: provider?.historyBehavior,
|
|
1038
|
+
scripts: provider?.scripts as any,
|
|
1039
|
+
});
|
|
1040
|
+
} catch (error: any) {
|
|
1041
|
+
const fallbackReason = `native_history_error:${error?.message || String(error)}`;
|
|
1042
|
+
messageSource = buildCliMessageSourceProvenance({
|
|
1043
|
+
selected: 'pty-parser',
|
|
1044
|
+
provider: adapter.cliType,
|
|
1045
|
+
fallbackReason,
|
|
1046
|
+
ptyMessages: returnedMessages,
|
|
1047
|
+
returnedMessages,
|
|
1048
|
+
ptyStatusApprovalOnly: false,
|
|
1049
|
+
});
|
|
1050
|
+
nativeHistory = null;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (nativeHistory) {
|
|
1054
|
+
const nativeMessages = Array.isArray((nativeHistory as any).messages)
|
|
1055
|
+
? normalizeChatMessages((nativeHistory as any).messages as ChatMessage[])
|
|
1056
|
+
: [];
|
|
1057
|
+
const historyProviderSessionId = typeof (nativeHistory as any)?.providerSessionId === 'string'
|
|
1058
|
+
? (nativeHistory as any).providerSessionId
|
|
1059
|
+
: historySessionId;
|
|
1060
|
+
const safeMapping = hasSafeNativeHistoryMapping({
|
|
1061
|
+
historySessionId,
|
|
1062
|
+
providerSessionId,
|
|
1063
|
+
workspace,
|
|
1064
|
+
nativeMessages,
|
|
1065
|
+
});
|
|
1066
|
+
const freshEnough = isNativeHistoryFreshEnough({
|
|
1067
|
+
sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
|
|
1068
|
+
nativeMessages,
|
|
1069
|
+
ptyMessages: returnedMessages,
|
|
1070
|
+
});
|
|
1071
|
+
if ((nativeHistory as any).source === 'provider-native' && nativeMessages.length > 0 && safeMapping && freshEnough) {
|
|
1072
|
+
selectedMessages = finalizeStreamingMessagesWhenIdle(nativeMessages, returnedStatus);
|
|
1073
|
+
selectedProviderSessionId = historyProviderSessionId || providerSessionId;
|
|
1074
|
+
selectedTranscriptAuthority = 'provider';
|
|
1075
|
+
selectedCoverage = (nativeHistory as any).hasMore ? 'tail' : 'full';
|
|
1076
|
+
messageSource = buildCliMessageSourceProvenance({
|
|
1077
|
+
selected: 'native-history',
|
|
1078
|
+
provider: adapter.cliType,
|
|
1079
|
+
nativeHandle: selectedProviderSessionId || historySessionId,
|
|
1080
|
+
nativeSource: (nativeHistory as any).source,
|
|
1081
|
+
sourcePath: (nativeHistory as any).sourcePath,
|
|
1082
|
+
sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
|
|
1083
|
+
nativeMessages,
|
|
1084
|
+
ptyMessages: returnedMessages,
|
|
1085
|
+
returnedMessages: selectedMessages,
|
|
1086
|
+
safeMapping,
|
|
1087
|
+
freshEnough,
|
|
1088
|
+
ptyStatusApprovalOnly: true,
|
|
1089
|
+
});
|
|
1090
|
+
} else {
|
|
1091
|
+
const fallbackReason = buildNativeHistoryFallbackReason({
|
|
1092
|
+
providerType,
|
|
1093
|
+
nativeSource: (nativeHistory as any).source,
|
|
1094
|
+
nativeMessageCount: nativeMessages.length,
|
|
1095
|
+
safeMapping,
|
|
1096
|
+
freshEnough,
|
|
1097
|
+
});
|
|
1098
|
+
messageSource = buildCliMessageSourceProvenance({
|
|
1099
|
+
selected: 'pty-parser',
|
|
1100
|
+
provider: adapter.cliType,
|
|
1101
|
+
nativeHandle: historyProviderSessionId || historySessionId,
|
|
1102
|
+
fallbackReason,
|
|
1103
|
+
nativeSource: (nativeHistory as any).source,
|
|
1104
|
+
sourcePath: (nativeHistory as any).sourcePath,
|
|
1105
|
+
sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
|
|
1106
|
+
nativeMessages,
|
|
1107
|
+
ptyMessages: returnedMessages,
|
|
1108
|
+
returnedMessages,
|
|
1109
|
+
safeMapping,
|
|
1110
|
+
freshEnough,
|
|
1111
|
+
ptyStatusApprovalOnly: false,
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
885
1116
|
LOG.debug('Command', `[read_chat] cli-like parsed provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord.status || '')} parsedMsgCount=${parsedRecord.messages.length} returnedMsgCount=${returnedMessages.length}`);
|
|
886
1117
|
return buildReadChatCommandResult({
|
|
887
|
-
messages:
|
|
1118
|
+
messages: selectedMessages,
|
|
888
1119
|
status: returnedStatus,
|
|
889
1120
|
activeModal,
|
|
1121
|
+
messageSource,
|
|
1122
|
+
transcriptProvenance: messageSource,
|
|
890
1123
|
debugReadChat: {
|
|
891
1124
|
provider: adapter.cliType,
|
|
892
1125
|
targetSessionId: String(args?.targetSessionId || ''),
|
|
893
1126
|
adapterStatus: String(adapterStatus.status || ''),
|
|
894
1127
|
parsedStatus: String(parsedRecord.status || ''),
|
|
895
1128
|
returnedStatus: String(returnedStatus || ''),
|
|
896
|
-
|
|
1129
|
+
selectedMessageSource: (messageSource as any).selected,
|
|
1130
|
+
messageSource,
|
|
1131
|
+
shouldPreferAdapterMessages: (messageSource as any).selected !== 'native-history',
|
|
897
1132
|
parsedMsgCount: parsedRecord.messages.length,
|
|
898
|
-
returnedMsgCount:
|
|
1133
|
+
returnedMsgCount: selectedMessages.length,
|
|
899
1134
|
},
|
|
900
|
-
...(
|
|
901
|
-
...(
|
|
902
|
-
...(
|
|
903
|
-
...(
|
|
1135
|
+
...(selectedTitle ? { title: selectedTitle } : {}),
|
|
1136
|
+
...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
|
|
1137
|
+
...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
|
|
1138
|
+
...(selectedCoverage ? { coverage: selectedCoverage } : {}),
|
|
904
1139
|
}, args);
|
|
905
1140
|
}
|
|
906
1141
|
const historyLimit = normalizeReadChatTailLimit(args);
|
|
@@ -80,6 +80,12 @@ export interface CliManagerDeps {
|
|
|
80
80
|
|
|
81
81
|
type CommandResult = { success: boolean;[key: string]: unknown };
|
|
82
82
|
|
|
83
|
+
const BUSY_AGENT_STATUSES = new Set(['generating', 'running', 'streaming', 'starting', 'busy', 'waiting', 'waiting_approval', 'long_generating']);
|
|
84
|
+
|
|
85
|
+
function normalizeAgentStatus(value: unknown): string {
|
|
86
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
export interface CliTransportFactoryParams {
|
|
84
90
|
runtimeId: string;
|
|
85
91
|
providerType: string;
|
|
@@ -1073,6 +1079,19 @@ export class DaemonCliManager {
|
|
|
1073
1079
|
const { adapter, key } = found;
|
|
1074
1080
|
|
|
1075
1081
|
if (action === 'send_chat') {
|
|
1082
|
+
const currentStatus = normalizeAgentStatus(adapter.getStatus?.()?.status);
|
|
1083
|
+
if (BUSY_AGENT_STATUSES.has(currentStatus)) {
|
|
1084
|
+
return {
|
|
1085
|
+
success: false,
|
|
1086
|
+
code: 'agent_runtime_busy',
|
|
1087
|
+
reason: 'agent_runtime_busy',
|
|
1088
|
+
retryable: true,
|
|
1089
|
+
retryRecommended: true,
|
|
1090
|
+
status: currentStatus,
|
|
1091
|
+
targetSessionId: args?.targetSessionId,
|
|
1092
|
+
error: `CLI agent '${agentType}' is currently ${currentStatus}; retry after the current turn finishes.`,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1076
1095
|
const input = normalizeInputEnvelope(args?.input ? { input: args.input } : args);
|
|
1077
1096
|
const provider = this.providerLoader.resolve(agentType) || this.providerLoader.getMeta(agentType);
|
|
1078
1097
|
if (provider?.category === 'acp') {
|
package/src/commands/router.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
|
55
55
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
56
56
|
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
57
57
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
58
|
-
import { homedir } from 'os';
|
|
58
|
+
import { homedir, tmpdir } from 'os';
|
|
59
59
|
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
60
60
|
import * as fs from 'fs';
|
|
61
61
|
|
|
@@ -1089,7 +1089,12 @@ type MeshRefineSubmoduleReachabilityEntry = {
|
|
|
1089
1089
|
path: string;
|
|
1090
1090
|
commit: string;
|
|
1091
1091
|
reachable: boolean;
|
|
1092
|
+
publishRequired?: boolean;
|
|
1092
1093
|
checkedLocal?: boolean;
|
|
1094
|
+
localReachable?: boolean;
|
|
1095
|
+
remote?: string;
|
|
1096
|
+
remoteUrl?: string;
|
|
1097
|
+
remoteReachable?: boolean;
|
|
1093
1098
|
fetchedFromOrigin?: boolean;
|
|
1094
1099
|
error?: string;
|
|
1095
1100
|
};
|
|
@@ -1161,6 +1166,13 @@ function recordMeshRefineStage(
|
|
|
1161
1166
|
});
|
|
1162
1167
|
}
|
|
1163
1168
|
|
|
1169
|
+
function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
|
|
1170
|
+
const refs = entries
|
|
1171
|
+
.map(entry => `${entry.path}@${entry.commit}`)
|
|
1172
|
+
.join(', ');
|
|
1173
|
+
return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to their configured submodule remote(s), then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from its configured remote.`;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1164
1176
|
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
1165
1177
|
const { execFileSync } = await import('node:child_process');
|
|
1166
1178
|
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
@@ -1255,6 +1267,16 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1255
1267
|
});
|
|
1256
1268
|
return String(stdout || '');
|
|
1257
1269
|
};
|
|
1270
|
+
const verifyRemoteCommitReachable = async (remoteUrl: string, commit: string): Promise<void> => {
|
|
1271
|
+
const probeDir = fs.mkdtempSync(pathJoin(tmpdir(), 'adhdev-submodule-reachability-'));
|
|
1272
|
+
try {
|
|
1273
|
+
await runGit(probeDir, ['init', '-q']);
|
|
1274
|
+
await runGit(probeDir, ['-c', 'protocol.file.allow=always', 'fetch', '--depth=1', remoteUrl, commit]);
|
|
1275
|
+
await runGit(probeDir, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1276
|
+
} finally {
|
|
1277
|
+
fs.rmSync(probeDir, { recursive: true, force: true });
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1258
1280
|
|
|
1259
1281
|
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
1260
1282
|
const gitlinks = treeOutput
|
|
@@ -1276,6 +1298,7 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1276
1298
|
try {
|
|
1277
1299
|
if (!fs.existsSync(submodulePath)) {
|
|
1278
1300
|
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
1301
|
+
entry.publishRequired = true;
|
|
1279
1302
|
entries.push(entry);
|
|
1280
1303
|
continue;
|
|
1281
1304
|
}
|
|
@@ -1283,23 +1306,38 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1283
1306
|
entry.checkedLocal = true;
|
|
1284
1307
|
try {
|
|
1285
1308
|
await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
|
|
1286
|
-
entry.
|
|
1287
|
-
entries.push(entry);
|
|
1288
|
-
continue;
|
|
1309
|
+
entry.localReachable = true;
|
|
1289
1310
|
} catch {
|
|
1311
|
+
entry.localReachable = false;
|
|
1290
1312
|
// Probe the submodule remote before allowing cleanup/completion.
|
|
1291
1313
|
}
|
|
1292
1314
|
|
|
1293
1315
|
try {
|
|
1294
|
-
|
|
1316
|
+
entry.remote = 'origin';
|
|
1317
|
+
let remoteUrl = '';
|
|
1318
|
+
try {
|
|
1319
|
+
remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
|
|
1320
|
+
if (!remoteUrl) throw new Error('origin remote has no URL');
|
|
1321
|
+
entry.remoteUrl = remoteUrl;
|
|
1322
|
+
} catch {
|
|
1323
|
+
entry.error = 'Submodule remote reachability check failed: no configured origin remote';
|
|
1324
|
+
entry.publishRequired = true;
|
|
1325
|
+
entries.push(entry);
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
await verifyRemoteCommitReachable(remoteUrl, gitlink.commit);
|
|
1295
1329
|
entry.fetchedFromOrigin = true;
|
|
1296
|
-
|
|
1330
|
+
entry.remoteReachable = true;
|
|
1297
1331
|
entry.reachable = true;
|
|
1298
1332
|
} catch (e: any) {
|
|
1299
|
-
entry.
|
|
1333
|
+
entry.remoteReachable = false;
|
|
1334
|
+
entry.publishRequired = true;
|
|
1335
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1336
|
+
entry.error = `Submodule remote reachability check failed for origin: ${details}`;
|
|
1300
1337
|
}
|
|
1301
1338
|
} catch (e: any) {
|
|
1302
1339
|
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
1340
|
+
entry.publishRequired = true;
|
|
1303
1341
|
}
|
|
1304
1342
|
entries.push(entry);
|
|
1305
1343
|
}
|
|
@@ -1308,16 +1346,17 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1308
1346
|
return {
|
|
1309
1347
|
status: unreachable.length ? 'failed' : 'passed',
|
|
1310
1348
|
checked: entries.length,
|
|
1311
|
-
unreachable,
|
|
1312
|
-
entries,
|
|
1349
|
+
unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
|
|
1350
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
|
|
1313
1351
|
durationMs: Date.now() - startedAt,
|
|
1314
1352
|
};
|
|
1315
1353
|
} catch (e: any) {
|
|
1354
|
+
const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
|
|
1316
1355
|
return {
|
|
1317
1356
|
status: 'failed',
|
|
1318
1357
|
checked: entries.length,
|
|
1319
|
-
unreachable
|
|
1320
|
-
entries,
|
|
1358
|
+
unreachable,
|
|
1359
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
|
|
1321
1360
|
durationMs: Date.now() - startedAt,
|
|
1322
1361
|
error: truncateValidationOutput(e?.message || String(e)),
|
|
1323
1362
|
};
|
|
@@ -2679,15 +2718,41 @@ export class DaemonCommandRouter {
|
|
|
2679
2718
|
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead);
|
|
2680
2719
|
recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
|
|
2681
2720
|
checked: submoduleReachability.checked,
|
|
2682
|
-
unreachable: submoduleReachability.unreachable.map(entry => ({
|
|
2721
|
+
unreachable: submoduleReachability.unreachable.map(entry => ({
|
|
2722
|
+
path: entry.path,
|
|
2723
|
+
commit: entry.commit,
|
|
2724
|
+
publishRequired: entry.publishRequired === true,
|
|
2725
|
+
remote: entry.remote,
|
|
2726
|
+
remoteUrl: entry.remoteUrl,
|
|
2727
|
+
remoteReachable: entry.remoteReachable,
|
|
2728
|
+
error: entry.error,
|
|
2729
|
+
})),
|
|
2683
2730
|
error: submoduleReachability.error,
|
|
2684
2731
|
});
|
|
2685
2732
|
if (submoduleReachability.status === 'failed') {
|
|
2733
|
+
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
2686
2734
|
return {
|
|
2687
2735
|
success: false,
|
|
2688
2736
|
code: 'submodule_reachability_failed',
|
|
2689
2737
|
convergenceStatus: 'blocked_review',
|
|
2690
|
-
|
|
2738
|
+
publishRequired: true,
|
|
2739
|
+
blockedReason: 'submodule_publish_required',
|
|
2740
|
+
error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote; merge/refine cleanup was not attempted.',
|
|
2741
|
+
nextStep,
|
|
2742
|
+
nextSteps: [
|
|
2743
|
+
'Ask the user for explicit approval before pushing or publishing any submodule commit.',
|
|
2744
|
+
'Push/publish each unreachable submodule commit to the configured submodule remote shown in the evidence.',
|
|
2745
|
+
'Rerun mesh_refine_node after remote reachability is confirmed.',
|
|
2746
|
+
'Do not merge the root branch until every submodule gitlink commit is reachable from its configured remote.',
|
|
2747
|
+
],
|
|
2748
|
+
unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
|
|
2749
|
+
path: entry.path,
|
|
2750
|
+
commit: entry.commit,
|
|
2751
|
+
remote: entry.remote,
|
|
2752
|
+
remoteUrl: entry.remoteUrl,
|
|
2753
|
+
remoteReachable: entry.remoteReachable,
|
|
2754
|
+
error: entry.error,
|
|
2755
|
+
})),
|
|
2691
2756
|
branch,
|
|
2692
2757
|
into: baseBranch,
|
|
2693
2758
|
validationSummary,
|
|
@@ -2703,6 +2768,8 @@ export class DaemonCommandRouter {
|
|
|
2703
2768
|
patchEquivalence: 'passed',
|
|
2704
2769
|
submoduleReachability: 'failed',
|
|
2705
2770
|
status: 'blocked_review',
|
|
2771
|
+
reason: 'submodule_publish_required',
|
|
2772
|
+
nextStep,
|
|
2706
2773
|
},
|
|
2707
2774
|
};
|
|
2708
2775
|
}
|
|
@@ -165,7 +165,7 @@ const WORKFLOW_SECTION = `## Orchestration Workflow
|
|
|
165
165
|
4. **Monitor** — Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly. Use \`mesh_view_queue\` to see the status of all pending, assigned, completed, and failed tasks. Do not call \`mesh_read_chat\` again within a few seconds for the same generating session. Use at most one compact \`mesh_read_chat\` check after a completion/approval signal. Handle approvals via \`mesh_approve\`.
|
|
166
166
|
5. **Verify** — When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
|
|
167
167
|
6. **Checkpoint** — Call \`mesh_checkpoint\` to save the work.
|
|
168
|
-
7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary. For obvious clean branch catch-up (ahead 0, behind > 0, upstream fresh, no dirty/stash/submodule issues), use \`mesh_fast_forward_node\` dry-run first and execute only when explicitly safe/approved; this avoids consuming an agent session. Use \`mesh_refine_node\` for clean worktree branches when safe. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
|
|
168
|
+
7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary. For obvious clean branch catch-up (ahead 0, behind > 0, upstream fresh, no dirty/stash/submodule issues), use \`mesh_fast_forward_node\` dry-run first and execute only when explicitly safe/approved; this avoids consuming an agent session. Use \`mesh_refine_node\` for clean worktree branches when safe. Before/refine merging root commits that contain submodule gitlink changes, require each submodule commit to be reachable from its configured remote. If \`mesh_refine_node\` returns \`submodule_reachability_failed\` or publish-required evidence, keep the public convergence bucket as \`blocked_review\`, ask the user for explicit approval to push/publish the unreachable submodule commit(s), then rerun \`mesh_refine_node\`; do not merge the root branch until the submodule commit(s) are reachable. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
|
|
169
169
|
8. **Clean up** — Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
|
|
170
170
|
9. **Report** — Summarize what was done, what changed, any issues, and the branch convergence state.
|
|
171
171
|
|
|
@@ -204,5 +204,6 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
204
204
|
- **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
|
|
205
205
|
- **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, fast-forward obvious clean behind-only branches with \`mesh_fast_forward_node\`, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
|
|
206
206
|
- **Keep Refinery validation project-configurable.** \`mesh_refine_node\` must execute validation from repo mesh/refine config (for example \`.adhdev/refine.{json,yaml,yml}\`, \`.adhdev/repo-mesh-refine.*\`, or \`repo-mesh.refine.*\`). Heuristics are suggestions/scaffolding only, not the execution path.
|
|
207
|
+
- **Treat submodule reachability as publish-needed.** A \`submodule_reachability_failed\` refine result means the root gitlink points at a submodule commit that is not reachable from the configured submodule remote. Do not retry validation blindly or start code review first. Classify it as \`blocked_review\`, request user approval to push/publish the submodule commit, then rerun \`mesh_refine_node\`.
|
|
207
208
|
- **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
|
|
208
209
|
}
|
|
@@ -891,7 +891,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
891
891
|
chatTitle: pending.chatTitle,
|
|
892
892
|
duration: pending.duration,
|
|
893
893
|
timestamp: pending.timestamp,
|
|
894
|
-
finalSummary:
|
|
894
|
+
finalSummary: blockReason.startsWith('parsed_status:')
|
|
895
|
+
? ''
|
|
896
|
+
: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
|
|
895
897
|
completionDiagnostic,
|
|
896
898
|
});
|
|
897
899
|
this.completedDebouncePending = null;
|