@xingular/harness 0.0.0-stage → 0.1.0
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 +269 -2
- package/dist/cli.js +11046 -0
- package/dist/terminal-pi.js +326 -0
- package/package.json +41 -4
- package/src/accounts/accounts.test.ts +223 -0
- package/src/accounts/accounts.ts +522 -0
- package/src/accounts/env.ts +204 -0
- package/src/accounts/secrets.ts +50 -0
- package/src/accounts/store.ts +338 -0
- package/src/capabilities/api.ts +75 -0
- package/src/capabilities/runSnapshot.ts +59 -0
- package/src/capabilities/types.ts +34 -0
- package/src/cli.ts +110 -0
- package/src/core/bus.ts +34 -0
- package/src/core/changes.ts +33 -0
- package/src/core/env.ts +27 -0
- package/src/core/errors.ts +32 -0
- package/src/core/frontmatter.ts +47 -0
- package/src/core/fs.test.ts +38 -0
- package/src/core/fs.ts +92 -0
- package/src/core/index.ts +193 -0
- package/src/core/path.ts +45 -0
- package/src/core/projects.ts +90 -0
- package/src/core/text.ts +39 -0
- package/src/harness.ts +289 -0
- package/src/index.ts +69 -0
- package/src/mcp/connections.ts +441 -0
- package/src/mcp/manager.ts +130 -0
- package/src/mcp/oauth.ts +195 -0
- package/src/mcp/service.test.ts +376 -0
- package/src/mcp/service.ts +567 -0
- package/src/mcp/types.ts +105 -0
- package/src/memory/api.test.ts +82 -0
- package/src/memory/api.ts +287 -0
- package/src/memory/blocks.test.ts +78 -0
- package/src/memory/blocks.ts +406 -0
- package/src/memory/caller.ts +57 -0
- package/src/memory/dispatch.ts +59 -0
- package/src/memory/facts.test.ts +41 -0
- package/src/memory/facts.ts +248 -0
- package/src/memory/graph.ts +103 -0
- package/src/memory/health.test.ts +130 -0
- package/src/memory/health.ts +74 -0
- package/src/memory/maintainer.ts +272 -0
- package/src/memory/maintenance.test.ts +176 -0
- package/src/memory/maintenance.ts +330 -0
- package/src/memory/migrate.ts +79 -0
- package/src/memory/operations.ts +138 -0
- package/src/memory/proposals.test.ts +168 -0
- package/src/memory/proposals.ts +576 -0
- package/src/memory/recall.test.ts +104 -0
- package/src/memory/recall.ts +271 -0
- package/src/memory/references.ts +77 -0
- package/src/memory/runContext.ts +93 -0
- package/src/memory/secrets.ts +6 -0
- package/src/memory/store.test.ts +66 -0
- package/src/memory/store.ts +385 -0
- package/src/memory/wiki.test.ts +47 -0
- package/src/memory/wiki.ts +422 -0
- package/src/memory/wikiIndex.test.ts +88 -0
- package/src/memory/wikiIndex.ts +116 -0
- package/src/plugins/api.ts +181 -0
- package/src/plugins/attachments.test.ts +84 -0
- package/src/plugins/attachments.ts +128 -0
- package/src/plugins/builtin.ts +82 -0
- package/src/plugins/catalogue.test.ts +254 -0
- package/src/plugins/catalogue.ts +250 -0
- package/src/plugins/cli.ts +184 -0
- package/src/plugins/discover.ts +151 -0
- package/src/plugins/firstParty.test.ts +143 -0
- package/src/plugins/firstParty.ts +129 -0
- package/src/plugins/hooks.test.ts +250 -0
- package/src/plugins/hooks.ts +239 -0
- package/src/plugins/identity.ts +37 -0
- package/src/plugins/install.test.ts +107 -0
- package/src/plugins/install.ts +106 -0
- package/src/plugins/inventory.ts +123 -0
- package/src/plugins/manifest.ts +44 -0
- package/src/plugins/plugins.test.ts +205 -0
- package/src/plugins/plugins.ts +184 -0
- package/src/plugins/proposal.ts +121 -0
- package/src/plugins/runtime.test.ts +40 -0
- package/src/plugins/runtime.ts +82 -0
- package/src/plugins/trust.ts +142 -0
- package/src/plugins/types.ts +130 -0
- package/src/prompt/compile.test.ts +261 -0
- package/src/prompt/compile.ts +732 -0
- package/src/server/methods.ts +308 -0
- package/src/server/remote.ts +77 -0
- package/src/server/rpc.ts +116 -0
- package/src/server/serve.ts +28 -0
- package/src/server/server.test.ts +174 -0
- package/src/sessions/approvals.ts +118 -0
- package/src/sessions/contracts.ts +299 -0
- package/src/sessions/conversation.test.ts +150 -0
- package/src/sessions/extensionSession.test.ts +74 -0
- package/src/sessions/extensionUi.test.ts +65 -0
- package/src/sessions/extensionUi.ts +102 -0
- package/src/sessions/history.ts +51 -0
- package/src/sessions/lineageJoin.ts +75 -0
- package/src/sessions/policy.ts +32 -0
- package/src/sessions/pricing.ts +60 -0
- package/src/sessions/reasoningSummaries.ts +59 -0
- package/src/sessions/registry.ts +294 -0
- package/src/sessions/runIndex.ts +99 -0
- package/src/sessions/runStore.test.ts +152 -0
- package/src/sessions/runStore.ts +332 -0
- package/src/sessions/runner.ts +556 -0
- package/src/sessions/runtimeResources.test.ts +61 -0
- package/src/sessions/sessions.test.ts +434 -0
- package/src/sessions/sessions.ts +295 -0
- package/src/skills/registry.test.ts +104 -0
- package/src/skills/registry.ts +394 -0
- package/src/terminal/cli.ts +149 -0
- package/src/terminal/client.ts +74 -0
- package/src/terminal/controller.ts +227 -0
- package/src/terminal/piExtension.ts +208 -0
- package/src/terminal/pluginControl.test.ts +146 -0
- package/src/terminal/pluginMenu.ts +112 -0
- package/src/terminal/socket.ts +182 -0
- package/src/terminal/standalone.test.ts +115 -0
- package/src/terminal/standalone.ts +126 -0
- package/src/terminal/terminal.test.ts +302 -0
- package/src/terminal/terminals.ts +429 -0
- package/src/tools/askUser.ts +45 -0
- package/src/tools/definitions.ts +187 -0
- package/src/tools/memoryTools.ts +22 -0
- package/src/tools/pluginPropose.ts +41 -0
- package/src/tools/types.ts +36 -0
- package/src/web/safeRequest.ts +171 -0
- package/src/web/text.ts +53 -0
- package/src/web/tools.ts +134 -0
- package/src/web/web.test.ts +141 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Canonical run records and their event logs.
|
|
2
|
+
//
|
|
3
|
+
// `<data root>/runs/<run id>/run.json` holds the run's fields; `events.jsonl` is the
|
|
4
|
+
// append-only, validated event log. The harness is the only writer of both.
|
|
5
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
import { notFound } from '../core/errors'
|
|
9
|
+
import { newId, now, readJson, safeId, writeJson } from '../core/fs'
|
|
10
|
+
import { compareText, lines } from '../core/text'
|
|
11
|
+
import { isActiveStatus, runEventSchema, runRecordSchema, usagePayloadSchema, type RunEvent, type RunRecord, type UsagePayload } from './contracts'
|
|
12
|
+
import { readAllRuns, recordRun, runsForSession } from './runIndex'
|
|
13
|
+
|
|
14
|
+
export function runDir(root: string, runId: string): string {
|
|
15
|
+
return join(root, 'runs', safeId(runId))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const recordPath = (root: string, runId: string): string => join(runDir(root, runId), 'run.json')
|
|
19
|
+
|
|
20
|
+
// Field order of the file, so a record written here reads the same as one written before.
|
|
21
|
+
function ordered(record: RunRecord): RunRecord {
|
|
22
|
+
return {
|
|
23
|
+
resumedFromRunId: record.resumedFromRunId,
|
|
24
|
+
parentRunId: record.parentRunId,
|
|
25
|
+
relation: record.relation,
|
|
26
|
+
terminalReason: record.terminalReason,
|
|
27
|
+
id: record.id,
|
|
28
|
+
projectId: record.projectId,
|
|
29
|
+
mode: record.mode,
|
|
30
|
+
harness: record.harness,
|
|
31
|
+
sessionType: record.sessionType,
|
|
32
|
+
sessionId: record.sessionId,
|
|
33
|
+
nativeSessionId: record.nativeSessionId,
|
|
34
|
+
workingDirectory: record.workingDirectory,
|
|
35
|
+
worktreeId: record.worktreeId,
|
|
36
|
+
goal: record.goal,
|
|
37
|
+
attachmentCount: record.attachmentCount,
|
|
38
|
+
startedAt: record.startedAt,
|
|
39
|
+
endedAt: record.endedAt,
|
|
40
|
+
status: record.status,
|
|
41
|
+
exitCode: record.exitCode,
|
|
42
|
+
handoffId: record.handoffId,
|
|
43
|
+
accountId: record.accountId,
|
|
44
|
+
agentId: record.agentId,
|
|
45
|
+
trigger: record.trigger
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function write(root: string, record: RunRecord): void {
|
|
50
|
+
writeJson(recordPath(root, record.id), ordered(record))
|
|
51
|
+
recordRun(root, record)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function saveRun(root: string, record: RunRecord): void {
|
|
55
|
+
write(root, record)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readRecord(path: string): RunRecord | null {
|
|
59
|
+
const parsed = runRecordSchema.safeParse(readJson(path))
|
|
60
|
+
return parsed.success ? parsed.data : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function loadRun(root: string, runId: string): RunRecord {
|
|
64
|
+
const record = readRecord(recordPath(root, runId))
|
|
65
|
+
if (!record) throw notFound('Run')
|
|
66
|
+
return record
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function findRun(root: string, runId: string): RunRecord | null {
|
|
70
|
+
try {
|
|
71
|
+
return loadRun(root, runId)
|
|
72
|
+
} catch {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Changes a run's record. A settled run keeps its outcome whatever the change says. */
|
|
78
|
+
export function updateRun(root: string, runId: string, update: (record: RunRecord) => void): void {
|
|
79
|
+
const record = readRecord(recordPath(root, runId))
|
|
80
|
+
if (!record) return
|
|
81
|
+
const { status, endedAt, terminalReason } = record
|
|
82
|
+
update(record)
|
|
83
|
+
if (!isActiveStatus(status)) Object.assign(record, { status, endedAt, terminalReason })
|
|
84
|
+
write(root, record)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function terminalStatus(event: Pick<RunEvent, 'type' | 'payload'>): 'completed' | 'failed' | 'interrupted' | null {
|
|
88
|
+
if (event.type === 'completed') return 'completed'
|
|
89
|
+
if (event.type === 'failed') return 'failed'
|
|
90
|
+
if (event.type !== 'status.changed') return null
|
|
91
|
+
const status = (event.payload as { status?: unknown } | null)?.status
|
|
92
|
+
return status === 'interrupted' || status === 'failed' || status === 'completed' ? status : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function reasonOf(event: RunEvent): string | null {
|
|
96
|
+
const payload = event.payload as { reason?: unknown; message?: unknown } | null
|
|
97
|
+
const reason = payload?.reason ?? payload?.message
|
|
98
|
+
return typeof reason === 'string' ? reason : null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function appendEvent(root: string, event: RunEvent): void {
|
|
102
|
+
const directory = runDir(root, event.runId)
|
|
103
|
+
mkdirSync(directory, { recursive: true })
|
|
104
|
+
// O_APPEND with a single write keeps each line intact.
|
|
105
|
+
appendFileSync(join(directory, 'events.jsonl'), `${JSON.stringify(event)}\n`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A stored event as it was written: structurally an event, whatever its payload. */
|
|
109
|
+
function storedEvent(line: string): RunEvent | null {
|
|
110
|
+
try {
|
|
111
|
+
const value = JSON.parse(line) as RunEvent
|
|
112
|
+
const valid = value && typeof value.id === 'string' && typeof value.runId === 'string' && typeof value.sessionId === 'string' && Number.isInteger(value.sequence) && value.sequence >= 0 && typeof value.timestamp === 'string' && typeof value.type === 'string' && value.payload !== undefined
|
|
113
|
+
return valid ? value : null
|
|
114
|
+
} catch {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A torn final line (a crash mid-write) is skipped rather than failing the whole log. */
|
|
120
|
+
export function readEvents(root: string, runId: string): RunEvent[] {
|
|
121
|
+
let text: string
|
|
122
|
+
try {
|
|
123
|
+
text = readFileSync(join(runDir(root, runId), 'events.jsonl'), 'utf8')
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []
|
|
126
|
+
throw error
|
|
127
|
+
}
|
|
128
|
+
return lines(text).flatMap((line) => storedEvent(line) ?? [])
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** An incoming event that passes the contract, or `null`. */
|
|
132
|
+
export function parseEvent(value: unknown): RunEvent | null {
|
|
133
|
+
const parsed = runEventSchema.safeParse(value)
|
|
134
|
+
if (!parsed.success) return null
|
|
135
|
+
try {
|
|
136
|
+
safeId(parsed.data.runId)
|
|
137
|
+
} catch {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
return parsed.data
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The events that affected execution; what arrived after the outcome stays in the raw log as evidence. */
|
|
144
|
+
export function executionEvents(events: readonly RunEvent[]): RunEvent[] {
|
|
145
|
+
const end = events.findIndex((event) => terminalStatus(event) !== null)
|
|
146
|
+
return events.slice(0, end < 0 ? events.length : end + 1).filter((event) => (event.native as { hostIgnored?: unknown } | null | undefined)?.hostIgnored === undefined)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Appends an event and applies it to the run record. An event that arrives after the run
|
|
151
|
+
* settled is kept as evidence and returns `false`: it is never applied or relayed.
|
|
152
|
+
*/
|
|
153
|
+
export function persistEvent(root: string, event: RunEvent): boolean {
|
|
154
|
+
const record = loadRun(root, event.runId)
|
|
155
|
+
if (!isActiveStatus(record.status)) {
|
|
156
|
+
event.native = { hostIgnored: 'run_already_ended', event: event.native ?? null }
|
|
157
|
+
appendEvent(root, event)
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
appendEvent(root, event)
|
|
161
|
+
// Most events (text deltas above all) leave the record as it is; it is rewritten only when it changes.
|
|
162
|
+
let changed = false
|
|
163
|
+
const outcome = terminalStatus(event)
|
|
164
|
+
if (outcome) {
|
|
165
|
+
record.status = outcome
|
|
166
|
+
record.endedAt = event.timestamp
|
|
167
|
+
record.terminalReason = reasonOf(event)
|
|
168
|
+
changed = true
|
|
169
|
+
} else if (event.type === 'status.changed') {
|
|
170
|
+
const status = (event.payload as { status?: unknown }).status
|
|
171
|
+
if (typeof status === 'string' && status !== record.status) {
|
|
172
|
+
record.status = status
|
|
173
|
+
changed = true
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const payload = event.payload as { role?: unknown; text?: unknown }
|
|
177
|
+
if (event.type === 'message.completed' && payload.role === 'user' && record.mode === 'code' && !record.goal) {
|
|
178
|
+
record.goal = typeof payload.text === 'string' ? payload.text : null
|
|
179
|
+
changed = true
|
|
180
|
+
}
|
|
181
|
+
if (changed) write(root, record)
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const STOPPED = 'The harness stopped before the run settled.'
|
|
186
|
+
|
|
187
|
+
/** Startup reconciliation for every run that did not settle before the process stopped. */
|
|
188
|
+
export function markInterruptedRuns(root: string, reason = STOPPED): number {
|
|
189
|
+
let count = 0
|
|
190
|
+
for (const record of readAllRuns(root)) {
|
|
191
|
+
const path = recordPath(root, record.id)
|
|
192
|
+
const events = readEvents(root, record.id)
|
|
193
|
+
const settled = events.find((event) => terminalStatus(event) !== null)
|
|
194
|
+
if (settled) {
|
|
195
|
+
const status = terminalStatus(settled)!
|
|
196
|
+
const settledReason = reasonOf(settled)
|
|
197
|
+
if (record.status !== status || record.endedAt !== settled.timestamp || record.terminalReason !== settledReason) {
|
|
198
|
+
const backup = `${path}.before-outcome-repair`
|
|
199
|
+
if (!existsSync(backup)) copyFileSync(path, backup)
|
|
200
|
+
Object.assign(record, { status, endedAt: settled.timestamp, terminalReason: settledReason })
|
|
201
|
+
write(root, record)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!isActiveStatus(record.status)) continue
|
|
205
|
+
record.status = 'interrupted'
|
|
206
|
+
record.terminalReason = reason
|
|
207
|
+
record.endedAt ??= now()
|
|
208
|
+
const last = events.reduce((highest, event) => Math.max(highest, event.sequence), -1)
|
|
209
|
+
appendEvent(root, { id: newId(), runId: record.id, sessionId: record.sessionId, sequence: last + 1, timestamp: now(), type: 'status.changed', payload: { status: 'interrupted', reason } })
|
|
210
|
+
write(root, record)
|
|
211
|
+
count += 1
|
|
212
|
+
}
|
|
213
|
+
return count
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** An approval or question a run asked for that nothing has answered yet. */
|
|
217
|
+
export interface PendingDecision {
|
|
218
|
+
kind: 'approval' | 'question'
|
|
219
|
+
id: string
|
|
220
|
+
eventId: string
|
|
221
|
+
payload: unknown
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The run's unanswered approvals and questions, oldest first. */
|
|
225
|
+
export function pendingDecisions(events: readonly RunEvent[]): PendingDecision[] {
|
|
226
|
+
const pending: PendingDecision[] = []
|
|
227
|
+
for (const requested of events) {
|
|
228
|
+
const shape = requested.type === 'approval.requested' ? (['approval', 'approvalId', 'approval.resolved'] as const) : requested.type === 'question.requested' ? (['question', 'questionId', 'question.answered'] as const) : null
|
|
229
|
+
if (!shape) continue
|
|
230
|
+
const [kind, key, answered] = shape
|
|
231
|
+
const id = (requested.payload as Record<string, unknown>)[key]
|
|
232
|
+
if (typeof id !== 'string') continue
|
|
233
|
+
if (events.some((entry) => entry.type === answered && (entry.payload as Record<string, unknown>)[key] === id)) continue
|
|
234
|
+
pending.push({ kind, id, eventId: requested.id, payload: requested.payload })
|
|
235
|
+
}
|
|
236
|
+
return pending
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface RunSnapshot {
|
|
240
|
+
run: RunRecord
|
|
241
|
+
events: RunEvent[]
|
|
242
|
+
lastSequence: number
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface RunFamilySnapshot {
|
|
246
|
+
rootRunId: string
|
|
247
|
+
runs: { run: RunRecord; usage: UsagePayload | null }[]
|
|
248
|
+
usage: {
|
|
249
|
+
inputTokens: number | null
|
|
250
|
+
outputTokens: number | null
|
|
251
|
+
cacheReadTokens: number | null
|
|
252
|
+
cacheWriteTokens: number | null
|
|
253
|
+
reasoningTokens: number | null
|
|
254
|
+
costUsd: number | null
|
|
255
|
+
requests: number | null
|
|
256
|
+
}
|
|
257
|
+
settled: boolean
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function belongsToRoot(record: RunRecord, rootRunId: string, records: Map<string, RunRecord>): boolean {
|
|
261
|
+
const visited = new Set<string>()
|
|
262
|
+
let current: RunRecord | undefined = record
|
|
263
|
+
while (current && !visited.has(current.id)) {
|
|
264
|
+
if (current.id === rootRunId) return true
|
|
265
|
+
visited.add(current.id)
|
|
266
|
+
current = current.parentRunId ? records.get(current.parentRunId) : undefined
|
|
267
|
+
}
|
|
268
|
+
return false
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** A root run and all descendants linked through opaque parent-run metadata. */
|
|
272
|
+
export function runFamily(root: string, runId: string): RunFamilySnapshot {
|
|
273
|
+
const all = readAllRuns(root)
|
|
274
|
+
const byId = new Map(all.map((record) => [record.id, record]))
|
|
275
|
+
const target = byId.get(runId)
|
|
276
|
+
if (!target) throw notFound('Run')
|
|
277
|
+
|
|
278
|
+
let familyRoot = target
|
|
279
|
+
const ancestors = new Set<string>([target.id])
|
|
280
|
+
while (familyRoot.parentRunId) {
|
|
281
|
+
const parent = byId.get(familyRoot.parentRunId)
|
|
282
|
+
if (!parent || ancestors.has(parent.id)) break
|
|
283
|
+
ancestors.add(parent.id)
|
|
284
|
+
familyRoot = parent
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const members = all.filter((record) => belongsToRoot(record, familyRoot.id, byId))
|
|
288
|
+
.sort((left, right) => compareText(left.startedAt, right.startedAt))
|
|
289
|
+
const runs = members.map((run) => {
|
|
290
|
+
const events = executionEvents(readEvents(root, run.id))
|
|
291
|
+
const lastUsage = events.filter((event) => event.type === 'usage.updated').at(-1)?.payload
|
|
292
|
+
const parsed = usagePayloadSchema.safeParse(lastUsage)
|
|
293
|
+
return { run, usage: parsed.success ? parsed.data : null }
|
|
294
|
+
})
|
|
295
|
+
const counters = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens', 'costUsd', 'requests'] as const
|
|
296
|
+
const usage = Object.fromEntries(counters.map((counter) => {
|
|
297
|
+
const values = runs.map(({ usage: current }) => current?.[counter])
|
|
298
|
+
return [counter, values.every((value) => typeof value === 'number') ? values.reduce<number>((total, value) => total + (value as number), 0) : null]
|
|
299
|
+
})) as RunFamilySnapshot['usage']
|
|
300
|
+
|
|
301
|
+
return { rootRunId: familyRoot.id, runs, usage, settled: runs.every(({ run }) => !isActiveStatus(run.status)) }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function snapshot(root: string, runId: string): RunSnapshot {
|
|
305
|
+
const run = loadRun(root, runId)
|
|
306
|
+
const events = executionEvents(readEvents(root, runId))
|
|
307
|
+
return { run, events, lastSequence: events.reduce((highest, event) => Math.max(highest, event.sequence), -1) }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface RunListFilter {
|
|
311
|
+
projectId?: string | null
|
|
312
|
+
sessionId?: string | null
|
|
313
|
+
activeOnly?: boolean
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** A session's history comes from the index; other filters scan run records. At most 200, newest first. */
|
|
317
|
+
export function listRuns(root: string, filter: RunListFilter): RunRecord[] {
|
|
318
|
+
if (filter.sessionId) {
|
|
319
|
+
try {
|
|
320
|
+
const records = runsForSession(root, filter.sessionId, 200).flatMap((id) => findRun(root, id) ?? [])
|
|
321
|
+
return filter.activeOnly ? records.filter((record) => isActiveStatus(record.status)) : records
|
|
322
|
+
} catch {
|
|
323
|
+
// Fall through to the files.
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return readAllRuns(root)
|
|
327
|
+
.filter((record) => !filter.projectId || record.projectId === filter.projectId)
|
|
328
|
+
.filter((record) => !filter.sessionId || record.sessionId === filter.sessionId)
|
|
329
|
+
.filter((record) => !filter.activeOnly || isActiveStatus(record.status))
|
|
330
|
+
.sort((left, right) => compareText(right.startedAt, left.startedAt))
|
|
331
|
+
.slice(0, 200)
|
|
332
|
+
}
|