Skip to content

Commit f3ff282

Browse files
committed
chore: land SmartPerfetto local follow-ups
1 parent 32f2fa7 commit f3ff282

52 files changed

Lines changed: 11412 additions & 68 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ logic, use Plan -> independent read-only review -> Revise -> Execute.
6868
Codex read-only review.
6969
- If the primary agent is Codex, do not call Codex to review itself. Prefer a
7070
read-only reviewer sub-agent/tool.
71+
- In ZCode/OpenCode, invoke the `codex` MCP tool for this gate. It exposes
72+
`codex` (start a read-only review session; pass a review prompt with
73+
`sandbox: "read-only"` and `approval-policy: "never"`) and `codex-reply`
74+
(continue a session by `threadId`). Registered as `mcp.codex` in
75+
`~/.zcode/v2/config.json`, backed by `codex mcp-server`.
7176
- If no stable reviewer is available, or the reviewer times out twice, use a
7277
structured self-review plus post-diff review, note the fallback, and rely on
7378
the relevant verification tier from `.claude/rules/testing.md`.

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ logic, use Plan -> independent read-only review -> Revise -> Execute.
6868
Codex read-only review.
6969
- If the primary agent is Codex, do not call Codex to review itself. Prefer a
7070
read-only reviewer sub-agent/tool.
71+
- In ZCode/OpenCode, invoke the `codex` MCP tool for this gate. It exposes
72+
`codex` (start a read-only review session; pass a review prompt with
73+
`sandbox: "read-only"` and `approval-policy: "never"`) and `codex-reply`
74+
(continue a session by `threadId`). Registered as `mcp.codex` in
75+
`~/.zcode/v2/config.json`, backed by `codex mcp-server`.
7176
- If no stable reviewer is available, or the reviewer times out twice, use a
7277
structured self-review plus post-diff review, note the fallback, and rely on
7378
the relevant verification tier from `.claude/rules/testing.md`.

backend/scripts/checkTypesSync.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ interface AnalysisQualitySyncExpectation {
179179
analysisCompletedHasIdentityResolutions: boolean;
180180
analysisCompletedHasTerminationReason: boolean;
181181
analysisCompletedHasTerminationMessage: boolean;
182+
analysisCompletedHasTerminalRunStatus: boolean;
182183
claimReferencesHaveArtifactIds: boolean;
183184
}
184185

@@ -248,6 +249,7 @@ function extractAnalysisQualityExpectations(content: string): AnalysisQualitySyn
248249
analysisCompletedHasIdentityResolutions: /\bidentityResolutions\s*\?:\s*IdentityResolutionV1\[\]\s*;/.test(analysisCompletedBlock),
249250
analysisCompletedHasTerminationReason: /\bterminationReason\s*\?:\s*string\s*;/.test(analysisCompletedBlock),
250251
analysisCompletedHasTerminationMessage: /\bterminationMessage\s*\?:\s*string\s*;/.test(analysisCompletedBlock),
252+
analysisCompletedHasTerminalRunStatus: /\bterminalRunStatus\s*\?:\s*'completed'\s*\|\s*'quota_exceeded'\s*;/.test(analysisCompletedBlock),
251253
claimReferencesHaveArtifactIds:
252254
/\bartifactId\s*\?:\s*string\s*;/.test(claimRefBlock) &&
253255
/\bsourceArtifactId\s*\?:\s*string\s*;/.test(claimRefBlock),

backend/scripts/generateFrontendTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ export interface AnalysisCompletedEvent {
643643
partial?: boolean;
644644
terminationReason?: string;
645645
terminationMessage?: string;
646+
terminalRunStatus?: 'completed' | 'quota_exceeded';
646647
findings: DiagnosticFinding[];
647648
suggestions: string[];
648649
};

