@adhdev/daemon-core 0.9.82-rc.6 → 0.9.82-rc.60
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/boot/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/router.d.ts +24 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/git/git-commands.d.ts +1 -0
- package/dist/git/git-status.d.ts +5 -0
- package/dist/git/git-types.d.ts +10 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +3518 -593
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3492 -587
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +48 -0
- package/dist/mesh/mesh-events.d.ts +17 -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 +23 -5
- package/dist/mesh/refine-config.d.ts +119 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/dist/repo-mesh-types.d.ts +160 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/commands/router.ts +2172 -419
- package/src/config/mesh-config.ts +244 -1
- package/src/git/git-commands.ts +3 -3
- package/src/git/git-status.ts +97 -6
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +11 -0
- package/src/index.ts +39 -5
- package/src/mesh/coordinator-prompt.ts +4 -2
- package/src/mesh/mesh-active-work.ts +205 -0
- package/src/mesh/mesh-events.ts +210 -38
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +137 -0
- package/src/mesh/mesh-work-queue.ts +202 -122
- package/src/mesh/refine-config.ts +306 -0
- package/src/providers/chat-message-normalization.ts +3 -1
- package/src/providers/cli-provider-instance.ts +66 -1
- package/src/repo-mesh-types.ts +174 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import * as yaml from 'js-yaml';
|
|
4
|
+
|
|
5
|
+
export const MESH_REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
6
|
+
export type MeshRefineValidationCategory = typeof MESH_REFINE_VALIDATION_CATEGORIES[number];
|
|
7
|
+
|
|
8
|
+
export interface RepoMeshRefineValidationCommandConfig {
|
|
9
|
+
/** Executable name or a whitespace-tokenized command string. Never executed through a shell. */
|
|
10
|
+
command: string;
|
|
11
|
+
/** Optional explicit argv. Prefer this over shell-like command strings. */
|
|
12
|
+
args?: string[];
|
|
13
|
+
category?: MeshRefineValidationCategory;
|
|
14
|
+
cwd?: string;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RepoMeshRefineConfig {
|
|
20
|
+
version: 1;
|
|
21
|
+
validation?: {
|
|
22
|
+
required?: boolean;
|
|
23
|
+
commands?: RepoMeshRefineValidationCommandConfig[];
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MeshRefineValidationCommandPlan {
|
|
28
|
+
command: string;
|
|
29
|
+
args: string[];
|
|
30
|
+
displayCommand: string;
|
|
31
|
+
category: MeshRefineValidationCategory | 'custom';
|
|
32
|
+
source: string;
|
|
33
|
+
cwd?: string;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
env?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MeshRefineConfigLoadResult {
|
|
39
|
+
config?: RepoMeshRefineConfig;
|
|
40
|
+
source: string;
|
|
41
|
+
sourceType: 'mesh_policy' | 'repo_file' | 'unavailable' | 'invalid';
|
|
42
|
+
path?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MeshRefineValidationPlan {
|
|
47
|
+
source: string;
|
|
48
|
+
sourceType: MeshRefineConfigLoadResult['sourceType'];
|
|
49
|
+
commands: MeshRefineValidationCommandPlan[];
|
|
50
|
+
rejectedCommands: Array<Record<string, unknown>>;
|
|
51
|
+
suggestions: RepoMeshRefineValidationCommandConfig[];
|
|
52
|
+
suggestedConfig?: RepoMeshRefineConfig;
|
|
53
|
+
unavailableReason?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const MESH_REFINE_CONFIG_LOCATIONS = [
|
|
57
|
+
'.adhdev/refine.json',
|
|
58
|
+
'.adhdev/refine.yaml',
|
|
59
|
+
'.adhdev/refine.yml',
|
|
60
|
+
'.adhdev/repo-mesh-refine.json',
|
|
61
|
+
'.adhdev/repo-mesh-refine.yaml',
|
|
62
|
+
'.adhdev/repo-mesh-refine.yml',
|
|
63
|
+
'repo-mesh.refine.json',
|
|
64
|
+
'repo-mesh.refine.yaml',
|
|
65
|
+
'repo-mesh.refine.yml',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
export const MESH_REFINE_CONFIG_SCHEMA = {
|
|
69
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
70
|
+
title: 'ADHDev Repo Mesh Refinery Config',
|
|
71
|
+
type: 'object',
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
required: ['version'],
|
|
74
|
+
properties: {
|
|
75
|
+
version: { const: 1 },
|
|
76
|
+
validation: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
properties: {
|
|
80
|
+
required: { type: 'boolean', default: true },
|
|
81
|
+
commands: {
|
|
82
|
+
type: 'array',
|
|
83
|
+
minItems: 1,
|
|
84
|
+
maxItems: 8,
|
|
85
|
+
items: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
required: ['command'],
|
|
89
|
+
properties: {
|
|
90
|
+
command: { type: 'string', minLength: 1 },
|
|
91
|
+
args: { type: 'array', items: { type: 'string' } },
|
|
92
|
+
category: { enum: [...MESH_REFINE_VALIDATION_CATEGORIES, 'custom'] },
|
|
93
|
+
cwd: { type: 'string' },
|
|
94
|
+
timeoutMs: { type: 'number', minimum: 1000, maximum: 600000 },
|
|
95
|
+
env: { type: 'object', additionalProperties: { type: 'string' } },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
} as const;
|
|
103
|
+
|
|
104
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
105
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function tokenizeCommandString(command: string): string[] | null {
|
|
109
|
+
const trimmed = command.trim();
|
|
110
|
+
if (!trimmed) return null;
|
|
111
|
+
// Explicit config may name any executable, but the Refinery never invokes a shell.
|
|
112
|
+
// Reject shell syntax, quotes and substitutions so config cannot smuggle a compound command.
|
|
113
|
+
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
114
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
115
|
+
if (!tokens.length) return null;
|
|
116
|
+
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
117
|
+
return tokens;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateCategory(value: unknown): MeshRefineValidationCategory | 'custom' {
|
|
121
|
+
return typeof value === 'string' && ([...MESH_REFINE_VALIDATION_CATEGORIES, 'custom'] as string[]).includes(value)
|
|
122
|
+
? value as MeshRefineValidationCategory | 'custom'
|
|
123
|
+
: 'custom';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeCommandConfig(entry: unknown, source: string): { command?: MeshRefineValidationCommandPlan; rejected?: Record<string, unknown> } {
|
|
127
|
+
if (!isRecord(entry) || typeof entry.command !== 'string') {
|
|
128
|
+
return { rejected: { source, reason: 'validation command must be an object with a command string' } };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const commandText = entry.command.trim();
|
|
132
|
+
const explicitArgs = Array.isArray(entry.args) ? entry.args : undefined;
|
|
133
|
+
if (explicitArgs && !explicitArgs.every(arg => typeof arg === 'string')) {
|
|
134
|
+
return { rejected: { source, command: commandText, reason: 'args must be an array of strings' } };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let command = commandText;
|
|
138
|
+
let args = explicitArgs ? [...explicitArgs] : [];
|
|
139
|
+
if (!explicitArgs) {
|
|
140
|
+
const tokens = tokenizeCommandString(commandText);
|
|
141
|
+
if (!tokens) return { rejected: { source, command: commandText, reason: 'unsafe command string is not allowlisted' } };
|
|
142
|
+
command = tokens[0];
|
|
143
|
+
args = tokens.slice(1);
|
|
144
|
+
} else if (!tokenizeCommandString(command)) {
|
|
145
|
+
return { rejected: { source, command: commandText, reason: 'unsafe executable name is not allowlisted' } };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (args.some(arg => /[\n\r\0]/.test(arg))) {
|
|
149
|
+
return { rejected: { source, command: commandText, reason: 'args cannot contain control characters' } };
|
|
150
|
+
}
|
|
151
|
+
if (entry.cwd !== undefined && typeof entry.cwd !== 'string') {
|
|
152
|
+
return { rejected: { source, command: commandText, reason: 'cwd must be a string when provided' } };
|
|
153
|
+
}
|
|
154
|
+
if (entry.timeoutMs !== undefined && (typeof entry.timeoutMs !== 'number' || !Number.isFinite(entry.timeoutMs) || entry.timeoutMs < 1000 || entry.timeoutMs > 600000)) {
|
|
155
|
+
return { rejected: { source, command: commandText, reason: 'timeoutMs must be between 1000 and 600000' } };
|
|
156
|
+
}
|
|
157
|
+
if (entry.env !== undefined && (!isRecord(entry.env) || !Object.values(entry.env).every(value => typeof value === 'string'))) {
|
|
158
|
+
return { rejected: { source, command: commandText, reason: 'env must be an object of string values' } };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
command: {
|
|
163
|
+
command,
|
|
164
|
+
args,
|
|
165
|
+
displayCommand: [command, ...args].join(' '),
|
|
166
|
+
category: validateCategory(entry.category),
|
|
167
|
+
source,
|
|
168
|
+
...(typeof entry.cwd === 'string' && entry.cwd.trim() ? { cwd: entry.cwd.trim() } : {}),
|
|
169
|
+
...(typeof entry.timeoutMs === 'number' ? { timeoutMs: entry.timeoutMs } : {}),
|
|
170
|
+
...(isRecord(entry.env) ? { env: entry.env as Record<string, string> } : {}),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function validateMeshRefineConfig(config: unknown, source = 'inline'): { valid: boolean; errors: string[]; commands: MeshRefineValidationCommandPlan[]; rejectedCommands: Array<Record<string, unknown>> } {
|
|
176
|
+
const errors: string[] = [];
|
|
177
|
+
const commands: MeshRefineValidationCommandPlan[] = [];
|
|
178
|
+
const rejectedCommands: Array<Record<string, unknown>> = [];
|
|
179
|
+
|
|
180
|
+
if (!isRecord(config)) return { valid: false, errors: ['config must be an object'], commands, rejectedCommands };
|
|
181
|
+
if (config.version !== 1) errors.push('version must be 1');
|
|
182
|
+
const validation = config.validation;
|
|
183
|
+
if (validation !== undefined && !isRecord(validation)) errors.push('validation must be an object');
|
|
184
|
+
const rawCommands = isRecord(validation) ? validation.commands : undefined;
|
|
185
|
+
if (rawCommands !== undefined && !Array.isArray(rawCommands)) errors.push('validation.commands must be an array');
|
|
186
|
+
if (Array.isArray(rawCommands)) {
|
|
187
|
+
rawCommands.forEach((entry, index) => {
|
|
188
|
+
const normalized = normalizeCommandConfig(entry, `${source}:validation.commands[${index}]`);
|
|
189
|
+
if (normalized.command) commands.push(normalized.command);
|
|
190
|
+
if (normalized.rejected) rejectedCommands.push(normalized.rejected);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (rejectedCommands.length) errors.push('one or more validation commands are invalid');
|
|
194
|
+
return { valid: errors.length === 0, errors, commands, rejectedCommands };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseConfigText(path: string, text: string): unknown {
|
|
198
|
+
if (/\.json$/i.test(path)) return JSON.parse(text);
|
|
199
|
+
return yaml.load(text);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function loadMeshRefineConfig(mesh: any, workspace: string): MeshRefineConfigLoadResult {
|
|
203
|
+
const policy = mesh?.policy && typeof mesh.policy === 'object' && !Array.isArray(mesh.policy) ? mesh.policy : {};
|
|
204
|
+
const inline = mesh?.refineConfig || (policy as any).refineConfig || (policy as any).refine;
|
|
205
|
+
if (inline !== undefined) {
|
|
206
|
+
const validation = validateMeshRefineConfig(inline, 'mesh.policy.refineConfig');
|
|
207
|
+
if (!validation.valid) return { source: 'mesh.policy.refineConfig', sourceType: 'invalid', error: String(validation.rejectedCommands[0]?.reason || validation.errors.join('; ')) };
|
|
208
|
+
return { config: inline as RepoMeshRefineConfig, source: 'mesh.policy.refineConfig', sourceType: 'mesh_policy' };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const relative of MESH_REFINE_CONFIG_LOCATIONS) {
|
|
212
|
+
const configPath = join(workspace, relative);
|
|
213
|
+
if (!existsSync(configPath)) continue;
|
|
214
|
+
try {
|
|
215
|
+
const parsed = parseConfigText(configPath, readFileSync(configPath, 'utf-8'));
|
|
216
|
+
const validation = validateMeshRefineConfig(parsed, relative);
|
|
217
|
+
if (!validation.valid) return { source: relative, sourceType: 'invalid', path: configPath, error: String(validation.rejectedCommands[0]?.reason || validation.errors.join('; ')) };
|
|
218
|
+
return { config: parsed as RepoMeshRefineConfig, source: relative, sourceType: 'repo_file', path: configPath };
|
|
219
|
+
} catch (error: any) {
|
|
220
|
+
return { source: relative, sourceType: 'invalid', path: configPath, error: error?.message || String(error) };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
source: 'unavailable',
|
|
226
|
+
sourceType: 'unavailable',
|
|
227
|
+
error: `No repo mesh/refine config found. Checked: ${MESH_REFINE_CONFIG_LOCATIONS.join(', ')}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function readPackageScripts(workspace: string): Record<string, string> {
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(readFileSync(join(workspace, 'package.json'), 'utf-8'));
|
|
234
|
+
return isRecord(parsed?.scripts) ? parsed.scripts as Record<string, string> : {};
|
|
235
|
+
} catch {
|
|
236
|
+
return {};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function collectProjectContextSuggestions(mesh: any): RepoMeshRefineValidationCommandConfig[] {
|
|
241
|
+
const commands = mesh?.projectContext?.commands;
|
|
242
|
+
if (!isRecord(commands)) return [];
|
|
243
|
+
const suggestions: RepoMeshRefineValidationCommandConfig[] = [];
|
|
244
|
+
for (const category of MESH_REFINE_VALIDATION_CATEGORIES) {
|
|
245
|
+
const entries = Array.isArray(commands[category]) ? commands[category] : [];
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
if (isRecord(entry) && typeof entry.command === 'string') suggestions.push({ command: entry.command, category });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return suggestions;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function collectPackageScriptSuggestions(workspace: string): RepoMeshRefineValidationCommandConfig[] {
|
|
254
|
+
const scripts = readPackageScripts(workspace);
|
|
255
|
+
const suggestions: RepoMeshRefineValidationCommandConfig[] = [];
|
|
256
|
+
for (const category of MESH_REFINE_VALIDATION_CATEGORIES) {
|
|
257
|
+
for (const scriptName of Object.keys(scripts)) {
|
|
258
|
+
if (scriptName === category || scriptName.startsWith(`${category}:`)) {
|
|
259
|
+
suggestions.push({ command: 'npm', args: ['run', scriptName], category });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return suggestions;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function suggestMeshRefineConfig(mesh: any, workspace: string): { suggestions: RepoMeshRefineValidationCommandConfig[]; suggestedConfig?: RepoMeshRefineConfig } {
|
|
267
|
+
const seen = new Set<string>();
|
|
268
|
+
const suggestions: RepoMeshRefineValidationCommandConfig[] = [];
|
|
269
|
+
for (const entry of [...collectProjectContextSuggestions(mesh), ...collectPackageScriptSuggestions(workspace)]) {
|
|
270
|
+
const key = `${entry.command} ${(entry.args || []).join(' ')}`.trim();
|
|
271
|
+
if (seen.has(key)) continue;
|
|
272
|
+
seen.add(key);
|
|
273
|
+
suggestions.push(entry);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
suggestions,
|
|
277
|
+
suggestedConfig: suggestions.length ? { version: 1, validation: { required: true, commands: suggestions.slice(0, 4) } } : undefined,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function resolveMeshRefineValidationPlan(mesh: any, workspace: string): MeshRefineValidationPlan {
|
|
282
|
+
const loaded = loadMeshRefineConfig(mesh, workspace);
|
|
283
|
+
const suggestion = suggestMeshRefineConfig(mesh, workspace);
|
|
284
|
+
if (!loaded.config) {
|
|
285
|
+
return {
|
|
286
|
+
source: loaded.source,
|
|
287
|
+
sourceType: loaded.sourceType,
|
|
288
|
+
commands: [],
|
|
289
|
+
rejectedCommands: loaded.error ? [{ source: loaded.source, reason: loaded.error }] : [],
|
|
290
|
+
suggestions: suggestion.suggestions,
|
|
291
|
+
suggestedConfig: suggestion.suggestedConfig,
|
|
292
|
+
unavailableReason: loaded.error || 'validation_unavailable: repo mesh/refine config missing',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const validation = validateMeshRefineConfig(loaded.config, loaded.source);
|
|
297
|
+
return {
|
|
298
|
+
source: loaded.path || loaded.source,
|
|
299
|
+
sourceType: loaded.sourceType,
|
|
300
|
+
commands: validation.commands,
|
|
301
|
+
rejectedCommands: validation.rejectedCommands,
|
|
302
|
+
suggestions: suggestion.suggestions,
|
|
303
|
+
suggestedConfig: suggestion.suggestedConfig,
|
|
304
|
+
unavailableReason: validation.commands.length ? undefined : 'validation_unavailable: repo mesh/refine config has no validation.commands',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ChatMessage } from '../types.js';
|
|
2
2
|
import { flattenContent } from './contracts.js';
|
|
3
3
|
|
|
4
|
+
export const DEFAULT_FINAL_SUMMARY_MAX_CHARS = 4_000;
|
|
5
|
+
|
|
4
6
|
export function extractFinalSummaryFromMessages(
|
|
5
7
|
messages: ChatMessage[] | null | undefined,
|
|
6
|
-
maxChars: number =
|
|
8
|
+
maxChars: number = DEFAULT_FINAL_SUMMARY_MAX_CHARS,
|
|
7
9
|
): string {
|
|
8
10
|
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
9
11
|
|
|
@@ -743,6 +743,55 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
743
743
|
return role === 'assistant' && !!content;
|
|
744
744
|
}
|
|
745
745
|
|
|
746
|
+
private buildCompletedFinalizationDiagnostic(args: {
|
|
747
|
+
blockReason: string;
|
|
748
|
+
latestStatus?: any;
|
|
749
|
+
latestVisibleStatus: string;
|
|
750
|
+
waitedMs: number;
|
|
751
|
+
pending: CompletedDebouncePending;
|
|
752
|
+
emittedAfterFinalizationTimeout: boolean;
|
|
753
|
+
}): Record<string, unknown> {
|
|
754
|
+
let parsed: any = null;
|
|
755
|
+
let parseError: string | undefined;
|
|
756
|
+
try {
|
|
757
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
758
|
+
} catch (error: any) {
|
|
759
|
+
parseError = error?.message || String(error);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const visibleMessages = (Array.isArray(parsed?.messages) ? parsed.messages : [])
|
|
763
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
764
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
765
|
+
const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
|
|
766
|
+
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
767
|
+
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
providerType: this.type,
|
|
771
|
+
sessionId: this.instanceId,
|
|
772
|
+
providerSessionId: this.providerSessionId || null,
|
|
773
|
+
workspace: this.workingDir,
|
|
774
|
+
blockReason: args.blockReason,
|
|
775
|
+
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
776
|
+
waitedMs: args.waitedMs,
|
|
777
|
+
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
778
|
+
adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
|
|
779
|
+
latestVisibleStatus: args.latestVisibleStatus,
|
|
780
|
+
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
781
|
+
parseError: parseError || undefined,
|
|
782
|
+
finalAssistantPresent: this.completionHasFinalAssistantMessage(parsed?.messages),
|
|
783
|
+
visibleMessageCount: visibleMessages.length,
|
|
784
|
+
lastVisibleRole,
|
|
785
|
+
lastVisibleKind,
|
|
786
|
+
lastVisibleContentLength,
|
|
787
|
+
pendingStartedAt: this.generatingStartedAt || null,
|
|
788
|
+
pendingFirstObservedAt: args.pending.firstObservedAt,
|
|
789
|
+
pendingTimestamp: args.pending.timestamp,
|
|
790
|
+
pendingDurationSec: args.pending.duration,
|
|
791
|
+
previousBlockReason: args.pending.loggedBlockReason || null,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
746
795
|
private hasAdapterPendingResponse(): boolean {
|
|
747
796
|
const adapterAny = this.adapter as any;
|
|
748
797
|
if (adapterAny?.isWaitingForResponse === true) return true;
|
|
@@ -828,7 +877,23 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
828
877
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
878
|
return;
|
|
830
879
|
}
|
|
831
|
-
|
|
880
|
+
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
881
|
+
blockReason,
|
|
882
|
+
latestStatus,
|
|
883
|
+
latestVisibleStatus,
|
|
884
|
+
waitedMs,
|
|
885
|
+
pending,
|
|
886
|
+
emittedAfterFinalizationTimeout: true,
|
|
887
|
+
});
|
|
888
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
|
|
889
|
+
this.pushEvent({
|
|
890
|
+
event: 'agent:generating_completed',
|
|
891
|
+
chatTitle: pending.chatTitle,
|
|
892
|
+
duration: pending.duration,
|
|
893
|
+
timestamp: pending.timestamp,
|
|
894
|
+
finalSummary: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
|
|
895
|
+
completionDiagnostic,
|
|
896
|
+
});
|
|
832
897
|
this.completedDebouncePending = null;
|
|
833
898
|
this.completedDebounceTimer = null;
|
|
834
899
|
this.generatingStartedAt = 0;
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -23,11 +23,42 @@ export interface RepoMesh {
|
|
|
23
23
|
defaultBranch?: string;
|
|
24
24
|
policy: RepoMeshPolicy;
|
|
25
25
|
coordinator: RepoMeshCoordinatorConfig;
|
|
26
|
+
meshHost?: RepoMeshHostMetadata;
|
|
26
27
|
projectContext: ProjectContextSnapshot;
|
|
27
28
|
nodes: RepoMeshNode[];
|
|
28
29
|
status: 'active' | 'archived' | 'deleted';
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export type RepoMeshDaemonRole = 'host' | 'member';
|
|
33
|
+
|
|
34
|
+
export interface RepoMeshHostPairingMetadata {
|
|
35
|
+
status: 'not_configured' | 'pairing' | 'paired' | 'rejected' | 'revoked';
|
|
36
|
+
tokenId?: string;
|
|
37
|
+
joinedAt?: string;
|
|
38
|
+
lastPairedAt?: string;
|
|
39
|
+
lastRejectedAt?: string;
|
|
40
|
+
expiresAt?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RepoMeshHostMetadata {
|
|
44
|
+
/** Local daemon role for this mesh. Missing metadata defaults to host for standalone compatibility. */
|
|
45
|
+
role: RepoMeshDaemonRole;
|
|
46
|
+
/** Daemon that owns mesh truth/status/git/queue/session/ledger/coordinator ownership. */
|
|
47
|
+
hostDaemonId?: string;
|
|
48
|
+
/** Mesh node that represents the host daemon, when known. */
|
|
49
|
+
hostNodeId?: string;
|
|
50
|
+
/** Future standalone manual pairing endpoint entered by member daemons. */
|
|
51
|
+
hostAddress?: string;
|
|
52
|
+
/** Redacted pairing state only; raw join tokens must not be persisted here. */
|
|
53
|
+
pairing?: RepoMeshHostPairingMetadata;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RepoMeshHostStatus extends RepoMeshHostMetadata {
|
|
57
|
+
canOwnCoordinator: boolean;
|
|
58
|
+
canOwnQueue: boolean;
|
|
59
|
+
defaulted: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
export interface RepoMeshNode {
|
|
32
63
|
id: string;
|
|
33
64
|
daemonId: string;
|
|
@@ -42,6 +73,7 @@ export interface RepoMeshNode {
|
|
|
42
73
|
effectiveCapabilities: RepoMeshNodeCapabilities;
|
|
43
74
|
policy: RepoMeshNodePolicy;
|
|
44
75
|
health: RepoMeshNodeHealth;
|
|
76
|
+
role?: RepoMeshDaemonRole;
|
|
45
77
|
status: 'enabled' | 'disabled' | 'removed';
|
|
46
78
|
}
|
|
47
79
|
|
|
@@ -229,6 +261,7 @@ export interface LocalMeshEntry {
|
|
|
229
261
|
defaultBranch?: string;
|
|
230
262
|
policy: RepoMeshPolicy;
|
|
231
263
|
coordinator: RepoMeshCoordinatorConfig;
|
|
264
|
+
meshHost?: RepoMeshHostMetadata;
|
|
232
265
|
nodes: LocalMeshNodeEntry[];
|
|
233
266
|
createdAt: string;
|
|
234
267
|
updatedAt: string;
|
|
@@ -251,6 +284,7 @@ export interface LocalMeshNodeEntry {
|
|
|
251
284
|
clonedFromNodeId?: string;
|
|
252
285
|
/** Optional associated/external repos configured as node metadata. */
|
|
253
286
|
relatedRepos?: RepoMeshRelatedRepo[];
|
|
287
|
+
role?: RepoMeshDaemonRole;
|
|
254
288
|
}
|
|
255
289
|
|
|
256
290
|
// ─── Mesh Status (runtime, not persisted) ───────
|
|
@@ -259,17 +293,157 @@ export interface RepoMeshStatus {
|
|
|
259
293
|
meshId: string;
|
|
260
294
|
meshName: string;
|
|
261
295
|
repoIdentity: string;
|
|
296
|
+
defaultBranch?: string;
|
|
262
297
|
refreshedAt: string;
|
|
298
|
+
meshHost?: RepoMeshHostStatus;
|
|
263
299
|
nodes: RepoMeshNodeStatus[];
|
|
300
|
+
queue?: RepoMeshQueueStatus;
|
|
301
|
+
ledger?: RepoMeshLedgerStatus;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface RepoMeshSessionStatus {
|
|
305
|
+
sessionId: string;
|
|
306
|
+
providerType?: string;
|
|
307
|
+
state?: string;
|
|
308
|
+
lifecycle?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'interrupted';
|
|
309
|
+
surfaceKind?: 'live_runtime' | 'recovery_snapshot' | 'inactive_record';
|
|
310
|
+
recoveryState?: string | null;
|
|
311
|
+
workspace?: string | null;
|
|
312
|
+
title?: string | null;
|
|
313
|
+
lastActivityAt?: string | null;
|
|
314
|
+
isCached?: boolean;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export type RepoMeshPeerConnectionState = 'self' | 'connected' | 'connecting' | 'disconnected' | 'failed' | 'closed' | 'unknown';
|
|
318
|
+
export type RepoMeshPeerConnectionTransport = 'local' | 'direct' | 'relay' | 'unknown';
|
|
319
|
+
|
|
320
|
+
export interface RepoMeshPeerConnectionStatus {
|
|
321
|
+
perspective: 'selected_coordinator';
|
|
322
|
+
source: 'mesh_peer_status' | 'not_reported';
|
|
323
|
+
state: RepoMeshPeerConnectionState;
|
|
324
|
+
transport: RepoMeshPeerConnectionTransport;
|
|
325
|
+
reported: boolean;
|
|
326
|
+
reason?: string;
|
|
327
|
+
lastStateChangeAt?: string;
|
|
328
|
+
lastConnectedAt?: string;
|
|
329
|
+
lastCommandAt?: string;
|
|
264
330
|
}
|
|
265
331
|
|
|
266
332
|
export interface RepoMeshNodeStatus {
|
|
267
333
|
nodeId: string;
|
|
268
334
|
machineLabel: string;
|
|
269
335
|
workspace: string;
|
|
336
|
+
repoRoot?: string;
|
|
337
|
+
daemonId?: string;
|
|
338
|
+
machineId?: string;
|
|
339
|
+
role?: RepoMeshDaemonRole;
|
|
340
|
+
machineStatus?: string;
|
|
341
|
+
isLocalWorktree?: boolean;
|
|
342
|
+
worktreeBranch?: string;
|
|
270
343
|
health: RepoMeshNodeHealth;
|
|
271
344
|
git?: GitRepoStatus;
|
|
345
|
+
/**
|
|
346
|
+
* True when the selected coordinator has evidence that a peer git probe is still
|
|
347
|
+
* in flight or just timed out during initial mesh handshake, so callers should
|
|
348
|
+
* treat missing git data as pending instead of authoritative absence.
|
|
349
|
+
*/
|
|
350
|
+
gitProbePending?: boolean;
|
|
272
351
|
providers: string[];
|
|
273
352
|
activeSessions: string[];
|
|
353
|
+
activeSessionDetails?: RepoMeshSessionStatus[];
|
|
354
|
+
providerPriority?: string[];
|
|
355
|
+
launchReady?: boolean;
|
|
356
|
+
lastSeenAt?: string;
|
|
357
|
+
updatedAt?: string;
|
|
358
|
+
connection?: RepoMeshPeerConnectionStatus;
|
|
274
359
|
error?: string;
|
|
275
360
|
}
|
|
361
|
+
|
|
362
|
+
export type RepoMeshQueueTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
|
|
363
|
+
|
|
364
|
+
export interface RepoMeshQueueTask {
|
|
365
|
+
id: string;
|
|
366
|
+
meshId: string;
|
|
367
|
+
message: string;
|
|
368
|
+
status: RepoMeshQueueTaskStatus;
|
|
369
|
+
targetNodeId?: string;
|
|
370
|
+
targetSessionId?: string;
|
|
371
|
+
assignedNodeId?: string;
|
|
372
|
+
assignedSessionId?: string;
|
|
373
|
+
cancelReason?: string;
|
|
374
|
+
cancelledAt?: string;
|
|
375
|
+
requeueReason?: string;
|
|
376
|
+
requeuedAt?: string;
|
|
377
|
+
requeueCount?: number;
|
|
378
|
+
autoLaunch?: {
|
|
379
|
+
status: 'skipped' | 'started' | 'failed' | 'completed';
|
|
380
|
+
reason?: string;
|
|
381
|
+
nodeId?: string;
|
|
382
|
+
providerType?: string;
|
|
383
|
+
sessionId?: string;
|
|
384
|
+
updatedAt: string;
|
|
385
|
+
};
|
|
386
|
+
dispatchTimestamp?: string;
|
|
387
|
+
createdAt: string;
|
|
388
|
+
updatedAt: string;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export interface RepoMeshQueueSummary {
|
|
392
|
+
total: number;
|
|
393
|
+
active: number;
|
|
394
|
+
historical: number;
|
|
395
|
+
pending: number;
|
|
396
|
+
assigned: number;
|
|
397
|
+
completed: number;
|
|
398
|
+
failed: number;
|
|
399
|
+
cancelled: number;
|
|
400
|
+
activeCounts: {
|
|
401
|
+
pending: number;
|
|
402
|
+
assigned: number;
|
|
403
|
+
};
|
|
404
|
+
historicalCounts: {
|
|
405
|
+
completed: number;
|
|
406
|
+
failed: number;
|
|
407
|
+
cancelled: number;
|
|
408
|
+
};
|
|
409
|
+
activeAssignments: Array<{
|
|
410
|
+
id: string;
|
|
411
|
+
nodeId?: string;
|
|
412
|
+
sessionId?: string;
|
|
413
|
+
message: string;
|
|
414
|
+
}>;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface RepoMeshQueueStatus {
|
|
418
|
+
tasks: RepoMeshQueueTask[];
|
|
419
|
+
summary: RepoMeshQueueSummary;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export interface RepoMeshLedgerEntryStatus {
|
|
423
|
+
id: string;
|
|
424
|
+
meshId: string;
|
|
425
|
+
timestamp: string;
|
|
426
|
+
kind: string;
|
|
427
|
+
nodeId?: string;
|
|
428
|
+
sessionId?: string;
|
|
429
|
+
providerType?: string;
|
|
430
|
+
payload: Record<string, unknown>;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface RepoMeshLedgerSummaryStatus {
|
|
434
|
+
meshId: string;
|
|
435
|
+
totalEntries: number;
|
|
436
|
+
taskDispatched: number;
|
|
437
|
+
taskCompleted: number;
|
|
438
|
+
taskFailed: number;
|
|
439
|
+
taskStalled: number;
|
|
440
|
+
sessionLaunched: number;
|
|
441
|
+
checkpointCreated: number;
|
|
442
|
+
lastActivityAt: string | null;
|
|
443
|
+
recentFailures: number;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export interface RepoMeshLedgerStatus {
|
|
447
|
+
entries: RepoMeshLedgerEntryStatus[];
|
|
448
|
+
summary: RepoMeshLedgerSummaryStatus;
|
|
449
|
+
}
|