@exaudeus/workrail 3.7.2 → 3.7.4
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/engine/engine-factory.js +4 -1
- package/dist/manifest.json +26 -26
- package/dist/mcp/handlers/v2-execution/start.js +16 -7
- package/dist/mcp/handlers/v2-resume.js +6 -0
- package/dist/mcp/output-schemas.d.ts +25 -5
- package/dist/mcp/output-schemas.js +10 -4
- package/dist/mcp/v2/tools.d.ts +9 -6
- package/dist/mcp/v2/tools.js +23 -10
- package/dist/mcp/v2-response-formatter.js +24 -4
- package/dist/mcp/workflow-protocol-contracts.js +13 -10
- package/dist/v2/infra/local/session-summary-provider/index.js +77 -0
- package/dist/v2/infra/local/workspace-anchor/index.js +23 -0
- package/dist/v2/projections/resume-ranking.d.ts +16 -8
- package/dist/v2/projections/resume-ranking.js +210 -39
- package/package.json +1 -1
- package/workflows/workflow-for-workflows.v2.json +40 -16
|
@@ -7,6 +7,7 @@ const enumerate_sessions_js_1 = require("../../../usecases/enumerate-sessions.js
|
|
|
7
7
|
const session_health_js_1 = require("../../../projections/session-health.js");
|
|
8
8
|
const run_dag_js_1 = require("../../../projections/run-dag.js");
|
|
9
9
|
const node_outputs_js_1 = require("../../../projections/node-outputs.js");
|
|
10
|
+
const run_context_js_1 = require("../../../projections/run-context.js");
|
|
10
11
|
const snapshot_state_js_1 = require("../../../durable-core/projections/snapshot-state.js");
|
|
11
12
|
const index_js_1 = require("../../../durable-core/ids/index.js");
|
|
12
13
|
const constants_js_1 = require("../../../durable-core/constants.js");
|
|
@@ -17,6 +18,7 @@ const EMPTY_OBSERVATIONS = {
|
|
|
17
18
|
gitBranch: null,
|
|
18
19
|
repoRootHash: null,
|
|
19
20
|
};
|
|
21
|
+
const TITLE_CONTEXT_KEYS = ['goal', 'taskDescription', 'mrTitle', 'prTitle', 'ticketTitle', 'problem'];
|
|
20
22
|
class LocalSessionSummaryProviderV2 {
|
|
21
23
|
constructor(ports) {
|
|
22
24
|
this.ports = ports;
|
|
@@ -99,6 +101,7 @@ function projectSessionSummary(sessionId, truth, mtimeMs) {
|
|
|
99
101
|
recapSnippet,
|
|
100
102
|
observations: extractObservations(truth.events),
|
|
101
103
|
workflow,
|
|
104
|
+
sessionTitle: deriveSessionTitle(truth.events, bestRun.run.runId),
|
|
102
105
|
lastModifiedMs: mtimeMs,
|
|
103
106
|
pendingStepId: null,
|
|
104
107
|
isComplete: false,
|
|
@@ -145,6 +148,80 @@ function extractWorkflowIdentity(events, runId) {
|
|
|
145
148
|
workflowHash: (0, index_js_1.asWorkflowHash)((0, index_js_1.asSha256Digest)(event.data.workflowHash)),
|
|
146
149
|
};
|
|
147
150
|
}
|
|
151
|
+
function deriveSessionTitle(events, runId) {
|
|
152
|
+
const contextRes = (0, run_context_js_1.projectRunContextV2)(events);
|
|
153
|
+
if (contextRes.isOk()) {
|
|
154
|
+
const runCtx = contextRes.value.byRunId[runId];
|
|
155
|
+
if (runCtx) {
|
|
156
|
+
for (const key of TITLE_CONTEXT_KEYS) {
|
|
157
|
+
const val = runCtx.context[key];
|
|
158
|
+
if (typeof val === 'string' && val.trim().length > 0) {
|
|
159
|
+
return truncateTitle(val.trim());
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return extractTitleFromFirstRecap(events);
|
|
165
|
+
}
|
|
166
|
+
function extractTitleFromFirstRecap(events) {
|
|
167
|
+
const outputsRes = (0, node_outputs_js_1.projectNodeOutputsV2)(events);
|
|
168
|
+
if (outputsRes.isErr())
|
|
169
|
+
return null;
|
|
170
|
+
let rootNodeId = null;
|
|
171
|
+
let minIndex = Infinity;
|
|
172
|
+
for (const e of events) {
|
|
173
|
+
if (e.kind === constants_js_1.EVENT_KIND.NODE_CREATED && e.eventIndex < minIndex) {
|
|
174
|
+
minIndex = e.eventIndex;
|
|
175
|
+
rootNodeId = e.scope.nodeId;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!rootNodeId)
|
|
179
|
+
return null;
|
|
180
|
+
const nodeOutputs = outputsRes.value.nodesById[rootNodeId];
|
|
181
|
+
if (!nodeOutputs)
|
|
182
|
+
return null;
|
|
183
|
+
const recaps = nodeOutputs.currentByChannel[constants_js_1.OUTPUT_CHANNEL.RECAP];
|
|
184
|
+
const first = recaps?.[0];
|
|
185
|
+
if (!first || first.payload.payloadKind !== constants_js_1.PAYLOAD_KIND.NOTES)
|
|
186
|
+
return null;
|
|
187
|
+
return extractDescriptiveText(first.payload.notesMarkdown);
|
|
188
|
+
}
|
|
189
|
+
function extractDescriptiveText(markdown) {
|
|
190
|
+
const lines = markdown
|
|
191
|
+
.split('\n')
|
|
192
|
+
.map((line) => line.trim())
|
|
193
|
+
.filter((line) => line.length > 0);
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
if (/^#{1,3}\s/.test(line))
|
|
196
|
+
continue;
|
|
197
|
+
if (/^[-_*]{3,}$/.test(line))
|
|
198
|
+
continue;
|
|
199
|
+
if (line.startsWith('|'))
|
|
200
|
+
continue;
|
|
201
|
+
const boldLabel = line.match(/^\*{2}[^*]+\*{2}:?\s*(.*)/);
|
|
202
|
+
if (boldLabel) {
|
|
203
|
+
const value = boldLabel[1]?.trim();
|
|
204
|
+
if (value && value.length > 10)
|
|
205
|
+
return truncateTitle(value);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const listBoldLabel = line.match(/^-\s+\*{2}[^*]+\*{2}:?\s*(.*)/);
|
|
209
|
+
if (listBoldLabel) {
|
|
210
|
+
const value = listBoldLabel[1]?.trim();
|
|
211
|
+
if (value && value.length > 10)
|
|
212
|
+
return truncateTitle(value);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (line.length > 10)
|
|
216
|
+
return truncateTitle(line);
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
function truncateTitle(text, maxLen = 120) {
|
|
221
|
+
if (text.length <= maxLen)
|
|
222
|
+
return text;
|
|
223
|
+
return text.slice(0, maxLen - 1) + '…';
|
|
224
|
+
}
|
|
148
225
|
function collectAncestorNodeIds(nodesById, nodeId, remainingDepth) {
|
|
149
226
|
if (remainingDepth === 0)
|
|
150
227
|
return [nodeId];
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LocalWorkspaceAnchorV2 = void 0;
|
|
4
4
|
const neverthrow_1 = require("neverthrow");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
6
7
|
const util_1 = require("util");
|
|
7
8
|
const url_1 = require("url");
|
|
8
9
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
@@ -32,6 +33,13 @@ class LocalWorkspaceAnchorV2 {
|
|
|
32
33
|
}
|
|
33
34
|
async runGitCommands(cwd) {
|
|
34
35
|
const anchors = [];
|
|
36
|
+
const repoRoot = await this.gitCommand('git rev-parse --show-toplevel', cwd);
|
|
37
|
+
if (!repoRoot)
|
|
38
|
+
return anchors;
|
|
39
|
+
const repoRootHash = hashRepoRoot(repoRoot);
|
|
40
|
+
if (repoRootHash) {
|
|
41
|
+
anchors.push({ key: 'repo_root_hash', value: repoRootHash });
|
|
42
|
+
}
|
|
35
43
|
const branch = await this.gitCommand('git rev-parse --abbrev-ref HEAD', cwd);
|
|
36
44
|
if (branch && branch !== 'HEAD') {
|
|
37
45
|
anchors.push({ key: 'git_branch', value: branch });
|
|
@@ -57,6 +65,21 @@ class LocalWorkspaceAnchorV2 {
|
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
exports.LocalWorkspaceAnchorV2 = LocalWorkspaceAnchorV2;
|
|
68
|
+
function hashRepoRoot(repoRoot) {
|
|
69
|
+
try {
|
|
70
|
+
const normalized = repoRoot.trim();
|
|
71
|
+
if (!normalized)
|
|
72
|
+
return null;
|
|
73
|
+
const digest = createSha256Hex(normalized);
|
|
74
|
+
return `sha256:${digest}`;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function createSha256Hex(input) {
|
|
81
|
+
return (0, crypto_1.createHash)('sha256').update(input).digest('hex');
|
|
82
|
+
}
|
|
60
83
|
function uriToFsPath(uri) {
|
|
61
84
|
if (!uri.startsWith('file://'))
|
|
62
85
|
return null;
|
|
@@ -28,32 +28,33 @@ export interface HealthySessionSummary {
|
|
|
28
28
|
readonly recapSnippet: RecapSnippet | null;
|
|
29
29
|
readonly observations: SessionObservations;
|
|
30
30
|
readonly workflow: IdentifiedWorkflow;
|
|
31
|
+
readonly sessionTitle: string | null;
|
|
31
32
|
readonly lastModifiedMs: number | null;
|
|
32
33
|
readonly pendingStepId: string | null;
|
|
33
34
|
readonly isComplete: boolean;
|
|
34
35
|
}
|
|
35
|
-
export type WhyMatched = 'matched_exact_id' | 'matched_head_sha' | 'matched_branch' | 'matched_notes' | 'matched_notes_partial' | 'matched_workflow_id' | 'recency_fallback';
|
|
36
|
+
export type WhyMatched = 'matched_exact_id' | 'matched_head_sha' | 'matched_repo_root' | 'matched_branch' | 'matched_notes' | 'matched_notes_partial' | 'matched_workflow_id' | 'recency_fallback';
|
|
36
37
|
export type TierAssignment = {
|
|
37
38
|
readonly tier: 0;
|
|
38
39
|
readonly kind: 'matched_exact_id';
|
|
39
40
|
readonly matchField: 'runId' | 'sessionId';
|
|
40
41
|
} | {
|
|
41
42
|
readonly tier: 1;
|
|
42
|
-
readonly kind: '
|
|
43
|
+
readonly kind: 'matched_notes';
|
|
43
44
|
} | {
|
|
44
45
|
readonly tier: 2;
|
|
45
|
-
readonly kind: '
|
|
46
|
-
readonly
|
|
46
|
+
readonly kind: 'matched_notes_partial';
|
|
47
|
+
readonly matchRatio: number;
|
|
47
48
|
} | {
|
|
48
49
|
readonly tier: 3;
|
|
49
|
-
readonly kind: '
|
|
50
|
+
readonly kind: 'matched_workflow_id';
|
|
50
51
|
} | {
|
|
51
52
|
readonly tier: 4;
|
|
52
|
-
readonly kind: '
|
|
53
|
-
readonly matchRatio: number;
|
|
53
|
+
readonly kind: 'matched_head_sha';
|
|
54
54
|
} | {
|
|
55
55
|
readonly tier: 5;
|
|
56
|
-
readonly kind: '
|
|
56
|
+
readonly kind: 'matched_branch';
|
|
57
|
+
readonly matchType: 'exact' | 'prefix';
|
|
57
58
|
} | {
|
|
58
59
|
readonly tier: 6;
|
|
59
60
|
readonly kind: 'recency_fallback';
|
|
@@ -66,10 +67,13 @@ export declare function fuzzyQueryTokenMatchRatio(queryTokens: ReadonlySet<strin
|
|
|
66
67
|
export interface ResumeQuery {
|
|
67
68
|
readonly gitHeadSha?: string;
|
|
68
69
|
readonly gitBranch?: string;
|
|
70
|
+
readonly repoRootHash?: string;
|
|
71
|
+
readonly sameWorkspaceOnly?: boolean;
|
|
69
72
|
readonly freeTextQuery?: string;
|
|
70
73
|
readonly runId?: string;
|
|
71
74
|
readonly sessionId?: string;
|
|
72
75
|
}
|
|
76
|
+
export declare function computeQueryRelevanceScore(summary: HealthySessionSummary, query: ResumeQuery): number;
|
|
73
77
|
export declare function assignTier(summary: HealthySessionSummary, query: ResumeQuery): TierAssignment;
|
|
74
78
|
export interface RankedResumeCandidate {
|
|
75
79
|
readonly sessionId: SessionId;
|
|
@@ -81,9 +85,13 @@ export interface RankedResumeCandidate {
|
|
|
81
85
|
readonly lastActivityEventIndex: number;
|
|
82
86
|
readonly workflowHash: WorkflowHash;
|
|
83
87
|
readonly workflowId: WorkflowId;
|
|
88
|
+
readonly sessionTitle: string | null;
|
|
89
|
+
readonly gitBranch: string | null;
|
|
84
90
|
readonly pendingStepId: string | null;
|
|
85
91
|
readonly isComplete: boolean;
|
|
86
92
|
readonly lastModifiedMs: number | null;
|
|
93
|
+
readonly confidence: 'strong' | 'medium' | 'weak';
|
|
94
|
+
readonly matchExplanation: string;
|
|
87
95
|
}
|
|
88
96
|
export declare const MAX_RESUME_CANDIDATES = 5;
|
|
89
97
|
export declare function rankResumeCandidates(summaries: readonly HealthySessionSummary[], query: ResumeQuery): readonly RankedResumeCandidate[];
|
|
@@ -7,6 +7,7 @@ exports.allQueryTokensMatch = allQueryTokensMatch;
|
|
|
7
7
|
exports.queryTokenMatchRatio = queryTokenMatchRatio;
|
|
8
8
|
exports.fuzzyTokenMatch = fuzzyTokenMatch;
|
|
9
9
|
exports.fuzzyQueryTokenMatchRatio = fuzzyQueryTokenMatchRatio;
|
|
10
|
+
exports.computeQueryRelevanceScore = computeQueryRelevanceScore;
|
|
10
11
|
exports.assignTier = assignTier;
|
|
11
12
|
exports.rankResumeCandidates = rankResumeCandidates;
|
|
12
13
|
const constants_js_1 = require("../durable-core/constants.js");
|
|
@@ -71,6 +72,159 @@ function fuzzyQueryTokenMatchRatio(queryTokens, candidateTokens) {
|
|
|
71
72
|
return matched / queryTokens.size;
|
|
72
73
|
}
|
|
73
74
|
const MIN_PARTIAL_MATCH_RATIO = 0.4;
|
|
75
|
+
const LOW_SIGNAL_QUERY_TOKENS = new Set([
|
|
76
|
+
'task',
|
|
77
|
+
'dev',
|
|
78
|
+
'work',
|
|
79
|
+
'workflow',
|
|
80
|
+
'coding',
|
|
81
|
+
'feature',
|
|
82
|
+
'phase',
|
|
83
|
+
'implement',
|
|
84
|
+
'implementation',
|
|
85
|
+
'review',
|
|
86
|
+
'fix',
|
|
87
|
+
'bug',
|
|
88
|
+
]);
|
|
89
|
+
function tokenSpecificityWeight(token) {
|
|
90
|
+
if (token.length <= 2)
|
|
91
|
+
return 0.2;
|
|
92
|
+
if (LOW_SIGNAL_QUERY_TOKENS.has(token))
|
|
93
|
+
return 0.35;
|
|
94
|
+
return Math.min(2.5, 1 + Math.max(0, token.length - 4) * 0.18);
|
|
95
|
+
}
|
|
96
|
+
function weightedFuzzyQueryTokenMatchRatio(queryTokens, candidateTokens) {
|
|
97
|
+
if (queryTokens.size === 0 || candidateTokens.size === 0)
|
|
98
|
+
return 0;
|
|
99
|
+
let matchedWeight = 0;
|
|
100
|
+
let totalWeight = 0;
|
|
101
|
+
for (const qt of queryTokens) {
|
|
102
|
+
const weight = tokenSpecificityWeight(qt);
|
|
103
|
+
totalWeight += weight;
|
|
104
|
+
if (candidateTokens.has(qt) || fuzzyTokenMatch(qt, candidateTokens)) {
|
|
105
|
+
matchedWeight += weight;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return totalWeight === 0 ? 0 : matchedWeight / totalWeight;
|
|
109
|
+
}
|
|
110
|
+
function repoScopeMatches(summary, query) {
|
|
111
|
+
return Boolean(query.repoRootHash &&
|
|
112
|
+
summary.observations.repoRootHash &&
|
|
113
|
+
query.repoRootHash === summary.observations.repoRootHash);
|
|
114
|
+
}
|
|
115
|
+
function shouldKeepSummary(summary, query) {
|
|
116
|
+
if (!query.sameWorkspaceOnly)
|
|
117
|
+
return true;
|
|
118
|
+
if (!query.repoRootHash)
|
|
119
|
+
return true;
|
|
120
|
+
return repoScopeMatches(summary, query);
|
|
121
|
+
}
|
|
122
|
+
function buildSearchableSessionText(summary) {
|
|
123
|
+
return [summary.sessionTitle, summary.recapSnippet]
|
|
124
|
+
.filter((part) => Boolean(part && part.trim().length > 0))
|
|
125
|
+
.join('\n\n');
|
|
126
|
+
}
|
|
127
|
+
function collectMatchReasons(summary, query, tier) {
|
|
128
|
+
const reasons = [tierToWhyMatched(tier)];
|
|
129
|
+
if (repoScopeMatches(summary, query) && !reasons.includes('matched_repo_root')) {
|
|
130
|
+
reasons.push('matched_repo_root');
|
|
131
|
+
}
|
|
132
|
+
if (query.gitHeadSha &&
|
|
133
|
+
summary.observations.gitHeadSha === query.gitHeadSha &&
|
|
134
|
+
!reasons.includes('matched_head_sha')) {
|
|
135
|
+
reasons.push('matched_head_sha');
|
|
136
|
+
}
|
|
137
|
+
if (query.gitBranch &&
|
|
138
|
+
summary.observations.gitBranch &&
|
|
139
|
+
(summary.observations.gitBranch === query.gitBranch ||
|
|
140
|
+
summary.observations.gitBranch.startsWith(query.gitBranch) ||
|
|
141
|
+
query.gitBranch.startsWith(summary.observations.gitBranch)) &&
|
|
142
|
+
!reasons.includes('matched_branch')) {
|
|
143
|
+
reasons.push('matched_branch');
|
|
144
|
+
}
|
|
145
|
+
return reasons;
|
|
146
|
+
}
|
|
147
|
+
function buildPreviewSnippet(summary, query) {
|
|
148
|
+
const previewSource = buildSearchableSessionText(summary);
|
|
149
|
+
if (!previewSource)
|
|
150
|
+
return '';
|
|
151
|
+
const queryTokens = query.freeTextQuery ? [...normalizeToTokens(query.freeTextQuery)] : [];
|
|
152
|
+
if (queryTokens.length === 0)
|
|
153
|
+
return summary.recapSnippet ?? previewSource;
|
|
154
|
+
const lower = previewSource.toLowerCase();
|
|
155
|
+
let bestIndex = -1;
|
|
156
|
+
for (const token of queryTokens) {
|
|
157
|
+
if (token.length < 3)
|
|
158
|
+
continue;
|
|
159
|
+
const idx = lower.indexOf(token);
|
|
160
|
+
if (idx !== -1 && (bestIndex === -1 || idx < bestIndex))
|
|
161
|
+
bestIndex = idx;
|
|
162
|
+
}
|
|
163
|
+
if (bestIndex === -1)
|
|
164
|
+
return summary.recapSnippet ?? previewSource;
|
|
165
|
+
const start = Math.max(0, bestIndex - 100);
|
|
166
|
+
const end = Math.min(previewSource.length, bestIndex + 180);
|
|
167
|
+
const slice = previewSource.slice(start, end).trim();
|
|
168
|
+
const prefix = start > 0 ? '...' : '';
|
|
169
|
+
const suffix = end < previewSource.length ? '...' : '';
|
|
170
|
+
return `${prefix}${slice}${suffix}`;
|
|
171
|
+
}
|
|
172
|
+
function deriveConfidence(tier, reasons) {
|
|
173
|
+
if (tier.kind === 'matched_exact_id' || tier.kind === 'matched_notes')
|
|
174
|
+
return 'strong';
|
|
175
|
+
if (tier.kind === 'matched_notes_partial' || tier.kind === 'matched_workflow_id')
|
|
176
|
+
return 'medium';
|
|
177
|
+
if (reasons.includes('matched_repo_root') || reasons.includes('matched_head_sha') || reasons.includes('matched_branch')) {
|
|
178
|
+
return 'medium';
|
|
179
|
+
}
|
|
180
|
+
return 'weak';
|
|
181
|
+
}
|
|
182
|
+
function buildMatchExplanation(tier, reasons, summary) {
|
|
183
|
+
const parts = [];
|
|
184
|
+
switch (tier.kind) {
|
|
185
|
+
case 'matched_exact_id':
|
|
186
|
+
parts.push(`Exact ${tier.matchField} match`);
|
|
187
|
+
break;
|
|
188
|
+
case 'matched_notes':
|
|
189
|
+
parts.push('Strong text match against session title/notes');
|
|
190
|
+
break;
|
|
191
|
+
case 'matched_notes_partial':
|
|
192
|
+
parts.push(`Partial text match (${Math.round(tier.matchRatio * 100)}%) against session title/notes`);
|
|
193
|
+
break;
|
|
194
|
+
case 'matched_workflow_id':
|
|
195
|
+
parts.push('Matched workflow type');
|
|
196
|
+
break;
|
|
197
|
+
case 'matched_head_sha':
|
|
198
|
+
parts.push('Matched current git commit');
|
|
199
|
+
break;
|
|
200
|
+
case 'matched_branch':
|
|
201
|
+
parts.push(tier.matchType === 'exact' ? 'Matched current git branch' : 'Partially matched current git branch');
|
|
202
|
+
break;
|
|
203
|
+
case 'recency_fallback':
|
|
204
|
+
parts.push('Recent session with no stronger explicit match');
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (reasons.includes('matched_repo_root'))
|
|
208
|
+
parts.push('same workspace');
|
|
209
|
+
if (summary.isComplete)
|
|
210
|
+
parts.push('completed');
|
|
211
|
+
return parts.join('; ');
|
|
212
|
+
}
|
|
213
|
+
function computeQueryRelevanceScore(summary, query) {
|
|
214
|
+
const queryTokens = query.freeTextQuery ? normalizeToTokens(query.freeTextQuery) : null;
|
|
215
|
+
const repoBonus = repoScopeMatches(summary, query) ? 0.2 : 0;
|
|
216
|
+
if (!queryTokens || queryTokens.size === 0)
|
|
217
|
+
return repoBonus;
|
|
218
|
+
const sessionText = buildSearchableSessionText(summary);
|
|
219
|
+
const sessionTextRatio = sessionText
|
|
220
|
+
? weightedFuzzyQueryTokenMatchRatio(queryTokens, normalizeToTokens(sessionText))
|
|
221
|
+
: 0;
|
|
222
|
+
const workflowRatio = weightedFuzzyQueryTokenMatchRatio(queryTokens, normalizeToTokens(String(summary.workflow.workflowId)));
|
|
223
|
+
const branchRatio = summary.observations.gitBranch
|
|
224
|
+
? weightedFuzzyQueryTokenMatchRatio(queryTokens, normalizeToTokens(summary.observations.gitBranch))
|
|
225
|
+
: 0;
|
|
226
|
+
return Math.max(sessionTextRatio + repoBonus, workflowRatio * 0.75 + repoBonus, branchRatio * 0.5 + repoBonus);
|
|
227
|
+
}
|
|
74
228
|
function assignTier(summary, query) {
|
|
75
229
|
if (query.runId && summary.runId === query.runId) {
|
|
76
230
|
return { tier: 0, kind: 'matched_exact_id', matchField: 'runId' };
|
|
@@ -78,37 +232,38 @@ function assignTier(summary, query) {
|
|
|
78
232
|
if (query.sessionId && String(summary.sessionId) === query.sessionId) {
|
|
79
233
|
return { tier: 0, kind: 'matched_exact_id', matchField: 'sessionId' };
|
|
80
234
|
}
|
|
235
|
+
const queryTokens = query.freeTextQuery ? normalizeToTokens(query.freeTextQuery) : null;
|
|
236
|
+
if (queryTokens && queryTokens.size > 0) {
|
|
237
|
+
const sessionText = buildSearchableSessionText(summary);
|
|
238
|
+
if (sessionText) {
|
|
239
|
+
const sessionTextTokens = normalizeToTokens(sessionText);
|
|
240
|
+
if (allQueryTokensMatch(queryTokens, sessionTextTokens)) {
|
|
241
|
+
return { tier: 1, kind: 'matched_notes' };
|
|
242
|
+
}
|
|
243
|
+
const ratio = weightedFuzzyQueryTokenMatchRatio(queryTokens, sessionTextTokens);
|
|
244
|
+
if (ratio >= MIN_PARTIAL_MATCH_RATIO) {
|
|
245
|
+
return { tier: 2, kind: 'matched_notes_partial', matchRatio: ratio };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const workflowTokens = normalizeToTokens(String(summary.workflow.workflowId));
|
|
249
|
+
if (allQueryTokensMatch(queryTokens, workflowTokens)) {
|
|
250
|
+
return { tier: 3, kind: 'matched_workflow_id' };
|
|
251
|
+
}
|
|
252
|
+
const workflowRatio = weightedFuzzyQueryTokenMatchRatio(queryTokens, workflowTokens);
|
|
253
|
+
if (workflowRatio >= MIN_PARTIAL_MATCH_RATIO) {
|
|
254
|
+
return { tier: 3, kind: 'matched_workflow_id' };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
81
257
|
if (query.gitHeadSha && summary.observations.gitHeadSha === query.gitHeadSha) {
|
|
82
|
-
return { tier:
|
|
258
|
+
return { tier: 4, kind: 'matched_head_sha' };
|
|
83
259
|
}
|
|
84
260
|
if (query.gitBranch && summary.observations.gitBranch) {
|
|
85
261
|
if (summary.observations.gitBranch === query.gitBranch) {
|
|
86
|
-
return { tier:
|
|
262
|
+
return { tier: 5, kind: 'matched_branch', matchType: 'exact' };
|
|
87
263
|
}
|
|
88
264
|
if (summary.observations.gitBranch.startsWith(query.gitBranch) ||
|
|
89
265
|
query.gitBranch.startsWith(summary.observations.gitBranch)) {
|
|
90
|
-
return { tier:
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const queryTokens = query.freeTextQuery ? normalizeToTokens(query.freeTextQuery) : null;
|
|
94
|
-
if (queryTokens && queryTokens.size > 0 && summary.recapSnippet) {
|
|
95
|
-
const noteTokens = normalizeToTokens(summary.recapSnippet);
|
|
96
|
-
if (allQueryTokensMatch(queryTokens, noteTokens)) {
|
|
97
|
-
return { tier: 3, kind: 'matched_notes' };
|
|
98
|
-
}
|
|
99
|
-
const ratio = fuzzyQueryTokenMatchRatio(queryTokens, noteTokens);
|
|
100
|
-
if (ratio >= MIN_PARTIAL_MATCH_RATIO) {
|
|
101
|
-
return { tier: 4, kind: 'matched_notes_partial', matchRatio: ratio };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (queryTokens && queryTokens.size > 0 && summary.workflow.kind === 'identified') {
|
|
105
|
-
const workflowTokens = normalizeToTokens(String(summary.workflow.workflowId));
|
|
106
|
-
if (allQueryTokensMatch(queryTokens, workflowTokens)) {
|
|
107
|
-
return { tier: 5, kind: 'matched_workflow_id' };
|
|
108
|
-
}
|
|
109
|
-
const ratio = fuzzyQueryTokenMatchRatio(queryTokens, workflowTokens);
|
|
110
|
-
if (ratio >= MIN_PARTIAL_MATCH_RATIO) {
|
|
111
|
-
return { tier: 5, kind: 'matched_workflow_id' };
|
|
266
|
+
return { tier: 5, kind: 'matched_branch', matchType: 'prefix' };
|
|
112
267
|
}
|
|
113
268
|
}
|
|
114
269
|
return { tier: 6, kind: 'recency_fallback' };
|
|
@@ -126,16 +281,25 @@ function tierToWhyMatched(tier) {
|
|
|
126
281
|
}
|
|
127
282
|
exports.MAX_RESUME_CANDIDATES = 5;
|
|
128
283
|
function rankResumeCandidates(summaries, query) {
|
|
129
|
-
const withTier = summaries
|
|
284
|
+
const withTier = summaries
|
|
285
|
+
.filter((summary) => shouldKeepSummary(summary, query))
|
|
286
|
+
.map((summary) => ({
|
|
130
287
|
summary,
|
|
131
288
|
tier: assignTier(summary, query),
|
|
289
|
+
queryScore: computeQueryRelevanceScore(summary, query),
|
|
132
290
|
}));
|
|
133
291
|
const sorted = [...withTier].sort((a, b) => {
|
|
134
292
|
if (a.tier.tier !== b.tier.tier)
|
|
135
293
|
return a.tier.tier - b.tier.tier;
|
|
294
|
+
const aSameRepo = repoScopeMatches(a.summary, query);
|
|
295
|
+
const bSameRepo = repoScopeMatches(b.summary, query);
|
|
296
|
+
if (aSameRepo !== bSameRepo)
|
|
297
|
+
return aSameRepo ? -1 : 1;
|
|
136
298
|
if (a.summary.isComplete !== b.summary.isComplete) {
|
|
137
299
|
return a.summary.isComplete ? 1 : -1;
|
|
138
300
|
}
|
|
301
|
+
if (a.queryScore !== b.queryScore)
|
|
302
|
+
return b.queryScore - a.queryScore;
|
|
139
303
|
if (a.tier.kind === 'matched_notes_partial' && b.tier.kind === 'matched_notes_partial') {
|
|
140
304
|
if (a.tier.matchRatio !== b.tier.matchRatio)
|
|
141
305
|
return b.tier.matchRatio - a.tier.matchRatio;
|
|
@@ -146,18 +310,25 @@ function rankResumeCandidates(summaries, query) {
|
|
|
146
310
|
return actB - actA;
|
|
147
311
|
return String(a.summary.sessionId).localeCompare(String(b.summary.sessionId));
|
|
148
312
|
});
|
|
149
|
-
return sorted.slice(0, exports.MAX_RESUME_CANDIDATES).map(({ summary, tier }) =>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
313
|
+
return sorted.slice(0, exports.MAX_RESUME_CANDIDATES).map(({ summary, tier }) => {
|
|
314
|
+
const whyMatched = collectMatchReasons(summary, query, tier);
|
|
315
|
+
return {
|
|
316
|
+
sessionId: summary.sessionId,
|
|
317
|
+
runId: summary.runId,
|
|
318
|
+
preferredTipNodeId: summary.preferredTip.nodeId,
|
|
319
|
+
snippet: buildPreviewSnippet(summary, query),
|
|
320
|
+
whyMatched,
|
|
321
|
+
tierAssignment: tier,
|
|
322
|
+
lastActivityEventIndex: summary.preferredTip.lastActivityEventIndex,
|
|
323
|
+
workflowHash: summary.workflow.workflowHash,
|
|
324
|
+
workflowId: summary.workflow.workflowId,
|
|
325
|
+
sessionTitle: summary.sessionTitle,
|
|
326
|
+
gitBranch: summary.observations.gitBranch,
|
|
327
|
+
pendingStepId: summary.pendingStepId,
|
|
328
|
+
isComplete: summary.isComplete,
|
|
329
|
+
lastModifiedMs: summary.lastModifiedMs,
|
|
330
|
+
confidence: deriveConfidence(tier, whyMatched),
|
|
331
|
+
matchExplanation: buildMatchExplanation(tier, whyMatched, summary),
|
|
332
|
+
};
|
|
333
|
+
});
|
|
163
334
|
}
|