backend/src/agent/core/executors/__tests__/hypothesisExecutor.test.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ describe('HypothesisExecutor', () => {
281281

282282
expect(result.rounds).toBe(1);
283283
expect(result.findings).toHaveLength(1);
284-
expect(result.stopReason).toBeNull();
284+
expect(result.stopReason).toBe('Strategy concluded');
285285
expect(services.circuitBreaker.canExecute).toHaveBeenCalled();
286286
expect(services.circuitBreaker.recordIteration).toHaveBeenCalledWith('hypothesis_loop');
287287
expect(services.messageBus.dispatchTasksParallel).toHaveBeenCalled();
@@ -457,6 +457,47 @@ describe('HypothesisExecutor', () => {
457457
expect(services.messageBus.updateHypothesis).toHaveBeenCalled();
458458
});
459459

460+
it('records a stop reason when the strategy planner concludes', async () => {
461+
const ctx = createMockExecutionContext();
462+
strategyPlanner.planNextIteration.mockResolvedValue({
463+
strategy: 'conclude',
464+
confidence: 0.8,
465+
reasoning: 'Sufficient findings collected',
466+
});
467+
468+
const result = await executor.execute(ctx, emitter);
469+
470+
expect(result.stopReason).toBe('Strategy concluded');
471+
expect(emittedUpdates).toEqual(expect.arrayContaining([
472+
expect.objectContaining({
473+
type: 'progress',
474+
content: expect.objectContaining({
475+
phase: 'early_stop',
476+
reason: 'Strategy concluded',
477+
}),
478+
}),
479+
]));
480+
});
481+
482+
it('does not replace an existing focused time range with the global trace range during deep_dive', async () => {
483+
const ctx = createMockExecutionContext({
484+
options: {
485+
traceProcessorService: {},
486+
packageName: 'com.example.app',
487+
timeRange: { start: '0', end: '9999' },
488+
},
489+
});
490+
ctx.sharedContext.focusedTimeRange = { start: '1000', end: '2000' };
491+
492+
strategyPlanner.planNextIteration
493+
.mockResolvedValueOnce({ strategy: 'deep_dive', confidence: 0.6, reasoning: 'Need deeper', focusArea: 'cpu' })
494+
.mockResolvedValueOnce({ strategy: 'conclude', confidence: 0.8, reasoning: 'Done' });
495+
496+
await executor.execute(ctx, emitter);
497+
498+
expect(ctx.sharedContext.focusedTimeRange).toEqual({ start: '1000', end: '2000' });
499+
});
500+
460501
it('handles pivot strategy', async () => {
461502
const ctx = createMockExecutionContext();
462503

@@ -599,8 +640,8 @@ describe('HypothesisExecutor', () => {
599640

600641
const result = await executor.execute(ctx, emitter);
601642

602-
// Should complete without early stop due to noProgress (we reset the counter)
603-
expect(result.stopReason).toBeNull();
643+
// Should complete by strategy conclusion, not no-progress early stop.
644+
expect(result.stopReason).toBe('Strategy concluded');
604645
expect(result.rounds).toBe(4);
605646
// Verify we accumulated findings from both rounds where we had findings
606647
expect(result.findings.length).toBeGreaterThanOrEqual(2);
@@ -626,7 +667,7 @@ describe('HypothesisExecutor', () => {
626667

627668
const result = await executor.execute(ctx, emitter);
628669

629-
expect(result.stopReason).toBeNull();
670+
expect(result.stopReason).toBe('Strategy concluded');
630671
expect(result.rounds).toBe(1);
631672
expect(emittedUpdates.some(
632673
update => update.type === 'progress' && String(update.content.phase || '').includes('intervention')

backend/src/agent/core/executors/hypothesisExecutor.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,13 @@ export class HypothesisExecutor implements AnalysisExecutor {
306306
});
307307

308308
if (lastStrategy.strategy === 'conclude') {
309-
emitter.log('Strategy: conclude - ending analysis');
309+
stopReason = 'Strategy concluded';
310+
emitter.log(`Strategy: conclude - ${stopReason}`);
311+
emitter.emitUpdate('progress', {
312+
phase: 'early_stop',
313+
reason: stopReason,
314+
message: `提前终止: ${stopReason}`,
315+
});
310316
break;
311317
}
312318

@@ -339,7 +345,11 @@ export class HypothesisExecutor implements AnalysisExecutor {
339345
// Handle deep_dive
340346
if (lastStrategy.strategy === 'deep_dive' && lastStrategy.focusArea) {
341347
emitter.log(`Strategy: deep_dive - focusing on ${lastStrategy.focusArea}`);
342-
ctx.sharedContext.focusedTimeRange = ctx.options.timeRange;
348+
const focusedTimeRange = ctx.sharedContext.focusedTimeRange
349+
?? this.getFocusedTimeRangeFromUserFocus(topFocuses);
350+
if (focusedTimeRange) {
351+
ctx.sharedContext.focusedTimeRange = focusedTimeRange;
352+
}
343353

344354
const deepDiveHypothesis = createHypothesis(
345355
`深入分析 ${lastStrategy.focusArea} 领域`, 0.6
@@ -501,6 +511,17 @@ export class HypothesisExecutor implements AnalysisExecutor {
501511
return `用户当前关注点:\n${descriptions.join('\n')}`;
502512
}
503513

514+
private getFocusedTimeRangeFromUserFocus(
515+
topFocuses: UserFocus[]
516+
): SharedAgentContext['focusedTimeRange'] | undefined {
517+
const focus = topFocuses.find(item => item.type === 'timeRange' && item.target.timeRange);
518+
if (!focus?.target.timeRange) return undefined;
519+
return {
520+
start: String(focus.target.timeRange.start),
521+
end: String(focus.target.timeRange.end),
522+
};
523+
}
524+
504525
/**
505526
* Calculate how well findings align with user focus (v2.0)
506527
*/

backend/src/agent/core/orchestratorTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export interface IOrchestrator {
185185
* @deprecated P1-7: Dead code — sessionMap is loaded from `claude_session_map.json` at construction.
186186
* Kept for backward compatibility but never called from route layer.
187187
*/
188-
restoreSessionMapping?(sessionId: string, sdkSessionId: string): void;
188+
restoreSessionMapping?(sessionId: string, sdkSessionId: string, referenceTraceId?: string): void;
189189
/** Restore a cached architecture result from persistence (agentv3). */
190190
restoreArchitectureCache?(traceId: string, architecture: any): void;
191191
/** Get cached architecture for persistence (agentv3). */
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+
import { createMergeStrategyRegistry } from '../mergeStrategies';
4+
import type { Finding, StageResult, SubAgentContext } from '../../types';
5+
6+
function finding(id: string, title: string): Finding {
7+
return {
8+
id,
9+
severity: 'warning',
10+
title,
11+
description: `${title} description`,
12+
source: 'test',
13+
};
14+
}
15+
16+
function stage(stageId: string, findings: Finding[]): StageResult {
17+
return {
18+
stageId,
19+
success: true,
20+
findings,
21+
startTime: 1,
22+
endTime: 2,
23+
retryCount: 0,
24+
};
25+
}
26+
27+
function context(previousResults: StageResult[]): SubAgentContext {
28+
return {
29+
sessionId: 'session-test',
30+
traceId: 'trace-test',
31+
intent: {
32+
primaryGoal: 'test merge findings',
33+
aspects: [],
34+
expectedOutputType: 'diagnosis',
35+
complexity: 'moderate',
36+
followUpType: 'initial',
37+
},
38+
previousResults,
39+
};
40+
}
41+
42+
describe('MergeStrategyRegistry', () => {
43+
it('keeps child findings discoverable when using merge_findings', () => {
44+
const registry = createMergeStrategyRegistry();
45+
const parentFinding = finding('parent-finding', 'Parent finding');
46+
const childFinding = finding('child-finding', 'Child finding');
47+
48+
const { mergedContext, result } = registry.merge(
49+
context([stage('parent-stage', [parentFinding])]),
50+
context([stage('child-stage', [childFinding])]),
51+
{
52+
strategy: 'merge_findings',
53+
conflictResolution: 'keep_both',
54+
childSessionId: 'child-session',
55+
deleteAfterMerge: false,
56+
},
57+
);
58+
59+
const mergedFindings = (mergedContext.previousResults || [])
60+
.flatMap(stageResult => stageResult.findings);
61+
62+
expect(result.mergedFindingsCount).toBe(1);
63+
expect(mergedFindings).toEqual(expect.arrayContaining([
64+
expect.objectContaining({ id: 'parent-finding' }),
65+
expect.objectContaining({ id: 'child-finding' }),
66+
]));
67+
});
68+
});

backend/src/agent/fork/mergeStrategies.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,11 +523,14 @@ export class MergeStrategyRegistry {
523523

524524
// 合并发现
525525
const findingsData = strategy.mergeFindings(parentFindings, childFindings, options);
526+
const mergedResults = options.strategy === 'merge_findings'
527+
? this.injectMergedFindingsResult(resultData.mergedResults, parentFindings, findingsData, options)
528+
: resultData.mergedResults;
526529

527530
// 构建合并后的上下文
528531
const mergedContext: SubAgentContext = {
529532
...parentContext,
530-
previousResults: resultData.mergedResults,
533+
previousResults: mergedResults,
531534
};
532535

533536
return {
@@ -553,6 +556,65 @@ export class MergeStrategyRegistry {
553556
}
554557
return findings;
555558
}
559+
560+
private injectMergedFindingsResult(
561+
parentResults: StageResult[],
562+
parentFindings: Finding[],
563+
findingsData: MergeFindingsData,
564+
options: MergeOptions
565+
): StageResult[] {
566+
const parentFindingCounts = this.countFindings(parentFindings);
567+
const newFindings = findingsData.mergedFindings.filter(finding => {
568+
const key = this.getFindingIdentity(finding);
569+
const count = parentFindingCounts.get(key) ?? 0;
570+
if (count > 0) {
571+
parentFindingCounts.set(key, count - 1);
572+
return false;
573+
}
574+
return true;
575+
});
576+
if (newFindings.length === 0) return parentResults;
577+
578+
const mergedAt = Date.now();
579+
return [
580+
...parentResults,
581+
{
582+
stageId: `merge_findings:${options.childSessionId.slice(0, 8)}`,
583+
success: true,
584+
findings: newFindings,
585+
data: {
586+
_mergeInfo: {
587+
findingsOnly: true,
588+
sourceSession: options.childSessionId,
589+
mergedAt,
590+
},
591+
},
592+
startTime: mergedAt,
593+
endTime: mergedAt,
594+
retryCount: 0,
595+
},
596+
];
597+
}
598+
599+
private countFindings(findings: Finding[]): Map<string, number> {
600+
const counts = new Map<string, number>();
601+
for (const finding of findings) {
602+
const key = this.getFindingIdentity(finding);
603+
counts.set(key, (counts.get(key) ?? 0) + 1);
604+
}
605+
return counts;
606+
}
607+
608+
private getFindingIdentity(finding: Finding): string {
609+
return JSON.stringify({
610+
id: finding.id,
611+
category: finding.category,
612+
type: finding.type,
613+
severity: finding.severity,
614+
title: finding.title,
615+
description: finding.description,
616+
});
617+
}
556618
}
557619

558620
// =============================================================================
@@ -578,4 +640,4 @@ export function createMergeStrategyRegistry(): MergeStrategyRegistry {
578640
return new MergeStrategyRegistry();
579641
}
580642

581-
export default MergeStrategyRegistry;
643+
export default MergeStrategyRegistry;

backend/src/agentOpenAI/__tests__/openAiRuntime.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,4 +1337,13 @@ describe('OpenAIRuntime previous response recovery', () => {
13371337
}));
13381338
expect(runtime.getSdkSessionId('s1', 'trace-b')).toBe('resp_compare_old');
13391339
});
1340+
1341+
it('restores explicit OpenAI session mappings under the comparison key', () => {
1342+
const runtime = new OpenAIRuntime({} as any) as any;
1343+
1344+
runtime.restoreSessionMapping('s1', 'resp_compare_restored', 'trace-b');
1345+
1346+
expect(runtime.getSdkSessionId('s1')).toBeUndefined();
1347+
expect(runtime.getSdkSessionId('s1', 'trace-b')).toBe('resp_compare_restored');
1348+
});
13401349
});

0 commit comments

Comments
 (0)