@exaudeus/workrail 3.7.2 → 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.
@@ -240,7 +240,8 @@ async function createWorkRailEngine(config = {}) {
240
240
  engineActive = true;
241
241
  const engine = {
242
242
  async startWorkflow(workflowId) {
243
- const result = await (0, start_js_1.executeStartWorkflow)({ workflowId }, v2Ctx);
243
+ const workspacePath = process.cwd();
244
+ const result = await (0, start_js_1.executeStartWorkflow)({ workflowId, workspacePath }, v2Ctx);
244
245
  if (result.isErr()) {
245
246
  return (0, types_js_1.engineErr)(mapStartError(result.error));
246
247
  }
@@ -248,9 +249,11 @@ async function createWorkRailEngine(config = {}) {
248
249
  },
249
250
  async continueWorkflow(stateToken, ackToken, output, context) {
250
251
  const intent = ackToken ? 'advance' : 'rehydrate';
252
+ const workspacePath = process.cwd();
251
253
  const input = {
252
254
  continueToken: (0, types_js_1.unwrapToken)(stateToken),
253
255
  intent: intent,
256
+ ...(intent === 'rehydrate' ? { workspacePath } : {}),
254
257
  ...(output ? {
255
258
  output: {
256
259
  notesMarkdown: output.notesMarkdown,
@@ -438,8 +438,8 @@
438
438
  "bytes": 213
439
439
  },
440
440
  "engine/engine-factory.js": {
441
- "sha256": "07d88be2ab8488615871ae363b9db4fc23caebba8231c8c2aaa2f97da2b18fdd",
442
- "bytes": 13658
441
+ "sha256": "5461827b470642059198115f5248e97463db9bfb012f63971a4c7f69acda5b7c",
442
+ "bytes": 13841
443
443
  },
444
444
  "engine/index.d.ts": {
445
445
  "sha256": "91e12882c565e96a9809fdf43a0dc0a77fdffdfd562aaf43d2e86e6590ed0b16",
@@ -838,8 +838,8 @@
838
838
  "bytes": 3422
839
839
  },
840
840
  "mcp/handlers/v2-execution/start.js": {
841
- "sha256": "593edf798bb1d2d0011ec81e4f170e4f4003b978cd9900fd785b583d15cd1085",
842
- "bytes": 18014
841
+ "sha256": "e572eb50c6a0a30aa138620f32b04702727f07ce2570c29022d1027981c4bcef",
842
+ "bytes": 18496
843
843
  },
844
844
  "mcp/handlers/v2-reference-resolver.d.ts": {
845
845
  "sha256": "0b1d2640306b6a0c1067ca66bb20e6ec35d291b33dc328a78d5bfa8006394234",
@@ -862,8 +862,8 @@
862
862
  "bytes": 471
863
863
  },
864
864
  "mcp/handlers/v2-resume.js": {
865
- "sha256": "780e4611d4fb7d7f7fac8cddf576ada1e9ba752f167a678ebd03d94ee518e298",
866
- "bytes": 3828
865
+ "sha256": "536ba5db0c8fedf8a55c63a59eae4da5ad1cc793e4abf137a5ac9536e6514f68",
866
+ "bytes": 4140
867
867
  },
868
868
  "mcp/handlers/v2-state-conversion.d.ts": {
869
869
  "sha256": "94bd06904ef58dd210ff17ffb75c2492beea8937eb06d99749e5d860c0e0d96b",
@@ -914,12 +914,12 @@
914
914
  "bytes": 7991
915
915
  },
916
916
  "mcp/output-schemas.d.ts": {
917
- "sha256": "b33bd6e889811ee4de7bd3b4b4fd6d52042cb7a6bdd23c8bd38baa42775e39c3",
918
- "bytes": 52320
917
+ "sha256": "191d696fd1c8d7dc8f1908c903f84117ceaed6c0aa323fb7606dd3566d739689",
918
+ "bytes": 53246
919
919
  },
920
920
  "mcp/output-schemas.js": {
921
- "sha256": "ba7dda4faec5a8bc1e497b41966b5033ab2930c9cae7b46e85ea1d25b39f79fa",
922
- "bytes": 13715
921
+ "sha256": "28aa9b75ef6c21522b458bd4d05c55e28673510356f8f7e56eb1304544d35132",
922
+ "bytes": 14384
923
923
  },
924
924
  "mcp/render-envelope.d.ts": {
925
925
  "sha256": "22e83e1aba52968a7136cf289125a217b5f462a5a66a1eebe4669006e3326fdb",
@@ -1062,8 +1062,8 @@
1062
1062
  "bytes": 527
1063
1063
  },
1064
1064
  "mcp/v2-response-formatter.js": {
1065
- "sha256": "f8a8cb5c1fe1b081eca9228a09732dde7247aaf396ff14556cc721c66eee5ab8",
1066
- "bytes": 21118
1065
+ "sha256": "308cc7c1fedd491f5d38607869d4b4e1196bfcf450835a53d52154827b323e23",
1066
+ "bytes": 22402
1067
1067
  },
1068
1068
  "mcp/v2/tool-registry.d.ts": {
1069
1069
  "sha256": "d4d4927728c3cab1c014661d499dd0119538371bc6c5e821a4cd31df7abebedf",
@@ -1074,12 +1074,12 @@
1074
1074
  "bytes": 3276
1075
1075
  },
1076
1076
  "mcp/v2/tools.d.ts": {
1077
- "sha256": "89fff4db9e2b8f734d7453801fbc404a1518fccafce2a942af6656a6cdc2dc1e",
1078
- "bytes": 7228
1077
+ "sha256": "93f1eac69e9d2be8ca354d714fbc64e3cea0e235e6669847677071d83a66450f",
1078
+ "bytes": 7288
1079
1079
  },
1080
1080
  "mcp/v2/tools.js": {
1081
- "sha256": "c8bfc63a04911d44f13e62c0d9357dacf62ec437dd21f5b6f09c289a973687cb",
1082
- "bytes": 9406
1081
+ "sha256": "2b5d12d4de385ca8c530532d12467f113d58a09a808fe075896d2b1c0caa52ae",
1082
+ "bytes": 10071
1083
1083
  },
1084
1084
  "mcp/validation/bounded-json.d.ts": {
1085
1085
  "sha256": "82203ac6123d5c6989606c3b5405aaea99ab829c8958835f9ae3ba45b8bc8fd5",
@@ -1150,8 +1150,8 @@
1150
1150
  "bytes": 1863
1151
1151
  },
1152
1152
  "mcp/workflow-protocol-contracts.js": {
1153
- "sha256": "8da5f9dfb6f87ec12de5f6e925ae25b022a91432c430738bff1ba14fafeb03fb",
1154
- "bytes": 13357
1153
+ "sha256": "250fb814372bbf75a4cff9adc2d257da4cf0f4f480dac263860e74ebdd405246",
1154
+ "bytes": 13981
1155
1155
  },
1156
1156
  "mcp/workflow-tool-edition-selector.d.ts": {
1157
1157
  "sha256": "75194908a89aaf7dc48fb661aea099827e7032ec76daf8f2de91bd59d46764ef",
@@ -2118,8 +2118,8 @@
2118
2118
  "bytes": 1136
2119
2119
  },
2120
2120
  "v2/infra/local/session-summary-provider/index.js": {
2121
- "sha256": "93ca7df8ae1ae5e29c29470a6fb947e05b2def481c2b3d8decf4a5dbe8fd8e1c",
2122
- "bytes": 7611
2121
+ "sha256": "504d432a35b3ceb75697c00b5db1576570b38c1415304f903ba77d0dfed04ce7",
2122
+ "bytes": 10452
2123
2123
  },
2124
2124
  "v2/infra/local/sha256/index.d.ts": {
2125
2125
  "sha256": "8a727b7e54a38275ca6f9f1b8730f97cfc0a212df035df1bdc58e716e6824230",
@@ -2166,8 +2166,8 @@
2166
2166
  "bytes": 537
2167
2167
  },
2168
2168
  "v2/infra/local/workspace-anchor/index.js": {
2169
- "sha256": "a0fe032a9205606c6d592221f1300a3113c7d0cb3e126e458fef3dc8fa6e823c",
2170
- "bytes": 2272
2169
+ "sha256": "d76172ecbe3457760abc0b6f0032eb4089758fb3c9ed473ba0f134ac57d37c1a",
2170
+ "bytes": 2993
2171
2171
  },
2172
2172
  "v2/ports/base32.port.d.ts": {
2173
2173
  "sha256": "64aa2f2003a552917cbf71474472fc5e4afffaa29577204bbcbe5ffa989ceb82",
@@ -2386,12 +2386,12 @@
2386
2386
  "bytes": 732
2387
2387
  },
2388
2388
  "v2/projections/resume-ranking.d.ts": {
2389
- "sha256": "5f15f10b632b8e771c74e1f3823616f761be4142493d1142176583e6495528b2",
2390
- "bytes": 3531
2389
+ "sha256": "b1d9e7d6fb3e59ced42f7c2e5fd9d8f9a18308aa6f4411a8aabf5dc174aa8dad",
2390
+ "bytes": 3960
2391
2391
  },
2392
2392
  "v2/projections/resume-ranking.js": {
2393
- "sha256": "b9d48c8e08ae8b794771b44fc592158422e832ff64633817fa11093f64282715",
2394
- "bytes": 6782
2393
+ "sha256": "1b256c992ded9ec52150b229799716d118ca2a3659a0cc523e727cde2ebf7b0c",
2394
+ "bytes": 13660
2395
2395
  },
2396
2396
  "v2/projections/run-context.d.ts": {
2397
2397
  "sha256": "a4d57470a435ac9860f60b3244d1b828853995027cd510d8da42762d21b2a687",
@@ -212,15 +212,24 @@ function mintStartTokens(args) {
212
212
  }
213
213
  function executeStartWorkflow(input, ctx) {
214
214
  const { gate, sessionStore, snapshotStore, pinnedStore, crypto, tokenCodecPorts, idFactory, validationPipelineDeps, tokenAliasStore, entropy } = ctx.v2;
215
- const workflowReader = (0, request_workflow_reader_js_1.hasRequestWorkspaceSignal)({
215
+ const shouldUseRequestReader = ctx.featureFlags != null && (0, request_workflow_reader_js_1.hasRequestWorkspaceSignal)({
216
216
  workspacePath: input.workspacePath,
217
217
  resolvedRootUris: ctx.v2.resolvedRootUris,
218
- })
219
- ? (0, request_workflow_reader_js_1.createWorkflowReaderForRequest)({
220
- featureFlags: ctx.featureFlags,
221
- workspacePath: input.workspacePath,
222
- resolvedRootUris: ctx.v2.resolvedRootUris,
223
- })
218
+ });
219
+ const workflowReader = shouldUseRequestReader
220
+ ? {
221
+ getWorkflowById: async (workflowId) => {
222
+ const requestReader = (0, request_workflow_reader_js_1.createWorkflowReaderForRequest)({
223
+ featureFlags: ctx.featureFlags,
224
+ workspacePath: input.workspacePath,
225
+ resolvedRootUris: ctx.v2.resolvedRootUris,
226
+ });
227
+ const requestResult = await requestReader.getWorkflowById(workflowId);
228
+ if (requestResult != null)
229
+ return requestResult;
230
+ return ctx.workflowService.getWorkflowById(workflowId);
231
+ },
232
+ }
224
233
  : ctx.workflowService;
225
234
  return loadAndPinWorkflow({
226
235
  workflowId: input.workflowId,
@@ -24,6 +24,8 @@ async function handleV2ResumeSession(input, ctx) {
24
24
  const query = {
25
25
  gitHeadSha: input.gitHeadSha ?? anchorValue(anchors, 'git_head_sha'),
26
26
  gitBranch: input.gitBranch ?? anchorValue(anchors, 'git_branch'),
27
+ repoRootHash: anchorValue(anchors, 'repo_root_hash'),
28
+ sameWorkspaceOnly: input.sameWorkspaceOnly,
27
29
  freeTextQuery: input.query,
28
30
  runId: input.runId,
29
31
  sessionId: input.sessionId,
@@ -74,9 +76,13 @@ function mintCandidateTokens(candidates, ports) {
74
76
  sessionId: candidate.sessionId,
75
77
  runId: candidate.runId,
76
78
  workflowId: candidate.workflowId,
79
+ sessionTitle: candidate.sessionTitle,
80
+ gitBranch: candidate.gitBranch,
77
81
  resumeToken: resumeTokenRes.value,
78
82
  snippet: candidate.snippet,
79
83
  whyMatched: [...candidate.whyMatched],
84
+ confidence: candidate.confidence,
85
+ matchExplanation: candidate.matchExplanation,
80
86
  pendingStepId: candidate.pendingStepId,
81
87
  isComplete: candidate.isComplete,
82
88
  lastModifiedMs: candidate.lastModifiedMs,
@@ -1160,12 +1160,16 @@ export declare const V2ResumeSessionOutputSchema: z.ZodObject<{
1160
1160
  sessionId: z.ZodString;
1161
1161
  runId: z.ZodString;
1162
1162
  workflowId: z.ZodString;
1163
+ sessionTitle: z.ZodNullable<z.ZodString>;
1164
+ gitBranch: z.ZodNullable<z.ZodString>;
1163
1165
  resumeToken: z.ZodString;
1164
1166
  snippet: z.ZodString;
1167
+ confidence: z.ZodEnum<["strong", "medium", "weak"]>;
1168
+ matchExplanation: z.ZodString;
1165
1169
  pendingStepId: z.ZodNullable<z.ZodString>;
1166
1170
  isComplete: z.ZodBoolean;
1167
1171
  lastModifiedMs: z.ZodNullable<z.ZodNumber>;
1168
- whyMatched: z.ZodArray<z.ZodEnum<["matched_exact_id", "matched_head_sha", "matched_branch", "matched_notes", "matched_notes_partial", "matched_workflow_id", "recency_fallback"]>, "many">;
1172
+ whyMatched: z.ZodArray<z.ZodEnum<["matched_exact_id", "matched_notes", "matched_notes_partial", "matched_workflow_id", "matched_head_sha", "matched_branch", "matched_repo_root", "recency_fallback"]>, "many">;
1169
1173
  nextCall: z.ZodObject<{
1170
1174
  tool: z.ZodLiteral<"continue_workflow">;
1171
1175
  params: z.ZodObject<{
@@ -1192,9 +1196,11 @@ export declare const V2ResumeSessionOutputSchema: z.ZodObject<{
1192
1196
  tool: "continue_workflow";
1193
1197
  }>;
1194
1198
  }, "strip", z.ZodTypeAny, {
1199
+ confidence: "strong" | "weak" | "medium";
1195
1200
  workflowId: string;
1196
1201
  runId: string;
1197
1202
  sessionId: string;
1203
+ gitBranch: string | null;
1198
1204
  isComplete: boolean;
1199
1205
  nextCall: {
1200
1206
  params: {
@@ -1203,15 +1209,19 @@ export declare const V2ResumeSessionOutputSchema: z.ZodObject<{
1203
1209
  };
1204
1210
  tool: "continue_workflow";
1205
1211
  };
1212
+ sessionTitle: string | null;
1206
1213
  resumeToken: string;
1207
1214
  snippet: string;
1215
+ matchExplanation: string;
1208
1216
  pendingStepId: string | null;
1209
1217
  lastModifiedMs: number | null;
1210
- whyMatched: ("matched_exact_id" | "matched_head_sha" | "matched_branch" | "matched_notes" | "matched_notes_partial" | "matched_workflow_id" | "recency_fallback")[];
1218
+ whyMatched: ("matched_exact_id" | "matched_notes" | "matched_notes_partial" | "matched_workflow_id" | "matched_head_sha" | "matched_branch" | "matched_repo_root" | "recency_fallback")[];
1211
1219
  }, {
1220
+ confidence: "strong" | "weak" | "medium";
1212
1221
  workflowId: string;
1213
1222
  runId: string;
1214
1223
  sessionId: string;
1224
+ gitBranch: string | null;
1215
1225
  isComplete: boolean;
1216
1226
  nextCall: {
1217
1227
  params: {
@@ -1220,18 +1230,22 @@ export declare const V2ResumeSessionOutputSchema: z.ZodObject<{
1220
1230
  };
1221
1231
  tool: "continue_workflow";
1222
1232
  };
1233
+ sessionTitle: string | null;
1223
1234
  resumeToken: string;
1224
1235
  snippet: string;
1236
+ matchExplanation: string;
1225
1237
  pendingStepId: string | null;
1226
1238
  lastModifiedMs: number | null;
1227
- whyMatched: ("matched_exact_id" | "matched_head_sha" | "matched_branch" | "matched_notes" | "matched_notes_partial" | "matched_workflow_id" | "recency_fallback")[];
1239
+ whyMatched: ("matched_exact_id" | "matched_notes" | "matched_notes_partial" | "matched_workflow_id" | "matched_head_sha" | "matched_branch" | "matched_repo_root" | "recency_fallback")[];
1228
1240
  }>, "many">;
1229
1241
  totalEligible: z.ZodNumber;
1230
1242
  }, "strip", z.ZodTypeAny, {
1231
1243
  candidates: {
1244
+ confidence: "strong" | "weak" | "medium";
1232
1245
  workflowId: string;
1233
1246
  runId: string;
1234
1247
  sessionId: string;
1248
+ gitBranch: string | null;
1235
1249
  isComplete: boolean;
1236
1250
  nextCall: {
1237
1251
  params: {
@@ -1240,18 +1254,22 @@ export declare const V2ResumeSessionOutputSchema: z.ZodObject<{
1240
1254
  };
1241
1255
  tool: "continue_workflow";
1242
1256
  };
1257
+ sessionTitle: string | null;
1243
1258
  resumeToken: string;
1244
1259
  snippet: string;
1260
+ matchExplanation: string;
1245
1261
  pendingStepId: string | null;
1246
1262
  lastModifiedMs: number | null;
1247
- whyMatched: ("matched_exact_id" | "matched_head_sha" | "matched_branch" | "matched_notes" | "matched_notes_partial" | "matched_workflow_id" | "recency_fallback")[];
1263
+ whyMatched: ("matched_exact_id" | "matched_notes" | "matched_notes_partial" | "matched_workflow_id" | "matched_head_sha" | "matched_branch" | "matched_repo_root" | "recency_fallback")[];
1248
1264
  }[];
1249
1265
  totalEligible: number;
1250
1266
  }, {
1251
1267
  candidates: {
1268
+ confidence: "strong" | "weak" | "medium";
1252
1269
  workflowId: string;
1253
1270
  runId: string;
1254
1271
  sessionId: string;
1272
+ gitBranch: string | null;
1255
1273
  isComplete: boolean;
1256
1274
  nextCall: {
1257
1275
  params: {
@@ -1260,11 +1278,13 @@ export declare const V2ResumeSessionOutputSchema: z.ZodObject<{
1260
1278
  };
1261
1279
  tool: "continue_workflow";
1262
1280
  };
1281
+ sessionTitle: string | null;
1263
1282
  resumeToken: string;
1264
1283
  snippet: string;
1284
+ matchExplanation: string;
1265
1285
  pendingStepId: string | null;
1266
1286
  lastModifiedMs: number | null;
1267
- whyMatched: ("matched_exact_id" | "matched_head_sha" | "matched_branch" | "matched_notes" | "matched_notes_partial" | "matched_workflow_id" | "recency_fallback")[];
1287
+ whyMatched: ("matched_exact_id" | "matched_notes" | "matched_notes_partial" | "matched_workflow_id" | "matched_head_sha" | "matched_branch" | "matched_repo_root" | "recency_fallback")[];
1268
1288
  }[];
1269
1289
  totalEligible: number;
1270
1290
  }>;
@@ -242,23 +242,29 @@ exports.V2ResumeSessionOutputSchema = zod_1.z.object({
242
242
  sessionId: zod_1.z.string().min(1),
243
243
  runId: zod_1.z.string().min(1),
244
244
  workflowId: zod_1.z.string().min(1),
245
+ sessionTitle: zod_1.z.string().nullable().describe('Human-readable task/session title derived from persisted workflow context or early recap text.'),
246
+ gitBranch: zod_1.z.string().nullable().describe('Git branch associated with the session, if available.'),
245
247
  resumeToken: zod_1.z.string().regex(token_patterns_js_1.STATE_TOKEN_PATTERN, 'Invalid resumeToken format'),
246
248
  snippet: zod_1.z.string().max(1024),
249
+ confidence: zod_1.z.enum(['strong', 'medium', 'weak']).describe('Coarse confidence band for how likely this candidate is the intended session.'),
250
+ matchExplanation: zod_1.z.string().min(1).describe('Short natural-language explanation of why this candidate ranked here.'),
247
251
  pendingStepId: zod_1.z.string().nullable().describe('The current pending step ID (e.g. "phase-3-implement") if the workflow is in progress. ' +
248
252
  'Null if the workflow is complete or the step could not be determined.'),
249
253
  isComplete: zod_1.z.boolean().describe('Whether the workflow run has completed. Completed sessions are deprioritized in ranking.'),
250
254
  lastModifiedMs: zod_1.z.number().nullable().describe('Filesystem modification time (epoch ms) of the session. Null if unavailable.'),
251
255
  whyMatched: zod_1.z.array(zod_1.z.enum([
252
256
  'matched_exact_id',
253
- 'matched_head_sha',
254
- 'matched_branch',
255
257
  'matched_notes',
256
258
  'matched_notes_partial',
257
259
  'matched_workflow_id',
260
+ 'matched_head_sha',
261
+ 'matched_branch',
262
+ 'matched_repo_root',
258
263
  'recency_fallback',
259
264
  ])).describe('Match signals explaining why this candidate was ranked. ' +
260
- 'matched_exact_id/head_sha/branch/notes = strong signal. ' +
261
- 'matched_notes_partial = moderate signal (some query words matched). ' +
265
+ 'matched_exact_id and matched_notes are strongest. ' +
266
+ 'matched_notes_partial/matched_workflow_id are moderate text signals. ' +
267
+ 'matched_repo_root/head_sha/branch are workspace-context signals. ' +
262
268
  'recency_fallback = no strong signal; inspect the snippet before resuming.'),
263
269
  nextCall: exports.V2ResumeNextCallSchema,
264
270
  })).max(5),
@@ -24,13 +24,13 @@ export declare const V2InspectWorkflowInput: z.ZodObject<{
24
24
  export type V2InspectWorkflowInput = z.infer<typeof V2InspectWorkflowInput>;
25
25
  export declare const V2StartWorkflowInput: z.ZodObject<{
26
26
  workflowId: z.ZodString;
27
- workspacePath: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
27
+ workspacePath: z.ZodEffects<z.ZodString, string, string>;
28
28
  }, "strip", z.ZodTypeAny, {
29
29
  workflowId: string;
30
- workspacePath?: string | undefined;
30
+ workspacePath: string;
31
31
  }, {
32
32
  workflowId: string;
33
- workspacePath?: string | undefined;
33
+ workspacePath: string;
34
34
  }>;
35
35
  export type V2StartWorkflowInput = z.infer<typeof V2StartWorkflowInput>;
36
36
  export declare const V2ContinueWorkflowInputShape: z.ZodObject<{
@@ -151,21 +151,24 @@ export declare const V2ResumeSessionInput: z.ZodObject<{
151
151
  sessionId: z.ZodOptional<z.ZodString>;
152
152
  gitBranch: z.ZodOptional<z.ZodString>;
153
153
  gitHeadSha: z.ZodOptional<z.ZodString>;
154
- workspacePath: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
154
+ workspacePath: z.ZodEffects<z.ZodString, string, string>;
155
+ sameWorkspaceOnly: z.ZodOptional<z.ZodBoolean>;
155
156
  }, "strict", z.ZodTypeAny, {
156
- workspacePath?: string | undefined;
157
+ workspacePath: string;
157
158
  query?: string | undefined;
158
159
  runId?: string | undefined;
159
160
  sessionId?: string | undefined;
160
161
  gitBranch?: string | undefined;
161
162
  gitHeadSha?: string | undefined;
163
+ sameWorkspaceOnly?: boolean | undefined;
162
164
  }, {
163
- workspacePath?: string | undefined;
165
+ workspacePath: string;
164
166
  query?: string | undefined;
165
167
  runId?: string | undefined;
166
168
  sessionId?: string | undefined;
167
169
  gitBranch?: string | undefined;
168
170
  gitHeadSha?: string | undefined;
171
+ sameWorkspaceOnly?: boolean | undefined;
169
172
  }>;
170
173
  export type V2ResumeSessionInput = z.infer<typeof V2ResumeSessionInput>;
171
174
  export declare const V2CheckpointWorkflowInput: z.ZodObject<{
@@ -1,26 +1,33 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.V2_TOOL_ANNOTATIONS = exports.V2_TOOL_TITLES = exports.V2CheckpointWorkflowInput = exports.V2ResumeSessionInput = exports.V2ContinueWorkflowInput = exports.V2ContinueWorkflowInputShape = exports.V2StartWorkflowInput = exports.V2InspectWorkflowInput = exports.V2ListWorkflowsInput = void 0;
7
+ const path_1 = __importDefault(require("path"));
4
8
  const zod_1 = require("zod");
5
9
  const workflow_protocol_contracts_js_1 = require("../workflow-protocol-contracts.js");
10
+ function isAbsoluteWorkspacePath(p) {
11
+ return path_1.default.isAbsolute(p);
12
+ }
6
13
  const workspacePathField = zod_1.z.string()
7
- .refine((p) => p.startsWith('/'), 'workspacePath must be an absolute path (starting with /)')
8
- .optional()
14
+ .refine((p) => isAbsoluteWorkspacePath(p), 'workspacePath must be an absolute path')
9
15
  .describe('Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). Used to resolve project-scoped workflow variants against the correct workspace. If omitted, WorkRail uses MCP roots when available, then falls back to the server process directory.');
16
+ const optionalWorkspacePathField = workspacePathField.optional();
10
17
  exports.V2ListWorkflowsInput = zod_1.z.object({
11
- workspacePath: workspacePathField,
18
+ workspacePath: optionalWorkspacePathField,
12
19
  });
13
20
  exports.V2InspectWorkflowInput = zod_1.z.object({
14
21
  workflowId: zod_1.z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, 'Workflow ID must contain only letters, numbers, hyphens, and underscores').describe('The workflow ID to inspect'),
15
22
  mode: zod_1.z.enum(['metadata', 'preview']).default('preview').describe('Detail level: metadata (name and description only) or preview (full step-by-step breakdown, default)'),
16
- workspacePath: workspacePathField,
23
+ workspacePath: optionalWorkspacePathField,
17
24
  });
18
25
  exports.V2StartWorkflowInput = zod_1.z.object({
19
26
  workflowId: zod_1.z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, 'Workflow ID must contain only letters, numbers, hyphens, and underscores').describe('The workflow ID to start'),
20
- workspacePath: workspacePathField.describe('Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). Used to resolve the correct project-scoped workflow variant and to anchor this session to your workspace for future resume_session discovery. Pass this on every start_workflow call. If omitted, WorkRail uses MCP roots when available, then falls back to the server process directory.'),
27
+ workspacePath: workspacePathField.describe('Required. Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). WorkRail uses this to resolve the correct project-scoped workflow variant and to anchor the session to the correct repo for future resume_session discovery. Shared MCP servers cannot infer this safely.'),
21
28
  });
22
29
  exports.V2ContinueWorkflowInputShape = zod_1.z.object({
23
- workspacePath: workspacePathField,
30
+ workspacePath: optionalWorkspacePathField,
24
31
  continueToken: zod_1.z.string().min(1).describe('The token for your next continue_workflow call. Two valid token kinds: ' +
25
32
  '(1) A continueToken (ct_...) from start_workflow or a previous continue_workflow — carries session identity AND advance authority. ' +
26
33
  '(2) A resumeToken (st_...) from resume_session or checkpoint_workflow — carries session identity only; valid for intent: "rehydrate", not "advance". ' +
@@ -69,6 +76,13 @@ exports.V2ContinueWorkflowInput = exports.V2ContinueWorkflowInputShape
69
76
  'Rehydration is read-only state recovery — it does not accept output.',
70
77
  });
71
78
  }
79
+ if (data.intent === 'rehydrate' && data.workspacePath === undefined) {
80
+ ctx.addIssue({
81
+ code: zod_1.z.ZodIssueCode.custom,
82
+ path: ['workspacePath'],
83
+ message: 'workspacePath is required for rehydration. Shared WorkRail servers cannot safely infer your current workspace, so pass the absolute "Workspace:" path from your system parameters.',
84
+ });
85
+ }
72
86
  })
73
87
  .transform((data) => {
74
88
  const normalized = workflow_protocol_contracts_js_1.CONTINUE_WORKFLOW_PROTOCOL.aliasMap
@@ -93,10 +107,9 @@ exports.V2ResumeSessionInput = zod_1.z.object({
93
107
  'When provided, the matching session is returned as the sole top-priority candidate.'),
94
108
  gitBranch: zod_1.z.string().max(256).optional().describe('Git branch name to match against session observations. Overrides auto-detected branch.'),
95
109
  gitHeadSha: zod_1.z.string().regex(/^[0-9a-f]{40}$/).optional().describe('Git HEAD SHA to match against session observations. Overrides auto-detected HEAD.'),
96
- workspacePath: zod_1.z.string()
97
- .refine((p) => p.startsWith('/'), 'workspacePath must be an absolute path (starting with /)')
98
- .optional()
99
- .describe('Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). Used to resolve your git branch and HEAD SHA for workspace-aware session matching. Pass the same path used in the original start_workflow call. If omitted, WorkRail uses the server process directory which may not match your workspace.'),
110
+ workspacePath: workspacePathField.describe('Required. Absolute path to your current workspace directory (e.g. the "Workspace:" value from your system parameters). WorkRail uses this to identify the current repo and resume the correct session on shared MCP servers.'),
111
+ sameWorkspaceOnly: zod_1.z.boolean().optional().describe('If true, only sessions from the same repo/workspace are considered when repo_root_hash is available. ' +
112
+ 'Use this when the user clearly means "resume work from this repo only".'),
100
113
  }).strict();
101
114
  exports.V2CheckpointWorkflowInput = zod_1.z.object({
102
115
  checkpointToken: zod_1.z.string().min(1).describe('The checkpoint token from the most recent start_workflow or continue_workflow response. ' +
@@ -400,11 +400,12 @@ function isResumeSessionResponse(data) {
400
400
  }
401
401
  const WHY_MATCHED_LABELS = {
402
402
  matched_exact_id: 'Exact ID match',
403
- matched_head_sha: 'Same git commit (HEAD SHA)',
404
- matched_branch: 'Same git branch',
405
403
  matched_notes: 'Query matched session notes',
406
404
  matched_notes_partial: 'Query partially matched session notes',
407
405
  matched_workflow_id: 'Query matched workflow type',
406
+ matched_head_sha: 'Same git commit (HEAD SHA)',
407
+ matched_branch: 'Same git branch',
408
+ matched_repo_root: 'Same workspace/repository',
408
409
  recency_fallback: 'No strong match signal (recent session)',
409
410
  };
410
411
  function formatRelativeTime(epochMs) {
@@ -433,10 +434,21 @@ function formatResumeCandidate(c, index) {
433
434
  const matchLabel = c.whyMatched.map(w => WHY_MATCHED_LABELS[w] ?? w).join(', ');
434
435
  const isWeak = c.whyMatched.every(w => w === 'recency_fallback');
435
436
  const statusTag = c.isComplete ? ' (completed)' : '';
436
- lines.push(`### Candidate ${index + 1}: \`${c.workflowId}\`${statusTag}${isWeak ? ' (weak match)' : ''}`);
437
+ const heading = c.sessionTitle?.trim() || c.workflowId;
438
+ lines.push(`### Candidate ${index + 1}: \`${heading}\`${statusTag}${isWeak ? ' (weak match)' : ''}`);
437
439
  lines.push(`- **Session**: \`${c.sessionId}\``);
438
440
  lines.push(`- **Run**: \`${c.runId}\``);
441
+ lines.push(`- **Workflow**: \`${c.workflowId}\``);
439
442
  lines.push(`- **Match reason**: ${matchLabel}`);
443
+ if (c.confidence) {
444
+ lines.push(`- **Confidence**: ${c.confidence}`);
445
+ }
446
+ if (c.matchExplanation) {
447
+ lines.push(`- **Why this ranked here**: ${c.matchExplanation}`);
448
+ }
449
+ if (c.gitBranch) {
450
+ lines.push(`- **Branch**: \`${c.gitBranch}\``);
451
+ }
440
452
  if (c.pendingStepId) {
441
453
  lines.push(`- **Current step**: \`${c.pendingStepId}\``);
442
454
  }
@@ -458,10 +470,11 @@ function formatResumeCandidate(c, index) {
458
470
  lines.push('> This workflow has already completed. Resuming it will show the final state.');
459
471
  }
460
472
  lines.push('');
461
- lines.push('To resume, call `continue_workflow` with:');
473
+ lines.push('To inspect or resume this candidate, call `continue_workflow` with:');
462
474
  lines.push('```json');
463
475
  lines.push(JSON.stringify(c.nextCall.params, null, 2));
464
476
  lines.push('```');
477
+ lines.push('This `rehydrate` call restores the exact workflow state and shows the current step/context.');
465
478
  return lines.join('\n');
466
479
  }
467
480
  const SEARCH_PARAMS_HELP = [
@@ -470,6 +483,7 @@ const SEARCH_PARAMS_HELP = [
470
483
  '- `runId`: Exact run ID if the user has one (e.g. "run_abc123def456")',
471
484
  '- `sessionId`: Exact session ID if the user has one (e.g. "sess_abc123")',
472
485
  '- `workspacePath`: Absolute path to the workspace (helps match by git branch/commit)',
486
+ '- `sameWorkspaceOnly`: Restrict results to the current repo/workspace when that is clearly what the user means',
473
487
  ].join('\n');
474
488
  function formatV2ResumeResponse(data) {
475
489
  if (!isResumeSessionResponse(data))
@@ -514,6 +528,12 @@ function formatV2ResumeResponse(data) {
514
528
  lines.push('');
515
529
  lines.push(`Found **${totalEligible}** session(s) total. Showing the top ${candidates.length} ranked by match strength.`);
516
530
  lines.push('');
531
+ const allWorkspaceDriven = candidates.every((c) => c.whyMatched.every((w) => w === 'matched_head_sha' || w === 'matched_branch'));
532
+ if (allWorkspaceDriven) {
533
+ lines.push('**Note**: These candidates are ranked primarily from current workspace git context (branch/commit), not from a strong text match on your query.');
534
+ lines.push('If the previews do not clearly match the user\'s request, inspect a candidate with `continue_workflow(..., intent: "rehydrate")` or ask for a more specific phrase / session ID.');
535
+ lines.push('');
536
+ }
517
537
  const best = candidates[0];
518
538
  const bestIsExact = best.whyMatched.includes('matched_exact_id');
519
539
  if (bestIsExact) {
@@ -46,8 +46,8 @@ function findAliasFieldConflicts(value, aliasMap) {
46
46
  }
47
47
  exports.START_WORKFLOW_PROTOCOL = {
48
48
  canonicalParams: {
49
- required: ['workflowId'],
50
- optional: ['workspacePath'],
49
+ required: ['workflowId', 'workspacePath'],
50
+ optional: [],
51
51
  },
52
52
  descriptions: {
53
53
  standard: {
@@ -56,6 +56,7 @@ exports.START_WORKFLOW_PROTOCOL = {
56
56
  rules: [
57
57
  'Follow the returned step exactly; treat it as the user\'s current instruction.',
58
58
  'When the step is done, call continue_workflow with the returned continueToken.',
59
+ 'Always pass workspacePath. Shared MCP servers cannot safely infer which repo/workspace you mean.',
59
60
  'Only pass context on later continue_workflow calls if facts changed.',
60
61
  ],
61
62
  examplePayload: {
@@ -70,7 +71,7 @@ exports.START_WORKFLOW_PROTOCOL = {
70
71
  rules: [
71
72
  'Execute the returned step exactly as written.',
72
73
  'When the step is complete, call continue_workflow with the returned continueToken.',
73
- 'Pass workspacePath when available so WorkRail anchors the session to the correct workspace.',
74
+ 'Pass workspacePath on every call. Shared MCP servers cannot safely infer the correct workspace.',
74
75
  ],
75
76
  examplePayload: {
76
77
  workflowId: 'coding-task-workflow-agentic',
@@ -83,7 +84,7 @@ exports.START_WORKFLOW_PROTOCOL = {
83
84
  exports.CONTINUE_WORKFLOW_PROTOCOL = {
84
85
  canonicalParams: {
85
86
  required: ['continueToken'],
86
- optional: ['intent', 'context', 'output'],
87
+ optional: ['intent', 'context', 'output', 'workspacePath'],
87
88
  },
88
89
  aliasMap: {
89
90
  contextVariables: 'context',
@@ -95,6 +96,7 @@ exports.CONTINUE_WORKFLOW_PROTOCOL = {
95
96
  rules: [
96
97
  'Advance by sending output (and intent: "advance" if you want to be explicit).',
97
98
  'Rehydrate by omitting output (and intent: "rehydrate" if you want to be explicit).',
99
+ 'When rehydrating, always pass workspacePath so WorkRail can restore the correct repo/workspace context on shared servers.',
98
100
  'Put changed facts under context only.',
99
101
  'Round-trip continueToken exactly as returned by WorkRail; use the single-token API only.',
100
102
  'Notes (output.notesMarkdown): write for a human reader. Include what you did and key decisions, what you produced (files, tests, numbers), and anything notable (risks, open questions, deliberate omissions). Use markdown headings, bullets, bold, code refs. Be specific. Scope: THIS step only (WorkRail concatenates automatically). 10-30 lines ideal. Omitting notes blocks the step.',
@@ -114,6 +116,7 @@ exports.CONTINUE_WORKFLOW_PROTOCOL = {
114
116
  'Use continueToken exactly as returned by WorkRail.',
115
117
  'Use the single-token API only.',
116
118
  'Advance by sending output; rehydrate by omitting output.',
119
+ 'For rehydrate calls, pass workspacePath so WorkRail can restore the correct workspace context.',
117
120
  'Put updated facts in context only.',
118
121
  'Notes (output.notesMarkdown): write for a human reader. Include what you did and key decisions, what you produced (files, tests, numbers), and anything notable (risks, open questions, deliberate omissions). Use markdown headings, bullets, bold, code refs. Be specific. Scope: THIS step only (WorkRail concatenates automatically). 10-30 lines ideal. Omitting notes blocks the step.',
119
122
  ],
@@ -164,8 +167,8 @@ exports.CHECKPOINT_WORKFLOW_PROTOCOL = {
164
167
  };
165
168
  exports.RESUME_SESSION_PROTOCOL = {
166
169
  canonicalParams: {
167
- required: [],
168
- optional: ['query', 'runId', 'sessionId', 'gitBranch', 'gitHeadSha', 'workspacePath'],
170
+ required: ['workspacePath'],
171
+ optional: ['query', 'runId', 'sessionId', 'gitBranch', 'gitHeadSha'],
169
172
  },
170
173
  descriptions: {
171
174
  standard: {
@@ -174,9 +177,9 @@ exports.RESUME_SESSION_PROTOCOL = {
174
177
  rules: [
175
178
  'If the user provides a run ID (run_...) or session ID (sess_...), pass it as runId or sessionId for an exact match. This is the most reliable way to find a specific session.',
176
179
  'If the user describes what they were working on (e.g. "the mr ownership task"), pass their words as query. This searches session recap notes and workflow IDs for matching keywords.',
177
- 'Always pass workspacePath (from your system parameters) so WorkRail can also match by git context (branch, commit).',
180
+ 'Always pass workspacePath (from your system parameters). Shared MCP servers cannot safely infer the current workspace, and resume quality depends on the current repo identity.',
178
181
  'The response includes ranked candidates with match explanations and ready-to-use continuation templates. Present the top candidates to the user if there is ambiguity.',
179
- 'To resume a candidate: call continue_workflow with the candidate\'s nextCall.params (continueToken and intent: "rehydrate"). The response will give you the full session context.',
182
+ 'To inspect or resume a candidate: call continue_workflow with the candidate\'s nextCall.params (continueToken and intent: "rehydrate"). This is the correct inspection path for v2 sessions and restores the full session context.',
180
183
  'If no candidates match, ask the user for more details or suggest starting a fresh workflow with start_workflow.',
181
184
  ],
182
185
  examplePayload: {
@@ -191,9 +194,9 @@ exports.RESUME_SESSION_PROTOCOL = {
191
194
  rules: [
192
195
  'If the user provides a run ID (run_...) or session ID (sess_...), pass it as runId or sessionId for exact lookup.',
193
196
  'If the user describes their task, pass their words as query to search session notes.',
194
- 'Always pass workspacePath from your system parameters for git-context matching.',
197
+ 'Always pass workspacePath from your system parameters. Shared MCP servers cannot safely infer the current workspace.',
195
198
  'Present candidates to the user when there is ambiguity. The response explains match strength.',
196
- 'To resume: call continue_workflow with the chosen candidate\'s nextCall.params (continueToken + intent: "rehydrate").',
199
+ 'To inspect or resume: call continue_workflow with the chosen candidate\'s nextCall.params (continueToken + intent: "rehydrate"). Do NOT use legacy session tools to inspect a v2 workflow resume candidate.',
197
200
  'If no candidates match, ask for more details or start a fresh workflow.',
198
201
  ],
199
202
  examplePayload: {
@@ -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: 'matched_head_sha';
43
+ readonly kind: 'matched_notes';
43
44
  } | {
44
45
  readonly tier: 2;
45
- readonly kind: 'matched_branch';
46
- readonly matchType: 'exact' | 'prefix';
46
+ readonly kind: 'matched_notes_partial';
47
+ readonly matchRatio: number;
47
48
  } | {
48
49
  readonly tier: 3;
49
- readonly kind: 'matched_notes';
50
+ readonly kind: 'matched_workflow_id';
50
51
  } | {
51
52
  readonly tier: 4;
52
- readonly kind: 'matched_notes_partial';
53
- readonly matchRatio: number;
53
+ readonly kind: 'matched_head_sha';
54
54
  } | {
55
55
  readonly tier: 5;
56
- readonly kind: 'matched_workflow_id';
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: 1, kind: 'matched_head_sha' };
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: 2, kind: 'matched_branch', matchType: 'exact' };
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: 2, kind: 'matched_branch', matchType: 'prefix' };
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.map((summary) => ({
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
- sessionId: summary.sessionId,
151
- runId: summary.runId,
152
- preferredTipNodeId: summary.preferredTip.nodeId,
153
- snippet: summary.recapSnippet ?? '',
154
- whyMatched: [tierToWhyMatched(tier)],
155
- tierAssignment: tier,
156
- lastActivityEventIndex: summary.preferredTip.lastActivityEventIndex,
157
- workflowHash: summary.workflow.workflowHash,
158
- workflowId: summary.workflow.workflowId,
159
- pendingStepId: summary.pendingStepId,
160
- isComplete: summary.isComplete,
161
- lastModifiedMs: summary.lastModifiedMs,
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "3.7.2",
3
+ "version": "3.7.3",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {