@exaudeus/workrail 3.7.0 → 3.7.2

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.
@@ -2183,8 +2183,8 @@ export declare const SessionContentsV1Schema: z.ZodObject<{
2183
2183
  bytes: z.ZodNumber;
2184
2184
  }, "strip", z.ZodTypeAny, {
2185
2185
  kind: "segment_closed";
2186
- sha256: string;
2187
2186
  sessionId: string;
2187
+ sha256: string;
2188
2188
  v: 1;
2189
2189
  manifestIndex: number;
2190
2190
  firstEventIndex: number;
@@ -2193,8 +2193,8 @@ export declare const SessionContentsV1Schema: z.ZodObject<{
2193
2193
  bytes: number;
2194
2194
  }, {
2195
2195
  kind: "segment_closed";
2196
- sha256: string;
2197
2196
  sessionId: string;
2197
+ sha256: string;
2198
2198
  v: 1;
2199
2199
  manifestIndex: number;
2200
2200
  firstEventIndex: number;
@@ -4601,8 +4601,8 @@ export declare const SessionContentsV1Schema: z.ZodObject<{
4601
4601
  })[];
4602
4602
  manifest: ({
4603
4603
  kind: "segment_closed";
4604
- sha256: string;
4605
4604
  sessionId: string;
4605
+ sha256: string;
4606
4606
  v: 1;
4607
4607
  manifestIndex: number;
4608
4608
  firstEventIndex: number;
@@ -5113,8 +5113,8 @@ export declare const SessionContentsV1Schema: z.ZodObject<{
5113
5113
  })[];
5114
5114
  manifest: ({
5115
5115
  kind: "segment_closed";
5116
- sha256: string;
5117
5116
  sessionId: string;
5117
+ sha256: string;
5118
5118
  v: 1;
5119
5119
  manifestIndex: number;
5120
5120
  firstEventIndex: number;
@@ -7305,8 +7305,8 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
7305
7305
  bytes: z.ZodNumber;
7306
7306
  }, "strip", z.ZodTypeAny, {
7307
7307
  kind: "segment_closed";
7308
- sha256: string;
7309
7308
  sessionId: string;
7309
+ sha256: string;
7310
7310
  v: 1;
7311
7311
  manifestIndex: number;
7312
7312
  firstEventIndex: number;
@@ -7315,8 +7315,8 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
7315
7315
  bytes: number;
7316
7316
  }, {
7317
7317
  kind: "segment_closed";
7318
- sha256: string;
7319
7318
  sessionId: string;
7319
+ sha256: string;
7320
7320
  v: 1;
7321
7321
  manifestIndex: number;
7322
7322
  firstEventIndex: number;
@@ -9723,8 +9723,8 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
9723
9723
  })[];
9724
9724
  manifest: ({
9725
9725
  kind: "segment_closed";
9726
- sha256: string;
9727
9726
  sessionId: string;
9727
+ sha256: string;
9728
9728
  v: 1;
9729
9729
  manifestIndex: number;
9730
9730
  firstEventIndex: number;
@@ -10235,8 +10235,8 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
10235
10235
  })[];
10236
10236
  manifest: ({
10237
10237
  kind: "segment_closed";
10238
- sha256: string;
10239
10238
  sessionId: string;
10239
+ sha256: string;
10240
10240
  v: 1;
10241
10241
  manifestIndex: number;
10242
10242
  firstEventIndex: number;
@@ -10776,8 +10776,8 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
10776
10776
  })[];
10777
10777
  manifest: ({
10778
10778
  kind: "segment_closed";
10779
- sha256: string;
10780
10779
  sessionId: string;
10780
+ sha256: string;
10781
10781
  v: 1;
10782
10782
  manifestIndex: number;
10783
10783
  firstEventIndex: number;
@@ -11305,8 +11305,8 @@ export declare const ExportBundleV1Schema: z.ZodObject<{
11305
11305
  })[];
11306
11306
  manifest: ({
11307
11307
  kind: "segment_closed";
11308
- sha256: string;
11309
11308
  sessionId: string;
11309
+ sha256: string;
11310
11310
  v: 1;
11311
11311
  manifestIndex: number;
11312
11312
  firstEventIndex: number;
@@ -11,8 +11,8 @@ export declare const ManifestRecordV1Schema: z.ZodDiscriminatedUnion<"kind", [z.
11
11
  bytes: z.ZodNumber;
12
12
  }, "strip", z.ZodTypeAny, {
13
13
  kind: "segment_closed";
14
- sha256: string;
15
14
  sessionId: string;
15
+ sha256: string;
16
16
  v: 1;
17
17
  manifestIndex: number;
18
18
  firstEventIndex: number;
@@ -21,8 +21,8 @@ export declare const ManifestRecordV1Schema: z.ZodDiscriminatedUnion<"kind", [z.
21
21
  bytes: number;
22
22
  }, {
23
23
  kind: "segment_closed";
24
- sha256: string;
25
24
  sessionId: string;
25
+ sha256: string;
26
26
  v: 1;
27
27
  manifestIndex: number;
28
28
  firstEventIndex: number;
@@ -14,13 +14,13 @@ export declare const StateTokenPayloadV1Schema: z.ZodObject<{
14
14
  nodeId: z.ZodEffects<z.ZodString, NodeId, string>;
15
15
  workflowHashRef: z.ZodEffects<z.ZodString, WorkflowHashRef, string>;
16
16
  }, "strip", z.ZodTypeAny, {
17
+ runId: string & {
18
+ readonly __brand: "v2.RunId";
19
+ };
17
20
  sessionId: string & {
18
21
  readonly __brand: "v2.SessionId";
19
22
  };
20
23
  tokenKind: "state";
21
- runId: string & {
22
- readonly __brand: "v2.RunId";
23
- };
24
24
  nodeId: string & {
25
25
  readonly __brand: "v2.NodeId";
26
26
  };
@@ -29,9 +29,9 @@ export declare const StateTokenPayloadV1Schema: z.ZodObject<{
29
29
  };
30
30
  tokenVersion: 1;
31
31
  }, {
32
+ runId: string;
32
33
  sessionId: string;
33
34
  tokenKind: "state";
34
- runId: string;
35
35
  nodeId: string;
36
36
  workflowHashRef: string;
37
37
  tokenVersion: 1;
@@ -52,13 +52,13 @@ export declare const AckTokenPayloadV1Schema: z.ZodObject<{
52
52
  nodeId: z.ZodEffects<z.ZodString, NodeId, string>;
53
53
  attemptId: z.ZodEffects<z.ZodString, AttemptId, string>;
54
54
  }, "strip", z.ZodTypeAny, {
55
+ runId: string & {
56
+ readonly __brand: "v2.RunId";
57
+ };
55
58
  sessionId: string & {
56
59
  readonly __brand: "v2.SessionId";
57
60
  };
58
61
  tokenKind: "ack";
59
- runId: string & {
60
- readonly __brand: "v2.RunId";
61
- };
62
62
  nodeId: string & {
63
63
  readonly __brand: "v2.NodeId";
64
64
  };
@@ -67,9 +67,9 @@ export declare const AckTokenPayloadV1Schema: z.ZodObject<{
67
67
  };
68
68
  tokenVersion: 1;
69
69
  }, {
70
+ runId: string;
70
71
  sessionId: string;
71
72
  tokenKind: "ack";
72
- runId: string;
73
73
  nodeId: string;
74
74
  attemptId: string;
75
75
  tokenVersion: 1;
@@ -90,13 +90,13 @@ export declare const CheckpointTokenPayloadV1Schema: z.ZodObject<{
90
90
  nodeId: z.ZodEffects<z.ZodString, NodeId, string>;
91
91
  attemptId: z.ZodEffects<z.ZodString, AttemptId, string>;
92
92
  }, "strip", z.ZodTypeAny, {
93
+ runId: string & {
94
+ readonly __brand: "v2.RunId";
95
+ };
93
96
  sessionId: string & {
94
97
  readonly __brand: "v2.SessionId";
95
98
  };
96
99
  tokenKind: "checkpoint";
97
- runId: string & {
98
- readonly __brand: "v2.RunId";
99
- };
100
100
  nodeId: string & {
101
101
  readonly __brand: "v2.NodeId";
102
102
  };
@@ -105,9 +105,9 @@ export declare const CheckpointTokenPayloadV1Schema: z.ZodObject<{
105
105
  };
106
106
  tokenVersion: 1;
107
107
  }, {
108
+ runId: string;
108
109
  sessionId: string;
109
110
  tokenKind: "checkpoint";
110
- runId: string;
111
111
  nodeId: string;
112
112
  attemptId: string;
113
113
  tokenVersion: 1;
@@ -128,13 +128,13 @@ export declare const TokenPayloadV1Schema: z.ZodDiscriminatedUnion<"tokenKind",
128
128
  nodeId: z.ZodEffects<z.ZodString, NodeId, string>;
129
129
  workflowHashRef: z.ZodEffects<z.ZodString, WorkflowHashRef, string>;
130
130
  }, "strip", z.ZodTypeAny, {
131
+ runId: string & {
132
+ readonly __brand: "v2.RunId";
133
+ };
131
134
  sessionId: string & {
132
135
  readonly __brand: "v2.SessionId";
133
136
  };
134
137
  tokenKind: "state";
135
- runId: string & {
136
- readonly __brand: "v2.RunId";
137
- };
138
138
  nodeId: string & {
139
139
  readonly __brand: "v2.NodeId";
140
140
  };
@@ -143,9 +143,9 @@ export declare const TokenPayloadV1Schema: z.ZodDiscriminatedUnion<"tokenKind",
143
143
  };
144
144
  tokenVersion: 1;
145
145
  }, {
146
+ runId: string;
146
147
  sessionId: string;
147
148
  tokenKind: "state";
148
- runId: string;
149
149
  nodeId: string;
150
150
  workflowHashRef: string;
151
151
  tokenVersion: 1;
@@ -157,13 +157,13 @@ export declare const TokenPayloadV1Schema: z.ZodDiscriminatedUnion<"tokenKind",
157
157
  nodeId: z.ZodEffects<z.ZodString, NodeId, string>;
158
158
  attemptId: z.ZodEffects<z.ZodString, AttemptId, string>;
159
159
  }, "strip", z.ZodTypeAny, {
160
+ runId: string & {
161
+ readonly __brand: "v2.RunId";
162
+ };
160
163
  sessionId: string & {
161
164
  readonly __brand: "v2.SessionId";
162
165
  };
163
166
  tokenKind: "ack";
164
- runId: string & {
165
- readonly __brand: "v2.RunId";
166
- };
167
167
  nodeId: string & {
168
168
  readonly __brand: "v2.NodeId";
169
169
  };
@@ -172,9 +172,9 @@ export declare const TokenPayloadV1Schema: z.ZodDiscriminatedUnion<"tokenKind",
172
172
  };
173
173
  tokenVersion: 1;
174
174
  }, {
175
+ runId: string;
175
176
  sessionId: string;
176
177
  tokenKind: "ack";
177
- runId: string;
178
178
  nodeId: string;
179
179
  attemptId: string;
180
180
  tokenVersion: 1;
@@ -186,13 +186,13 @@ export declare const TokenPayloadV1Schema: z.ZodDiscriminatedUnion<"tokenKind",
186
186
  nodeId: z.ZodEffects<z.ZodString, NodeId, string>;
187
187
  attemptId: z.ZodEffects<z.ZodString, AttemptId, string>;
188
188
  }, "strip", z.ZodTypeAny, {
189
+ runId: string & {
190
+ readonly __brand: "v2.RunId";
191
+ };
189
192
  sessionId: string & {
190
193
  readonly __brand: "v2.SessionId";
191
194
  };
192
195
  tokenKind: "checkpoint";
193
- runId: string & {
194
- readonly __brand: "v2.RunId";
195
- };
196
196
  nodeId: string & {
197
197
  readonly __brand: "v2.NodeId";
198
198
  };
@@ -201,9 +201,9 @@ export declare const TokenPayloadV1Schema: z.ZodDiscriminatedUnion<"tokenKind",
201
201
  };
202
202
  tokenVersion: 1;
203
203
  }, {
204
+ runId: string;
204
205
  sessionId: string;
205
206
  tokenKind: "checkpoint";
206
- runId: string;
207
207
  nodeId: string;
208
208
  attemptId: string;
209
209
  tokenVersion: 1;
@@ -2,12 +2,14 @@ import type { ResultAsync } from 'neverthrow';
2
2
  import type { DirectoryListingPortV2 } from '../../../ports/directory-listing.port.js';
3
3
  import type { DataDirPortV2 } from '../../../ports/data-dir.port.js';
4
4
  import type { SessionEventLogReadonlyStorePortV2 } from '../../../ports/session-event-log-store.port.js';
5
+ import type { SnapshotStorePortV2 } from '../../../ports/snapshot-store.port.js';
5
6
  import type { SessionSummaryProviderPortV2, SessionSummaryError } from '../../../ports/session-summary-provider.port.js';
6
7
  import type { HealthySessionSummary } from '../../../projections/resume-ranking.js';
7
8
  export interface LocalSessionSummaryProviderPorts {
8
9
  readonly directoryListing: DirectoryListingPortV2;
9
10
  readonly dataDir: DataDirPortV2;
10
11
  readonly sessionStore: SessionEventLogReadonlyStorePortV2;
12
+ readonly snapshotStore?: SnapshotStorePortV2;
11
13
  }
12
14
  export declare class LocalSessionSummaryProviderV2 implements SessionSummaryProviderPortV2 {
13
15
  private readonly ports;
@@ -7,9 +7,10 @@ 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 snapshot_state_js_1 = require("../../../durable-core/projections/snapshot-state.js");
10
11
  const index_js_1 = require("../../../durable-core/ids/index.js");
11
12
  const constants_js_1 = require("../../../durable-core/constants.js");
12
- const MAX_SESSIONS_TO_SCAN = 50;
13
+ const MAX_SESSIONS_TO_SCAN = 200;
13
14
  const MAX_RECAP_ANCESTOR_DEPTH = 100;
14
15
  const EMPTY_OBSERVATIONS = {
15
16
  gitHeadSha: null,
@@ -29,20 +30,48 @@ class LocalSessionSummaryProviderV2 {
29
30
  code: 'SESSION_SUMMARY_ENUMERATION_FAILED',
30
31
  message: `Failed to enumerate sessions: ${fsErr.message}`,
31
32
  }))
32
- .andThen((sessionIds) => collectHealthySummaries(sessionIds.slice(0, MAX_SESSIONS_TO_SCAN), this.ports.sessionStore));
33
+ .andThen((entries) => collectHealthySummaries(entries.slice(0, MAX_SESSIONS_TO_SCAN), this.ports.sessionStore, this.ports.snapshotStore ?? null));
33
34
  }
34
35
  }
35
36
  exports.LocalSessionSummaryProviderV2 = LocalSessionSummaryProviderV2;
36
- function collectHealthySummaries(sessionIds, sessionStore) {
37
- return sessionIds.reduce((acc, sessionId) => acc.andThen((summaries) => loadSessionSummary(sessionId, sessionStore).map((summary) => summary !== null ? [...summaries, summary] : summaries)), (0, neverthrow_1.okAsync)([]));
37
+ function collectHealthySummaries(entries, sessionStore, snapshotStore) {
38
+ return entries.reduce((acc, entry) => acc.andThen((summaries) => loadSessionSummary(entry, sessionStore, snapshotStore).map((summary) => summary !== null ? [...summaries, summary] : summaries)), (0, neverthrow_1.okAsync)([]));
38
39
  }
39
- function loadSessionSummary(sessionId, sessionStore) {
40
+ function loadSessionSummary(entry, sessionStore, snapshotStore) {
40
41
  return sessionStore
41
- .load(sessionId)
42
- .map((truth) => projectSessionSummary(sessionId, truth))
42
+ .load(entry.sessionId)
43
+ .andThen((truth) => {
44
+ const projected = projectSessionSummary(entry.sessionId, truth, entry.mtimeMs);
45
+ if (!projected)
46
+ return (0, neverthrow_1.okAsync)(null);
47
+ if (!snapshotStore)
48
+ return (0, neverthrow_1.okAsync)(projected.summary);
49
+ const ref = safeSnapshotRef(projected.tipSnapshotRef);
50
+ if (!ref)
51
+ return (0, neverthrow_1.okAsync)(projected.summary);
52
+ return snapshotStore.getExecutionSnapshotV1(ref)
53
+ .map((snapshot) => {
54
+ if (!snapshot)
55
+ return projected.summary;
56
+ const engineState = snapshot.enginePayload.engineState;
57
+ const pending = (0, snapshot_state_js_1.derivePendingStep)(engineState);
58
+ const isComplete = (0, snapshot_state_js_1.deriveIsComplete)(engineState);
59
+ return {
60
+ ...projected.summary,
61
+ pendingStepId: pending?.stepId ?? null,
62
+ isComplete,
63
+ };
64
+ })
65
+ .orElse(() => (0, neverthrow_1.okAsync)(projected.summary));
66
+ })
43
67
  .orElse(() => (0, neverthrow_1.okAsync)(null));
44
68
  }
45
- function projectSessionSummary(sessionId, truth) {
69
+ function safeSnapshotRef(raw) {
70
+ if (!raw || !constants_js_1.SHA256_DIGEST_PATTERN.test(raw))
71
+ return null;
72
+ return (0, index_js_1.asSnapshotRef)((0, index_js_1.asSha256Digest)(raw));
73
+ }
74
+ function projectSessionSummary(sessionId, truth, mtimeMs) {
46
75
  const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
47
76
  if (health.isErr() || health.value.kind !== 'healthy')
48
77
  return null;
@@ -60,15 +89,21 @@ function projectSessionSummary(sessionId, truth) {
60
89
  ? extractAggregateRecap(outputsRes.value, bestRun.run, bestRun.tipNodeId)
61
90
  : null;
62
91
  return {
63
- sessionId,
64
- runId: bestRun.run.runId,
65
- preferredTip: {
66
- nodeId: bestRun.tipNodeId,
67
- lastActivityEventIndex: bestRun.lastActivityEventIndex,
92
+ summary: {
93
+ sessionId,
94
+ runId: bestRun.run.runId,
95
+ preferredTip: {
96
+ nodeId: bestRun.tipNodeId,
97
+ lastActivityEventIndex: bestRun.lastActivityEventIndex,
98
+ },
99
+ recapSnippet,
100
+ observations: extractObservations(truth.events),
101
+ workflow,
102
+ lastModifiedMs: mtimeMs,
103
+ pendingStepId: null,
104
+ isComplete: false,
68
105
  },
69
- recapSnippet,
70
- observations: extractObservations(truth.events),
71
- workflow,
106
+ tipSnapshotRef: bestRun.tipSnapshotRef,
72
107
  };
73
108
  }
74
109
  function selectBestRun(runs) {
@@ -77,7 +112,9 @@ function selectBestRun(runs) {
77
112
  if (!tipNodeId)
78
113
  return [];
79
114
  const tipNode = r.nodesById[tipNodeId];
80
- return [{ run: r, tipNodeId, lastActivityEventIndex: tipNode?.createdAtEventIndex ?? 0 }];
115
+ if (!tipNode)
116
+ return [];
117
+ return [{ run: r, tipNodeId, tipSnapshotRef: tipNode.snapshotRef, lastActivityEventIndex: tipNode.createdAtEventIndex }];
81
118
  });
82
119
  if (runsWithTip.length === 0)
83
120
  return null;
@@ -28,9 +28,16 @@ export interface HealthySessionSummary {
28
28
  readonly recapSnippet: RecapSnippet | null;
29
29
  readonly observations: SessionObservations;
30
30
  readonly workflow: IdentifiedWorkflow;
31
+ readonly lastModifiedMs: number | null;
32
+ readonly pendingStepId: string | null;
33
+ readonly isComplete: boolean;
31
34
  }
32
- export type WhyMatched = 'matched_head_sha' | 'matched_branch' | 'matched_notes' | 'matched_workflow_id' | 'recency_fallback';
35
+ export type WhyMatched = 'matched_exact_id' | 'matched_head_sha' | 'matched_branch' | 'matched_notes' | 'matched_notes_partial' | 'matched_workflow_id' | 'recency_fallback';
33
36
  export type TierAssignment = {
37
+ readonly tier: 0;
38
+ readonly kind: 'matched_exact_id';
39
+ readonly matchField: 'runId' | 'sessionId';
40
+ } | {
34
41
  readonly tier: 1;
35
42
  readonly kind: 'matched_head_sha';
36
43
  } | {
@@ -42,17 +49,26 @@ export type TierAssignment = {
42
49
  readonly kind: 'matched_notes';
43
50
  } | {
44
51
  readonly tier: 4;
45
- readonly kind: 'matched_workflow_id';
52
+ readonly kind: 'matched_notes_partial';
53
+ readonly matchRatio: number;
46
54
  } | {
47
55
  readonly tier: 5;
56
+ readonly kind: 'matched_workflow_id';
57
+ } | {
58
+ readonly tier: 6;
48
59
  readonly kind: 'recency_fallback';
49
60
  };
50
61
  export declare function normalizeToTokens(text: string): ReadonlySet<string>;
51
62
  export declare function allQueryTokensMatch(queryTokens: ReadonlySet<string>, candidateTokens: ReadonlySet<string>): boolean;
63
+ export declare function queryTokenMatchRatio(queryTokens: ReadonlySet<string>, candidateTokens: ReadonlySet<string>): number;
64
+ export declare function fuzzyTokenMatch(queryToken: string, candidateTokens: ReadonlySet<string>): boolean;
65
+ export declare function fuzzyQueryTokenMatchRatio(queryTokens: ReadonlySet<string>, candidateTokens: ReadonlySet<string>): number;
52
66
  export interface ResumeQuery {
53
67
  readonly gitHeadSha?: string;
54
68
  readonly gitBranch?: string;
55
69
  readonly freeTextQuery?: string;
70
+ readonly runId?: string;
71
+ readonly sessionId?: string;
56
72
  }
57
73
  export declare function assignTier(summary: HealthySessionSummary, query: ResumeQuery): TierAssignment;
58
74
  export interface RankedResumeCandidate {
@@ -65,6 +81,9 @@ export interface RankedResumeCandidate {
65
81
  readonly lastActivityEventIndex: number;
66
82
  readonly workflowHash: WorkflowHash;
67
83
  readonly workflowId: WorkflowId;
84
+ readonly pendingStepId: string | null;
85
+ readonly isComplete: boolean;
86
+ readonly lastModifiedMs: number | null;
68
87
  }
69
88
  export declare const MAX_RESUME_CANDIDATES = 5;
70
89
  export declare function rankResumeCandidates(summaries: readonly HealthySessionSummary[], query: ResumeQuery): readonly RankedResumeCandidate[];
@@ -4,6 +4,9 @@ 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;
7
10
  exports.assignTier = assignTier;
8
11
  exports.rankResumeCandidates = rankResumeCandidates;
9
12
  const constants_js_1 = require("../durable-core/constants.js");
@@ -36,7 +39,45 @@ function allQueryTokensMatch(queryTokens, candidateTokens) {
36
39
  }
37
40
  return true;
38
41
  }
42
+ function queryTokenMatchRatio(queryTokens, candidateTokens) {
43
+ if (queryTokens.size === 0 || candidateTokens.size === 0)
44
+ return 0;
45
+ let matched = 0;
46
+ for (const token of queryTokens) {
47
+ if (candidateTokens.has(token))
48
+ matched++;
49
+ }
50
+ return matched / queryTokens.size;
51
+ }
52
+ function fuzzyTokenMatch(queryToken, candidateTokens) {
53
+ if (queryToken.length < 3)
54
+ return false;
55
+ for (const ct of candidateTokens) {
56
+ if (ct.length < 3)
57
+ continue;
58
+ if (ct.includes(queryToken) || queryToken.includes(ct))
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ function fuzzyQueryTokenMatchRatio(queryTokens, candidateTokens) {
64
+ if (queryTokens.size === 0 || candidateTokens.size === 0)
65
+ return 0;
66
+ let matched = 0;
67
+ for (const qt of queryTokens) {
68
+ if (candidateTokens.has(qt) || fuzzyTokenMatch(qt, candidateTokens))
69
+ matched++;
70
+ }
71
+ return matched / queryTokens.size;
72
+ }
73
+ const MIN_PARTIAL_MATCH_RATIO = 0.4;
39
74
  function assignTier(summary, query) {
75
+ if (query.runId && summary.runId === query.runId) {
76
+ return { tier: 0, kind: 'matched_exact_id', matchField: 'runId' };
77
+ }
78
+ if (query.sessionId && String(summary.sessionId) === query.sessionId) {
79
+ return { tier: 0, kind: 'matched_exact_id', matchField: 'sessionId' };
80
+ }
40
81
  if (query.gitHeadSha && summary.observations.gitHeadSha === query.gitHeadSha) {
41
82
  return { tier: 1, kind: 'matched_head_sha' };
42
83
  }
@@ -49,27 +90,36 @@ function assignTier(summary, query) {
49
90
  return { tier: 2, kind: 'matched_branch', matchType: 'prefix' };
50
91
  }
51
92
  }
52
- if (query.freeTextQuery && summary.recapSnippet) {
53
- const queryTokens = normalizeToTokens(query.freeTextQuery);
93
+ const queryTokens = query.freeTextQuery ? normalizeToTokens(query.freeTextQuery) : null;
94
+ if (queryTokens && queryTokens.size > 0 && summary.recapSnippet) {
54
95
  const noteTokens = normalizeToTokens(summary.recapSnippet);
55
96
  if (allQueryTokensMatch(queryTokens, noteTokens)) {
56
97
  return { tier: 3, kind: 'matched_notes' };
57
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
+ }
58
103
  }
59
- if (query.freeTextQuery && summary.workflow.kind === 'identified') {
60
- const queryTokens = normalizeToTokens(query.freeTextQuery);
104
+ if (queryTokens && queryTokens.size > 0 && summary.workflow.kind === 'identified') {
61
105
  const workflowTokens = normalizeToTokens(String(summary.workflow.workflowId));
62
106
  if (allQueryTokensMatch(queryTokens, workflowTokens)) {
63
- return { tier: 4, kind: 'matched_workflow_id' };
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' };
64
112
  }
65
113
  }
66
- return { tier: 5, kind: 'recency_fallback' };
114
+ return { tier: 6, kind: 'recency_fallback' };
67
115
  }
68
116
  function tierToWhyMatched(tier) {
69
117
  switch (tier.kind) {
118
+ case 'matched_exact_id': return 'matched_exact_id';
70
119
  case 'matched_head_sha': return 'matched_head_sha';
71
120
  case 'matched_branch': return 'matched_branch';
72
121
  case 'matched_notes': return 'matched_notes';
122
+ case 'matched_notes_partial': return 'matched_notes_partial';
73
123
  case 'matched_workflow_id': return 'matched_workflow_id';
74
124
  case 'recency_fallback': return 'recency_fallback';
75
125
  }
@@ -83,6 +133,13 @@ function rankResumeCandidates(summaries, query) {
83
133
  const sorted = [...withTier].sort((a, b) => {
84
134
  if (a.tier.tier !== b.tier.tier)
85
135
  return a.tier.tier - b.tier.tier;
136
+ if (a.summary.isComplete !== b.summary.isComplete) {
137
+ return a.summary.isComplete ? 1 : -1;
138
+ }
139
+ if (a.tier.kind === 'matched_notes_partial' && b.tier.kind === 'matched_notes_partial') {
140
+ if (a.tier.matchRatio !== b.tier.matchRatio)
141
+ return b.tier.matchRatio - a.tier.matchRatio;
142
+ }
86
143
  const actA = a.summary.preferredTip.lastActivityEventIndex;
87
144
  const actB = b.summary.preferredTip.lastActivityEventIndex;
88
145
  if (actA !== actB)
@@ -99,5 +156,8 @@ function rankResumeCandidates(summaries, query) {
99
156
  lastActivityEventIndex: summary.preferredTip.lastActivityEventIndex,
100
157
  workflowHash: summary.workflow.workflowHash,
101
158
  workflowId: summary.workflow.workflowId,
159
+ pendingStepId: summary.pendingStepId,
160
+ isComplete: summary.isComplete,
161
+ lastModifiedMs: summary.lastModifiedMs,
102
162
  }));
103
163
  }
@@ -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 SessionId[], FsError>;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "3.7.0",
3
+ "version": "3.7.2",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {