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.
@@ -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 ? 10 : args.limit,
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
- return this.previewWithIdentity(args, executionIdentity(exec), { signal: exec?.signal })
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
- throw new Error('NO_ELIGIBLE_SESSIONS: no completed top-level DSH Sessions with direct human input and assistant output were found in this workspace')
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: 'exact-cwd', order: 'last-activity-desc' },
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: 'exact-cwd',
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, identity.projectRoot)
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
- export function buildHistoricalGenerationBatch({ projectRoot, selections, observations, limit = 10, createdAfter, now = new Date() }) {
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: 'exact-cwd',
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',
@@ -34,12 +34,25 @@ function sessionHeaderIdentity(header, effectiveAgentPreset) {
34
34
  }
35
35
 
36
36
  function sameProjectRoot(value, projectRoot) {
37
- if (typeof value !== 'string' || !path.isAbsolute(value)) return false
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 Number.isSafeInteger(value) && value > 0 ? new Date(value).toISOString() : null
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 (createdAfter !== undefined && (!Number.isSafeInteger(createdAfter) || createdAfter < 0)) {
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
- for (const record of Array.isArray(listed) ? listed : []) {
166
+ const seenSessionIds = new Set()
167
+ for (const record of listed) {
137
168
  const header = record?.header ?? {}
138
- if (!sameProjectRoot(header.cwd, projectRoot)) {
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
- const snapshots = await mapConcurrent(candidates, concurrency, async record => {
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
- if (snapshot?.session?.id !== record.header.id) {
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
- excludedCounts.empty += 1
182
- continue
183
- }
184
- if (index.openTurn) {
185
- excludedCounts.openTurn += 1
186
- continue
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
- eligible.push({
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
- warnings: excludedCounts.unreadable
238
- ? [`${excludedCounts.unreadable} Session(s) could not be read and were excluded.`]
239
- : [],
300
+ scan,
301
+ warnings,
240
302
  }
241
303
  }
242
304
 
243
305
  export function verifySessionSnapshot(expected, snapshot, projectRoot) {
244
- if (snapshot?.session?.id !== expected.rawSessionId || !sameProjectRoot(snapshot?.session?.cwd, projectRoot)) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-harbor-evolution",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "DeepSeek Harness plugin and Skill for Harbor Candidate and Historical Session evaluation workflows.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -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": { "const": "exact-cwd" },
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" },