dsh-harbor-evolution 0.9.2 → 0.9.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/README.md +10 -9
- package/lib/client.js +203 -398
- package/lib/historical-web.js +2 -2
- package/lib/session-diagnostic.js +22 -7
- package/lib/session-materializer.js +42 -2
- package/lib/session-selection.js +115 -52
- package/package.json +1 -1
- package/schemas/historical-generation-batch.schema.json +23 -2
package/lib/historical-web.js
CHANGED
|
@@ -97,10 +97,10 @@ export class HistoricalWebController {
|
|
|
97
97
|
ownerSessionId: `web-historical:${ownerSessionId}:${this.randomId()}`,
|
|
98
98
|
}
|
|
99
99
|
const preview = await this.sessionDiagnostic.previewWithIdentity({
|
|
100
|
-
limit: args.limit === undefined ?
|
|
100
|
+
limit: args.limit === undefined ? 3 : args.limit,
|
|
101
101
|
createdAfter: args.createdAfter,
|
|
102
102
|
includeFeedback: args.includeFeedback !== false,
|
|
103
|
-
}, identity, { config: resolved.config })
|
|
103
|
+
}, identity, { config: resolved.config, currentSessionId: ownerSessionId, scope: 'dsh-history' })
|
|
104
104
|
const { selectionToken, ...visible } = preview
|
|
105
105
|
this.previews.set(previewId, {
|
|
106
106
|
previewId,
|
|
@@ -157,10 +157,12 @@ export class SessionDiagnosticService {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
async preview(args = {}, exec) {
|
|
160
|
-
|
|
160
|
+
// Tool argument schemas may allow undeclared fields. Never let model-supplied
|
|
161
|
+
// arguments widen the public tool's exact-workspace history boundary.
|
|
162
|
+
return this.previewWithIdentity(args, executionIdentity(exec), { signal: exec?.signal, scope: 'exact-cwd' })
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
async previewWithIdentity(args = {}, requestedIdentity, { signal, config = this.config } = {}) {
|
|
165
|
+
async previewWithIdentity(args = {}, requestedIdentity, { signal, config = this.config, currentSessionId, scope = 'exact-cwd' } = {}) {
|
|
164
166
|
const identity = normalizedIdentity(requestedIdentity)
|
|
165
167
|
const sessionQuery = capability(this.ctx, 'sessionQuery')
|
|
166
168
|
const limit = args.limit ?? 10
|
|
@@ -168,7 +170,8 @@ export class SessionDiagnosticService {
|
|
|
168
170
|
const result = await selectRecentSessions({
|
|
169
171
|
sessionQuery,
|
|
170
172
|
projectRoot: identity.projectRoot,
|
|
171
|
-
currentSessionId: identity.ownerSessionId,
|
|
173
|
+
currentSessionId: currentSessionId ?? identity.ownerSessionId,
|
|
174
|
+
scope,
|
|
172
175
|
limit,
|
|
173
176
|
maxSessionReads: config.sessionMaxReads ?? 100,
|
|
174
177
|
concurrency: config.sessionReadConcurrency ?? 4,
|
|
@@ -176,7 +179,13 @@ export class SessionDiagnosticService {
|
|
|
176
179
|
signal,
|
|
177
180
|
})
|
|
178
181
|
if (!result.selected.length) {
|
|
179
|
-
|
|
182
|
+
if (result.excludedCounts.unreadable > 0 || result.excludedCounts.invalidHeader > 0) {
|
|
183
|
+
throw new Error('SESSION_HISTORY_READ_FAILED: historical conversations could not be read; retry without changing the working directory')
|
|
184
|
+
}
|
|
185
|
+
if (result.scan?.partial) {
|
|
186
|
+
throw new Error('SESSION_HISTORY_WINDOW_EXHAUSTED: no completed conversations were available in the recent sample; older history was not evaluated')
|
|
187
|
+
}
|
|
188
|
+
throw new Error('NO_ELIGIBLE_SESSIONS: no completed conversations with direct human input and assistant output were available in the selected history')
|
|
180
189
|
}
|
|
181
190
|
const judgeBinding = await resolveJudge(this.modelRuntime, args)
|
|
182
191
|
const includeFeedback = args.includeFeedback !== false
|
|
@@ -204,7 +213,7 @@ export class SessionDiagnosticService {
|
|
|
204
213
|
feedbackSnapshots,
|
|
205
214
|
judgeBinding,
|
|
206
215
|
evaluation,
|
|
207
|
-
parameters: { limit, includeFeedback, createdAfter, scope
|
|
216
|
+
parameters: { limit, includeFeedback, createdAfter, scope, order: 'last-activity-desc', ...(result.scan ? { scan: result.scan } : {}) },
|
|
208
217
|
})
|
|
209
218
|
const warnings = [...result.warnings]
|
|
210
219
|
if (feedbackObservations.some(item => item.failed)) {
|
|
@@ -215,8 +224,9 @@ export class SessionDiagnosticService {
|
|
|
215
224
|
schema_version: 1,
|
|
216
225
|
capability: 'historical-generation-evaluation',
|
|
217
226
|
jobKind: 'historical-generation-evaluation',
|
|
218
|
-
scope
|
|
227
|
+
scope,
|
|
219
228
|
order: 'last-activity-desc',
|
|
229
|
+
...(result.scan ? { scan: result.scan } : {}),
|
|
220
230
|
...(createdAfter === undefined ? {} : { createdAfter: new Date(createdAfter).toISOString() }),
|
|
221
231
|
executionMode: 'observe-existing',
|
|
222
232
|
promotionEligible: false,
|
|
@@ -269,7 +279,10 @@ export class SessionDiagnosticService {
|
|
|
269
279
|
}
|
|
270
280
|
const snapshots = reads.map(item => item.value)
|
|
271
281
|
if (snapshots.some((snapshot, index) => (
|
|
272
|
-
!verifySessionSnapshot(selectedState.selection[index], snapshot,
|
|
282
|
+
!verifySessionSnapshot(selectedState.selection[index], snapshot,
|
|
283
|
+
selectedState.parameters.scope === 'dsh-history'
|
|
284
|
+
? selectedState.selection[index].header.cwd
|
|
285
|
+
: identity.projectRoot)
|
|
273
286
|
))) {
|
|
274
287
|
throw new Error('SESSION_SAMPLE_CHANGED: at least one selected Session changed after Preview; no Batch was written, preview again')
|
|
275
288
|
}
|
|
@@ -303,6 +316,8 @@ export class SessionDiagnosticService {
|
|
|
303
316
|
observations,
|
|
304
317
|
limit: selectedState.parameters.limit,
|
|
305
318
|
createdAfter: selectedState.parameters.createdAfter,
|
|
319
|
+
scope: selectedState.parameters.scope,
|
|
320
|
+
scan: selectedState.parameters.scan,
|
|
306
321
|
now: this.now(),
|
|
307
322
|
})
|
|
308
323
|
const written = await writePrivateHistoricalBatch({
|
|
@@ -12,7 +12,40 @@ function routeName(route) {
|
|
|
12
12
|
return `${route.provider}/${route.model}`
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
function materializeScan(scan, scope) {
|
|
16
|
+
if (scan === undefined) return undefined
|
|
17
|
+
if (
|
|
18
|
+
!scan || scan.scope !== scope
|
|
19
|
+
|| ![scan.listedCount, scan.candidateCount, scan.readCount, scan.unscannedCount]
|
|
20
|
+
.every(value => Number.isSafeInteger(value) && value >= 0)
|
|
21
|
+
|| scan.candidateCount > scan.listedCount
|
|
22
|
+
|| scan.readCount + scan.unscannedCount !== scan.candidateCount
|
|
23
|
+
|| scan.partial !== (scan.unscannedCount > 0)
|
|
24
|
+
|| scan.windowOrder !== (scope === 'exact-cwd' ? 'all-candidates' : 'created-at-desc')
|
|
25
|
+
|| scan.selectionOrder !== 'last-activity-desc'
|
|
26
|
+
) {
|
|
27
|
+
throw new Error('HISTORICAL_BATCH_SCAN_INVALID')
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
scope,
|
|
31
|
+
listed_count: scan.listedCount,
|
|
32
|
+
candidate_count: scan.candidateCount,
|
|
33
|
+
read_count: scan.readCount,
|
|
34
|
+
unscanned_count: scan.unscannedCount,
|
|
35
|
+
partial: scan.partial,
|
|
36
|
+
window_order: scan.windowOrder,
|
|
37
|
+
selection_order: scan.selectionOrder,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildHistoricalGenerationBatch({
|
|
42
|
+
projectRoot, selections, observations, limit = 10, createdAfter,
|
|
43
|
+
scope = 'exact-cwd', scan, now = new Date(),
|
|
44
|
+
}) {
|
|
45
|
+
if (!['exact-cwd', 'dsh-history'].includes(scope)) {
|
|
46
|
+
throw new Error('HISTORICAL_BATCH_SCOPE_INVALID')
|
|
47
|
+
}
|
|
48
|
+
const selectionScan = materializeScan(scan, scope)
|
|
16
49
|
if (!Array.isArray(selections) || !selections.length || selections.length > 10) {
|
|
17
50
|
throw new Error('HISTORICAL_BATCH_SIZE_INVALID: a batch requires 1 to 10 Session observations')
|
|
18
51
|
}
|
|
@@ -35,6 +68,11 @@ export function buildHistoricalGenerationBatch({ projectRoot, selections, observ
|
|
|
35
68
|
source_ref: selection.sourceRef,
|
|
36
69
|
captured_through_seq: selection.capturedThroughSeq,
|
|
37
70
|
source_digest: selection.sourceDigest,
|
|
71
|
+
...(typeof selection.header?.cwd === 'string' && path.isAbsolute(selection.header.cwd)
|
|
72
|
+
? { source_project_digest: canonicalDigest(
|
|
73
|
+
{ cwd: path.resolve(selection.header.cwd) }, 'harbor-dsh-project-cwd-v1',
|
|
74
|
+
) }
|
|
75
|
+
: {}),
|
|
38
76
|
observation_digest: observation.digest,
|
|
39
77
|
last_activity_at: observation.source.last_activity_at,
|
|
40
78
|
generator: {
|
|
@@ -60,15 +98,17 @@ export function buildHistoricalGenerationBatch({ projectRoot, selections, observ
|
|
|
60
98
|
batch_id: batchId,
|
|
61
99
|
created_at: now.toISOString(),
|
|
62
100
|
project: {
|
|
101
|
+
// This identifies the output workspace, not the projects the sessions came from.
|
|
63
102
|
cwd_digest: canonicalDigest({ cwd: path.resolve(projectRoot) }, 'harbor-dsh-project-cwd-v1'),
|
|
64
103
|
},
|
|
65
104
|
selection: {
|
|
66
|
-
scope
|
|
105
|
+
scope,
|
|
67
106
|
order: 'last-activity-desc',
|
|
68
107
|
requested_limit: limit,
|
|
69
108
|
selected_count: records.length,
|
|
70
109
|
current_session_excluded: true,
|
|
71
110
|
...(createdAfter === undefined ? {} : { created_after: new Date(createdAfter).toISOString() }),
|
|
111
|
+
...(selectionScan === undefined ? {} : { scan: selectionScan }),
|
|
72
112
|
},
|
|
73
113
|
source: {
|
|
74
114
|
kind: 'dsh-session',
|
package/lib/session-selection.js
CHANGED
|
@@ -34,12 +34,25 @@ function sessionHeaderIdentity(header, effectiveAgentPreset) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function sameProjectRoot(value, projectRoot) {
|
|
37
|
-
if (
|
|
37
|
+
if (!validSourceRoot(value) || !validSourceRoot(projectRoot)) return false
|
|
38
38
|
return path.resolve(value) === path.resolve(projectRoot)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function validSourceRoot(value) {
|
|
42
|
+
return typeof value === 'string' && !value.includes('\0') && path.isAbsolute(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validTimestamp(value) {
|
|
46
|
+
return Number.isSafeInteger(value) && value >= 0 && value <= 8_640_000_000_000_000
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validSourceHeader(header) {
|
|
50
|
+
return typeof header?.id === 'string' && header.id.trim().length > 0
|
|
51
|
+
&& validSourceRoot(header.cwd) && validTimestamp(header.createdAt)
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
function isoTime(value) {
|
|
42
|
-
return
|
|
55
|
+
return validTimestamp(value) && value > 0 ? new Date(value).toISOString() : null
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
function safeIdentity(value, rawSessionId) {
|
|
@@ -97,6 +110,7 @@ export async function selectRecentSessions({
|
|
|
97
110
|
sessionQuery,
|
|
98
111
|
projectRoot,
|
|
99
112
|
currentSessionId,
|
|
113
|
+
scope = 'exact-cwd',
|
|
100
114
|
limit = 10,
|
|
101
115
|
maxSessionReads = 100,
|
|
102
116
|
concurrency = 4,
|
|
@@ -109,15 +123,29 @@ export async function selectRecentSessions({
|
|
|
109
123
|
if (!Number.isInteger(limit) || limit < 1 || limit > 10) {
|
|
110
124
|
throw new Error('SESSION_LIMIT_INVALID: limit must be an integer from 1 to 10')
|
|
111
125
|
}
|
|
112
|
-
if (
|
|
126
|
+
if (scope !== 'exact-cwd' && scope !== 'dsh-history') {
|
|
127
|
+
throw new Error('SESSION_SELECTION_SCOPE_INVALID: scope must be exact-cwd or dsh-history')
|
|
128
|
+
}
|
|
129
|
+
if (!Number.isSafeInteger(maxSessionReads) || maxSessionReads < 1) {
|
|
130
|
+
throw new Error('SESSION_READ_BUDGET_INVALID: maxSessionReads must be a positive integer')
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
133
|
+
throw new Error('SESSION_READ_CONCURRENCY_INVALID: concurrency must be a positive integer')
|
|
134
|
+
}
|
|
135
|
+
if (createdAfter !== undefined && !validTimestamp(createdAfter)) {
|
|
113
136
|
throw new Error('SESSION_CREATED_AFTER_INVALID: createdAfter must be a valid timestamp')
|
|
114
137
|
}
|
|
138
|
+
signal?.throwIfAborted()
|
|
115
139
|
const listed = typeof sessionQuery.filterSessions === 'function'
|
|
116
140
|
? await sessionQuery.filterSessions([
|
|
117
|
-
{ kind: 'cwd', values: [projectRoot] },
|
|
141
|
+
...(scope === 'exact-cwd' ? [{ kind: 'cwd', values: [projectRoot] }] : []),
|
|
118
142
|
...(createdAfter === undefined ? [] : [{ kind: 'created-at', from: createdAfter }]),
|
|
119
143
|
], signal)
|
|
120
144
|
: await sessionQuery.listSessions(signal)
|
|
145
|
+
signal?.throwIfAborted()
|
|
146
|
+
if (!Array.isArray(listed)) {
|
|
147
|
+
throw new Error('DSH_SESSION_QUERY_INVALID: Session Query returned an invalid Session list')
|
|
148
|
+
}
|
|
121
149
|
const excludedCounts = {
|
|
122
150
|
outsideWorkspace: 0,
|
|
123
151
|
beforeCreatedAfter: 0,
|
|
@@ -131,11 +159,23 @@ export async function selectRecentSessions({
|
|
|
131
159
|
harborInternal: 0,
|
|
132
160
|
empty: 0,
|
|
133
161
|
unreadable: 0,
|
|
162
|
+
invalidHeader: 0,
|
|
163
|
+
duplicate: 0,
|
|
134
164
|
}
|
|
135
165
|
const candidates = []
|
|
136
|
-
|
|
166
|
+
const seenSessionIds = new Set()
|
|
167
|
+
for (const record of listed) {
|
|
137
168
|
const header = record?.header ?? {}
|
|
138
|
-
if (!
|
|
169
|
+
if (!validSourceHeader(header)) {
|
|
170
|
+
excludedCounts.invalidHeader += 1
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if (seenSessionIds.has(header.id)) {
|
|
174
|
+
excludedCounts.duplicate += 1
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
seenSessionIds.add(header.id)
|
|
178
|
+
if (scope === 'exact-cwd' && !sameProjectRoot(header.cwd, projectRoot)) {
|
|
139
179
|
excludedCounts.outsideWorkspace += 1
|
|
140
180
|
} else if (header.id === currentSessionId) {
|
|
141
181
|
excludedCounts.currentSession += 1
|
|
@@ -153,57 +193,43 @@ export async function selectRecentSessions({
|
|
|
153
193
|
candidates.push(record)
|
|
154
194
|
}
|
|
155
195
|
}
|
|
156
|
-
if (candidates.length > maxSessionReads) {
|
|
196
|
+
if (scope === 'exact-cwd' && candidates.length > maxSessionReads) {
|
|
157
197
|
throw new Error(
|
|
158
198
|
`SESSION_SELECTION_TOO_EXPENSIVE: ${candidates.length} exact Session reads exceed maxSessionReads=${maxSessionReads}; preview again with createdAfter to narrow the scan`,
|
|
159
199
|
)
|
|
160
200
|
}
|
|
161
201
|
|
|
162
|
-
|
|
202
|
+
// SessionRecord exposes creation time, not last activity. Bound quick-start
|
|
203
|
+
// reads by the newest-created candidate window, then rank the exact snapshots
|
|
204
|
+
// by activity. Explicit scan metadata prevents claiming a global activity rank.
|
|
205
|
+
if (scope === 'dsh-history') {
|
|
206
|
+
candidates.sort((left, right) => (
|
|
207
|
+
right.header.createdAt - left.header.createdAt
|
|
208
|
+
|| left.header.id.localeCompare(right.header.id)
|
|
209
|
+
))
|
|
210
|
+
}
|
|
211
|
+
const inspectRecord = async record => {
|
|
212
|
+
signal?.throwIfAborted()
|
|
163
213
|
const snapshot = await sessionQuery.readSession(record.header.id)
|
|
164
|
-
|
|
214
|
+
signal?.throwIfAborted()
|
|
215
|
+
if (!validSourceHeader(snapshot?.session) || !Array.isArray(snapshot?.events)) {
|
|
216
|
+
throw new Error('Session Query returned an invalid Session snapshot')
|
|
217
|
+
}
|
|
218
|
+
if (JSON.stringify(canonicalize(sessionHeaderIdentity(snapshot.session)))
|
|
219
|
+
!== JSON.stringify(canonicalize(sessionHeaderIdentity(record.header)))) {
|
|
165
220
|
throw new Error('Session Query returned a mismatched Session header')
|
|
166
221
|
}
|
|
167
|
-
if (!sameProjectRoot(snapshot.session.cwd, projectRoot)) {
|
|
222
|
+
if (scope === 'exact-cwd' && !sameProjectRoot(snapshot.session.cwd, projectRoot)) {
|
|
168
223
|
throw new Error('Session Query changed the Session workspace boundary')
|
|
169
224
|
}
|
|
170
|
-
return snapshot
|
|
171
|
-
})
|
|
172
|
-
const eligible = []
|
|
173
|
-
for (const outcome of snapshots) {
|
|
174
|
-
if (outcome.status === 'rejected') {
|
|
175
|
-
excludedCounts.unreadable += 1
|
|
176
|
-
continue
|
|
177
|
-
}
|
|
178
|
-
const snapshot = outcome.value
|
|
179
225
|
const index = foldSessionDiagnosticIndex(snapshot.events, snapshot.session)
|
|
180
|
-
if (index.lastSeq === null) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
if (index.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
if (index.lastTurnReason === 'aborted') {
|
|
189
|
-
excludedCounts.userAborted += 1
|
|
190
|
-
continue
|
|
191
|
-
}
|
|
192
|
-
if (index.humanMessageCount < 1) {
|
|
193
|
-
excludedCounts.noDirectHumanInput += 1
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
if (index.assistantMessageCount < 1) {
|
|
197
|
-
excludedCounts.noAssistantOutput += 1
|
|
198
|
-
continue
|
|
199
|
-
}
|
|
200
|
-
if (index.hasHarborToolCall) {
|
|
201
|
-
excludedCounts.harborInternal += 1
|
|
202
|
-
continue
|
|
203
|
-
}
|
|
204
|
-
if (/harbor/i.test(String(index.effectiveAgentPreset ?? ''))) {
|
|
205
|
-
excludedCounts.harborInternal += 1
|
|
206
|
-
continue
|
|
226
|
+
if (index.lastSeq === null) return { excluded: 'empty' }
|
|
227
|
+
if (index.openTurn) return { excluded: 'openTurn' }
|
|
228
|
+
if (index.lastTurnReason === 'aborted') return { excluded: 'userAborted' }
|
|
229
|
+
if (index.humanMessageCount < 1) return { excluded: 'noDirectHumanInput' }
|
|
230
|
+
if (index.assistantMessageCount < 1) return { excluded: 'noAssistantOutput' }
|
|
231
|
+
if (index.hasHarborToolCall || /harbor/i.test(String(index.effectiveAgentPreset ?? ''))) {
|
|
232
|
+
return { excluded: 'harborInternal' }
|
|
207
233
|
}
|
|
208
234
|
const header = sessionHeaderIdentity(snapshot.session, index.effectiveAgentPreset)
|
|
209
235
|
const sourceDigest = canonicalDigest(
|
|
@@ -214,7 +240,7 @@ export async function selectRecentSessions({
|
|
|
214
240
|
{ id: snapshot.session.id, header },
|
|
215
241
|
'harbor-dsh-session-source-ref-v1',
|
|
216
242
|
)
|
|
217
|
-
|
|
243
|
+
return { selected: {
|
|
218
244
|
rawSessionId: snapshot.session.id,
|
|
219
245
|
header,
|
|
220
246
|
events: snapshot.events,
|
|
@@ -223,25 +249,62 @@ export async function selectRecentSessions({
|
|
|
223
249
|
sourceRef,
|
|
224
250
|
capturedThroughSeq: index.lastSeq,
|
|
225
251
|
trialId: `session-${sourceRef.slice('sha256:'.length, 'sha256:'.length + 12)}`,
|
|
226
|
-
}
|
|
252
|
+
} }
|
|
253
|
+
}
|
|
254
|
+
const eligible = []
|
|
255
|
+
const readBudget = Math.min(candidates.length, maxSessionReads)
|
|
256
|
+
let readCount = 0
|
|
257
|
+
// Quick experience needs a few useful examples, not every transcript. Stop
|
|
258
|
+
// after a small concurrent batch supplies enough, without losing the cap.
|
|
259
|
+
while (readCount < readBudget) {
|
|
260
|
+
const batch = candidates.slice(readCount, Math.min(readCount + concurrency, readBudget))
|
|
261
|
+
const snapshots = await mapConcurrent(batch, concurrency, inspectRecord)
|
|
262
|
+
readCount += batch.length
|
|
263
|
+
signal?.throwIfAborted()
|
|
264
|
+
for (const outcome of snapshots) {
|
|
265
|
+
if (outcome.status === 'rejected') excludedCounts.unreadable += 1
|
|
266
|
+
else if (outcome.value.excluded) excludedCounts[outcome.value.excluded] += 1
|
|
267
|
+
else eligible.push(outcome.value.selected)
|
|
268
|
+
}
|
|
269
|
+
if (scope === 'dsh-history' && eligible.length >= limit) break
|
|
270
|
+
}
|
|
271
|
+
const scan = {
|
|
272
|
+
scope,
|
|
273
|
+
listedCount: listed.length,
|
|
274
|
+
candidateCount: candidates.length,
|
|
275
|
+
readCount,
|
|
276
|
+
unscannedCount: candidates.length - readCount,
|
|
277
|
+
partial: candidates.length > readCount,
|
|
278
|
+
windowOrder: scope === 'dsh-history' ? 'created-at-desc' : 'all-candidates',
|
|
279
|
+
selectionOrder: 'last-activity-desc',
|
|
227
280
|
}
|
|
228
281
|
eligible.sort((left, right) => (
|
|
229
282
|
right.index.lastActivityAt - left.index.lastActivityAt
|
|
230
283
|
|| left.sourceRef.localeCompare(right.sourceRef)
|
|
231
284
|
))
|
|
232
285
|
const selected = eligible.slice(0, limit)
|
|
286
|
+
const warnings = []
|
|
287
|
+
if (excludedCounts.unreadable) {
|
|
288
|
+
warnings.push(`${excludedCounts.unreadable} Session(s) could not be read and were excluded.`)
|
|
289
|
+
}
|
|
290
|
+
if (excludedCounts.invalidHeader) {
|
|
291
|
+
warnings.push(`${excludedCounts.invalidHeader} Session record(s) had invalid metadata and were excluded.`)
|
|
292
|
+
}
|
|
293
|
+
if (scan.partial) {
|
|
294
|
+
warnings.push(`Only the ${scan.readCount} most recently created eligible Session candidates were checked; ${scan.unscannedCount} older candidate(s) were not read. Results are ranked by activity within this window, not across all DSH history.`)
|
|
295
|
+
}
|
|
233
296
|
return {
|
|
234
297
|
selected,
|
|
235
298
|
publicSelected: selected.map(publicSelection),
|
|
236
299
|
excludedCounts,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
: [],
|
|
300
|
+
scan,
|
|
301
|
+
warnings,
|
|
240
302
|
}
|
|
241
303
|
}
|
|
242
304
|
|
|
243
305
|
export function verifySessionSnapshot(expected, snapshot, projectRoot) {
|
|
244
|
-
if (snapshot?.session
|
|
306
|
+
if (!validSourceHeader(snapshot?.session) || !Array.isArray(snapshot?.events)
|
|
307
|
+
|| snapshot.session.id !== expected.rawSessionId || !sameProjectRoot(snapshot.session.cwd, projectRoot)) {
|
|
245
308
|
return false
|
|
246
309
|
}
|
|
247
310
|
const index = foldSessionDiagnosticIndex(snapshot.events, snapshot.session)
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"type": "object",
|
|
15
15
|
"additionalProperties": false,
|
|
16
16
|
"required": ["cwd_digest"],
|
|
17
|
+
"description": "Output workspace identity; source projects are recorded per Generation Record.",
|
|
17
18
|
"properties": { "cwd_digest": { "$ref": "#/$defs/digest" } }
|
|
18
19
|
},
|
|
19
20
|
"selection": {
|
|
@@ -21,12 +22,31 @@
|
|
|
21
22
|
"additionalProperties": false,
|
|
22
23
|
"required": ["scope", "order", "requested_limit", "selected_count", "current_session_excluded"],
|
|
23
24
|
"properties": {
|
|
24
|
-
"scope": { "
|
|
25
|
+
"scope": { "enum": ["exact-cwd", "dsh-history"] },
|
|
25
26
|
"order": { "const": "last-activity-desc" },
|
|
26
27
|
"requested_limit": { "type": "integer", "minimum": 1, "maximum": 10 },
|
|
27
28
|
"selected_count": { "type": "integer", "minimum": 1, "maximum": 10 },
|
|
28
29
|
"current_session_excluded": { "const": true },
|
|
29
|
-
"created_after": { "type": "string", "format": "date-time" }
|
|
30
|
+
"created_after": { "type": "string", "format": "date-time" },
|
|
31
|
+
"scan": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"description": "Discovery window metadata. Activity ordering only applies to records read within this window, not unscanned history.",
|
|
35
|
+
"required": ["scope", "listed_count", "candidate_count", "read_count", "unscanned_count", "partial", "window_order", "selection_order"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"scope": { "enum": ["exact-cwd", "dsh-history"] },
|
|
38
|
+
"listed_count": { "type": "integer", "minimum": 0 },
|
|
39
|
+
"candidate_count": { "type": "integer", "minimum": 0 },
|
|
40
|
+
"read_count": { "type": "integer", "minimum": 0 },
|
|
41
|
+
"unscanned_count": { "type": "integer", "minimum": 0 },
|
|
42
|
+
"partial": { "type": "boolean" },
|
|
43
|
+
"window_order": { "enum": ["all-candidates", "created-at-desc"] },
|
|
44
|
+
"selection_order": { "const": "last-activity-desc" }
|
|
45
|
+
},
|
|
46
|
+
"if": { "properties": { "scope": { "const": "exact-cwd" } } },
|
|
47
|
+
"then": { "properties": { "window_order": { "const": "all-candidates" } } },
|
|
48
|
+
"else": { "properties": { "window_order": { "const": "created-at-desc" } } }
|
|
49
|
+
}
|
|
30
50
|
}
|
|
31
51
|
},
|
|
32
52
|
"source": {
|
|
@@ -62,6 +82,7 @@
|
|
|
62
82
|
"source_ref": { "$ref": "#/$defs/digest" },
|
|
63
83
|
"captured_through_seq": { "type": "integer", "minimum": 0 },
|
|
64
84
|
"source_digest": { "$ref": "#/$defs/digest" },
|
|
85
|
+
"source_project_digest": { "$ref": "#/$defs/digest", "description": "Digest of the source Session project, when known; never inferred from the output workspace." },
|
|
65
86
|
"observation_digest": { "$ref": "#/$defs/digest" },
|
|
66
87
|
"last_activity_at": { "type": "string", "format": "date-time" },
|
|
67
88
|
"generator": { "type": "object" },
|