@adhdev/daemon-core 0.9.82-rc.7 → 0.9.82-rc.70
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/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.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 +4619 -1143
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4582 -1128
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +48 -0
- package/dist/mesh/mesh-events.d.ts +28 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/refine-config.d.ts +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/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +4 -0
- package/src/cli-adapters/provider-cli-adapter.ts +91 -3
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +4 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +20 -10
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +2452 -409
- package/src/config/chat-history.ts +9 -7
- package/src/config/mesh-config.ts +244 -1
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- 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/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +176 -0
- package/src/mesh/coordinator-prompt.ts +4 -2
- package/src/mesh/mesh-active-work.ts +205 -0
- package/src/mesh/mesh-events.ts +291 -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 +138 -1
- package/src/mesh/mesh-work-queue.ts +199 -137
- 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/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +174 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
|
@@ -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;
|
|
@@ -43,6 +43,20 @@ type ReadChatPayload = {
|
|
|
43
43
|
[key: string]: unknown;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
47
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
48
|
+
try {
|
|
49
|
+
return await Promise.race([
|
|
50
|
+
promise,
|
|
51
|
+
new Promise<never>((_, reject) => {
|
|
52
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
} finally {
|
|
56
|
+
if (timer) clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
export class IdeProviderInstance implements ProviderInstance {
|
|
47
61
|
readonly type: string;
|
|
48
62
|
readonly category = 'ide' as const;
|
|
@@ -320,7 +334,7 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
320
334
|
if (webviewScript) {
|
|
321
335
|
const matchText = this.provider.webviewMatchText;
|
|
322
336
|
const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
|
|
323
|
-
const webviewRaw = await cdp.evaluateInWebviewFrame(webviewScript, matchFn);
|
|
337
|
+
const webviewRaw = await withTimeout(cdp.evaluateInWebviewFrame(webviewScript, matchFn), 30000, 'evaluateInWebviewFrame');
|
|
324
338
|
if (webviewRaw) {
|
|
325
339
|
raw = typeof webviewRaw === 'string' ? (() => { try { return JSON.parse(webviewRaw); } catch { return null; } })() : webviewRaw;
|
|
326
340
|
}
|
|
@@ -331,7 +345,7 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
331
345
|
if (!raw) {
|
|
332
346
|
const readChatScript = this.getReadChatScript();
|
|
333
347
|
if (!readChatScript) return;
|
|
334
|
-
raw = await cdp.evaluate(readChatScript, 30000);
|
|
348
|
+
raw = await withTimeout(cdp.evaluate(readChatScript, 30000), 30000, 'evaluate.readChatScript');
|
|
335
349
|
if (typeof raw === 'string') {
|
|
336
350
|
try { raw = JSON.parse(raw); } catch { return; }
|
|
337
351
|
}
|
|
@@ -706,7 +720,7 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
706
720
|
);
|
|
707
721
|
|
|
708
722
|
LOG.info('IdeInstance', `[IdeInstance:${this.type}] autoApprove: executing resolveAction for "${targetButton}"`);
|
|
709
|
-
let rawResult = await cdp.evaluate(script, 10000);
|
|
723
|
+
let rawResult = await withTimeout(cdp.evaluate(script, 10000), 10000, 'evaluate.autoApprove');
|
|
710
724
|
if (typeof rawResult === 'string') {
|
|
711
725
|
try { rawResult = JSON.parse(rawResult); } catch { }
|
|
712
726
|
}
|
|
@@ -1067,13 +1067,17 @@ export class ProviderLoader {
|
|
|
1067
1067
|
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
|
|
1068
1068
|
});
|
|
1069
1069
|
|
|
1070
|
+
let reloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1070
1071
|
const handleChange = (filePath: string) => {
|
|
1071
1072
|
if (/[\/\\]fixtures[\/\\]/.test(filePath)) {
|
|
1072
1073
|
return;
|
|
1073
1074
|
}
|
|
1074
1075
|
if (filePath.endsWith('.js') || filePath.endsWith('.json')) {
|
|
1075
|
-
|
|
1076
|
-
|
|
1076
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
1077
|
+
reloadTimer = setTimeout(() => {
|
|
1078
|
+
this.log(`File changed: ${path.basename(filePath)}, reloading...`);
|
|
1079
|
+
this.reload();
|
|
1080
|
+
}, 300);
|
|
1077
1081
|
}
|
|
1078
1082
|
};
|
|
1079
1083
|
|
|
@@ -1130,7 +1134,9 @@ export class ProviderLoader {
|
|
|
1130
1134
|
return { updated: false };
|
|
1131
1135
|
}
|
|
1132
1136
|
const https = require('https') as typeof import('https');
|
|
1133
|
-
const {
|
|
1137
|
+
const { exec } = require('child_process') as typeof import('child_process');
|
|
1138
|
+
const { promisify } = require('util');
|
|
1139
|
+
const execAsync = promisify(exec);
|
|
1134
1140
|
|
|
1135
1141
|
const metaPath = path.join(this.upstreamDir, ProviderLoader.META_FILE);
|
|
1136
1142
|
let prevEtag = '';
|
|
@@ -1207,7 +1213,7 @@ export class ProviderLoader {
|
|
|
1207
1213
|
|
|
1208
1214
|
// Extract
|
|
1209
1215
|
fs.mkdirSync(tmpExtract, { recursive: true });
|
|
1210
|
-
|
|
1216
|
+
await execAsync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
|
|
1211
1217
|
|
|
1212
1218
|
// Tarball internal structure: adhdev-providers-main/ide/... → strip 1 level
|
|
1213
1219
|
const extracted = fs.readdirSync(tmpExtract);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as os from 'os';
|
|
15
|
-
|
|
15
|
+
// Removed execSync import
|
|
16
16
|
import { platform } from 'os';
|
|
17
17
|
import type { ProviderLoader } from './provider-loader.js';
|
|
18
18
|
import type { ProviderModule } from './contracts.js';
|
|
@@ -117,22 +117,40 @@ export class VersionArchive {
|
|
|
117
117
|
|
|
118
118
|
// ─── Version Detection ──────────────────────────────
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
import { exec } from 'child_process';
|
|
121
|
+
|
|
122
|
+
async function runCommand(cmd: string, timeout = 10000): Promise<string | null> {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
exec(cmd, {
|
|
123
125
|
encoding: 'utf-8',
|
|
124
126
|
timeout,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
127
|
+
}, (error, stdout) => {
|
|
128
|
+
if (error) return resolve(null);
|
|
129
|
+
resolve(stdout.trim());
|
|
130
|
+
});
|
|
131
|
+
});
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
function findBinary(name: string): string | null {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
135
|
+
const isWin = platform() === 'win32';
|
|
136
|
+
const paths = (process.env.PATH || '').split(isWin ? ';' : ':');
|
|
137
|
+
const exes = isWin ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
138
|
+
|
|
139
|
+
for (const p of paths) {
|
|
140
|
+
if (!p) continue;
|
|
141
|
+
for (const ext of exes) {
|
|
142
|
+
const fullPath = path.join(p, name + ext);
|
|
143
|
+
try {
|
|
144
|
+
if (fs.existsSync(fullPath)) {
|
|
145
|
+
const stat = fs.statSync(fullPath);
|
|
146
|
+
if (stat.isFile() && (isWin || (stat.mode & 0o111))) {
|
|
147
|
+
return fullPath;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch { }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
/** Extract version string from CLI output */
|
|
@@ -162,16 +180,16 @@ function getPlatformVersionCommand(
|
|
|
162
180
|
return undefined;
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
function getVersion(binary: string, versionCommand?: string): string | null {
|
|
183
|
+
async function getVersion(binary: string, versionCommand?: string): Promise<string | null> {
|
|
166
184
|
// Custom version command from provider.json
|
|
167
185
|
if (versionCommand) {
|
|
168
|
-
const raw = runCommand(versionCommand);
|
|
186
|
+
const raw = await runCommand(versionCommand);
|
|
169
187
|
return raw ? parseVersion(raw) : null;
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
// Default: try --version, then -V, then -v
|
|
173
191
|
for (const flag of ['--version', '-V', '-v']) {
|
|
174
|
-
const raw = runCommand(`"${binary}" ${flag}`);
|
|
192
|
+
const raw = await runCommand(`"${binary}" ${flag}`);
|
|
175
193
|
if (raw && raw.length < 500) return parseVersion(raw);
|
|
176
194
|
}
|
|
177
195
|
return null;
|
|
@@ -191,11 +209,11 @@ function checkPathExists(paths: string[]): string | null {
|
|
|
191
209
|
}
|
|
192
210
|
|
|
193
211
|
/** macOS: Get app version from Info.plist */
|
|
194
|
-
function getMacAppVersion(appPath: string): string | null {
|
|
212
|
+
async function getMacAppVersion(appPath: string): Promise<string | null> {
|
|
195
213
|
if (platform() !== 'darwin' || !appPath.endsWith('.app')) return null;
|
|
196
214
|
const plistPath = path.join(appPath, 'Contents', 'Info.plist');
|
|
197
215
|
if (!fs.existsSync(plistPath)) return null;
|
|
198
|
-
const raw = runCommand(`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${plistPath}"`);
|
|
216
|
+
const raw = await runCommand(`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${plistPath}"`);
|
|
199
217
|
return raw || null;
|
|
200
218
|
}
|
|
201
219
|
|
|
@@ -242,10 +260,10 @@ export async function detectAllVersions(
|
|
|
242
260
|
|
|
243
261
|
// Version: try CLI first, then plist
|
|
244
262
|
if (resolvedBin) {
|
|
245
|
-
info.version = getVersion(resolvedBin, versionCommand);
|
|
263
|
+
info.version = await getVersion(resolvedBin, versionCommand);
|
|
246
264
|
}
|
|
247
265
|
if (!info.version && appPath) {
|
|
248
|
-
info.version = getMacAppVersion(appPath);
|
|
266
|
+
info.version = await getMacAppVersion(appPath);
|
|
249
267
|
}
|
|
250
268
|
|
|
251
269
|
} else if (provider.category === 'cli' || provider.category === 'acp') {
|
|
@@ -256,7 +274,7 @@ export async function detectAllVersions(
|
|
|
256
274
|
info.binary = binPath || null;
|
|
257
275
|
|
|
258
276
|
if (binPath) {
|
|
259
|
-
info.version = getVersion(binPath, versionCommand);
|
|
277
|
+
info.version = await getVersion(binPath, versionCommand);
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
} else if (provider.category === 'extension') {
|