@exaudeus/workrail 3.7.1 → 3.7.3
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 +41 -41
- package/dist/mcp/handler-factory.js +1 -1
- package/dist/mcp/handlers/v2-execution/start.js +16 -7
- package/dist/mcp/handlers/v2-resume.js +11 -0
- package/dist/mcp/output-schemas.d.ts +44 -9
- package/dist/mcp/output-schemas.js +16 -3
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/v2/tools.d.ts +15 -6
- package/dist/mcp/v2/tools.js +28 -10
- package/dist/mcp/v2-response-formatter.d.ts +1 -0
- package/dist/mcp/v2-response-formatter.js +168 -0
- package/dist/mcp/workflow-protocol-contracts.js +27 -24
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +10 -10
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +2 -2
- package/dist/v2/durable-core/tokens/payloads.d.ts +24 -24
- package/dist/v2/infra/local/session-summary-provider/index.d.ts +2 -0
- package/dist/v2/infra/local/session-summary-provider/index.js +131 -17
- package/dist/v2/infra/local/workspace-anchor/index.js +23 -0
- package/dist/v2/projections/resume-ranking.d.ts +33 -6
- package/dist/v2/projections/resume-ranking.js +261 -30
- package/dist/v2/usecases/enumerate-sessions.d.ts +5 -1
- package/dist/v2/usecases/enumerate-sessions.js +1 -1
- package/package.json +1 -1
|
@@ -7,15 +7,18 @@ 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");
|
|
11
|
+
const snapshot_state_js_1 = require("../../../durable-core/projections/snapshot-state.js");
|
|
10
12
|
const index_js_1 = require("../../../durable-core/ids/index.js");
|
|
11
13
|
const constants_js_1 = require("../../../durable-core/constants.js");
|
|
12
|
-
const MAX_SESSIONS_TO_SCAN =
|
|
14
|
+
const MAX_SESSIONS_TO_SCAN = 200;
|
|
13
15
|
const MAX_RECAP_ANCESTOR_DEPTH = 100;
|
|
14
16
|
const EMPTY_OBSERVATIONS = {
|
|
15
17
|
gitHeadSha: null,
|
|
16
18
|
gitBranch: null,
|
|
17
19
|
repoRootHash: null,
|
|
18
20
|
};
|
|
21
|
+
const TITLE_CONTEXT_KEYS = ['goal', 'taskDescription', 'mrTitle', 'prTitle', 'ticketTitle', 'problem'];
|
|
19
22
|
class LocalSessionSummaryProviderV2 {
|
|
20
23
|
constructor(ports) {
|
|
21
24
|
this.ports = ports;
|
|
@@ -29,20 +32,48 @@ class LocalSessionSummaryProviderV2 {
|
|
|
29
32
|
code: 'SESSION_SUMMARY_ENUMERATION_FAILED',
|
|
30
33
|
message: `Failed to enumerate sessions: ${fsErr.message}`,
|
|
31
34
|
}))
|
|
32
|
-
.andThen((
|
|
35
|
+
.andThen((entries) => collectHealthySummaries(entries.slice(0, MAX_SESSIONS_TO_SCAN), this.ports.sessionStore, this.ports.snapshotStore ?? null));
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
exports.LocalSessionSummaryProviderV2 = LocalSessionSummaryProviderV2;
|
|
36
|
-
function collectHealthySummaries(
|
|
37
|
-
return
|
|
39
|
+
function collectHealthySummaries(entries, sessionStore, snapshotStore) {
|
|
40
|
+
return entries.reduce((acc, entry) => acc.andThen((summaries) => loadSessionSummary(entry, sessionStore, snapshotStore).map((summary) => summary !== null ? [...summaries, summary] : summaries)), (0, neverthrow_1.okAsync)([]));
|
|
38
41
|
}
|
|
39
|
-
function loadSessionSummary(
|
|
42
|
+
function loadSessionSummary(entry, sessionStore, snapshotStore) {
|
|
40
43
|
return sessionStore
|
|
41
|
-
.load(sessionId)
|
|
42
|
-
.
|
|
44
|
+
.load(entry.sessionId)
|
|
45
|
+
.andThen((truth) => {
|
|
46
|
+
const projected = projectSessionSummary(entry.sessionId, truth, entry.mtimeMs);
|
|
47
|
+
if (!projected)
|
|
48
|
+
return (0, neverthrow_1.okAsync)(null);
|
|
49
|
+
if (!snapshotStore)
|
|
50
|
+
return (0, neverthrow_1.okAsync)(projected.summary);
|
|
51
|
+
const ref = safeSnapshotRef(projected.tipSnapshotRef);
|
|
52
|
+
if (!ref)
|
|
53
|
+
return (0, neverthrow_1.okAsync)(projected.summary);
|
|
54
|
+
return snapshotStore.getExecutionSnapshotV1(ref)
|
|
55
|
+
.map((snapshot) => {
|
|
56
|
+
if (!snapshot)
|
|
57
|
+
return projected.summary;
|
|
58
|
+
const engineState = snapshot.enginePayload.engineState;
|
|
59
|
+
const pending = (0, snapshot_state_js_1.derivePendingStep)(engineState);
|
|
60
|
+
const isComplete = (0, snapshot_state_js_1.deriveIsComplete)(engineState);
|
|
61
|
+
return {
|
|
62
|
+
...projected.summary,
|
|
63
|
+
pendingStepId: pending?.stepId ?? null,
|
|
64
|
+
isComplete,
|
|
65
|
+
};
|
|
66
|
+
})
|
|
67
|
+
.orElse(() => (0, neverthrow_1.okAsync)(projected.summary));
|
|
68
|
+
})
|
|
43
69
|
.orElse(() => (0, neverthrow_1.okAsync)(null));
|
|
44
70
|
}
|
|
45
|
-
function
|
|
71
|
+
function safeSnapshotRef(raw) {
|
|
72
|
+
if (!raw || !constants_js_1.SHA256_DIGEST_PATTERN.test(raw))
|
|
73
|
+
return null;
|
|
74
|
+
return (0, index_js_1.asSnapshotRef)((0, index_js_1.asSha256Digest)(raw));
|
|
75
|
+
}
|
|
76
|
+
function projectSessionSummary(sessionId, truth, mtimeMs) {
|
|
46
77
|
const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
|
|
47
78
|
if (health.isErr() || health.value.kind !== 'healthy')
|
|
48
79
|
return null;
|
|
@@ -60,15 +91,22 @@ function projectSessionSummary(sessionId, truth) {
|
|
|
60
91
|
? extractAggregateRecap(outputsRes.value, bestRun.run, bestRun.tipNodeId)
|
|
61
92
|
: null;
|
|
62
93
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
summary: {
|
|
95
|
+
sessionId,
|
|
96
|
+
runId: bestRun.run.runId,
|
|
97
|
+
preferredTip: {
|
|
98
|
+
nodeId: bestRun.tipNodeId,
|
|
99
|
+
lastActivityEventIndex: bestRun.lastActivityEventIndex,
|
|
100
|
+
},
|
|
101
|
+
recapSnippet,
|
|
102
|
+
observations: extractObservations(truth.events),
|
|
103
|
+
workflow,
|
|
104
|
+
sessionTitle: deriveSessionTitle(truth.events, bestRun.run.runId),
|
|
105
|
+
lastModifiedMs: mtimeMs,
|
|
106
|
+
pendingStepId: null,
|
|
107
|
+
isComplete: false,
|
|
68
108
|
},
|
|
69
|
-
|
|
70
|
-
observations: extractObservations(truth.events),
|
|
71
|
-
workflow,
|
|
109
|
+
tipSnapshotRef: bestRun.tipSnapshotRef,
|
|
72
110
|
};
|
|
73
111
|
}
|
|
74
112
|
function selectBestRun(runs) {
|
|
@@ -77,7 +115,9 @@ function selectBestRun(runs) {
|
|
|
77
115
|
if (!tipNodeId)
|
|
78
116
|
return [];
|
|
79
117
|
const tipNode = r.nodesById[tipNodeId];
|
|
80
|
-
|
|
118
|
+
if (!tipNode)
|
|
119
|
+
return [];
|
|
120
|
+
return [{ run: r, tipNodeId, tipSnapshotRef: tipNode.snapshotRef, lastActivityEventIndex: tipNode.createdAtEventIndex }];
|
|
81
121
|
});
|
|
82
122
|
if (runsWithTip.length === 0)
|
|
83
123
|
return null;
|
|
@@ -108,6 +148,80 @@ function extractWorkflowIdentity(events, runId) {
|
|
|
108
148
|
workflowHash: (0, index_js_1.asWorkflowHash)((0, index_js_1.asSha256Digest)(event.data.workflowHash)),
|
|
109
149
|
};
|
|
110
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
|
+
}
|
|
111
225
|
function collectAncestorNodeIds(nodesById, nodeId, remainingDepth) {
|
|
112
226
|
if (remainingDepth === 0)
|
|
113
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,52 @@ export interface HealthySessionSummary {
|
|
|
28
28
|
readonly recapSnippet: RecapSnippet | null;
|
|
29
29
|
readonly observations: SessionObservations;
|
|
30
30
|
readonly workflow: IdentifiedWorkflow;
|
|
31
|
+
readonly sessionTitle: string | null;
|
|
32
|
+
readonly lastModifiedMs: number | null;
|
|
33
|
+
readonly pendingStepId: string | null;
|
|
34
|
+
readonly isComplete: boolean;
|
|
31
35
|
}
|
|
32
|
-
export type WhyMatched = 'matched_head_sha' | 'matched_branch' | 'matched_notes' | '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';
|
|
33
37
|
export type TierAssignment = {
|
|
38
|
+
readonly tier: 0;
|
|
39
|
+
readonly kind: 'matched_exact_id';
|
|
40
|
+
readonly matchField: 'runId' | 'sessionId';
|
|
41
|
+
} | {
|
|
34
42
|
readonly tier: 1;
|
|
35
|
-
readonly kind: '
|
|
43
|
+
readonly kind: 'matched_notes';
|
|
36
44
|
} | {
|
|
37
45
|
readonly tier: 2;
|
|
38
|
-
readonly kind: '
|
|
39
|
-
readonly
|
|
46
|
+
readonly kind: 'matched_notes_partial';
|
|
47
|
+
readonly matchRatio: number;
|
|
40
48
|
} | {
|
|
41
49
|
readonly tier: 3;
|
|
42
|
-
readonly kind: '
|
|
50
|
+
readonly kind: 'matched_workflow_id';
|
|
43
51
|
} | {
|
|
44
52
|
readonly tier: 4;
|
|
45
|
-
readonly kind: '
|
|
53
|
+
readonly kind: 'matched_head_sha';
|
|
46
54
|
} | {
|
|
47
55
|
readonly tier: 5;
|
|
56
|
+
readonly kind: 'matched_branch';
|
|
57
|
+
readonly matchType: 'exact' | 'prefix';
|
|
58
|
+
} | {
|
|
59
|
+
readonly tier: 6;
|
|
48
60
|
readonly kind: 'recency_fallback';
|
|
49
61
|
};
|
|
50
62
|
export declare function normalizeToTokens(text: string): ReadonlySet<string>;
|
|
51
63
|
export declare function allQueryTokensMatch(queryTokens: ReadonlySet<string>, candidateTokens: ReadonlySet<string>): boolean;
|
|
64
|
+
export declare function queryTokenMatchRatio(queryTokens: ReadonlySet<string>, candidateTokens: ReadonlySet<string>): number;
|
|
65
|
+
export declare function fuzzyTokenMatch(queryToken: string, candidateTokens: ReadonlySet<string>): boolean;
|
|
66
|
+
export declare function fuzzyQueryTokenMatchRatio(queryTokens: ReadonlySet<string>, candidateTokens: ReadonlySet<string>): number;
|
|
52
67
|
export interface ResumeQuery {
|
|
53
68
|
readonly gitHeadSha?: string;
|
|
54
69
|
readonly gitBranch?: string;
|
|
70
|
+
readonly repoRootHash?: string;
|
|
71
|
+
readonly sameWorkspaceOnly?: boolean;
|
|
55
72
|
readonly freeTextQuery?: string;
|
|
73
|
+
readonly runId?: string;
|
|
74
|
+
readonly sessionId?: string;
|
|
56
75
|
}
|
|
76
|
+
export declare function computeQueryRelevanceScore(summary: HealthySessionSummary, query: ResumeQuery): number;
|
|
57
77
|
export declare function assignTier(summary: HealthySessionSummary, query: ResumeQuery): TierAssignment;
|
|
58
78
|
export interface RankedResumeCandidate {
|
|
59
79
|
readonly sessionId: SessionId;
|
|
@@ -65,6 +85,13 @@ export interface RankedResumeCandidate {
|
|
|
65
85
|
readonly lastActivityEventIndex: number;
|
|
66
86
|
readonly workflowHash: WorkflowHash;
|
|
67
87
|
readonly workflowId: WorkflowId;
|
|
88
|
+
readonly sessionTitle: string | null;
|
|
89
|
+
readonly gitBranch: string | null;
|
|
90
|
+
readonly pendingStepId: string | null;
|
|
91
|
+
readonly isComplete: boolean;
|
|
92
|
+
readonly lastModifiedMs: number | null;
|
|
93
|
+
readonly confidence: 'strong' | 'medium' | 'weak';
|
|
94
|
+
readonly matchExplanation: string;
|
|
68
95
|
}
|
|
69
96
|
export declare const MAX_RESUME_CANDIDATES = 5;
|
|
70
97
|
export declare function rankResumeCandidates(summaries: readonly HealthySessionSummary[], query: ResumeQuery): readonly RankedResumeCandidate[];
|
|
@@ -4,6 +4,10 @@ exports.MAX_RESUME_CANDIDATES = void 0;
|
|
|
4
4
|
exports.asRecapSnippet = asRecapSnippet;
|
|
5
5
|
exports.normalizeToTokens = normalizeToTokens;
|
|
6
6
|
exports.allQueryTokensMatch = allQueryTokensMatch;
|
|
7
|
+
exports.queryTokenMatchRatio = queryTokenMatchRatio;
|
|
8
|
+
exports.fuzzyTokenMatch = fuzzyTokenMatch;
|
|
9
|
+
exports.fuzzyQueryTokenMatchRatio = fuzzyQueryTokenMatchRatio;
|
|
10
|
+
exports.computeQueryRelevanceScore = computeQueryRelevanceScore;
|
|
7
11
|
exports.assignTier = assignTier;
|
|
8
12
|
exports.rankResumeCandidates = rankResumeCandidates;
|
|
9
13
|
const constants_js_1 = require("../durable-core/constants.js");
|
|
@@ -36,68 +40,295 @@ function allQueryTokensMatch(queryTokens, candidateTokens) {
|
|
|
36
40
|
}
|
|
37
41
|
return true;
|
|
38
42
|
}
|
|
43
|
+
function queryTokenMatchRatio(queryTokens, candidateTokens) {
|
|
44
|
+
if (queryTokens.size === 0 || candidateTokens.size === 0)
|
|
45
|
+
return 0;
|
|
46
|
+
let matched = 0;
|
|
47
|
+
for (const token of queryTokens) {
|
|
48
|
+
if (candidateTokens.has(token))
|
|
49
|
+
matched++;
|
|
50
|
+
}
|
|
51
|
+
return matched / queryTokens.size;
|
|
52
|
+
}
|
|
53
|
+
function fuzzyTokenMatch(queryToken, candidateTokens) {
|
|
54
|
+
if (queryToken.length < 3)
|
|
55
|
+
return false;
|
|
56
|
+
for (const ct of candidateTokens) {
|
|
57
|
+
if (ct.length < 3)
|
|
58
|
+
continue;
|
|
59
|
+
if (ct.includes(queryToken) || queryToken.includes(ct))
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
function fuzzyQueryTokenMatchRatio(queryTokens, candidateTokens) {
|
|
65
|
+
if (queryTokens.size === 0 || candidateTokens.size === 0)
|
|
66
|
+
return 0;
|
|
67
|
+
let matched = 0;
|
|
68
|
+
for (const qt of queryTokens) {
|
|
69
|
+
if (candidateTokens.has(qt) || fuzzyTokenMatch(qt, candidateTokens))
|
|
70
|
+
matched++;
|
|
71
|
+
}
|
|
72
|
+
return matched / queryTokens.size;
|
|
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
|
+
}
|
|
39
228
|
function assignTier(summary, query) {
|
|
229
|
+
if (query.runId && summary.runId === query.runId) {
|
|
230
|
+
return { tier: 0, kind: 'matched_exact_id', matchField: 'runId' };
|
|
231
|
+
}
|
|
232
|
+
if (query.sessionId && String(summary.sessionId) === query.sessionId) {
|
|
233
|
+
return { tier: 0, kind: 'matched_exact_id', matchField: 'sessionId' };
|
|
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
|
+
}
|
|
40
257
|
if (query.gitHeadSha && summary.observations.gitHeadSha === query.gitHeadSha) {
|
|
41
|
-
return { tier:
|
|
258
|
+
return { tier: 4, kind: 'matched_head_sha' };
|
|
42
259
|
}
|
|
43
260
|
if (query.gitBranch && summary.observations.gitBranch) {
|
|
44
261
|
if (summary.observations.gitBranch === query.gitBranch) {
|
|
45
|
-
return { tier:
|
|
262
|
+
return { tier: 5, kind: 'matched_branch', matchType: 'exact' };
|
|
46
263
|
}
|
|
47
264
|
if (summary.observations.gitBranch.startsWith(query.gitBranch) ||
|
|
48
265
|
query.gitBranch.startsWith(summary.observations.gitBranch)) {
|
|
49
|
-
return { tier:
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (query.freeTextQuery && summary.recapSnippet) {
|
|
53
|
-
const queryTokens = normalizeToTokens(query.freeTextQuery);
|
|
54
|
-
const noteTokens = normalizeToTokens(summary.recapSnippet);
|
|
55
|
-
if (allQueryTokensMatch(queryTokens, noteTokens)) {
|
|
56
|
-
return { tier: 3, kind: 'matched_notes' };
|
|
266
|
+
return { tier: 5, kind: 'matched_branch', matchType: 'prefix' };
|
|
57
267
|
}
|
|
58
268
|
}
|
|
59
|
-
|
|
60
|
-
const queryTokens = normalizeToTokens(query.freeTextQuery);
|
|
61
|
-
const workflowTokens = normalizeToTokens(String(summary.workflow.workflowId));
|
|
62
|
-
if (allQueryTokensMatch(queryTokens, workflowTokens)) {
|
|
63
|
-
return { tier: 4, kind: 'matched_workflow_id' };
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return { tier: 5, kind: 'recency_fallback' };
|
|
269
|
+
return { tier: 6, kind: 'recency_fallback' };
|
|
67
270
|
}
|
|
68
271
|
function tierToWhyMatched(tier) {
|
|
69
272
|
switch (tier.kind) {
|
|
273
|
+
case 'matched_exact_id': return 'matched_exact_id';
|
|
70
274
|
case 'matched_head_sha': return 'matched_head_sha';
|
|
71
275
|
case 'matched_branch': return 'matched_branch';
|
|
72
276
|
case 'matched_notes': return 'matched_notes';
|
|
277
|
+
case 'matched_notes_partial': return 'matched_notes_partial';
|
|
73
278
|
case 'matched_workflow_id': return 'matched_workflow_id';
|
|
74
279
|
case 'recency_fallback': return 'recency_fallback';
|
|
75
280
|
}
|
|
76
281
|
}
|
|
77
282
|
exports.MAX_RESUME_CANDIDATES = 5;
|
|
78
283
|
function rankResumeCandidates(summaries, query) {
|
|
79
|
-
const withTier = summaries
|
|
284
|
+
const withTier = summaries
|
|
285
|
+
.filter((summary) => shouldKeepSummary(summary, query))
|
|
286
|
+
.map((summary) => ({
|
|
80
287
|
summary,
|
|
81
288
|
tier: assignTier(summary, query),
|
|
289
|
+
queryScore: computeQueryRelevanceScore(summary, query),
|
|
82
290
|
}));
|
|
83
291
|
const sorted = [...withTier].sort((a, b) => {
|
|
84
292
|
if (a.tier.tier !== b.tier.tier)
|
|
85
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;
|
|
298
|
+
if (a.summary.isComplete !== b.summary.isComplete) {
|
|
299
|
+
return a.summary.isComplete ? 1 : -1;
|
|
300
|
+
}
|
|
301
|
+
if (a.queryScore !== b.queryScore)
|
|
302
|
+
return b.queryScore - a.queryScore;
|
|
303
|
+
if (a.tier.kind === 'matched_notes_partial' && b.tier.kind === 'matched_notes_partial') {
|
|
304
|
+
if (a.tier.matchRatio !== b.tier.matchRatio)
|
|
305
|
+
return b.tier.matchRatio - a.tier.matchRatio;
|
|
306
|
+
}
|
|
86
307
|
const actA = a.summary.preferredTip.lastActivityEventIndex;
|
|
87
308
|
const actB = b.summary.preferredTip.lastActivityEventIndex;
|
|
88
309
|
if (actA !== actB)
|
|
89
310
|
return actB - actA;
|
|
90
311
|
return String(a.summary.sessionId).localeCompare(String(b.summary.sessionId));
|
|
91
312
|
});
|
|
92
|
-
return sorted.slice(0, exports.MAX_RESUME_CANDIDATES).map(({ summary, tier }) =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
});
|
|
103
334
|
}
|
|
@@ -7,7 +7,11 @@ export declare function enumerateSessions(ports: {
|
|
|
7
7
|
readonly directoryListing: DirectoryListingPortV2;
|
|
8
8
|
readonly dataDir: DataDirPortV2;
|
|
9
9
|
}): ResultAsync<readonly SessionId[], FsError>;
|
|
10
|
+
export interface SessionWithMtime {
|
|
11
|
+
readonly sessionId: SessionId;
|
|
12
|
+
readonly mtimeMs: number;
|
|
13
|
+
}
|
|
10
14
|
export declare function enumerateSessionsByRecency(ports: {
|
|
11
15
|
readonly directoryListing: DirectoryListingPortV2;
|
|
12
16
|
readonly dataDir: DataDirPortV2;
|
|
13
|
-
}): ResultAsync<readonly
|
|
17
|
+
}): ResultAsync<readonly SessionWithMtime[], FsError>;
|
|
@@ -22,5 +22,5 @@ function enumerateSessionsByRecency(ports) {
|
|
|
22
22
|
return b.mtimeMs - a.mtimeMs;
|
|
23
23
|
return a.name.localeCompare(b.name);
|
|
24
24
|
})
|
|
25
|
-
.map((entry) => (0, index_js_1.asSessionId)(entry.name)));
|
|
25
|
+
.map((entry) => ({ sessionId: (0, index_js_1.asSessionId)(entry.name), mtimeMs: entry.mtimeMs })));
|
|
26
26
|
}
|