@aiwg/cli 2026.7.24 → 2026.8.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/dist/src/artifacts/cli.js +53 -10
- package/dist/src/artifacts/fortemi-shard-export.js +107 -18
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/job.js +97 -0
- package/dist/src/cli/handlers/runtime-info.js +2 -2
- package/dist/src/cli/handlers/serve.js +2 -2
- package/dist/src/cli/handlers/sessions.js +188 -0
- package/dist/src/cli/handlers/steward.js +16 -3
- package/dist/src/extensions/commands/definitions.js +30 -5
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/features/catalog.js +3 -3
- package/dist/src/jobs/executor.js +83 -0
- package/dist/src/jobs/flow.js +106 -0
- package/dist/src/jobs/gitea.js +91 -0
- package/dist/src/jobs/render.js +53 -0
- package/dist/src/jobs/runner.js +315 -0
- package/dist/src/jobs/types.js +3 -0
- package/dist/src/providers/capability-matrix.js +11 -4
- package/dist/src/providers/capability-matrix.yaml +39 -42
- package/dist/src/sessions/analytics.js +303 -0
- package/dist/src/sessions/importer.js +7 -1
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/policy.js +1 -1
- package/dist/src/sessions/repository.js +213 -0
- package/package.json +10 -10
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { sha256 } from './contracts.js';
|
|
3
|
+
export const SESSION_ANALYTICS_VERSION = '1.0.0';
|
|
4
|
+
export const SessionAnalyticsCategorySchema = z.enum([
|
|
5
|
+
'tool-call',
|
|
6
|
+
'tool-result',
|
|
7
|
+
'escalation',
|
|
8
|
+
'hitl',
|
|
9
|
+
'boundary',
|
|
10
|
+
'indicator',
|
|
11
|
+
]);
|
|
12
|
+
export const SessionAnalyticsStatusSchema = z.enum([
|
|
13
|
+
'requested',
|
|
14
|
+
'running',
|
|
15
|
+
'succeeded',
|
|
16
|
+
'failed',
|
|
17
|
+
'granted',
|
|
18
|
+
'denied',
|
|
19
|
+
'timed-out',
|
|
20
|
+
'unsupported',
|
|
21
|
+
'provider-unknown',
|
|
22
|
+
'observed',
|
|
23
|
+
]);
|
|
24
|
+
export const SessionAnalyticsFactSchema = z.object({
|
|
25
|
+
analyticsVersion: z.literal(SESSION_ANALYTICS_VERSION),
|
|
26
|
+
factId: z.string().min(1),
|
|
27
|
+
category: SessionAnalyticsCategorySchema,
|
|
28
|
+
status: SessionAnalyticsStatusSchema,
|
|
29
|
+
provider: z.string().min(1),
|
|
30
|
+
workspaceId: z.string().min(1),
|
|
31
|
+
sessionId: z.string().min(1),
|
|
32
|
+
eventId: z.string().min(1),
|
|
33
|
+
sourceId: z.string().min(1),
|
|
34
|
+
importRunId: z.string().min(1),
|
|
35
|
+
occurredAt: z.string().datetime({ offset: true }).nullable(),
|
|
36
|
+
sequence: z.number().int().nonnegative(),
|
|
37
|
+
actor: z.string().min(1).nullable(),
|
|
38
|
+
participant: z.string().min(1).nullable(),
|
|
39
|
+
toolName: z.string().min(1).nullable(),
|
|
40
|
+
toolCallId: z.string().min(1).nullable(),
|
|
41
|
+
retryGroupId: z.string().min(1).nullable(),
|
|
42
|
+
retryOrdinal: z.number().int().positive().nullable(),
|
|
43
|
+
errorClass: z.string().min(1).nullable(),
|
|
44
|
+
capability: z.string().min(1).nullable(),
|
|
45
|
+
decision: z.string().min(1).nullable(),
|
|
46
|
+
promptType: z.string().min(1).nullable(),
|
|
47
|
+
latencyMs: z.number().int().nonnegative().nullable(),
|
|
48
|
+
transition: z.string().min(1).nullable(),
|
|
49
|
+
indicator: z.string().min(1).nullable(),
|
|
50
|
+
sensitivity: z.enum(['none', 'sensitive']),
|
|
51
|
+
extractionState: z.string().min(1).nullable(),
|
|
52
|
+
sourceCitation: z.object({
|
|
53
|
+
provider: z.string().min(1),
|
|
54
|
+
sessionId: z.string().min(1),
|
|
55
|
+
eventId: z.string().min(1),
|
|
56
|
+
importRunId: z.string().min(1),
|
|
57
|
+
sourceId: z.string().min(1),
|
|
58
|
+
locatorClass: z.string().min(1),
|
|
59
|
+
sequence: z.number().int().nonnegative(),
|
|
60
|
+
}).strict(),
|
|
61
|
+
}).strict();
|
|
62
|
+
/**
|
|
63
|
+
* Derive content-free analytics facts from normalized session events.
|
|
64
|
+
*
|
|
65
|
+
* Historical text is deliberately excluded. Classification uses the canonical
|
|
66
|
+
* event shape and already-sanitized native extension metadata only; commands,
|
|
67
|
+
* URLs, and provider payloads are never executed or copied into the index.
|
|
68
|
+
*/
|
|
69
|
+
export function deriveSessionAnalytics(session, events) {
|
|
70
|
+
const ordered = [...events].sort((left, right) => left.sequence - right.sequence || left.eventId.localeCompare(right.eventId));
|
|
71
|
+
const facts = [];
|
|
72
|
+
const retries = new Map();
|
|
73
|
+
const calls = new Map();
|
|
74
|
+
for (const event of ordered) {
|
|
75
|
+
const metadata = flattenMetadata(event.extensions);
|
|
76
|
+
const normalizedKind = event.kind.toLowerCase();
|
|
77
|
+
const status = classifyStatus(normalizedKind, metadata);
|
|
78
|
+
const common = baseFact(session, event);
|
|
79
|
+
if (isToolCall(event, normalizedKind)) {
|
|
80
|
+
const retryKey = `${event.toolName ?? 'unknown'}\0${metadata.input_hash ?? metadata.arguments_digest ?? ''}`;
|
|
81
|
+
const prior = retries.get(retryKey);
|
|
82
|
+
const sameGroup = prior && event.sequence - prior.lastSequence <= 4;
|
|
83
|
+
const retry = sameGroup
|
|
84
|
+
? { ...prior, count: prior.count + 1, lastSequence: event.sequence }
|
|
85
|
+
: {
|
|
86
|
+
groupId: sha256(['session-tool-retry', session.sessionId, retryKey, event.sequence].join('\0')),
|
|
87
|
+
count: 1,
|
|
88
|
+
lastSequence: event.sequence,
|
|
89
|
+
};
|
|
90
|
+
retries.set(retryKey, retry);
|
|
91
|
+
if (event.toolCallId) {
|
|
92
|
+
calls.set(event.toolCallId, { occurredAt: event.occurredAt, sequence: event.sequence });
|
|
93
|
+
}
|
|
94
|
+
facts.push({
|
|
95
|
+
...common,
|
|
96
|
+
category: 'tool-call',
|
|
97
|
+
status: status === 'failed' ? 'failed' : 'requested',
|
|
98
|
+
retryGroupId: retry.groupId,
|
|
99
|
+
retryOrdinal: retry.count,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (normalizedKind.includes('tool-result')) {
|
|
103
|
+
const call = event.toolCallId ? calls.get(event.toolCallId) : undefined;
|
|
104
|
+
facts.push({
|
|
105
|
+
...common,
|
|
106
|
+
category: 'tool-result',
|
|
107
|
+
status: status === 'failed' ? 'failed' : 'succeeded',
|
|
108
|
+
latencyMs: elapsedMs(call?.occurredAt, event.occurredAt),
|
|
109
|
+
errorClass: status === 'failed'
|
|
110
|
+
? (metadata.error_class ?? metadata.error_code ?? 'provider-error')
|
|
111
|
+
: null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (isEscalation(normalizedKind, metadata)) {
|
|
115
|
+
const decision = escalationDecision(status, metadata);
|
|
116
|
+
facts.push({
|
|
117
|
+
...common,
|
|
118
|
+
category: 'escalation',
|
|
119
|
+
status: decision,
|
|
120
|
+
capability: metadata.capability ?? metadata.permission ?? metadata.scope ?? 'provider-unknown',
|
|
121
|
+
decision,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (isHitl(normalizedKind, metadata)) {
|
|
125
|
+
facts.push({
|
|
126
|
+
...common,
|
|
127
|
+
category: 'hitl',
|
|
128
|
+
status,
|
|
129
|
+
promptType: metadata.prompt_type ?? metadata.input_type ?? 'provider-unknown',
|
|
130
|
+
transition: metadata.transition ?? metadata.task_state ?? metadata.session_state ?? null,
|
|
131
|
+
latencyMs: integerMetadata(metadata.latency_ms),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (event.activityBoundary) {
|
|
135
|
+
facts.push({
|
|
136
|
+
...common,
|
|
137
|
+
category: 'boundary',
|
|
138
|
+
status: 'observed',
|
|
139
|
+
transition: event.activityBoundary,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const indicators = eventIndicators(event, normalizedKind, status, metadata);
|
|
143
|
+
for (const indicator of indicators) {
|
|
144
|
+
facts.push({
|
|
145
|
+
...common,
|
|
146
|
+
category: 'indicator',
|
|
147
|
+
status: 'observed',
|
|
148
|
+
indicator,
|
|
149
|
+
errorClass: status === 'failed'
|
|
150
|
+
? (metadata.error_class ?? metadata.error_code ?? 'provider-error')
|
|
151
|
+
: null,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const retry of retries.values()) {
|
|
156
|
+
if (retry.count < 3)
|
|
157
|
+
continue;
|
|
158
|
+
const anchor = facts.find((fact) => fact.retryGroupId === retry.groupId);
|
|
159
|
+
if (!anchor)
|
|
160
|
+
continue;
|
|
161
|
+
facts.push({
|
|
162
|
+
...anchor,
|
|
163
|
+
category: 'indicator',
|
|
164
|
+
status: 'observed',
|
|
165
|
+
indicator: 'tool-quota-pressure',
|
|
166
|
+
factId: undefined,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return facts.map((fact) => SessionAnalyticsFactSchema.parse({
|
|
170
|
+
...fact,
|
|
171
|
+
factId: sha256([
|
|
172
|
+
'session-analytics',
|
|
173
|
+
SESSION_ANALYTICS_VERSION,
|
|
174
|
+
fact.category,
|
|
175
|
+
fact.eventId,
|
|
176
|
+
fact.indicator ?? '',
|
|
177
|
+
].join('\0')),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
function baseFact(session, event) {
|
|
181
|
+
return {
|
|
182
|
+
analyticsVersion: SESSION_ANALYTICS_VERSION,
|
|
183
|
+
category: 'indicator',
|
|
184
|
+
status: 'observed',
|
|
185
|
+
provider: session.provider,
|
|
186
|
+
workspaceId: session.workspaceId,
|
|
187
|
+
sessionId: session.sessionId,
|
|
188
|
+
eventId: event.eventId,
|
|
189
|
+
sourceId: event.sourceId,
|
|
190
|
+
importRunId: event.importRunId,
|
|
191
|
+
occurredAt: event.occurredAt,
|
|
192
|
+
sequence: event.sequence,
|
|
193
|
+
actor: event.role,
|
|
194
|
+
participant: event.participant,
|
|
195
|
+
toolName: event.toolName,
|
|
196
|
+
toolCallId: event.toolCallId,
|
|
197
|
+
retryGroupId: null,
|
|
198
|
+
retryOrdinal: null,
|
|
199
|
+
errorClass: null,
|
|
200
|
+
capability: null,
|
|
201
|
+
decision: null,
|
|
202
|
+
promptType: null,
|
|
203
|
+
latencyMs: null,
|
|
204
|
+
transition: null,
|
|
205
|
+
indicator: null,
|
|
206
|
+
sensitivity: event.sensitivity.classification,
|
|
207
|
+
extractionState: event.extractionState,
|
|
208
|
+
sourceCitation: {
|
|
209
|
+
provider: session.provider,
|
|
210
|
+
sessionId: session.sessionId,
|
|
211
|
+
eventId: event.eventId,
|
|
212
|
+
importRunId: event.importRunId,
|
|
213
|
+
sourceId: event.sourceId,
|
|
214
|
+
locatorClass: event.rawReference.locatorClass,
|
|
215
|
+
sequence: event.sequence,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function flattenMetadata(value) {
|
|
220
|
+
const output = {};
|
|
221
|
+
const visit = (input, depth) => {
|
|
222
|
+
if (depth > 3 || !input || typeof input !== 'object' || Array.isArray(input))
|
|
223
|
+
return;
|
|
224
|
+
for (const [key, child] of Object.entries(input)) {
|
|
225
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]+/g, '_');
|
|
226
|
+
if (typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean') {
|
|
227
|
+
if (!(normalized in output))
|
|
228
|
+
output[normalized] = String(child).slice(0, 160);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
visit(child, depth + 1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
visit(value, 0);
|
|
236
|
+
return output;
|
|
237
|
+
}
|
|
238
|
+
function classifyStatus(kind, metadata) {
|
|
239
|
+
const value = `${kind} ${metadata.status ?? ''} ${metadata.decision ?? ''}`.toLowerCase();
|
|
240
|
+
if (/(timeout|timed.out)/.test(value))
|
|
241
|
+
return 'timed-out';
|
|
242
|
+
if (/(unsupported|unavailable)/.test(value))
|
|
243
|
+
return 'unsupported';
|
|
244
|
+
if (/(deny|denied|reject|rejected)/.test(value))
|
|
245
|
+
return 'denied';
|
|
246
|
+
if (/(grant|granted|approve|approved|allow|allowed)/.test(value))
|
|
247
|
+
return 'granted';
|
|
248
|
+
if (/(fail|failed|error|errored)/.test(value))
|
|
249
|
+
return 'failed';
|
|
250
|
+
if (/(success|succeeded|complete|completed)/.test(value))
|
|
251
|
+
return 'succeeded';
|
|
252
|
+
if (/(request|requested|input.required|prompt)/.test(value))
|
|
253
|
+
return 'requested';
|
|
254
|
+
if (/(running|started|pending)/.test(value))
|
|
255
|
+
return 'running';
|
|
256
|
+
return 'provider-unknown';
|
|
257
|
+
}
|
|
258
|
+
function isToolCall(event, kind) {
|
|
259
|
+
return kind.includes('tool-call') || kind.includes('tool_use')
|
|
260
|
+
|| Boolean(event.toolName && !kind.includes('tool-result'));
|
|
261
|
+
}
|
|
262
|
+
function isEscalation(kind, metadata) {
|
|
263
|
+
return /(escalat|permission|approval|sandbox|capability.required)/.test(kind)
|
|
264
|
+
|| ['permission', 'capability', 'approval', 'scope'].some((key) => key in metadata);
|
|
265
|
+
}
|
|
266
|
+
function isHitl(kind, metadata) {
|
|
267
|
+
return /(hitl|human|input.required|input_required|operator.prompt)/.test(kind)
|
|
268
|
+
|| ['prompt_type', 'input_type', 'reviewer'].some((key) => key in metadata);
|
|
269
|
+
}
|
|
270
|
+
function escalationDecision(status, metadata) {
|
|
271
|
+
if (['granted', 'denied', 'timed-out', 'unsupported'].includes(status))
|
|
272
|
+
return status;
|
|
273
|
+
if (metadata.decision)
|
|
274
|
+
return classifyStatus('', { decision: metadata.decision });
|
|
275
|
+
return status === 'requested' ? 'requested' : 'provider-unknown';
|
|
276
|
+
}
|
|
277
|
+
function eventIndicators(event, kind, status, metadata) {
|
|
278
|
+
const indicators = new Set();
|
|
279
|
+
if (event.opaque || /(unknown|malformed|unsupported)/.test(kind)) {
|
|
280
|
+
indicators.add('provider-schema-drift');
|
|
281
|
+
}
|
|
282
|
+
if (status === 'failed')
|
|
283
|
+
indicators.add('failed-operation');
|
|
284
|
+
if (event.sensitivity.classification === 'sensitive')
|
|
285
|
+
indicators.add('sensitive-field-redaction');
|
|
286
|
+
if (metadata.redaction_hit === 'true' || metadata.redacted === 'true') {
|
|
287
|
+
indicators.add('sensitive-field-redaction');
|
|
288
|
+
}
|
|
289
|
+
return [...indicators].sort();
|
|
290
|
+
}
|
|
291
|
+
function elapsedMs(start, end) {
|
|
292
|
+
if (!start || !end)
|
|
293
|
+
return null;
|
|
294
|
+
const value = Date.parse(end) - Date.parse(start);
|
|
295
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
296
|
+
}
|
|
297
|
+
function integerMetadata(value) {
|
|
298
|
+
if (!value || !/^\d+$/.test(value))
|
|
299
|
+
return null;
|
|
300
|
+
const parsed = Number(value);
|
|
301
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -237,7 +237,7 @@ function normalizeBatch(request, records, importRunId, sourceObservedAt) {
|
|
|
237
237
|
? 'sensitive' : 'none',
|
|
238
238
|
classes: [...new Set([...redacted.classes, ...native.classes])].sort(),
|
|
239
239
|
},
|
|
240
|
-
opaque: !
|
|
240
|
+
opaque: !isKnownEventKind(record.kind),
|
|
241
241
|
extensions,
|
|
242
242
|
});
|
|
243
243
|
if (!sessions.has(sessionId)) {
|
|
@@ -281,6 +281,12 @@ function normalizeBatch(request, records, importRunId, sourceObservedAt) {
|
|
|
281
281
|
return { sessions: [...sessions.values()], events };
|
|
282
282
|
}
|
|
283
283
|
const KNOWN_EVENT_KINDS = new Set(['message', 'tool-call', 'tool-result', 'artifact', 'attachment', 'summary']);
|
|
284
|
+
function isKnownEventKind(kind) {
|
|
285
|
+
const normalized = kind.toLowerCase();
|
|
286
|
+
return KNOWN_EVENT_KINDS.has(normalized)
|
|
287
|
+
|| /^(?:tool-call|tool-result)(?:[._-]|$)/.test(normalized)
|
|
288
|
+
|| /^(?:sandbox|permission|approval|hitl|human|lifecycle)(?:[._-]|$)/.test(normalized);
|
|
289
|
+
}
|
|
284
290
|
function earlierTimestamp(left, right) {
|
|
285
291
|
if (!left)
|
|
286
292
|
return right;
|
|
@@ -16,6 +16,7 @@ export * from './batch-import.js';
|
|
|
16
16
|
export * from './workspace-discovery.js';
|
|
17
17
|
export * from './timeline.js';
|
|
18
18
|
export * from './origin.js';
|
|
19
|
+
export * from './analytics.js';
|
|
19
20
|
export * from './adapters/generic.js';
|
|
20
21
|
export * from './adapters/claude.js';
|
|
21
22
|
export * from './adapters/codex.js';
|
|
@@ -70,7 +70,7 @@ const SECRET_PATTERNS = [
|
|
|
70
70
|
const SENSITIVE_KEY = /(?:authorization|auth[_-]?header|cookie|credential|password|passwd|secret|token|api[_-]?key|private[_-]?key|connection[_-]?string)/i;
|
|
71
71
|
const CONTENT_KEY = /(?:^|[_-])(?:text|content|prompt|command|arguments?|args|result|output|body|request|response|source|code|query)(?:$|[_-])/i;
|
|
72
72
|
const PATH_KEY = /(?:^|[_-])(?:path|cwd|directory|filename|file)(?:$|[_-])/i;
|
|
73
|
-
const SAFE_STRING_KEY = /(?:^|[_-])(?:id|kind|type|role|status|state|lifecycle|reason|name|product|provider|version|schema|model|format|class|mode|event|operation|tool|method|language|scope|consistency|disposition|phase|visibility|protocol|category|classification)(?:$|[_-])/i;
|
|
73
|
+
const SAFE_STRING_KEY = /(?:^|[_-])(?:id|kind|type|role|status|state|lifecycle|reason|name|product|provider|version|schema|model|format|class|mode|event|operation|tool|method|language|scope|consistency|disposition|phase|visibility|protocol|category|classification|decision|capability|permission|transition|hash|digest)(?:$|[_-])/i;
|
|
74
74
|
const CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
75
75
|
const DEFAULT_NATIVE_LIMITS = {
|
|
76
76
|
maxDepth: 8,
|
|
@@ -2,6 +2,7 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { CandidateReviewReceiptSchema, DeletionReceiptSchema, IntelligenceCandidateSchema, PromotionDependencyDecisionSchema, PromotionReceiptSchema, SessionContractError, sha256, } from './contracts.js';
|
|
3
3
|
import { coverageFromBatchRun, } from './batch-contracts.js';
|
|
4
4
|
import { redactSessionText, sanitizeNativeExtensions } from './policy.js';
|
|
5
|
+
import { deriveSessionAnalytics, } from './analytics.js';
|
|
5
6
|
const require = createRequire(import.meta.url);
|
|
6
7
|
const POLICY_PROVIDER_MIGRATION = 'policy-provider-identity:v2';
|
|
7
8
|
const EVENT_ORIGIN_INTENT_MIGRATION = 'event-origin-intent:v1';
|
|
@@ -103,10 +104,23 @@ export class SessionRepository {
|
|
|
103
104
|
CREATE TABLE IF NOT EXISTS session_catalog_meta (
|
|
104
105
|
key TEXT PRIMARY KEY, value TEXT NOT NULL
|
|
105
106
|
);
|
|
107
|
+
CREATE TABLE IF NOT EXISTS session_analytics_facts (
|
|
108
|
+
fact_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL,
|
|
109
|
+
session_id TEXT NOT NULL, event_id TEXT NOT NULL,
|
|
110
|
+
category TEXT NOT NULL, status TEXT NOT NULL,
|
|
111
|
+
provider TEXT NOT NULL, tool_name TEXT, occurred_at TEXT,
|
|
112
|
+
data TEXT NOT NULL,
|
|
113
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
|
|
114
|
+
FOREIGN KEY(event_id) REFERENCES session_events(event_id) ON DELETE CASCADE
|
|
115
|
+
);
|
|
106
116
|
CREATE INDEX IF NOT EXISTS idx_session_workspace_provider
|
|
107
117
|
ON sessions(workspace_id, source_id, lifecycle);
|
|
108
118
|
CREATE INDEX IF NOT EXISTS idx_event_session_sequence
|
|
109
119
|
ON session_events(session_id, sequence_no);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_session_analytics_scope
|
|
121
|
+
ON session_analytics_facts(
|
|
122
|
+
workspace_id, category, provider, status, occurred_at, fact_id
|
|
123
|
+
);
|
|
110
124
|
CREATE VIRTUAL TABLE IF NOT EXISTS session_event_fts USING fts5(
|
|
111
125
|
event_id UNINDEXED, searchable_text, tokenize='unicode61'
|
|
112
126
|
);
|
|
@@ -139,6 +153,23 @@ export class SessionRepository {
|
|
|
139
153
|
this.alignSessionFtsRowids();
|
|
140
154
|
this.migrateSessionPolicyAndProviderIdentity();
|
|
141
155
|
this.migrateEventOriginAndIntent();
|
|
156
|
+
this.alignSessionAnalytics();
|
|
157
|
+
}
|
|
158
|
+
alignSessionAnalytics() {
|
|
159
|
+
const eventCount = Number(this.db.prepare(`SELECT COUNT(*) AS count
|
|
160
|
+
FROM session_events e
|
|
161
|
+
JOIN import_runs r ON r.import_run_id=e.import_run_id
|
|
162
|
+
JOIN sessions s ON s.session_id=e.session_id
|
|
163
|
+
WHERE r.status='committed' AND s.lifecycle!='tombstoned'`).get()?.count ?? 0);
|
|
164
|
+
const factSessions = Number(this.db.prepare('SELECT COUNT(DISTINCT session_id) AS count FROM session_analytics_facts').get()?.count ?? 0);
|
|
165
|
+
const sourceSessions = Number(this.db.prepare(`SELECT COUNT(DISTINCT e.session_id) AS count
|
|
166
|
+
FROM session_events e
|
|
167
|
+
JOIN import_runs r ON r.import_run_id=e.import_run_id
|
|
168
|
+
JOIN sessions s ON s.session_id=e.session_id
|
|
169
|
+
WHERE r.status='committed' AND s.lifecycle!='tombstoned'`).get()?.count ?? 0);
|
|
170
|
+
if (eventCount === 0 || factSessions === sourceSessions)
|
|
171
|
+
return;
|
|
172
|
+
this.rebuildAnalytics();
|
|
142
173
|
}
|
|
143
174
|
alignSessionFtsRowids() {
|
|
144
175
|
const eventCount = Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_events').get()?.count ?? 0);
|
|
@@ -470,6 +501,11 @@ export class SessionRepository {
|
|
|
470
501
|
}
|
|
471
502
|
const outcome = publish ? 'committed' : 'staged';
|
|
472
503
|
this.db.prepare(`UPDATE import_runs SET status=?, checkpoint=? WHERE import_run_id=?`).run(outcome, JSON.stringify(checkpoint), batch.run.importRunId);
|
|
504
|
+
if (publish) {
|
|
505
|
+
for (const session of batch.sessions) {
|
|
506
|
+
this.rebuildAnalytics(session.workspaceId, session.sessionId);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
473
509
|
const counts = { sessions: sessionsInserted, events: eventsInserted };
|
|
474
510
|
this.db.prepare(`INSERT INTO import_receipts(operation_id, import_run_id, outcome, counts, checkpoint)
|
|
475
511
|
VALUES (?, ?, ?, ?, ?)`).run(batch.run.importRunId, batch.run.importRunId, outcome, JSON.stringify(counts), JSON.stringify(checkpoint));
|
|
@@ -525,6 +561,8 @@ export class SessionRepository {
|
|
|
525
561
|
this.db.prepare(`UPDATE mutation_audit SET outcome='committed', occurred_at=?, data=?
|
|
526
562
|
WHERE operation_id=?`).run(updated.observedAt, JSON.stringify(updated), operationId);
|
|
527
563
|
}
|
|
564
|
+
if (result.changes > 0)
|
|
565
|
+
this.rebuildAnalytics();
|
|
528
566
|
return result.changes;
|
|
529
567
|
});
|
|
530
568
|
return commit();
|
|
@@ -561,6 +599,8 @@ export class SessionRepository {
|
|
|
561
599
|
this.db.prepare(`UPDATE mutation_audit SET outcome='committed', occurred_at=?, data=?
|
|
562
600
|
WHERE operation_id=?`).run(updated.observedAt, JSON.stringify(updated), operationId);
|
|
563
601
|
}
|
|
602
|
+
if (result.changes > 0)
|
|
603
|
+
this.rebuildAnalytics();
|
|
564
604
|
return result.changes;
|
|
565
605
|
});
|
|
566
606
|
return commit();
|
|
@@ -876,6 +916,163 @@ export class SessionRepository {
|
|
|
876
916
|
snapshotRowid,
|
|
877
917
|
};
|
|
878
918
|
}
|
|
919
|
+
rebuildAnalytics(workspaceId, sessionId) {
|
|
920
|
+
const where = [
|
|
921
|
+
`r.status='committed'`,
|
|
922
|
+
`s.lifecycle!='tombstoned'`,
|
|
923
|
+
];
|
|
924
|
+
const params = [];
|
|
925
|
+
if (workspaceId) {
|
|
926
|
+
where.push('s.workspace_id=?');
|
|
927
|
+
params.push(workspaceId);
|
|
928
|
+
}
|
|
929
|
+
if (sessionId) {
|
|
930
|
+
where.push('s.session_id=?');
|
|
931
|
+
params.push(sessionId);
|
|
932
|
+
}
|
|
933
|
+
const sessions = this.db.prepare(`SELECT s.session_id, s.data
|
|
934
|
+
FROM sessions s
|
|
935
|
+
WHERE s.lifecycle!='tombstoned'
|
|
936
|
+
${workspaceId ? 'AND s.workspace_id=?' : ''}
|
|
937
|
+
${sessionId ? 'AND s.session_id=?' : ''}
|
|
938
|
+
AND EXISTS (
|
|
939
|
+
SELECT 1 FROM session_events e
|
|
940
|
+
JOIN import_runs r ON r.import_run_id=e.import_run_id
|
|
941
|
+
WHERE e.session_id=s.session_id AND r.status='committed'
|
|
942
|
+
)
|
|
943
|
+
ORDER BY s.session_id`).all(...[
|
|
944
|
+
...(workspaceId ? [workspaceId] : []),
|
|
945
|
+
...(sessionId ? [sessionId] : []),
|
|
946
|
+
]);
|
|
947
|
+
if (!workspaceId && !sessionId) {
|
|
948
|
+
this.db.exec('DELETE FROM session_analytics_facts');
|
|
949
|
+
}
|
|
950
|
+
else if (sessionId) {
|
|
951
|
+
this.db.prepare('DELETE FROM session_analytics_facts WHERE session_id=?').run(sessionId);
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
this.db.prepare('DELETE FROM session_analytics_facts WHERE workspace_id=?').run(workspaceId);
|
|
955
|
+
}
|
|
956
|
+
const eventQuery = this.db.prepare(`SELECT e.data
|
|
957
|
+
FROM session_events e
|
|
958
|
+
JOIN import_runs r ON r.import_run_id=e.import_run_id
|
|
959
|
+
JOIN sessions s ON s.session_id=e.session_id
|
|
960
|
+
WHERE e.session_id=? AND ${where.join(' AND ')}
|
|
961
|
+
ORDER BY e.sequence_no, e.event_id`);
|
|
962
|
+
const insert = this.db.prepare(`INSERT OR REPLACE INTO session_analytics_facts(
|
|
963
|
+
fact_id, workspace_id, session_id, event_id, category, status,
|
|
964
|
+
provider, tool_name, occurred_at, data
|
|
965
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
966
|
+
let inserted = 0;
|
|
967
|
+
for (const row of sessions) {
|
|
968
|
+
const session = JSON.parse(String(row.data));
|
|
969
|
+
const events = eventQuery.all(session.sessionId, ...params)
|
|
970
|
+
.map((eventRow) => JSON.parse(String(eventRow.data)));
|
|
971
|
+
for (const fact of deriveSessionAnalytics(session, events)) {
|
|
972
|
+
inserted += insert.run(fact.factId, fact.workspaceId, fact.sessionId, fact.eventId, fact.category, fact.status, fact.provider, fact.toolName, fact.occurredAt, JSON.stringify(fact)).changes;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return inserted;
|
|
976
|
+
}
|
|
977
|
+
listAnalyticsFacts(options) {
|
|
978
|
+
const where = [
|
|
979
|
+
'f.workspace_id=?',
|
|
980
|
+
`s.lifecycle!='tombstoned'`,
|
|
981
|
+
`r.status='committed'`,
|
|
982
|
+
];
|
|
983
|
+
const params = [options.workspaceId];
|
|
984
|
+
if (options.categories?.length) {
|
|
985
|
+
where.push(`f.category IN (${options.categories.map(() => '?').join(', ')})`);
|
|
986
|
+
params.push(...options.categories);
|
|
987
|
+
}
|
|
988
|
+
const scalar = [
|
|
989
|
+
['f.provider', options.provider],
|
|
990
|
+
['f.session_id', options.sessionId],
|
|
991
|
+
['f.tool_name', options.tool],
|
|
992
|
+
['f.status', options.status],
|
|
993
|
+
[`json_extract(f.data, '$.actor')`, options.actor],
|
|
994
|
+
[`json_extract(f.data, '$.sensitivity')`, options.sensitivity],
|
|
995
|
+
[`json_extract(f.data, '$.extractionState')`, options.extractionState],
|
|
996
|
+
];
|
|
997
|
+
for (const [column, value] of scalar) {
|
|
998
|
+
if (value === undefined)
|
|
999
|
+
continue;
|
|
1000
|
+
where.push(`${column}=?`);
|
|
1001
|
+
params.push(value);
|
|
1002
|
+
}
|
|
1003
|
+
if (options.dateFrom) {
|
|
1004
|
+
where.push('f.occurred_at>=?');
|
|
1005
|
+
params.push(options.dateFrom);
|
|
1006
|
+
}
|
|
1007
|
+
if (options.dateTo) {
|
|
1008
|
+
where.push('f.occurred_at<=?');
|
|
1009
|
+
params.push(options.dateTo);
|
|
1010
|
+
}
|
|
1011
|
+
if (options.tag) {
|
|
1012
|
+
where.push(`EXISTS (
|
|
1013
|
+
SELECT 1 FROM session_tags t
|
|
1014
|
+
WHERE t.session_id=f.session_id AND t.tag=?
|
|
1015
|
+
)`);
|
|
1016
|
+
params.push(options.tag);
|
|
1017
|
+
}
|
|
1018
|
+
const limit = options.limit ?? 500;
|
|
1019
|
+
if (limit < 1 || limit > 5_000) {
|
|
1020
|
+
throw new SessionContractError('RESOURCE_LIMIT_EXCEEDED', 'analytics limit must be between 1 and 5000');
|
|
1021
|
+
}
|
|
1022
|
+
return this.db.prepare(`SELECT f.data
|
|
1023
|
+
FROM session_analytics_facts f
|
|
1024
|
+
JOIN sessions s ON s.session_id=f.session_id
|
|
1025
|
+
JOIN session_events e ON e.event_id=f.event_id
|
|
1026
|
+
JOIN import_runs r ON r.import_run_id=e.import_run_id
|
|
1027
|
+
WHERE ${where.join(' AND ')}
|
|
1028
|
+
ORDER BY
|
|
1029
|
+
CASE WHEN f.occurred_at IS NULL THEN 1 ELSE 0 END,
|
|
1030
|
+
f.occurred_at, f.session_id,
|
|
1031
|
+
json_extract(f.data, '$.sequence'), f.fact_id
|
|
1032
|
+
LIMIT ?`).all(...params, limit)
|
|
1033
|
+
.map((row) => JSON.parse(String(row.data)));
|
|
1034
|
+
}
|
|
1035
|
+
analyticsSummary(options) {
|
|
1036
|
+
const facts = this.listAnalyticsFacts({ ...options, limit: options.limit ?? 5_000 });
|
|
1037
|
+
const byCategory = countBy(facts, (fact) => fact.category);
|
|
1038
|
+
const byStatus = countBy(facts, (fact) => fact.status);
|
|
1039
|
+
const byProvider = countBy(facts, (fact) => fact.provider);
|
|
1040
|
+
const byTool = countBy(facts.filter((fact) => fact.toolName), (fact) => fact.toolName);
|
|
1041
|
+
return {
|
|
1042
|
+
analyticsVersion: '1.0.0',
|
|
1043
|
+
workspaceId: options.workspaceId,
|
|
1044
|
+
totals: {
|
|
1045
|
+
facts: facts.length,
|
|
1046
|
+
sessions: new Set(facts.map((fact) => fact.sessionId)).size,
|
|
1047
|
+
toolCalls: facts.filter((fact) => fact.category === 'tool-call').length,
|
|
1048
|
+
toolFailures: facts.filter((fact) => fact.category === 'tool-result' && fact.status === 'failed').length,
|
|
1049
|
+
escalations: facts.filter((fact) => fact.category === 'escalation').length,
|
|
1050
|
+
hitlDecisions: facts.filter((fact) => fact.category === 'hitl').length,
|
|
1051
|
+
indicators: facts.filter((fact) => fact.category === 'indicator').length,
|
|
1052
|
+
retryGroups: new Set(facts.map((fact) => fact.retryGroupId).filter(Boolean)).size,
|
|
1053
|
+
},
|
|
1054
|
+
byCategory,
|
|
1055
|
+
byStatus,
|
|
1056
|
+
byProvider,
|
|
1057
|
+
byTool,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
getAnalyticsEvidence(id, workspaceId) {
|
|
1061
|
+
const row = this.db.prepare(`SELECT f.data AS fact_data, e.data AS event_data
|
|
1062
|
+
FROM session_analytics_facts f
|
|
1063
|
+
JOIN sessions s ON s.session_id=f.session_id
|
|
1064
|
+
JOIN session_events e ON e.event_id=f.event_id
|
|
1065
|
+
JOIN import_runs r ON r.import_run_id=e.import_run_id
|
|
1066
|
+
WHERE (f.fact_id=? OR f.event_id=?)
|
|
1067
|
+
AND f.workspace_id=? AND s.lifecycle!='tombstoned' AND r.status='committed'
|
|
1068
|
+
ORDER BY f.fact_id LIMIT 1`).get(id, id, workspaceId);
|
|
1069
|
+
return row
|
|
1070
|
+
? {
|
|
1071
|
+
fact: JSON.parse(String(row.fact_data)),
|
|
1072
|
+
event: JSON.parse(String(row.event_data)),
|
|
1073
|
+
}
|
|
1074
|
+
: { fact: null, event: null };
|
|
1075
|
+
}
|
|
879
1076
|
saveCandidates(candidates) {
|
|
880
1077
|
const save = this.db.transaction(() => {
|
|
881
1078
|
let inserted = 0;
|
|
@@ -1161,6 +1358,9 @@ export class SessionRepository {
|
|
|
1161
1358
|
counts: { sessions: 1 },
|
|
1162
1359
|
});
|
|
1163
1360
|
}
|
|
1361
|
+
if (changed) {
|
|
1362
|
+
this.db.prepare('DELETE FROM session_analytics_facts WHERE session_id=?').run(sessionId);
|
|
1363
|
+
}
|
|
1164
1364
|
return changed;
|
|
1165
1365
|
});
|
|
1166
1366
|
return tombstone();
|
|
@@ -1187,6 +1387,8 @@ export class SessionRepository {
|
|
|
1187
1387
|
counts: { sessions: 1 },
|
|
1188
1388
|
});
|
|
1189
1389
|
}
|
|
1390
|
+
if (changed)
|
|
1391
|
+
this.rebuildAnalytics(workspaceId, sessionId);
|
|
1190
1392
|
return changed;
|
|
1191
1393
|
});
|
|
1192
1394
|
return restore();
|
|
@@ -1385,6 +1587,7 @@ export class SessionRepository {
|
|
|
1385
1587
|
dependencyDispositions: validatedDecisions.length,
|
|
1386
1588
|
},
|
|
1387
1589
|
});
|
|
1590
|
+
this.db.prepare('DELETE FROM session_analytics_facts WHERE session_id=?').run(input.preview.sessionId);
|
|
1388
1591
|
return receipt;
|
|
1389
1592
|
});
|
|
1390
1593
|
return apply();
|
|
@@ -1400,6 +1603,7 @@ export class SessionRepository {
|
|
|
1400
1603
|
SELECT rowid, event_id, json_extract(data, '$.searchableText') FROM session_events
|
|
1401
1604
|
`);
|
|
1402
1605
|
indexed = Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_events').get()?.count ?? 0);
|
|
1606
|
+
this.rebuildAnalytics();
|
|
1403
1607
|
}
|
|
1404
1608
|
else {
|
|
1405
1609
|
this.db.prepare(`DELETE FROM session_event_fts WHERE rowid IN (
|
|
@@ -1412,6 +1616,7 @@ export class SessionRepository {
|
|
|
1412
1616
|
FROM session_events e
|
|
1413
1617
|
JOIN sessions s ON s.session_id=e.session_id
|
|
1414
1618
|
WHERE s.workspace_id=?`).run(workspaceId).changes;
|
|
1619
|
+
this.rebuildAnalytics(workspaceId);
|
|
1415
1620
|
}
|
|
1416
1621
|
this.emitRepositoryMutation({
|
|
1417
1622
|
operationId: sha256([
|
|
@@ -1506,6 +1711,14 @@ function canonicalDevinLocator(value) {
|
|
|
1506
1711
|
? `devin-desktop-${value.slice('windsurf-'.length)}`
|
|
1507
1712
|
: value;
|
|
1508
1713
|
}
|
|
1714
|
+
function countBy(items, key) {
|
|
1715
|
+
const counts = {};
|
|
1716
|
+
for (const item of items) {
|
|
1717
|
+
const value = key(item);
|
|
1718
|
+
counts[value] = (counts[value] ?? 0) + 1;
|
|
1719
|
+
}
|
|
1720
|
+
return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)));
|
|
1721
|
+
}
|
|
1509
1722
|
function encodeMutationCursor(rowid, workspaceId) {
|
|
1510
1723
|
const unsigned = { rowid, workspaceId };
|
|
1511
1724
|
return Buffer.from(JSON.stringify({
|