@pcircle/footprint 1.5.0 → 1.7.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 +301 -132
- package/SKILL.md +72 -28
- package/bin/footprint.js +16 -0
- package/dist/src/adapters/claude.d.ts +2 -0
- package/dist/src/adapters/claude.d.ts.map +1 -0
- package/dist/src/adapters/claude.js +7 -0
- package/dist/src/adapters/claude.js.map +1 -0
- package/dist/src/adapters/codex.d.ts +2 -0
- package/dist/src/adapters/codex.d.ts.map +1 -0
- package/dist/src/adapters/codex.js +7 -0
- package/dist/src/adapters/codex.js.map +1 -0
- package/dist/src/adapters/gemini.d.ts +2 -0
- package/dist/src/adapters/gemini.d.ts.map +1 -0
- package/dist/src/adapters/gemini.js +7 -0
- package/dist/src/adapters/gemini.js.map +1 -0
- package/dist/src/adapters/index.d.ts +5 -0
- package/dist/src/adapters/index.d.ts.map +1 -0
- package/dist/src/adapters/index.js +12 -0
- package/dist/src/adapters/index.js.map +1 -0
- package/dist/src/adapters/structured-prefix.d.ts +10 -0
- package/dist/src/adapters/structured-prefix.d.ts.map +1 -0
- package/dist/src/adapters/structured-prefix.js +59 -0
- package/dist/src/adapters/structured-prefix.js.map +1 -0
- package/dist/src/adapters/types.d.ts +32 -0
- package/dist/src/adapters/types.d.ts.map +1 -0
- package/dist/src/adapters/types.js +2 -0
- package/dist/src/adapters/types.js.map +1 -0
- package/dist/src/cli/context-flow.d.ts +92 -0
- package/dist/src/cli/context-flow.d.ts.map +1 -0
- package/dist/src/cli/context-flow.js +724 -0
- package/dist/src/cli/context-flow.js.map +1 -0
- package/dist/src/cli/history-display.d.ts +27 -0
- package/dist/src/cli/history-display.d.ts.map +1 -0
- package/dist/src/cli/history-display.js +167 -0
- package/dist/src/cli/history-display.js.map +1 -0
- package/dist/src/cli/index.js +924 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/launch-spec.d.ts +31 -0
- package/dist/src/cli/launch-spec.d.ts.map +1 -0
- package/dist/src/cli/launch-spec.js +182 -0
- package/dist/src/cli/launch-spec.js.map +1 -0
- package/dist/src/cli/live-demo.d.ts +34 -0
- package/dist/src/cli/live-demo.d.ts.map +1 -0
- package/dist/src/cli/live-demo.js +254 -0
- package/dist/src/cli/live-demo.js.map +1 -0
- package/dist/src/cli/pty-transcript.d.ts +34 -0
- package/dist/src/cli/pty-transcript.d.ts.map +1 -0
- package/dist/src/cli/pty-transcript.js +174 -0
- package/dist/src/cli/pty-transcript.js.map +1 -0
- package/dist/src/cli/session-display.d.ts +74 -0
- package/dist/src/cli/session-display.d.ts.map +1 -0
- package/dist/src/cli/session-display.js +922 -0
- package/dist/src/cli/session-display.js.map +1 -0
- package/dist/src/cli/session-execution.d.ts +55 -0
- package/dist/src/cli/session-execution.d.ts.map +1 -0
- package/dist/src/cli/session-execution.js +817 -0
- package/dist/src/cli/session-execution.js.map +1 -0
- package/dist/src/cli/session-runtime.d.ts +5 -0
- package/dist/src/cli/session-runtime.d.ts.map +1 -0
- package/dist/src/cli/session-runtime.js +11 -0
- package/dist/src/cli/session-runtime.js.map +1 -0
- package/dist/src/cli/setup.d.ts.map +1 -1
- package/dist/src/cli/setup.js +2 -0
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +148 -7
- package/dist/src/index.js.map +1 -1
- package/dist/src/ingestion/deterministic.d.ts +3 -0
- package/dist/src/ingestion/deterministic.d.ts.map +1 -0
- package/dist/src/ingestion/deterministic.js +862 -0
- package/dist/src/ingestion/deterministic.js.map +1 -0
- package/dist/src/ingestion/index.d.ts +5 -0
- package/dist/src/ingestion/index.d.ts.map +1 -0
- package/dist/src/ingestion/index.js +27 -0
- package/dist/src/ingestion/index.js.map +1 -0
- package/dist/src/ingestion/semantic.d.ts +6 -0
- package/dist/src/ingestion/semantic.d.ts.map +1 -0
- package/dist/src/ingestion/semantic.js +627 -0
- package/dist/src/ingestion/semantic.js.map +1 -0
- package/dist/src/ingestion/types.d.ts +10 -0
- package/dist/src/ingestion/types.d.ts.map +1 -0
- package/dist/src/ingestion/types.js +2 -0
- package/dist/src/ingestion/types.js.map +1 -0
- package/dist/src/lib/context-memory.d.ts +140 -0
- package/dist/src/lib/context-memory.d.ts.map +1 -0
- package/dist/src/lib/context-memory.js +974 -0
- package/dist/src/lib/context-memory.js.map +1 -0
- package/dist/src/lib/history-handoff.d.ts +43 -0
- package/dist/src/lib/history-handoff.d.ts.map +1 -0
- package/dist/src/lib/history-handoff.js +179 -0
- package/dist/src/lib/history-handoff.js.map +1 -0
- package/dist/src/lib/observability.d.ts +3 -0
- package/dist/src/lib/observability.d.ts.map +1 -0
- package/dist/src/lib/observability.js +63 -0
- package/dist/src/lib/observability.js.map +1 -0
- package/dist/src/lib/session-artifacts.d.ts +51 -0
- package/dist/src/lib/session-artifacts.d.ts.map +1 -0
- package/dist/src/lib/session-artifacts.js +132 -0
- package/dist/src/lib/session-artifacts.js.map +1 -0
- package/dist/src/lib/session-filters.d.ts +11 -0
- package/dist/src/lib/session-filters.d.ts.map +1 -0
- package/dist/src/lib/session-filters.js +16 -0
- package/dist/src/lib/session-filters.js.map +1 -0
- package/dist/src/lib/session-history.d.ts +50 -0
- package/dist/src/lib/session-history.d.ts.map +1 -0
- package/dist/src/lib/session-history.js +73 -0
- package/dist/src/lib/session-history.js.map +1 -0
- package/dist/src/lib/session-trends.d.ts +129 -0
- package/dist/src/lib/session-trends.d.ts.map +1 -0
- package/dist/src/lib/session-trends.js +361 -0
- package/dist/src/lib/session-trends.js.map +1 -0
- package/dist/src/lib/storage/database.d.ts +212 -1
- package/dist/src/lib/storage/database.d.ts.map +1 -1
- package/dist/src/lib/storage/database.js +1694 -114
- package/dist/src/lib/storage/database.js.map +1 -1
- package/dist/src/lib/storage/export-sessions.d.ts +33 -0
- package/dist/src/lib/storage/export-sessions.d.ts.map +1 -0
- package/dist/src/lib/storage/export-sessions.js +525 -0
- package/dist/src/lib/storage/export-sessions.js.map +1 -0
- package/dist/src/lib/storage/index.d.ts +7 -6
- package/dist/src/lib/storage/index.d.ts.map +1 -1
- package/dist/src/lib/storage/index.js +6 -5
- package/dist/src/lib/storage/index.js.map +1 -1
- package/dist/src/lib/storage/schema.d.ts +6 -1
- package/dist/src/lib/storage/schema.d.ts.map +1 -1
- package/dist/src/lib/storage/schema.js +337 -2
- package/dist/src/lib/storage/schema.js.map +1 -1
- package/dist/src/lib/storage/types.d.ts +122 -0
- package/dist/src/lib/storage/types.d.ts.map +1 -1
- package/dist/src/prompts/skill-prompt.d.ts.map +1 -1
- package/dist/src/prompts/skill-prompt.js +13 -0
- package/dist/src/prompts/skill-prompt.js.map +1 -1
- package/dist/src/tools/confirm-context-link.d.ts +62 -0
- package/dist/src/tools/confirm-context-link.d.ts.map +1 -0
- package/dist/src/tools/confirm-context-link.js +36 -0
- package/dist/src/tools/confirm-context-link.js.map +1 -0
- package/dist/src/tools/context-schemas.d.ts +694 -0
- package/dist/src/tools/context-schemas.d.ts.map +1 -0
- package/dist/src/tools/context-schemas.js +171 -0
- package/dist/src/tools/context-schemas.js.map +1 -0
- package/dist/src/tools/export-sessions.d.ts +111 -0
- package/dist/src/tools/export-sessions.d.ts.map +1 -0
- package/dist/src/tools/export-sessions.js +136 -0
- package/dist/src/tools/export-sessions.js.map +1 -0
- package/dist/src/tools/get-context.d.ts +208 -0
- package/dist/src/tools/get-context.d.ts.map +1 -0
- package/dist/src/tools/get-context.js +27 -0
- package/dist/src/tools/get-context.js.map +1 -0
- package/dist/src/tools/get-history-handoff.d.ts +109 -0
- package/dist/src/tools/get-history-handoff.d.ts.map +1 -0
- package/dist/src/tools/get-history-handoff.js +85 -0
- package/dist/src/tools/get-history-handoff.js.map +1 -0
- package/dist/src/tools/get-history-trends.d.ts +155 -0
- package/dist/src/tools/get-history-trends.d.ts.map +1 -0
- package/dist/src/tools/get-history-trends.js +123 -0
- package/dist/src/tools/get-history-trends.js.map +1 -0
- package/dist/src/tools/get-session-artifacts.d.ts +151 -0
- package/dist/src/tools/get-session-artifacts.d.ts.map +1 -0
- package/dist/src/tools/get-session-artifacts.js +184 -0
- package/dist/src/tools/get-session-artifacts.js.map +1 -0
- package/dist/src/tools/get-session-decisions.d.ts +69 -0
- package/dist/src/tools/get-session-decisions.d.ts.map +1 -0
- package/dist/src/tools/get-session-decisions.js +99 -0
- package/dist/src/tools/get-session-decisions.js.map +1 -0
- package/dist/src/tools/get-session-messages.d.ts +55 -0
- package/dist/src/tools/get-session-messages.d.ts.map +1 -0
- package/dist/src/tools/get-session-messages.js +89 -0
- package/dist/src/tools/get-session-messages.js.map +1 -0
- package/dist/src/tools/get-session-narrative.d.ts +72 -0
- package/dist/src/tools/get-session-narrative.d.ts.map +1 -0
- package/dist/src/tools/get-session-narrative.js +106 -0
- package/dist/src/tools/get-session-narrative.js.map +1 -0
- package/dist/src/tools/get-session-timeline.d.ts +55 -0
- package/dist/src/tools/get-session-timeline.d.ts.map +1 -0
- package/dist/src/tools/get-session-timeline.js +93 -0
- package/dist/src/tools/get-session-timeline.js.map +1 -0
- package/dist/src/tools/get-session-trends.d.ts +108 -0
- package/dist/src/tools/get-session-trends.d.ts.map +1 -0
- package/dist/src/tools/get-session-trends.js +130 -0
- package/dist/src/tools/get-session-trends.js.map +1 -0
- package/dist/src/tools/get-session.d.ts +251 -0
- package/dist/src/tools/get-session.d.ts.map +1 -0
- package/dist/src/tools/get-session.js +290 -0
- package/dist/src/tools/get-session.js.map +1 -0
- package/dist/src/tools/index.d.ts +22 -0
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +22 -0
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-contexts.d.ts +50 -0
- package/dist/src/tools/list-contexts.d.ts.map +1 -0
- package/dist/src/tools/list-contexts.js +28 -0
- package/dist/src/tools/list-contexts.js.map +1 -0
- package/dist/src/tools/list-sessions.d.ts +86 -0
- package/dist/src/tools/list-sessions.d.ts.map +1 -0
- package/dist/src/tools/list-sessions.js +97 -0
- package/dist/src/tools/list-sessions.js.map +1 -0
- package/dist/src/tools/merge-contexts.d.ts +58 -0
- package/dist/src/tools/merge-contexts.d.ts.map +1 -0
- package/dist/src/tools/merge-contexts.js +27 -0
- package/dist/src/tools/merge-contexts.js.map +1 -0
- package/dist/src/tools/move-session-context.d.ts +62 -0
- package/dist/src/tools/move-session-context.d.ts.map +1 -0
- package/dist/src/tools/move-session-context.js +33 -0
- package/dist/src/tools/move-session-context.js.map +1 -0
- package/dist/src/tools/reingest-session.d.ts +31 -0
- package/dist/src/tools/reingest-session.d.ts.map +1 -0
- package/dist/src/tools/reingest-session.js +43 -0
- package/dist/src/tools/reingest-session.js.map +1 -0
- package/dist/src/tools/reject-context-link.d.ts +58 -0
- package/dist/src/tools/reject-context-link.d.ts.map +1 -0
- package/dist/src/tools/reject-context-link.js +26 -0
- package/dist/src/tools/reject-context-link.js.map +1 -0
- package/dist/src/tools/resolve-context.d.ts +287 -0
- package/dist/src/tools/resolve-context.d.ts.map +1 -0
- package/dist/src/tools/resolve-context.js +35 -0
- package/dist/src/tools/resolve-context.js.map +1 -0
- package/dist/src/tools/search-history.d.ts +86 -0
- package/dist/src/tools/search-history.d.ts.map +1 -0
- package/dist/src/tools/search-history.js +103 -0
- package/dist/src/tools/search-history.js.map +1 -0
- package/dist/src/tools/session-ui-metadata.d.ts +15 -0
- package/dist/src/tools/session-ui-metadata.d.ts.map +1 -0
- package/dist/src/tools/session-ui-metadata.js +15 -0
- package/dist/src/tools/session-ui-metadata.js.map +1 -0
- package/dist/src/tools/set-active-context.d.ts +58 -0
- package/dist/src/tools/set-active-context.d.ts.map +1 -0
- package/dist/src/tools/set-active-context.js +26 -0
- package/dist/src/tools/set-active-context.js.map +1 -0
- package/dist/src/tools/split-context.d.ts +62 -0
- package/dist/src/tools/split-context.d.ts.map +1 -0
- package/dist/src/tools/split-context.js +36 -0
- package/dist/src/tools/split-context.js.map +1 -0
- package/dist/src/tools/verify-footprint.js +1 -1
- package/dist/src/tools/verify-footprint.js.map +1 -1
- package/dist/src/types.d.ts +2 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/ui/register.d.ts +6 -1
- package/dist/src/ui/register.d.ts.map +1 -1
- package/dist/src/ui/register.js +60 -16
- package/dist/src/ui/register.js.map +1 -1
- package/dist/ui/dashboard.html +239 -868
- package/dist/ui/detail.html +107 -248
- package/dist/ui/export.html +115 -298
- package/dist/ui/session-dashboard-live.html +264 -0
- package/dist/ui/session-dashboard.html +329 -0
- package/dist/ui/session-detail-live.html +336 -0
- package/dist/ui/session-detail.html +355 -0
- package/package.json +34 -9
|
@@ -1,15 +1,66 @@
|
|
|
1
1
|
/* global Buffer, crypto */
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { getArtifactSearchableText, parseArtifactMetadata, } from "../session-artifacts.js";
|
|
6
|
+
import { traceSyncOperation } from "../observability.js";
|
|
3
7
|
import { createSchema } from "./schema.js";
|
|
4
8
|
function escapeLikePattern(pattern) {
|
|
5
9
|
return pattern.replace(/[%_\\]/g, "\\$&");
|
|
6
10
|
}
|
|
11
|
+
function normalizeWorkspaceKeyForMatching(value) {
|
|
12
|
+
const resolved = path.resolve(value);
|
|
13
|
+
try {
|
|
14
|
+
return fs.realpathSync.native(resolved);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return resolved;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function matchesWorkspaceKey(session, workspaceKey) {
|
|
21
|
+
const normalizedWorkspaceKey = normalizeWorkspaceKeyForMatching(workspaceKey);
|
|
22
|
+
return (normalizeWorkspaceKeyForMatching(session.projectRoot) ===
|
|
23
|
+
normalizedWorkspaceKey ||
|
|
24
|
+
normalizeWorkspaceKeyForMatching(session.cwd) === normalizedWorkspaceKey);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Append LIMIT/OFFSET clause to SQL query string.
|
|
28
|
+
* Mutates the params array by pushing limit/offset values.
|
|
29
|
+
* @returns The query string with pagination appended.
|
|
30
|
+
*/
|
|
31
|
+
function appendPaginationClause(query, params, limit, offset) {
|
|
32
|
+
const off = offset ?? 0;
|
|
33
|
+
if (limit !== undefined) {
|
|
34
|
+
query += " LIMIT ?";
|
|
35
|
+
params.push(limit);
|
|
36
|
+
if (off > 0) {
|
|
37
|
+
query += " OFFSET ?";
|
|
38
|
+
params.push(off);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (off > 0) {
|
|
42
|
+
query += " LIMIT -1 OFFSET ?";
|
|
43
|
+
params.push(off);
|
|
44
|
+
}
|
|
45
|
+
return query;
|
|
46
|
+
}
|
|
47
|
+
function formatDbError(action, error) {
|
|
48
|
+
return new Error(`Failed to ${action}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
49
|
+
}
|
|
50
|
+
const TREND_FAILED_OUTCOME_PATTERN = /\b(?:fail|failed|error|timeout|timed-out|interrupted|non-zero)\b/i;
|
|
51
|
+
const TREND_SUCCEEDED_OUTCOME_PATTERN = /\b(?:success|succeeded|passed|completed|captured|ok)\b/i;
|
|
52
|
+
const SESSION_HISTORY_CACHE_VERSION_KEY = "session_history_cache_version";
|
|
53
|
+
const SESSION_TREND_CACHE_VERSION_KEY = "session_trend_cache_version";
|
|
54
|
+
const CURRENT_SESSION_HISTORY_CACHE_VERSION = 1;
|
|
55
|
+
const CURRENT_SESSION_TREND_CACHE_VERSION = 1;
|
|
7
56
|
/**
|
|
8
57
|
* Evidence database with CRUD operations
|
|
9
58
|
* Manages encrypted evidence storage with SQLite backend
|
|
10
59
|
*/
|
|
11
60
|
export class EvidenceDatabase {
|
|
12
61
|
db;
|
|
62
|
+
sessionHistoryCacheBackfilled = false;
|
|
63
|
+
sessionTrendAttemptsBackfilled = false;
|
|
13
64
|
/**
|
|
14
65
|
* Creates or opens an evidence database
|
|
15
66
|
* @param dbPath - Path to SQLite database file
|
|
@@ -19,12 +70,101 @@ export class EvidenceDatabase {
|
|
|
19
70
|
this.db = new Database(dbPath);
|
|
20
71
|
try {
|
|
21
72
|
createSchema(this.db);
|
|
73
|
+
this.initializeMaterializedCaches();
|
|
22
74
|
}
|
|
23
75
|
catch (error) {
|
|
24
76
|
// Clean up database connection on any initialization failure
|
|
25
77
|
this.db.close();
|
|
26
|
-
throw
|
|
78
|
+
throw formatDbError("initialize database", error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
getMetadataValue(key) {
|
|
82
|
+
const row = this.db
|
|
83
|
+
.prepare(`
|
|
84
|
+
SELECT value
|
|
85
|
+
FROM metadata
|
|
86
|
+
WHERE key = ?
|
|
87
|
+
`)
|
|
88
|
+
.get(key);
|
|
89
|
+
return row?.value ?? null;
|
|
90
|
+
}
|
|
91
|
+
getMetadataVersion(key) {
|
|
92
|
+
const value = this.getMetadataValue(key);
|
|
93
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
94
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
95
|
+
}
|
|
96
|
+
setMetadataValue(key, value) {
|
|
97
|
+
this.db
|
|
98
|
+
.prepare(`
|
|
99
|
+
INSERT INTO metadata (key, value)
|
|
100
|
+
VALUES (?, ?)
|
|
101
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
102
|
+
`)
|
|
103
|
+
.run(key, value);
|
|
104
|
+
}
|
|
105
|
+
getAllSessionIds() {
|
|
106
|
+
return this.db
|
|
107
|
+
.prepare(`
|
|
108
|
+
SELECT id
|
|
109
|
+
FROM sessions
|
|
110
|
+
ORDER BY startedAt ASC, id ASC
|
|
111
|
+
`)
|
|
112
|
+
.all().map((row) => row.id);
|
|
113
|
+
}
|
|
114
|
+
initializeMaterializedCaches() {
|
|
115
|
+
if (this.getMetadataVersion(SESSION_HISTORY_CACHE_VERSION_KEY) <
|
|
116
|
+
CURRENT_SESSION_HISTORY_CACHE_VERSION) {
|
|
117
|
+
this.rebuildAllSessionHistoryCaches();
|
|
118
|
+
this.setMetadataValue(SESSION_HISTORY_CACHE_VERSION_KEY, String(CURRENT_SESSION_HISTORY_CACHE_VERSION));
|
|
119
|
+
this.sessionHistoryCacheBackfilled = true;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.sessionHistoryCacheBackfilled = false;
|
|
123
|
+
this.ensureSessionHistoryCacheBackfilled();
|
|
124
|
+
}
|
|
125
|
+
if (this.getMetadataVersion(SESSION_TREND_CACHE_VERSION_KEY) <
|
|
126
|
+
CURRENT_SESSION_TREND_CACHE_VERSION) {
|
|
127
|
+
this.rebuildAllSessionTrendAttempts();
|
|
128
|
+
this.setMetadataValue(SESSION_TREND_CACHE_VERSION_KEY, String(CURRENT_SESSION_TREND_CACHE_VERSION));
|
|
129
|
+
this.sessionTrendAttemptsBackfilled = true;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.sessionTrendAttemptsBackfilled = false;
|
|
133
|
+
this.ensureSessionTrendAttemptsBackfilled();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
dbOp(action, fn) {
|
|
137
|
+
try {
|
|
138
|
+
return fn();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
throw formatDbError(action, error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
resolveActiveContextIdOrThrow(contextId) {
|
|
145
|
+
let currentId = contextId.trim();
|
|
146
|
+
const visited = new Set();
|
|
147
|
+
while (currentId) {
|
|
148
|
+
if (visited.has(currentId)) {
|
|
149
|
+
throw new Error(`Context merge loop detected for ${contextId}`);
|
|
150
|
+
}
|
|
151
|
+
visited.add(currentId);
|
|
152
|
+
const row = this.db
|
|
153
|
+
.prepare(`
|
|
154
|
+
SELECT id, status, mergedIntoContextId
|
|
155
|
+
FROM contexts
|
|
156
|
+
WHERE id = ?
|
|
157
|
+
`)
|
|
158
|
+
.get(currentId);
|
|
159
|
+
if (!row) {
|
|
160
|
+
throw new Error(`Context not found: ${contextId}`);
|
|
161
|
+
}
|
|
162
|
+
if (row.status !== "merged" || !row.mergedIntoContextId) {
|
|
163
|
+
return row.id;
|
|
164
|
+
}
|
|
165
|
+
currentId = row.mergedIntoContextId;
|
|
27
166
|
}
|
|
167
|
+
throw new Error(`Context not found: ${contextId}`);
|
|
28
168
|
}
|
|
29
169
|
/**
|
|
30
170
|
* Creates a new evidence record
|
|
@@ -34,7 +174,7 @@ export class EvidenceDatabase {
|
|
|
34
174
|
create(evidence) {
|
|
35
175
|
const id = crypto.randomUUID();
|
|
36
176
|
const now = new Date().toISOString();
|
|
37
|
-
|
|
177
|
+
return this.dbOp("create evidence", () => {
|
|
38
178
|
const stmt = this.db.prepare(`
|
|
39
179
|
INSERT INTO evidences (
|
|
40
180
|
id, timestamp, conversationId, llmProvider,
|
|
@@ -44,10 +184,7 @@ export class EvidenceDatabase {
|
|
|
44
184
|
`);
|
|
45
185
|
stmt.run(id, evidence.timestamp, evidence.conversationId, evidence.llmProvider, Buffer.from(evidence.encryptedContent), Buffer.from(evidence.nonce), evidence.contentHash, evidence.messageCount, evidence.gitCommitHash, evidence.gitTimestamp, evidence.tags, now, now);
|
|
46
186
|
return id;
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
throw new Error(`Failed to create evidence: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
-
}
|
|
187
|
+
});
|
|
51
188
|
}
|
|
52
189
|
/**
|
|
53
190
|
* Finds evidence by ID
|
|
@@ -55,7 +192,7 @@ export class EvidenceDatabase {
|
|
|
55
192
|
* @returns Evidence or null if not found
|
|
56
193
|
*/
|
|
57
194
|
findById(id) {
|
|
58
|
-
|
|
195
|
+
return this.dbOp("find evidence by ID", () => {
|
|
59
196
|
const stmt = this.db.prepare(`
|
|
60
197
|
SELECT * FROM evidences WHERE id = ?
|
|
61
198
|
`);
|
|
@@ -64,11 +201,7 @@ export class EvidenceDatabase {
|
|
|
64
201
|
return null;
|
|
65
202
|
}
|
|
66
203
|
return this.rowToEvidence(row);
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
// Don't swallow database errors - re-throw with context
|
|
70
|
-
throw new Error(`Failed to find evidence by ID: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
-
}
|
|
204
|
+
});
|
|
72
205
|
}
|
|
73
206
|
/**
|
|
74
207
|
* Finds all evidences for a conversation
|
|
@@ -76,7 +209,7 @@ export class EvidenceDatabase {
|
|
|
76
209
|
* @returns Array of evidences (empty if none found)
|
|
77
210
|
*/
|
|
78
211
|
findByConversationId(conversationId) {
|
|
79
|
-
|
|
212
|
+
return this.dbOp("find evidences by conversationId", () => {
|
|
80
213
|
const stmt = this.db.prepare(`
|
|
81
214
|
SELECT * FROM evidences
|
|
82
215
|
WHERE conversationId = ?
|
|
@@ -84,10 +217,7 @@ export class EvidenceDatabase {
|
|
|
84
217
|
`);
|
|
85
218
|
const rows = stmt.all(conversationId);
|
|
86
219
|
return rows.map((row) => this.rowToEvidence(row));
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
throw new Error(`Failed to find evidences by conversationId: ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
-
}
|
|
220
|
+
});
|
|
91
221
|
}
|
|
92
222
|
/**
|
|
93
223
|
* Lists evidences with pagination
|
|
@@ -95,31 +225,1407 @@ export class EvidenceDatabase {
|
|
|
95
225
|
* @returns Array of evidences
|
|
96
226
|
*/
|
|
97
227
|
list(options) {
|
|
228
|
+
return this.dbOp("list evidences", () => {
|
|
229
|
+
const params = [];
|
|
230
|
+
const query = appendPaginationClause("SELECT * FROM evidences ORDER BY timestamp DESC", params, options?.limit, options?.offset);
|
|
231
|
+
const stmt = this.db.prepare(query);
|
|
232
|
+
const rows = stmt.all(...params);
|
|
233
|
+
return rows.map((row) => this.rowToEvidence(row));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
static joinSearchParts(parts) {
|
|
237
|
+
return parts
|
|
238
|
+
.map((part) => part.trim())
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.join("\n");
|
|
241
|
+
}
|
|
242
|
+
static appendSearchPart(existing, addition) {
|
|
243
|
+
const normalizedAddition = addition.trim();
|
|
244
|
+
if (!normalizedAddition) {
|
|
245
|
+
return existing;
|
|
246
|
+
}
|
|
247
|
+
return existing.trim()
|
|
248
|
+
? `${existing}\n${normalizedAddition}`
|
|
249
|
+
: normalizedAddition;
|
|
250
|
+
}
|
|
251
|
+
static normalizeTrendOutcome(value) {
|
|
252
|
+
if (!value) {
|
|
253
|
+
return "other";
|
|
254
|
+
}
|
|
255
|
+
if (TREND_FAILED_OUTCOME_PATTERN.test(value)) {
|
|
256
|
+
return "failed";
|
|
257
|
+
}
|
|
258
|
+
if (TREND_SUCCEEDED_OUTCOME_PATTERN.test(value)) {
|
|
259
|
+
return "succeeded";
|
|
260
|
+
}
|
|
261
|
+
return "other";
|
|
262
|
+
}
|
|
263
|
+
buildArtifactHistoryCache(artifacts) {
|
|
264
|
+
const issueKeys = new Set();
|
|
265
|
+
const text = EvidenceDatabase.joinSearchParts(artifacts.flatMap((artifact) => {
|
|
266
|
+
const metadata = parseArtifactMetadata(artifact.metadata);
|
|
267
|
+
if (metadata.issueKey) {
|
|
268
|
+
issueKeys.add(metadata.issueKey);
|
|
269
|
+
}
|
|
270
|
+
return getArtifactSearchableText(artifact);
|
|
271
|
+
}));
|
|
272
|
+
return {
|
|
273
|
+
text,
|
|
274
|
+
issueKeys: Array.from(issueKeys).sort(),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
buildNarrativeHistoryCache(narratives) {
|
|
278
|
+
return EvidenceDatabase.joinSearchParts(narratives.map((narrative) => narrative.content));
|
|
279
|
+
}
|
|
280
|
+
buildDecisionHistoryCache(decisions) {
|
|
281
|
+
return EvidenceDatabase.joinSearchParts(decisions.map((decision) => decision.summary));
|
|
282
|
+
}
|
|
283
|
+
ensureSessionHistoryCacheRow(sessionId) {
|
|
284
|
+
this.db
|
|
285
|
+
.prepare(`
|
|
286
|
+
INSERT INTO session_history_cache (
|
|
287
|
+
sessionId, titleText, metadataText, messagesText,
|
|
288
|
+
artifactsText, narrativesText, decisionsText, updatedAt
|
|
289
|
+
)
|
|
290
|
+
SELECT
|
|
291
|
+
id,
|
|
292
|
+
COALESCE(title, ''),
|
|
293
|
+
COALESCE(metadata, ''),
|
|
294
|
+
'',
|
|
295
|
+
'',
|
|
296
|
+
'',
|
|
297
|
+
'',
|
|
298
|
+
?
|
|
299
|
+
FROM sessions
|
|
300
|
+
WHERE id = ?
|
|
301
|
+
ON CONFLICT(sessionId) DO NOTHING
|
|
302
|
+
`)
|
|
303
|
+
.run(new Date().toISOString(), sessionId);
|
|
304
|
+
}
|
|
305
|
+
updateSessionHistoryCache(sessionId, updates) {
|
|
306
|
+
this.ensureSessionHistoryCacheRow(sessionId);
|
|
307
|
+
const current = this.db
|
|
308
|
+
.prepare(`
|
|
309
|
+
SELECT * FROM session_history_cache
|
|
310
|
+
WHERE sessionId = ?
|
|
311
|
+
`)
|
|
312
|
+
.get(sessionId);
|
|
313
|
+
if (!current) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const next = {
|
|
317
|
+
...current,
|
|
318
|
+
...updates,
|
|
319
|
+
sessionId,
|
|
320
|
+
updatedAt: new Date().toISOString(),
|
|
321
|
+
};
|
|
322
|
+
this.db
|
|
323
|
+
.prepare(`
|
|
324
|
+
UPDATE session_history_cache
|
|
325
|
+
SET titleText = ?,
|
|
326
|
+
metadataText = ?,
|
|
327
|
+
messagesText = ?,
|
|
328
|
+
artifactsText = ?,
|
|
329
|
+
narrativesText = ?,
|
|
330
|
+
decisionsText = ?,
|
|
331
|
+
updatedAt = ?
|
|
332
|
+
WHERE sessionId = ?
|
|
333
|
+
`)
|
|
334
|
+
.run(next.titleText, next.metadataText, next.messagesText, next.artifactsText, next.narrativesText, next.decisionsText, next.updatedAt, sessionId);
|
|
335
|
+
}
|
|
336
|
+
replaceSessionIssueKeys(sessionId, issueKeys) {
|
|
337
|
+
this.db
|
|
338
|
+
.prepare(`DELETE FROM session_issue_keys WHERE sessionId = ?`)
|
|
339
|
+
.run(sessionId);
|
|
340
|
+
const insertIssueKey = this.db.prepare(`
|
|
341
|
+
INSERT INTO session_issue_keys (sessionId, issueKey)
|
|
342
|
+
VALUES (?, ?)
|
|
343
|
+
`);
|
|
344
|
+
for (const issueKey of issueKeys) {
|
|
345
|
+
insertIssueKey.run(sessionId, issueKey);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
rebuildSessionHistoryCache(sessionId) {
|
|
349
|
+
const session = this.findSessionById(sessionId);
|
|
350
|
+
if (!session) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const messagesText = EvidenceDatabase.joinSearchParts(this.getSessionMessages(sessionId).map((message) => message.content));
|
|
354
|
+
const artifacts = this.getSessionArtifacts(sessionId);
|
|
355
|
+
const artifactCache = this.buildArtifactHistoryCache(artifacts);
|
|
356
|
+
const narrativesText = this.buildNarrativeHistoryCache(this.getSessionNarratives(sessionId));
|
|
357
|
+
const decisionsText = this.buildDecisionHistoryCache(this.getSessionDecisions(sessionId));
|
|
358
|
+
this.updateSessionHistoryCache(sessionId, {
|
|
359
|
+
titleText: session.title ?? "",
|
|
360
|
+
metadataText: session.metadata ?? "",
|
|
361
|
+
messagesText,
|
|
362
|
+
artifactsText: artifactCache.text,
|
|
363
|
+
narrativesText,
|
|
364
|
+
decisionsText,
|
|
365
|
+
});
|
|
366
|
+
this.replaceSessionIssueKeys(sessionId, artifactCache.issueKeys);
|
|
367
|
+
}
|
|
368
|
+
ensureSessionHistoryCacheBackfilled() {
|
|
369
|
+
if (this.sessionHistoryCacheBackfilled) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const missingRows = this.db
|
|
373
|
+
.prepare(`
|
|
374
|
+
SELECT s.id
|
|
375
|
+
FROM sessions s
|
|
376
|
+
LEFT JOIN session_history_cache cache ON cache.sessionId = s.id
|
|
377
|
+
WHERE cache.sessionId IS NULL
|
|
378
|
+
ORDER BY s.startedAt ASC, s.id ASC
|
|
379
|
+
`)
|
|
380
|
+
.all();
|
|
381
|
+
for (const row of missingRows) {
|
|
382
|
+
this.rebuildSessionHistoryCache(row.id);
|
|
383
|
+
}
|
|
384
|
+
this.sessionHistoryCacheBackfilled = true;
|
|
385
|
+
}
|
|
386
|
+
rebuildAllSessionHistoryCaches() {
|
|
387
|
+
for (const sessionId of this.getAllSessionIds()) {
|
|
388
|
+
this.rebuildSessionHistoryCache(sessionId);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
fetchTrendSeenAtByEventIds(sessionId, eventIds) {
|
|
392
|
+
if (eventIds.length === 0) {
|
|
393
|
+
return new Map();
|
|
394
|
+
}
|
|
395
|
+
const placeholders = eventIds.map(() => "?").join(", ");
|
|
396
|
+
const rows = this.db
|
|
397
|
+
.prepare(`
|
|
398
|
+
SELECT id, COALESCE(endedAt, startedAt) as seenAt
|
|
399
|
+
FROM timeline_events
|
|
400
|
+
WHERE sessionId = ? AND id IN (${placeholders})
|
|
401
|
+
`)
|
|
402
|
+
.all(sessionId, ...eventIds);
|
|
403
|
+
return new Map(rows.map((row) => [row.id, row.seenAt]));
|
|
404
|
+
}
|
|
405
|
+
buildSessionTrendAttempts(sessionId, artifacts) {
|
|
406
|
+
const eventIds = [
|
|
407
|
+
...new Set(artifacts
|
|
408
|
+
.map((artifact) => artifact.eventId)
|
|
409
|
+
.filter((eventId) => Boolean(eventId))),
|
|
410
|
+
];
|
|
411
|
+
const seenAtByEventId = this.fetchTrendSeenAtByEventIds(sessionId, eventIds);
|
|
412
|
+
return artifacts.flatMap((artifact) => {
|
|
413
|
+
const metadata = parseArtifactMetadata(artifact.metadata);
|
|
414
|
+
const eventType = typeof metadata.details.eventType === "string"
|
|
415
|
+
? metadata.details.eventType
|
|
416
|
+
: null;
|
|
417
|
+
const outcome = metadata.outcome ?? metadata.status ?? "captured";
|
|
418
|
+
const outcomeCategory = EvidenceDatabase.normalizeTrendOutcome(outcome);
|
|
419
|
+
if (!artifact.eventId || !metadata.issueKey) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
if (!eventType ||
|
|
423
|
+
(!eventType.startsWith("command.") && !eventType.startsWith("test."))) {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
if (outcomeCategory === "other") {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
return [
|
|
430
|
+
{
|
|
431
|
+
artifactId: artifact.id,
|
|
432
|
+
sessionId,
|
|
433
|
+
issueKey: metadata.issueKey,
|
|
434
|
+
issueLabel: metadata.issueLabel ?? metadata.issueKey,
|
|
435
|
+
kind: metadata.intent ?? metadata.category,
|
|
436
|
+
issueFamilyKey: metadata.issueFamilyKey,
|
|
437
|
+
issueFamilyLabel: metadata.issueFamilyLabel ??
|
|
438
|
+
metadata.issueFamilyKey ??
|
|
439
|
+
metadata.issueLabel,
|
|
440
|
+
outcome,
|
|
441
|
+
outcomeCategory,
|
|
442
|
+
seenAt: seenAtByEventId.get(artifact.eventId) ?? artifact.createdAt,
|
|
443
|
+
createdAt: artifact.createdAt,
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
markSessionTrendAttemptsFresh(sessionId) {
|
|
449
|
+
this.db
|
|
450
|
+
.prepare(`
|
|
451
|
+
INSERT INTO session_trend_cache_state (sessionId, updatedAt)
|
|
452
|
+
VALUES (?, ?)
|
|
453
|
+
ON CONFLICT(sessionId) DO UPDATE SET updatedAt = excluded.updatedAt
|
|
454
|
+
`)
|
|
455
|
+
.run(sessionId, new Date().toISOString());
|
|
456
|
+
}
|
|
457
|
+
replaceSessionTrendAttempts(sessionId, artifacts) {
|
|
458
|
+
this.db
|
|
459
|
+
.prepare(`DELETE FROM session_trend_attempts WHERE sessionId = ?`)
|
|
460
|
+
.run(sessionId);
|
|
461
|
+
const attempts = this.buildSessionTrendAttempts(sessionId, artifacts);
|
|
462
|
+
if (attempts.length === 0) {
|
|
463
|
+
this.markSessionTrendAttemptsFresh(sessionId);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const insertAttempt = this.db.prepare(`
|
|
467
|
+
INSERT INTO session_trend_attempts (
|
|
468
|
+
artifactId, sessionId, issueKey, issueLabel, kind,
|
|
469
|
+
issueFamilyKey, issueFamilyLabel, outcome, outcomeCategory,
|
|
470
|
+
seenAt, createdAt
|
|
471
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
472
|
+
`);
|
|
473
|
+
for (const attempt of attempts) {
|
|
474
|
+
insertAttempt.run(attempt.artifactId, attempt.sessionId, attempt.issueKey, attempt.issueLabel, attempt.kind, attempt.issueFamilyKey, attempt.issueFamilyLabel, attempt.outcome, attempt.outcomeCategory, attempt.seenAt, attempt.createdAt);
|
|
475
|
+
}
|
|
476
|
+
this.markSessionTrendAttemptsFresh(sessionId);
|
|
477
|
+
}
|
|
478
|
+
insertSessionTrendAttempts(sessionId, artifacts) {
|
|
479
|
+
const attempts = this.buildSessionTrendAttempts(sessionId, artifacts);
|
|
480
|
+
if (attempts.length === 0) {
|
|
481
|
+
this.markSessionTrendAttemptsFresh(sessionId);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const insertAttempt = this.db.prepare(`
|
|
485
|
+
INSERT INTO session_trend_attempts (
|
|
486
|
+
artifactId, sessionId, issueKey, issueLabel, kind,
|
|
487
|
+
issueFamilyKey, issueFamilyLabel, outcome, outcomeCategory,
|
|
488
|
+
seenAt, createdAt
|
|
489
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
490
|
+
ON CONFLICT(artifactId) DO UPDATE SET
|
|
491
|
+
sessionId = excluded.sessionId,
|
|
492
|
+
issueKey = excluded.issueKey,
|
|
493
|
+
issueLabel = excluded.issueLabel,
|
|
494
|
+
kind = excluded.kind,
|
|
495
|
+
issueFamilyKey = excluded.issueFamilyKey,
|
|
496
|
+
issueFamilyLabel = excluded.issueFamilyLabel,
|
|
497
|
+
outcome = excluded.outcome,
|
|
498
|
+
outcomeCategory = excluded.outcomeCategory,
|
|
499
|
+
seenAt = excluded.seenAt,
|
|
500
|
+
createdAt = excluded.createdAt
|
|
501
|
+
`);
|
|
502
|
+
for (const attempt of attempts) {
|
|
503
|
+
insertAttempt.run(attempt.artifactId, attempt.sessionId, attempt.issueKey, attempt.issueLabel, attempt.kind, attempt.issueFamilyKey, attempt.issueFamilyLabel, attempt.outcome, attempt.outcomeCategory, attempt.seenAt, attempt.createdAt);
|
|
504
|
+
}
|
|
505
|
+
this.markSessionTrendAttemptsFresh(sessionId);
|
|
506
|
+
}
|
|
507
|
+
rebuildSessionTrendAttempts(sessionId) {
|
|
508
|
+
const session = this.findSessionById(sessionId);
|
|
509
|
+
if (!session) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
this.replaceSessionTrendAttempts(sessionId, this.getSessionArtifacts(sessionId));
|
|
513
|
+
}
|
|
514
|
+
ensureSessionTrendAttemptsBackfilled() {
|
|
515
|
+
if (this.sessionTrendAttemptsBackfilled) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const missingRows = this.db
|
|
519
|
+
.prepare(`
|
|
520
|
+
SELECT s.id
|
|
521
|
+
FROM sessions s
|
|
522
|
+
LEFT JOIN session_trend_cache_state state ON state.sessionId = s.id
|
|
523
|
+
WHERE state.sessionId IS NULL
|
|
524
|
+
ORDER BY s.startedAt ASC, s.id ASC
|
|
525
|
+
`)
|
|
526
|
+
.all();
|
|
527
|
+
for (const row of missingRows) {
|
|
528
|
+
this.rebuildSessionTrendAttempts(row.id);
|
|
529
|
+
}
|
|
530
|
+
this.sessionTrendAttemptsBackfilled = true;
|
|
531
|
+
}
|
|
532
|
+
rebuildAllSessionTrendAttempts() {
|
|
533
|
+
for (const sessionId of this.getAllSessionIds()) {
|
|
534
|
+
this.rebuildSessionTrendAttempts(sessionId);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
querySessionsByHistory(options) {
|
|
538
|
+
return traceSyncOperation("db.query-sessions-by-history", {
|
|
539
|
+
host: options.host,
|
|
540
|
+
status: options.status,
|
|
541
|
+
hasQuery: Boolean(options.query?.trim()),
|
|
542
|
+
issueKey: options.issueKey?.trim() || undefined,
|
|
543
|
+
sessionIds: options.sessionIds?.length ?? 0,
|
|
544
|
+
limit: options.limit,
|
|
545
|
+
offset: options.offset,
|
|
546
|
+
}, () => this.dbOp("query sessions by history", () => {
|
|
547
|
+
this.ensureSessionHistoryCacheBackfilled();
|
|
548
|
+
const conditions = [];
|
|
549
|
+
const params = [];
|
|
550
|
+
if (options.host) {
|
|
551
|
+
conditions.push(`s.host = ?`);
|
|
552
|
+
params.push(options.host);
|
|
553
|
+
}
|
|
554
|
+
if (options.status) {
|
|
555
|
+
conditions.push(`s.status = ?`);
|
|
556
|
+
params.push(options.status);
|
|
557
|
+
}
|
|
558
|
+
if (options.query && options.query.trim()) {
|
|
559
|
+
const pattern = `%${escapeLikePattern(options.query.trim())}%`;
|
|
560
|
+
conditions.push(`
|
|
561
|
+
(
|
|
562
|
+
cache.titleText LIKE ? ESCAPE '\\'
|
|
563
|
+
OR cache.metadataText LIKE ? ESCAPE '\\'
|
|
564
|
+
OR cache.messagesText LIKE ? ESCAPE '\\'
|
|
565
|
+
OR cache.artifactsText LIKE ? ESCAPE '\\'
|
|
566
|
+
OR cache.narrativesText LIKE ? ESCAPE '\\'
|
|
567
|
+
OR cache.decisionsText LIKE ? ESCAPE '\\'
|
|
568
|
+
)
|
|
569
|
+
`);
|
|
570
|
+
params.push(pattern, pattern, pattern, pattern, pattern, pattern);
|
|
571
|
+
}
|
|
572
|
+
if (options.issueKey && options.issueKey.trim()) {
|
|
573
|
+
conditions.push(`
|
|
574
|
+
EXISTS (
|
|
575
|
+
SELECT 1
|
|
576
|
+
FROM session_issue_keys issue_keys
|
|
577
|
+
WHERE issue_keys.sessionId = s.id
|
|
578
|
+
AND issue_keys.issueKey = ?
|
|
579
|
+
)
|
|
580
|
+
`);
|
|
581
|
+
params.push(options.issueKey.trim());
|
|
582
|
+
}
|
|
583
|
+
if (options.sessionIds && options.sessionIds.length > 0) {
|
|
584
|
+
const normalizedIds = options.sessionIds
|
|
585
|
+
.map((sessionId) => sessionId.trim())
|
|
586
|
+
.filter(Boolean);
|
|
587
|
+
if (normalizedIds.length === 0) {
|
|
588
|
+
return {
|
|
589
|
+
sessions: [],
|
|
590
|
+
total: 0,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
conditions.push(`s.id IN (${normalizedIds.map(() => "?").join(", ")})`);
|
|
594
|
+
params.push(...normalizedIds);
|
|
595
|
+
}
|
|
596
|
+
const whereSql = conditions.length
|
|
597
|
+
? `WHERE ${conditions.join(" AND ")}`
|
|
598
|
+
: "";
|
|
599
|
+
const fromSql = `
|
|
600
|
+
FROM sessions s
|
|
601
|
+
LEFT JOIN session_history_cache cache ON cache.sessionId = s.id
|
|
602
|
+
${whereSql}
|
|
603
|
+
`;
|
|
604
|
+
const total = this.db
|
|
605
|
+
.prepare(`SELECT COUNT(*) as total ${fromSql}`)
|
|
606
|
+
.get(...params)?.total ?? 0;
|
|
607
|
+
let query = `
|
|
608
|
+
SELECT s.*
|
|
609
|
+
${fromSql}
|
|
610
|
+
ORDER BY s.startedAt DESC, s.id DESC
|
|
611
|
+
`;
|
|
612
|
+
const pageParams = [...params];
|
|
613
|
+
query = appendPaginationClause(query, pageParams, options.limit, options.offset);
|
|
614
|
+
const rows = this.db
|
|
615
|
+
.prepare(query)
|
|
616
|
+
.all(...pageParams);
|
|
617
|
+
return {
|
|
618
|
+
sessions: rows.map((row) => this.rowToSession(row)),
|
|
619
|
+
total,
|
|
620
|
+
};
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
querySessionTrendAttempts(options) {
|
|
624
|
+
return traceSyncOperation("db.query-session-trend-attempts", {
|
|
625
|
+
host: options?.host,
|
|
626
|
+
status: options?.status,
|
|
627
|
+
sessionIds: options?.sessionIds?.length ?? 0,
|
|
628
|
+
}, () => this.dbOp("query session trend attempts", () => {
|
|
629
|
+
this.ensureSessionTrendAttemptsBackfilled();
|
|
630
|
+
const conditions = [];
|
|
631
|
+
const params = [];
|
|
632
|
+
if (options?.host) {
|
|
633
|
+
conditions.push(`s.host = ?`);
|
|
634
|
+
params.push(options.host);
|
|
635
|
+
}
|
|
636
|
+
if (options?.status) {
|
|
637
|
+
conditions.push(`s.status = ?`);
|
|
638
|
+
params.push(options.status);
|
|
639
|
+
}
|
|
640
|
+
if (options?.sessionIds && options.sessionIds.length > 0) {
|
|
641
|
+
const normalizedIds = options.sessionIds
|
|
642
|
+
.map((sessionId) => sessionId.trim())
|
|
643
|
+
.filter(Boolean);
|
|
644
|
+
if (normalizedIds.length === 0) {
|
|
645
|
+
return [];
|
|
646
|
+
}
|
|
647
|
+
conditions.push(`ta.sessionId IN (${normalizedIds.map(() => "?").join(", ")})`);
|
|
648
|
+
params.push(...normalizedIds);
|
|
649
|
+
}
|
|
650
|
+
const whereSql = conditions.length
|
|
651
|
+
? `WHERE ${conditions.join(" AND ")}`
|
|
652
|
+
: "";
|
|
653
|
+
return this.db
|
|
654
|
+
.prepare(`
|
|
655
|
+
SELECT
|
|
656
|
+
ta.*,
|
|
657
|
+
s.host,
|
|
658
|
+
s.status,
|
|
659
|
+
s.cwd,
|
|
660
|
+
s.startedAt,
|
|
661
|
+
s.endedAt,
|
|
662
|
+
s.title
|
|
663
|
+
FROM session_trend_attempts ta
|
|
664
|
+
INNER JOIN sessions s ON s.id = ta.sessionId
|
|
665
|
+
${whereSql}
|
|
666
|
+
ORDER BY ta.seenAt DESC, ta.artifactId DESC
|
|
667
|
+
`)
|
|
668
|
+
.all(...params);
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
querySessionTrendContextAttempts(sessionId) {
|
|
672
|
+
return this.dbOp("query session trend context attempts", () => {
|
|
673
|
+
const normalizedSessionId = sessionId.trim();
|
|
674
|
+
if (!normalizedSessionId) {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
this.ensureSessionTrendAttemptsBackfilled();
|
|
678
|
+
return this.db
|
|
679
|
+
.prepare(`
|
|
680
|
+
WITH target_issue_keys AS (
|
|
681
|
+
SELECT DISTINCT issueKey
|
|
682
|
+
FROM session_trend_attempts
|
|
683
|
+
WHERE sessionId = ?
|
|
684
|
+
)
|
|
685
|
+
SELECT
|
|
686
|
+
ta.*,
|
|
687
|
+
s.host,
|
|
688
|
+
s.status,
|
|
689
|
+
s.cwd,
|
|
690
|
+
s.startedAt,
|
|
691
|
+
s.endedAt,
|
|
692
|
+
s.title
|
|
693
|
+
FROM session_trend_attempts ta
|
|
694
|
+
INNER JOIN target_issue_keys tik ON tik.issueKey = ta.issueKey
|
|
695
|
+
INNER JOIN sessions s ON s.id = ta.sessionId
|
|
696
|
+
ORDER BY ta.issueKey ASC, ta.seenAt DESC, ta.artifactId DESC
|
|
697
|
+
`)
|
|
698
|
+
.all(normalizedSessionId);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
getSessionFollowUpMessages(sessionIds, options) {
|
|
702
|
+
return traceSyncOperation("db.get-session-follow-up-messages", {
|
|
703
|
+
sessionIds: sessionIds.length,
|
|
704
|
+
limit: options?.limit,
|
|
705
|
+
offset: options?.offset,
|
|
706
|
+
}, () => this.dbOp("get session follow-up messages", () => {
|
|
707
|
+
const normalizedIds = sessionIds
|
|
708
|
+
.map((sessionId) => sessionId.trim())
|
|
709
|
+
.filter(Boolean);
|
|
710
|
+
if (normalizedIds.length === 0) {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
const params = [...normalizedIds];
|
|
714
|
+
let query = `
|
|
715
|
+
SELECT id, sessionId, content, capturedAt, seq
|
|
716
|
+
FROM messages
|
|
717
|
+
WHERE sessionId IN (${normalizedIds.map(() => "?").join(", ")})
|
|
718
|
+
AND (
|
|
719
|
+
content LIKE '%?%'
|
|
720
|
+
OR lower(content) LIKE 'next:%'
|
|
721
|
+
)
|
|
722
|
+
ORDER BY capturedAt DESC, sessionId DESC, seq DESC, id DESC
|
|
723
|
+
`;
|
|
724
|
+
query = appendPaginationClause(query, params, options?.limit, options?.offset);
|
|
725
|
+
const rows = this.db
|
|
726
|
+
.prepare(query)
|
|
727
|
+
.all(...params);
|
|
728
|
+
return rows.map((row) => ({
|
|
729
|
+
sessionId: row.sessionId,
|
|
730
|
+
content: row.content,
|
|
731
|
+
capturedAt: row.capturedAt,
|
|
732
|
+
}));
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
createSession(session) {
|
|
736
|
+
const id = crypto.randomUUID();
|
|
737
|
+
const now = new Date().toISOString();
|
|
738
|
+
return this.dbOp("create session", () => {
|
|
739
|
+
const stmt = this.db.prepare(`
|
|
740
|
+
INSERT INTO sessions (
|
|
741
|
+
id, host, projectRoot, cwd, title, status,
|
|
742
|
+
startedAt, endedAt, metadata, createdAt, updatedAt
|
|
743
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
744
|
+
`);
|
|
745
|
+
stmt.run(id, session.host, session.projectRoot, session.cwd, session.title, session.status, session.startedAt, session.endedAt, session.metadata, now, now);
|
|
746
|
+
this.db
|
|
747
|
+
.prepare(`
|
|
748
|
+
INSERT INTO session_history_cache (
|
|
749
|
+
sessionId, titleText, metadataText, messagesText,
|
|
750
|
+
artifactsText, narrativesText, decisionsText, updatedAt
|
|
751
|
+
) VALUES (?, ?, ?, '', '', '', '', ?)
|
|
752
|
+
`)
|
|
753
|
+
.run(id, session.title ?? "", session.metadata ?? "", now);
|
|
754
|
+
this.markSessionTrendAttemptsFresh(id);
|
|
755
|
+
return id;
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
appendMessage(message) {
|
|
759
|
+
const id = crypto.randomUUID();
|
|
760
|
+
return this.dbOp("append message", () => {
|
|
761
|
+
const stmt = this.db.prepare(`
|
|
762
|
+
INSERT INTO messages (
|
|
763
|
+
id, sessionId, seq, role, source, content, capturedAt, metadata
|
|
764
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
765
|
+
`);
|
|
766
|
+
stmt.run(id, message.sessionId, message.seq, message.role, message.source, message.content, message.capturedAt, message.metadata);
|
|
767
|
+
const currentCache = this.db
|
|
768
|
+
.prepare(`
|
|
769
|
+
SELECT messagesText FROM session_history_cache
|
|
770
|
+
WHERE sessionId = ?
|
|
771
|
+
`)
|
|
772
|
+
.get(message.sessionId);
|
|
773
|
+
this.updateSessionHistoryCache(message.sessionId, {
|
|
774
|
+
messagesText: EvidenceDatabase.appendSearchPart(currentCache?.messagesText ?? "", message.content),
|
|
775
|
+
});
|
|
776
|
+
return id;
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
appendTimelineEvent(event) {
|
|
780
|
+
const id = crypto.randomUUID();
|
|
781
|
+
return this.dbOp("append timeline event", () => {
|
|
782
|
+
const stmt = this.db.prepare(`
|
|
783
|
+
INSERT INTO timeline_events (
|
|
784
|
+
id, sessionId, seq, eventType, eventSubType, source,
|
|
785
|
+
summary, payload, startedAt, endedAt, status, relatedMessageId
|
|
786
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
787
|
+
`);
|
|
788
|
+
stmt.run(id, event.sessionId, event.seq, event.eventType, event.eventSubType, event.source, event.summary, event.payload, event.startedAt, event.endedAt, event.status, event.relatedMessageId);
|
|
789
|
+
return id;
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
updateSessionTitle(id, title) {
|
|
793
|
+
this.dbOp("update session title", () => {
|
|
794
|
+
const stmt = this.db.prepare(`
|
|
795
|
+
UPDATE sessions
|
|
796
|
+
SET title = ?,
|
|
797
|
+
updatedAt = ?
|
|
798
|
+
WHERE id = ?
|
|
799
|
+
`);
|
|
800
|
+
const result = stmt.run(title, new Date().toISOString(), id);
|
|
801
|
+
if (result.changes === 0) {
|
|
802
|
+
throw new Error(`Session with id ${id} not found`);
|
|
803
|
+
}
|
|
804
|
+
this.updateSessionHistoryCache(id, {
|
|
805
|
+
titleText: title,
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
finalizeSession(id, updates) {
|
|
810
|
+
this.dbOp("finalize session", () => {
|
|
811
|
+
const stmt = this.db.prepare(`
|
|
812
|
+
UPDATE sessions
|
|
813
|
+
SET status = ?,
|
|
814
|
+
endedAt = ?,
|
|
815
|
+
title = COALESCE(?, title),
|
|
816
|
+
updatedAt = ?
|
|
817
|
+
WHERE id = ?
|
|
818
|
+
`);
|
|
819
|
+
const result = stmt.run(updates.status, updates.endedAt, updates.title, new Date().toISOString(), id);
|
|
820
|
+
if (result.changes === 0) {
|
|
821
|
+
throw new Error(`Session with id ${id} not found`);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
findSessionById(id) {
|
|
826
|
+
return this.dbOp("find session by ID", () => {
|
|
827
|
+
const row = this.db
|
|
828
|
+
.prepare(`SELECT * FROM sessions WHERE id = ?`)
|
|
829
|
+
.get(id);
|
|
830
|
+
return row ? this.rowToSession(row) : null;
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
listSessions(options) {
|
|
834
|
+
return this.dbOp("list sessions", () => {
|
|
835
|
+
const params = [];
|
|
836
|
+
const query = appendPaginationClause("SELECT * FROM sessions ORDER BY startedAt DESC", params, options?.limit, options?.offset);
|
|
837
|
+
const rows = this.db.prepare(query).all(...params);
|
|
838
|
+
return rows.map((row) => this.rowToSession(row));
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
createContext(context) {
|
|
842
|
+
const id = crypto.randomUUID();
|
|
843
|
+
const now = new Date().toISOString();
|
|
844
|
+
return this.dbOp("create context", () => {
|
|
845
|
+
this.db
|
|
846
|
+
.prepare(`
|
|
847
|
+
INSERT INTO contexts (
|
|
848
|
+
id, label, workspaceKey, status, mergedIntoContextId,
|
|
849
|
+
metadata, createdAt, updatedAt
|
|
850
|
+
) VALUES (?, ?, ?, 'active', NULL, ?, ?, ?)
|
|
851
|
+
`)
|
|
852
|
+
.run(id, context.label, context.workspaceKey, context.metadata ?? null, now, now);
|
|
853
|
+
return {
|
|
854
|
+
id,
|
|
855
|
+
label: context.label,
|
|
856
|
+
workspaceKey: context.workspaceKey,
|
|
857
|
+
status: "active",
|
|
858
|
+
mergedIntoContextId: null,
|
|
859
|
+
metadata: context.metadata ?? null,
|
|
860
|
+
createdAt: now,
|
|
861
|
+
updatedAt: now,
|
|
862
|
+
};
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
findContextById(id) {
|
|
866
|
+
return this.dbOp("find context by ID", () => {
|
|
867
|
+
const row = this.db
|
|
868
|
+
.prepare(`SELECT * FROM contexts WHERE id = ?`)
|
|
869
|
+
.get(id);
|
|
870
|
+
return row ? this.rowToContext(row) : null;
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
resolveContextById(id) {
|
|
98
874
|
try {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
875
|
+
const activeId = this.resolveActiveContextIdOrThrow(id);
|
|
876
|
+
return this.findContextById(activeId);
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
listContexts(options) {
|
|
883
|
+
return this.dbOp("list contexts", () => {
|
|
884
|
+
const conditions = [];
|
|
885
|
+
const params = [];
|
|
886
|
+
if (options?.workspaceKey) {
|
|
887
|
+
conditions.push("workspaceKey = ?");
|
|
888
|
+
params.push(options.workspaceKey);
|
|
889
|
+
}
|
|
890
|
+
if (options?.status) {
|
|
891
|
+
conditions.push("status = ?");
|
|
892
|
+
params.push(options.status);
|
|
893
|
+
}
|
|
894
|
+
else if (!options?.includeMerged) {
|
|
895
|
+
conditions.push("status = 'active'");
|
|
896
|
+
}
|
|
897
|
+
const whereSql = conditions.length
|
|
898
|
+
? `WHERE ${conditions.join(" AND ")}`
|
|
899
|
+
: "";
|
|
900
|
+
const query = appendPaginationClause(`
|
|
901
|
+
SELECT *
|
|
902
|
+
FROM contexts
|
|
903
|
+
${whereSql}
|
|
904
|
+
ORDER BY updatedAt DESC, createdAt DESC, id DESC
|
|
905
|
+
`, params, options?.limit, options?.offset);
|
|
906
|
+
const rows = this.db.prepare(query).all(...params);
|
|
907
|
+
return rows.map((row) => this.rowToContext(row));
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
getContextCount(options) {
|
|
911
|
+
return this.dbOp("count contexts", () => {
|
|
912
|
+
const conditions = [];
|
|
913
|
+
const params = [];
|
|
914
|
+
if (options?.workspaceKey) {
|
|
915
|
+
conditions.push("workspaceKey = ?");
|
|
916
|
+
params.push(options.workspaceKey);
|
|
917
|
+
}
|
|
918
|
+
if (options?.status) {
|
|
919
|
+
conditions.push("status = ?");
|
|
920
|
+
params.push(options.status);
|
|
921
|
+
}
|
|
922
|
+
else if (!options?.includeMerged) {
|
|
923
|
+
conditions.push("status = 'active'");
|
|
924
|
+
}
|
|
925
|
+
const whereSql = conditions.length
|
|
926
|
+
? `WHERE ${conditions.join(" AND ")}`
|
|
927
|
+
: "";
|
|
928
|
+
const row = this.db
|
|
929
|
+
.prepare(`SELECT COUNT(*) as count FROM contexts ${whereSql}`)
|
|
930
|
+
.get(...params);
|
|
931
|
+
return row?.count ?? 0;
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
listSessionsForContext(contextId) {
|
|
935
|
+
return this.dbOp("list sessions for context", () => {
|
|
936
|
+
const activeId = this.resolveActiveContextIdOrThrow(contextId);
|
|
937
|
+
const rows = this.db
|
|
938
|
+
.prepare(`
|
|
939
|
+
SELECT s.*
|
|
940
|
+
FROM context_session_links links
|
|
941
|
+
INNER JOIN sessions s ON s.id = links.sessionId
|
|
942
|
+
WHERE links.contextId = ?
|
|
943
|
+
ORDER BY s.startedAt ASC, s.id ASC
|
|
944
|
+
`)
|
|
945
|
+
.all(activeId);
|
|
946
|
+
return rows.map((row) => this.rowToSession(row));
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
listUnlinkedSessions(options) {
|
|
950
|
+
return this.dbOp("list unlinked sessions", () => {
|
|
951
|
+
const conditions = [
|
|
952
|
+
"NOT EXISTS (SELECT 1 FROM context_session_links links WHERE links.sessionId = s.id)",
|
|
953
|
+
];
|
|
102
954
|
const params = [];
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
955
|
+
let query = `
|
|
956
|
+
SELECT s.*
|
|
957
|
+
FROM sessions s
|
|
958
|
+
WHERE ${conditions.join(" AND ")}
|
|
959
|
+
ORDER BY s.startedAt ASC, s.id ASC
|
|
960
|
+
`;
|
|
961
|
+
if (!options?.workspaceKey) {
|
|
962
|
+
query = appendPaginationClause(query, params, options?.limit, options?.offset);
|
|
963
|
+
}
|
|
964
|
+
const rows = this.db.prepare(query).all(...params);
|
|
965
|
+
let sessions = rows.map((row) => this.rowToSession(row));
|
|
966
|
+
if (options?.workspaceKey) {
|
|
967
|
+
sessions = sessions.filter((session) => matchesWorkspaceKey(session, options.workspaceKey));
|
|
968
|
+
const off = options.offset ?? 0;
|
|
969
|
+
if (off > 0) {
|
|
970
|
+
sessions = sessions.slice(off);
|
|
971
|
+
}
|
|
972
|
+
if (options.limit !== undefined) {
|
|
973
|
+
sessions = sessions.slice(0, options.limit);
|
|
109
974
|
}
|
|
110
975
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
976
|
+
return sessions;
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
findContextLinkForSession(sessionId) {
|
|
980
|
+
return this.dbOp("find context link for session", () => {
|
|
981
|
+
const row = this.db
|
|
982
|
+
.prepare(`
|
|
983
|
+
SELECT *
|
|
984
|
+
FROM context_session_links
|
|
985
|
+
WHERE sessionId = ?
|
|
986
|
+
`)
|
|
987
|
+
.get(sessionId);
|
|
988
|
+
return row ? this.rowToContextSessionLink(row) : null;
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
listContextSessionLinks(contextId) {
|
|
992
|
+
return this.dbOp("list context session links", () => {
|
|
993
|
+
const activeId = this.resolveActiveContextIdOrThrow(contextId);
|
|
994
|
+
const rows = this.db
|
|
995
|
+
.prepare(`
|
|
996
|
+
SELECT *
|
|
997
|
+
FROM context_session_links
|
|
998
|
+
WHERE contextId = ?
|
|
999
|
+
ORDER BY updatedAt DESC, sessionId DESC
|
|
1000
|
+
`)
|
|
1001
|
+
.all(activeId);
|
|
1002
|
+
return rows.map((row) => this.rowToContextSessionLink(row));
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
assignSessionToContext(input) {
|
|
1006
|
+
const now = new Date().toISOString();
|
|
1007
|
+
return this.dbOp("assign session to context", () => {
|
|
1008
|
+
const session = this.findSessionById(input.sessionId);
|
|
1009
|
+
if (!session) {
|
|
1010
|
+
throw new Error(`Session not found: ${input.sessionId}`);
|
|
1011
|
+
}
|
|
1012
|
+
const activeContextId = this.resolveActiveContextIdOrThrow(input.contextId);
|
|
1013
|
+
this.db
|
|
1014
|
+
.prepare(`
|
|
1015
|
+
INSERT INTO context_session_links (
|
|
1016
|
+
sessionId, contextId, linkSource, createdAt, updatedAt
|
|
1017
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
1018
|
+
ON CONFLICT(sessionId) DO UPDATE SET
|
|
1019
|
+
contextId = excluded.contextId,
|
|
1020
|
+
linkSource = excluded.linkSource,
|
|
1021
|
+
updatedAt = excluded.updatedAt
|
|
1022
|
+
`)
|
|
1023
|
+
.run(input.sessionId, activeContextId, input.linkSource, now, now);
|
|
1024
|
+
this.db
|
|
1025
|
+
.prepare(`
|
|
1026
|
+
DELETE FROM context_link_rejections
|
|
1027
|
+
WHERE sessionId = ? AND contextId = ?
|
|
1028
|
+
`)
|
|
1029
|
+
.run(input.sessionId, activeContextId);
|
|
1030
|
+
this.db
|
|
1031
|
+
.prepare(`
|
|
1032
|
+
UPDATE contexts
|
|
1033
|
+
SET updatedAt = ?
|
|
1034
|
+
WHERE id = ?
|
|
1035
|
+
`)
|
|
1036
|
+
.run(now, activeContextId);
|
|
1037
|
+
return {
|
|
1038
|
+
sessionId: input.sessionId,
|
|
1039
|
+
contextId: activeContextId,
|
|
1040
|
+
linkSource: input.linkSource,
|
|
1041
|
+
createdAt: now,
|
|
1042
|
+
updatedAt: now,
|
|
1043
|
+
};
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
rejectContextForSession(sessionId, contextId) {
|
|
1047
|
+
const now = new Date().toISOString();
|
|
1048
|
+
return this.dbOp("reject context for session", () => {
|
|
1049
|
+
if (!this.findSessionById(sessionId)) {
|
|
1050
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
1051
|
+
}
|
|
1052
|
+
const activeContextId = this.resolveActiveContextIdOrThrow(contextId);
|
|
1053
|
+
this.db
|
|
1054
|
+
.prepare(`
|
|
1055
|
+
INSERT INTO context_link_rejections (sessionId, contextId, createdAt)
|
|
1056
|
+
VALUES (?, ?, ?)
|
|
1057
|
+
ON CONFLICT(sessionId, contextId) DO NOTHING
|
|
1058
|
+
`)
|
|
1059
|
+
.run(sessionId, activeContextId, now);
|
|
1060
|
+
return {
|
|
1061
|
+
sessionId,
|
|
1062
|
+
contextId: activeContextId,
|
|
1063
|
+
createdAt: now,
|
|
1064
|
+
};
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
listContextRejectionsForSession(sessionId) {
|
|
1068
|
+
return this.dbOp("list context rejections", () => {
|
|
1069
|
+
const rows = this.db
|
|
1070
|
+
.prepare(`
|
|
1071
|
+
SELECT *
|
|
1072
|
+
FROM context_link_rejections
|
|
1073
|
+
WHERE sessionId = ?
|
|
1074
|
+
ORDER BY createdAt DESC, contextId DESC
|
|
1075
|
+
`)
|
|
1076
|
+
.all(sessionId);
|
|
1077
|
+
return rows.map((row) => this.rowToContextLinkRejection(row));
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
setWorkspacePreferredContext(workspaceKey, contextId) {
|
|
1081
|
+
const now = new Date().toISOString();
|
|
1082
|
+
return this.dbOp("set preferred context", () => {
|
|
1083
|
+
const activeContextId = this.resolveActiveContextIdOrThrow(contextId);
|
|
1084
|
+
this.db
|
|
1085
|
+
.prepare(`
|
|
1086
|
+
INSERT INTO context_workspace_preferences (
|
|
1087
|
+
workspaceKey, contextId, createdAt, updatedAt
|
|
1088
|
+
) VALUES (?, ?, ?, ?)
|
|
1089
|
+
ON CONFLICT(workspaceKey) DO UPDATE SET
|
|
1090
|
+
contextId = excluded.contextId,
|
|
1091
|
+
updatedAt = excluded.updatedAt
|
|
1092
|
+
`)
|
|
1093
|
+
.run(workspaceKey, activeContextId, now, now);
|
|
1094
|
+
return {
|
|
1095
|
+
workspaceKey,
|
|
1096
|
+
contextId: activeContextId,
|
|
1097
|
+
createdAt: now,
|
|
1098
|
+
updatedAt: now,
|
|
1099
|
+
};
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
getWorkspacePreferredContext(workspaceKey) {
|
|
1103
|
+
return this.dbOp("get preferred context", () => {
|
|
1104
|
+
const row = this.db
|
|
1105
|
+
.prepare(`
|
|
1106
|
+
SELECT *
|
|
1107
|
+
FROM context_workspace_preferences
|
|
1108
|
+
WHERE workspaceKey = ?
|
|
1109
|
+
`)
|
|
1110
|
+
.get(workspaceKey);
|
|
1111
|
+
if (!row) {
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
const activeContextId = this.resolveActiveContextIdOrThrow(row.contextId);
|
|
1116
|
+
if (activeContextId !== row.contextId) {
|
|
1117
|
+
const now = new Date().toISOString();
|
|
1118
|
+
this.db
|
|
1119
|
+
.prepare(`
|
|
1120
|
+
UPDATE context_workspace_preferences
|
|
1121
|
+
SET contextId = ?, updatedAt = ?
|
|
1122
|
+
WHERE workspaceKey = ?
|
|
1123
|
+
`)
|
|
1124
|
+
.run(activeContextId, now, workspaceKey);
|
|
1125
|
+
return this.rowToContextWorkspacePreference({
|
|
1126
|
+
...row,
|
|
1127
|
+
contextId: activeContextId,
|
|
1128
|
+
updatedAt: now,
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
return this.rowToContextWorkspacePreference(row);
|
|
1134
|
+
}
|
|
1135
|
+
return this.rowToContextWorkspacePreference(row);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
mergeContexts(sourceContextId, targetContextId) {
|
|
1139
|
+
this.dbOp("merge contexts", () => {
|
|
1140
|
+
const sourceId = this.resolveActiveContextIdOrThrow(sourceContextId);
|
|
1141
|
+
const targetId = this.resolveActiveContextIdOrThrow(targetContextId);
|
|
1142
|
+
if (sourceId === targetId) {
|
|
1143
|
+
throw new Error("Cannot merge a context into itself");
|
|
1144
|
+
}
|
|
1145
|
+
const now = new Date().toISOString();
|
|
1146
|
+
const transaction = this.db.transaction(() => {
|
|
1147
|
+
this.db
|
|
1148
|
+
.prepare(`
|
|
1149
|
+
UPDATE context_session_links
|
|
1150
|
+
SET contextId = ?, linkSource = 'merge', updatedAt = ?
|
|
1151
|
+
WHERE contextId = ?
|
|
1152
|
+
`)
|
|
1153
|
+
.run(targetId, now, sourceId);
|
|
1154
|
+
this.db
|
|
1155
|
+
.prepare(`
|
|
1156
|
+
UPDATE context_workspace_preferences
|
|
1157
|
+
SET contextId = ?, updatedAt = ?
|
|
1158
|
+
WHERE contextId = ?
|
|
1159
|
+
`)
|
|
1160
|
+
.run(targetId, now, sourceId);
|
|
1161
|
+
this.db
|
|
1162
|
+
.prepare(`
|
|
1163
|
+
UPDATE contexts
|
|
1164
|
+
SET status = 'merged',
|
|
1165
|
+
mergedIntoContextId = ?,
|
|
1166
|
+
updatedAt = ?
|
|
1167
|
+
WHERE id = ?
|
|
1168
|
+
`)
|
|
1169
|
+
.run(targetId, now, sourceId);
|
|
1170
|
+
this.db
|
|
1171
|
+
.prepare(`
|
|
1172
|
+
UPDATE contexts
|
|
1173
|
+
SET updatedAt = ?
|
|
1174
|
+
WHERE id = ?
|
|
1175
|
+
`)
|
|
1176
|
+
.run(now, targetId);
|
|
1177
|
+
});
|
|
1178
|
+
transaction();
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
countSessionMessages(sessionId) {
|
|
1182
|
+
return this.dbOp("count session messages", () => {
|
|
1183
|
+
const row = this.db
|
|
1184
|
+
.prepare(`SELECT COUNT(*) as total FROM messages WHERE sessionId = ?`)
|
|
1185
|
+
.get(sessionId);
|
|
1186
|
+
return row?.total ?? 0;
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
getSessionMessageStats(sessionId) {
|
|
1190
|
+
return this.dbOp("summarize session messages", () => {
|
|
1191
|
+
const summaryRow = this.db
|
|
1192
|
+
.prepare(`
|
|
1193
|
+
SELECT
|
|
1194
|
+
COUNT(*) as total,
|
|
1195
|
+
COALESCE(SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END), 0) as userCount,
|
|
1196
|
+
COALESCE(SUM(CASE WHEN role = 'assistant' THEN 1 ELSE 0 END), 0) as assistantCount,
|
|
1197
|
+
COALESCE(SUM(CASE WHEN role = 'system' THEN 1 ELSE 0 END), 0) as systemCount,
|
|
1198
|
+
MIN(capturedAt) as firstCapturedAt,
|
|
1199
|
+
MAX(capturedAt) as lastCapturedAt
|
|
1200
|
+
FROM messages
|
|
1201
|
+
WHERE sessionId = ?
|
|
1202
|
+
`)
|
|
1203
|
+
.get(sessionId);
|
|
1204
|
+
const previewRow = this.db
|
|
1205
|
+
.prepare(`
|
|
1206
|
+
SELECT content FROM messages
|
|
1207
|
+
WHERE sessionId = ?
|
|
1208
|
+
ORDER BY seq ASC, capturedAt ASC
|
|
1209
|
+
LIMIT 1
|
|
1210
|
+
`)
|
|
1211
|
+
.get(sessionId);
|
|
1212
|
+
return {
|
|
1213
|
+
total: summaryRow?.total ?? 0,
|
|
1214
|
+
byRole: {
|
|
1215
|
+
user: summaryRow?.userCount ?? 0,
|
|
1216
|
+
assistant: summaryRow?.assistantCount ?? 0,
|
|
1217
|
+
system: summaryRow?.systemCount ?? 0,
|
|
1218
|
+
},
|
|
1219
|
+
firstCapturedAt: summaryRow?.firstCapturedAt ?? null,
|
|
1220
|
+
lastCapturedAt: summaryRow?.lastCapturedAt ?? null,
|
|
1221
|
+
previewContent: previewRow?.content ?? null,
|
|
1222
|
+
};
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
getSessionMessages(sessionId, options) {
|
|
1226
|
+
return this.dbOp("get session messages", () => {
|
|
1227
|
+
const params = [sessionId];
|
|
1228
|
+
const query = appendPaginationClause("SELECT * FROM messages WHERE sessionId = ? ORDER BY seq ASC, capturedAt ASC", params, options?.limit, options?.offset);
|
|
1229
|
+
const rows = this.db.prepare(query).all(...params);
|
|
1230
|
+
return rows.map((row) => this.rowToMessage(row));
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
countSessionTimeline(sessionId) {
|
|
1234
|
+
return this.dbOp("count session timeline", () => {
|
|
1235
|
+
const row = this.db
|
|
1236
|
+
.prepare(`SELECT COUNT(*) as total FROM timeline_events WHERE sessionId = ?`)
|
|
1237
|
+
.get(sessionId);
|
|
1238
|
+
return row?.total ?? 0;
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
getSessionTimelineStats(sessionId) {
|
|
1242
|
+
return this.dbOp("summarize session timeline", () => {
|
|
1243
|
+
const summaryRow = this.db
|
|
1244
|
+
.prepare(`
|
|
1245
|
+
SELECT
|
|
1246
|
+
COUNT(*) as total,
|
|
1247
|
+
MIN(startedAt) as firstStartedAt,
|
|
1248
|
+
MAX(COALESCE(endedAt, startedAt)) as lastEndedAt
|
|
1249
|
+
FROM timeline_events
|
|
1250
|
+
WHERE sessionId = ?
|
|
1251
|
+
`)
|
|
1252
|
+
.get(sessionId);
|
|
1253
|
+
const eventTypeRows = this.db
|
|
1254
|
+
.prepare(`
|
|
1255
|
+
SELECT eventType FROM timeline_events
|
|
1256
|
+
WHERE sessionId = ?
|
|
1257
|
+
GROUP BY eventType
|
|
1258
|
+
ORDER BY MIN(seq) ASC, MIN(startedAt) ASC
|
|
1259
|
+
`)
|
|
1260
|
+
.all(sessionId);
|
|
1261
|
+
const statusRows = this.db
|
|
1262
|
+
.prepare(`
|
|
1263
|
+
SELECT status FROM timeline_events
|
|
1264
|
+
WHERE sessionId = ? AND status IS NOT NULL
|
|
1265
|
+
GROUP BY status
|
|
1266
|
+
ORDER BY MIN(seq) ASC, MIN(startedAt) ASC
|
|
1267
|
+
`)
|
|
1268
|
+
.all(sessionId);
|
|
1269
|
+
return {
|
|
1270
|
+
total: summaryRow?.total ?? 0,
|
|
1271
|
+
eventTypes: eventTypeRows.map((row) => row.eventType),
|
|
1272
|
+
statuses: statusRows.map((row) => row.status),
|
|
1273
|
+
firstStartedAt: summaryRow?.firstStartedAt ?? null,
|
|
1274
|
+
lastEndedAt: summaryRow?.lastEndedAt ?? null,
|
|
1275
|
+
};
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
getSessionTimeline(sessionId, options) {
|
|
1279
|
+
return this.dbOp("get session timeline", () => {
|
|
1280
|
+
const params = [sessionId];
|
|
1281
|
+
const query = appendPaginationClause(`
|
|
1282
|
+
SELECT * FROM timeline_events
|
|
1283
|
+
WHERE sessionId = ?
|
|
1284
|
+
ORDER BY seq ASC, startedAt ASC
|
|
1285
|
+
`, params, options?.limit, options?.offset);
|
|
1286
|
+
const rows = this.db.prepare(query).all(...params);
|
|
1287
|
+
return rows.map((row) => this.rowToTimelineEvent(row));
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
createArtifact(artifact) {
|
|
1291
|
+
const id = crypto.randomUUID();
|
|
1292
|
+
const createdAt = new Date().toISOString();
|
|
1293
|
+
return this.dbOp("create artifact", () => {
|
|
1294
|
+
this.db
|
|
1295
|
+
.prepare(`
|
|
1296
|
+
INSERT INTO artifacts (
|
|
1297
|
+
id, sessionId, eventId, artifactType, path, metadata, createdAt
|
|
1298
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1299
|
+
`)
|
|
1300
|
+
.run(id, artifact.sessionId, artifact.eventId, artifact.artifactType, artifact.path, artifact.metadata, createdAt);
|
|
1301
|
+
const artifactRecord = {
|
|
1302
|
+
id,
|
|
1303
|
+
sessionId: artifact.sessionId,
|
|
1304
|
+
eventId: artifact.eventId,
|
|
1305
|
+
artifactType: artifact.artifactType,
|
|
1306
|
+
path: artifact.path,
|
|
1307
|
+
metadata: artifact.metadata,
|
|
1308
|
+
createdAt,
|
|
1309
|
+
};
|
|
1310
|
+
const artifactCache = this.buildArtifactHistoryCache([artifactRecord]);
|
|
1311
|
+
const currentCache = this.db
|
|
1312
|
+
.prepare(`
|
|
1313
|
+
SELECT artifactsText FROM session_history_cache
|
|
1314
|
+
WHERE sessionId = ?
|
|
1315
|
+
`)
|
|
1316
|
+
.get(artifact.sessionId);
|
|
1317
|
+
this.updateSessionHistoryCache(artifact.sessionId, {
|
|
1318
|
+
artifactsText: EvidenceDatabase.appendSearchPart(currentCache?.artifactsText ?? "", artifactCache.text),
|
|
1319
|
+
});
|
|
1320
|
+
for (const issueKey of artifactCache.issueKeys) {
|
|
1321
|
+
this.db
|
|
1322
|
+
.prepare(`
|
|
1323
|
+
INSERT INTO session_issue_keys (sessionId, issueKey)
|
|
1324
|
+
VALUES (?, ?)
|
|
1325
|
+
ON CONFLICT(sessionId, issueKey) DO NOTHING
|
|
1326
|
+
`)
|
|
1327
|
+
.run(artifact.sessionId, issueKey);
|
|
1328
|
+
}
|
|
1329
|
+
this.insertSessionTrendAttempts(artifact.sessionId, [artifactRecord]);
|
|
1330
|
+
return artifactRecord;
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
replaceArtifactsForSession(sessionId, artifacts) {
|
|
1334
|
+
return this.dbOp("replace artifacts", () => {
|
|
1335
|
+
const transaction = this.db.transaction(() => {
|
|
1336
|
+
this.db
|
|
1337
|
+
.prepare(`DELETE FROM artifacts WHERE sessionId = ?`)
|
|
1338
|
+
.run(sessionId);
|
|
1339
|
+
const insertArtifact = this.db.prepare(`
|
|
1340
|
+
INSERT INTO artifacts (
|
|
1341
|
+
id, sessionId, eventId, artifactType, path, metadata, createdAt
|
|
1342
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1343
|
+
`);
|
|
1344
|
+
const createdArtifacts = artifacts.map((artifact) => {
|
|
1345
|
+
const id = crypto.randomUUID();
|
|
1346
|
+
const createdAt = new Date().toISOString();
|
|
1347
|
+
insertArtifact.run(id, sessionId, artifact.eventId, artifact.artifactType, artifact.path, artifact.metadata, createdAt);
|
|
1348
|
+
return {
|
|
1349
|
+
id,
|
|
1350
|
+
sessionId,
|
|
1351
|
+
eventId: artifact.eventId,
|
|
1352
|
+
artifactType: artifact.artifactType,
|
|
1353
|
+
path: artifact.path,
|
|
1354
|
+
metadata: artifact.metadata,
|
|
1355
|
+
createdAt,
|
|
1356
|
+
};
|
|
1357
|
+
});
|
|
1358
|
+
const artifactCache = this.buildArtifactHistoryCache(createdArtifacts);
|
|
1359
|
+
this.updateSessionHistoryCache(sessionId, {
|
|
1360
|
+
artifactsText: artifactCache.text,
|
|
1361
|
+
});
|
|
1362
|
+
this.replaceSessionIssueKeys(sessionId, artifactCache.issueKeys);
|
|
1363
|
+
this.replaceSessionTrendAttempts(sessionId, createdArtifacts);
|
|
1364
|
+
return createdArtifacts;
|
|
1365
|
+
});
|
|
1366
|
+
return transaction();
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
countSessionArtifacts(sessionId, options) {
|
|
1370
|
+
return this.dbOp("count session artifacts", () => {
|
|
1371
|
+
const row = (options?.artifactType
|
|
1372
|
+
? this.db
|
|
1373
|
+
.prepare(`SELECT COUNT(*) as count FROM artifacts WHERE sessionId = ? AND artifactType = ?`)
|
|
1374
|
+
.get(sessionId, options.artifactType)
|
|
1375
|
+
: this.db
|
|
1376
|
+
.prepare(`SELECT COUNT(*) as count FROM artifacts WHERE sessionId = ?`)
|
|
1377
|
+
.get(sessionId));
|
|
1378
|
+
return row?.count ?? 0;
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
getSessionArtifactSummary(sessionId) {
|
|
1382
|
+
return this.dbOp("summarize session artifacts", () => {
|
|
1383
|
+
const row = this.db
|
|
1384
|
+
.prepare(`
|
|
1385
|
+
SELECT
|
|
1386
|
+
COUNT(*) as total,
|
|
1387
|
+
COALESCE(SUM(CASE WHEN artifactType = 'file-change' THEN 1 ELSE 0 END), 0) as fileChange,
|
|
1388
|
+
COALESCE(SUM(CASE WHEN artifactType = 'command-output' THEN 1 ELSE 0 END), 0) as commandOutput,
|
|
1389
|
+
COALESCE(SUM(CASE WHEN artifactType = 'test-result' THEN 1 ELSE 0 END), 0) as testResult,
|
|
1390
|
+
COALESCE(SUM(CASE WHEN artifactType = 'git-commit' THEN 1 ELSE 0 END), 0) as gitCommit
|
|
1391
|
+
FROM artifacts
|
|
1392
|
+
WHERE sessionId = ?
|
|
1393
|
+
`)
|
|
1394
|
+
.get(sessionId);
|
|
1395
|
+
return {
|
|
1396
|
+
total: row?.total ?? 0,
|
|
1397
|
+
byType: {
|
|
1398
|
+
fileChange: row?.fileChange ?? 0,
|
|
1399
|
+
commandOutput: row?.commandOutput ?? 0,
|
|
1400
|
+
testResult: row?.testResult ?? 0,
|
|
1401
|
+
gitCommit: row?.gitCommit ?? 0,
|
|
1402
|
+
},
|
|
1403
|
+
};
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
getSessionArtifacts(sessionId, options) {
|
|
1407
|
+
return this.dbOp("get session artifacts", () => {
|
|
1408
|
+
const conditions = ["sessionId = ?"];
|
|
1409
|
+
const parameters = [sessionId];
|
|
1410
|
+
if (options?.artifactType) {
|
|
1411
|
+
conditions.push("artifactType = ?");
|
|
1412
|
+
parameters.push(options.artifactType);
|
|
1413
|
+
}
|
|
1414
|
+
const query = appendPaginationClause(`SELECT * FROM artifacts WHERE ${conditions.join(" AND ")} ORDER BY createdAt ASC, id ASC`, parameters, options?.limit, options?.offset);
|
|
1415
|
+
const rows = this.db.prepare(query).all(...parameters);
|
|
1416
|
+
return rows.map((row) => this.rowToArtifact(row));
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
replaceDecisionsForSession(sessionId, decisions) {
|
|
1420
|
+
return this.dbOp("replace decisions", () => {
|
|
1421
|
+
const transaction = this.db.transaction(() => {
|
|
1422
|
+
this.db
|
|
1423
|
+
.prepare(`DELETE FROM decisions WHERE sessionId = ?`)
|
|
1424
|
+
.run(sessionId);
|
|
1425
|
+
const createdDecisions = decisions.map((decision) => {
|
|
1426
|
+
const id = crypto.randomUUID();
|
|
1427
|
+
const createdAt = new Date().toISOString();
|
|
1428
|
+
this.db
|
|
1429
|
+
.prepare(`
|
|
1430
|
+
INSERT INTO decisions (
|
|
1431
|
+
id, sessionId, title, summary, rationale, status, sourceRefs, createdAt
|
|
1432
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1433
|
+
`)
|
|
1434
|
+
.run(id, sessionId, decision.title, decision.summary, decision.rationale, decision.status, decision.sourceRefs, createdAt);
|
|
1435
|
+
return {
|
|
1436
|
+
id,
|
|
1437
|
+
sessionId,
|
|
1438
|
+
title: decision.title,
|
|
1439
|
+
summary: decision.summary,
|
|
1440
|
+
rationale: decision.rationale,
|
|
1441
|
+
status: decision.status,
|
|
1442
|
+
sourceRefs: decision.sourceRefs,
|
|
1443
|
+
createdAt,
|
|
1444
|
+
};
|
|
1445
|
+
});
|
|
1446
|
+
this.updateSessionHistoryCache(sessionId, {
|
|
1447
|
+
decisionsText: this.buildDecisionHistoryCache(createdDecisions),
|
|
1448
|
+
});
|
|
1449
|
+
return createdDecisions;
|
|
1450
|
+
});
|
|
1451
|
+
return transaction();
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
countSessionDecisions(sessionId) {
|
|
1455
|
+
return this.dbOp("count session decisions", () => {
|
|
1456
|
+
const row = this.db
|
|
1457
|
+
.prepare(`SELECT COUNT(*) as count FROM decisions WHERE sessionId = ?`)
|
|
1458
|
+
.get(sessionId);
|
|
1459
|
+
return row?.count ?? 0;
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
getSessionDecisions(sessionId, options) {
|
|
1463
|
+
return this.dbOp("get session decisions", () => {
|
|
1464
|
+
const params = [sessionId];
|
|
1465
|
+
const query = appendPaginationClause(`
|
|
1466
|
+
SELECT * FROM decisions
|
|
1467
|
+
WHERE sessionId = ?
|
|
1468
|
+
ORDER BY createdAt ASC, id ASC
|
|
1469
|
+
`, params, options?.limit, options?.offset);
|
|
1470
|
+
const rows = this.db.prepare(query).all(...params);
|
|
1471
|
+
return rows.map((row) => this.rowToDecision(row));
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
replaceNarrativesForSession(sessionId, narratives) {
|
|
1475
|
+
return this.dbOp("replace narratives", () => {
|
|
1476
|
+
const transaction = this.db.transaction(() => {
|
|
1477
|
+
this.db
|
|
1478
|
+
.prepare(`DELETE FROM narratives WHERE sessionId = ?`)
|
|
1479
|
+
.run(sessionId);
|
|
1480
|
+
const createdNarratives = narratives.map((narrative) => {
|
|
1481
|
+
const id = crypto.randomUUID();
|
|
1482
|
+
const now = new Date().toISOString();
|
|
1483
|
+
this.db
|
|
1484
|
+
.prepare(`
|
|
1485
|
+
INSERT INTO narratives (
|
|
1486
|
+
id, sessionId, kind, content, sourceRefs, createdAt, updatedAt
|
|
1487
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1488
|
+
`)
|
|
1489
|
+
.run(id, sessionId, narrative.kind, narrative.content, narrative.sourceRefs, now, now);
|
|
1490
|
+
return {
|
|
1491
|
+
id,
|
|
1492
|
+
sessionId,
|
|
1493
|
+
kind: narrative.kind,
|
|
1494
|
+
content: narrative.content,
|
|
1495
|
+
sourceRefs: narrative.sourceRefs,
|
|
1496
|
+
createdAt: now,
|
|
1497
|
+
updatedAt: now,
|
|
1498
|
+
};
|
|
1499
|
+
});
|
|
1500
|
+
this.updateSessionHistoryCache(sessionId, {
|
|
1501
|
+
narrativesText: this.buildNarrativeHistoryCache(createdNarratives),
|
|
1502
|
+
});
|
|
1503
|
+
return createdNarratives;
|
|
1504
|
+
});
|
|
1505
|
+
return transaction();
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
countSessionNarratives(sessionId, options) {
|
|
1509
|
+
return this.dbOp("count session narratives", () => {
|
|
1510
|
+
const row = (options?.kind
|
|
1511
|
+
? this.db
|
|
1512
|
+
.prepare(`SELECT COUNT(*) as count FROM narratives WHERE sessionId = ? AND kind = ?`)
|
|
1513
|
+
.get(sessionId, options.kind)
|
|
1514
|
+
: this.db
|
|
1515
|
+
.prepare(`SELECT COUNT(*) as count FROM narratives WHERE sessionId = ?`)
|
|
1516
|
+
.get(sessionId));
|
|
1517
|
+
return row?.count ?? 0;
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
getSessionNarratives(sessionId, options) {
|
|
1521
|
+
return this.dbOp("get session narratives", () => {
|
|
1522
|
+
const conditions = ["sessionId = ?"];
|
|
1523
|
+
const params = [sessionId];
|
|
1524
|
+
if (options?.kind) {
|
|
1525
|
+
conditions.push("kind = ?");
|
|
1526
|
+
params.push(options.kind);
|
|
1527
|
+
}
|
|
1528
|
+
let query = `
|
|
1529
|
+
SELECT * FROM narratives
|
|
1530
|
+
WHERE ${conditions.join(" AND ")}
|
|
1531
|
+
ORDER BY CASE kind
|
|
1532
|
+
WHEN 'journal' THEN 1
|
|
1533
|
+
WHEN 'project-summary' THEN 2
|
|
1534
|
+
WHEN 'handoff' THEN 3
|
|
1535
|
+
ELSE 99
|
|
1536
|
+
END, createdAt ASC, id ASC
|
|
1537
|
+
`;
|
|
1538
|
+
query = appendPaginationClause(query, params, options?.limit, options?.offset);
|
|
1539
|
+
const rows = this.db.prepare(query).all(...params);
|
|
1540
|
+
return rows.map((row) => this.rowToNarrative(row));
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
hasNarrativesForSession(sessionId) {
|
|
1544
|
+
return this.dbOp("check session narratives", () => {
|
|
1545
|
+
const row = this.db
|
|
1546
|
+
.prepare(`SELECT EXISTS(SELECT 1 FROM narratives WHERE sessionId = ?) as present`)
|
|
1547
|
+
.get(sessionId);
|
|
1548
|
+
return Boolean(row?.present);
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
createIngestionRun(run) {
|
|
1552
|
+
const id = crypto.randomUUID();
|
|
1553
|
+
const startedAt = new Date().toISOString();
|
|
1554
|
+
return this.dbOp("create ingestion run", () => {
|
|
1555
|
+
this.db
|
|
1556
|
+
.prepare(`
|
|
1557
|
+
INSERT INTO ingestion_runs (
|
|
1558
|
+
id, sessionId, stage, status, error, startedAt, endedAt
|
|
1559
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1560
|
+
`)
|
|
1561
|
+
.run(id, run.sessionId, run.stage, run.status, run.error ?? null, startedAt, null);
|
|
1562
|
+
return {
|
|
1563
|
+
id,
|
|
1564
|
+
sessionId: run.sessionId,
|
|
1565
|
+
stage: run.stage,
|
|
1566
|
+
status: run.status,
|
|
1567
|
+
error: run.error ?? null,
|
|
1568
|
+
startedAt,
|
|
1569
|
+
endedAt: null,
|
|
1570
|
+
};
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
completeIngestionRun(id, status, error = null) {
|
|
1574
|
+
try {
|
|
1575
|
+
const endedAt = new Date().toISOString();
|
|
1576
|
+
const result = this.db
|
|
1577
|
+
.prepare(`
|
|
1578
|
+
UPDATE ingestion_runs
|
|
1579
|
+
SET status = ?, error = ?, endedAt = ?
|
|
1580
|
+
WHERE id = ?
|
|
1581
|
+
`)
|
|
1582
|
+
.run(status, error, endedAt, id);
|
|
1583
|
+
if (result.changes === 0) {
|
|
1584
|
+
throw new Error(`Ingestion run with id ${id} not found`);
|
|
114
1585
|
}
|
|
115
|
-
const stmt = this.db.prepare(query);
|
|
116
|
-
const rows = stmt.all(...params);
|
|
117
|
-
return rows.map((row) => this.rowToEvidence(row));
|
|
118
1586
|
}
|
|
119
|
-
catch (
|
|
120
|
-
throw new Error(`Failed to
|
|
1587
|
+
catch (cause) {
|
|
1588
|
+
throw new Error(`Failed to complete ingestion run: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
121
1589
|
}
|
|
122
1590
|
}
|
|
1591
|
+
getSessionIngestionRuns(sessionId) {
|
|
1592
|
+
return this.dbOp("get ingestion runs", () => {
|
|
1593
|
+
const rows = this.db
|
|
1594
|
+
.prepare(`
|
|
1595
|
+
SELECT * FROM ingestion_runs
|
|
1596
|
+
WHERE sessionId = ?
|
|
1597
|
+
ORDER BY startedAt ASC
|
|
1598
|
+
`)
|
|
1599
|
+
.all(sessionId);
|
|
1600
|
+
return rows.map((row) => this.rowToIngestionRun(row));
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
getSessionDetail(sessionId) {
|
|
1604
|
+
// Wrap all reads in a single transaction for atomic snapshot
|
|
1605
|
+
const readAll = this.db.transaction(() => {
|
|
1606
|
+
const session = this.findSessionById(sessionId);
|
|
1607
|
+
if (!session) {
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
const messages = this.getSessionMessages(sessionId);
|
|
1611
|
+
const timeline = this.getSessionTimeline(sessionId);
|
|
1612
|
+
const artifacts = this.getSessionArtifacts(sessionId);
|
|
1613
|
+
const narratives = this.getSessionNarratives(sessionId);
|
|
1614
|
+
const decisions = this.getSessionDecisions(sessionId);
|
|
1615
|
+
const ingestionRuns = this.getSessionIngestionRuns(sessionId);
|
|
1616
|
+
return {
|
|
1617
|
+
session,
|
|
1618
|
+
messages,
|
|
1619
|
+
timeline,
|
|
1620
|
+
artifacts,
|
|
1621
|
+
narratives,
|
|
1622
|
+
decisions,
|
|
1623
|
+
ingestionRuns,
|
|
1624
|
+
hasNarratives: narratives.length > 0,
|
|
1625
|
+
};
|
|
1626
|
+
});
|
|
1627
|
+
return readAll();
|
|
1628
|
+
}
|
|
123
1629
|
/**
|
|
124
1630
|
* Updates git commit information for an evidence
|
|
125
1631
|
* @param id - Evidence UUID
|
|
@@ -127,7 +1633,7 @@ export class EvidenceDatabase {
|
|
|
127
1633
|
* @param gitTimestamp - Git commit timestamp (ISO 8601)
|
|
128
1634
|
*/
|
|
129
1635
|
updateGitInfo(id, gitCommitHash, gitTimestamp) {
|
|
130
|
-
|
|
1636
|
+
this.dbOp("update git info", () => {
|
|
131
1637
|
const stmt = this.db.prepare(`
|
|
132
1638
|
UPDATE evidences
|
|
133
1639
|
SET gitCommitHash = ?,
|
|
@@ -139,10 +1645,7 @@ export class EvidenceDatabase {
|
|
|
139
1645
|
if (result.changes === 0) {
|
|
140
1646
|
throw new Error(`Evidence with id ${id} not found`);
|
|
141
1647
|
}
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
throw new Error(`Failed to update git info: ${error instanceof Error ? error.message : String(error)}`);
|
|
145
|
-
}
|
|
1648
|
+
});
|
|
146
1649
|
}
|
|
147
1650
|
/**
|
|
148
1651
|
* Adds tags to an evidence (appends to existing tags)
|
|
@@ -151,7 +1654,7 @@ export class EvidenceDatabase {
|
|
|
151
1654
|
* @throws Error if tags array is empty or all tags are whitespace
|
|
152
1655
|
*/
|
|
153
1656
|
addTags(id, tags) {
|
|
154
|
-
|
|
1657
|
+
this.dbOp("add tags", () => {
|
|
155
1658
|
if (tags.length === 0) {
|
|
156
1659
|
throw new Error("Tags array cannot be empty");
|
|
157
1660
|
}
|
|
@@ -185,10 +1688,7 @@ export class EvidenceDatabase {
|
|
|
185
1688
|
stmt.run(mergedTags.join(","), new Date().toISOString(), id);
|
|
186
1689
|
});
|
|
187
1690
|
transaction();
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
throw new Error(`Failed to add tags: ${error instanceof Error ? error.message : String(error)}`);
|
|
191
|
-
}
|
|
1691
|
+
});
|
|
192
1692
|
}
|
|
193
1693
|
/**
|
|
194
1694
|
* Builds a WHERE clause from search/filter options
|
|
@@ -227,23 +1727,20 @@ export class EvidenceDatabase {
|
|
|
227
1727
|
* @returns Number of matching evidences
|
|
228
1728
|
*/
|
|
229
1729
|
getFilteredCount(options) {
|
|
230
|
-
|
|
1730
|
+
return this.dbOp("get filtered count", () => {
|
|
231
1731
|
const { sql: whereClause, params } = this.buildWhereClause(options);
|
|
232
1732
|
const row = this.db
|
|
233
1733
|
.prepare(`SELECT COUNT(*) as count FROM evidences${whereClause}`)
|
|
234
1734
|
.get(...params);
|
|
235
1735
|
return row.count;
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
throw new Error(`Failed to get filtered count: ${error instanceof Error ? error.message : String(error)}`);
|
|
239
|
-
}
|
|
1736
|
+
});
|
|
240
1737
|
}
|
|
241
1738
|
/**
|
|
242
1739
|
* Search evidences and return both paginated results and total matching count
|
|
243
1740
|
* in a single pass (builds WHERE clause once instead of twice)
|
|
244
1741
|
*/
|
|
245
1742
|
searchWithCount(options) {
|
|
246
|
-
|
|
1743
|
+
return this.dbOp("search evidences", () => {
|
|
247
1744
|
const { limit, offset = 0 } = options;
|
|
248
1745
|
const { sql: whereClause, params: baseParams } = this.buildWhereClause(options);
|
|
249
1746
|
// Wrap both queries in a transaction for consistent snapshot
|
|
@@ -254,19 +1751,7 @@ export class EvidenceDatabase {
|
|
|
254
1751
|
.get(...baseParams);
|
|
255
1752
|
// Build paginated query (clone params since we append to it)
|
|
256
1753
|
const searchParams = [...baseParams];
|
|
257
|
-
|
|
258
|
-
if (limit !== undefined) {
|
|
259
|
-
sql += " LIMIT ?";
|
|
260
|
-
searchParams.push(limit);
|
|
261
|
-
if (offset > 0) {
|
|
262
|
-
sql += " OFFSET ?";
|
|
263
|
-
searchParams.push(offset);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
else if (offset > 0) {
|
|
267
|
-
sql += " LIMIT -1 OFFSET ?";
|
|
268
|
-
searchParams.push(offset);
|
|
269
|
-
}
|
|
1754
|
+
const sql = appendPaginationClause(`SELECT * FROM evidences${whereClause} ORDER BY timestamp DESC`, searchParams, limit, offset);
|
|
270
1755
|
const rows = this.db.prepare(sql).all(...searchParams);
|
|
271
1756
|
return {
|
|
272
1757
|
evidences: rows.map((row) => this.rowToEvidence(row)),
|
|
@@ -274,10 +1759,7 @@ export class EvidenceDatabase {
|
|
|
274
1759
|
};
|
|
275
1760
|
});
|
|
276
1761
|
return query();
|
|
277
|
-
}
|
|
278
|
-
catch (error) {
|
|
279
|
-
throw new Error(`Failed to search evidences: ${error instanceof Error ? error.message : String(error)}`);
|
|
280
|
-
}
|
|
1762
|
+
});
|
|
281
1763
|
}
|
|
282
1764
|
/**
|
|
283
1765
|
* Search and filter evidences by various criteria
|
|
@@ -285,31 +1767,17 @@ export class EvidenceDatabase {
|
|
|
285
1767
|
* @returns Array of matching evidences
|
|
286
1768
|
*/
|
|
287
1769
|
search(options) {
|
|
288
|
-
|
|
1770
|
+
return this.dbOp("search evidences", () => {
|
|
289
1771
|
const { limit, offset = 0 } = options;
|
|
290
1772
|
const { sql: whereClause, params } = this.buildWhereClause(options);
|
|
291
1773
|
// Build final query
|
|
292
1774
|
let sql = `SELECT * FROM evidences${whereClause} ORDER BY timestamp DESC`;
|
|
293
1775
|
// Add pagination
|
|
294
|
-
|
|
295
|
-
sql += " LIMIT ?";
|
|
296
|
-
params.push(limit);
|
|
297
|
-
if (offset > 0) {
|
|
298
|
-
sql += " OFFSET ?";
|
|
299
|
-
params.push(offset);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
else if (offset > 0) {
|
|
303
|
-
sql += " LIMIT -1 OFFSET ?";
|
|
304
|
-
params.push(offset);
|
|
305
|
-
}
|
|
1776
|
+
sql = appendPaginationClause(sql, params, limit, offset);
|
|
306
1777
|
const stmt = this.db.prepare(sql);
|
|
307
1778
|
const rows = stmt.all(...params);
|
|
308
1779
|
return rows.map((row) => this.rowToEvidence(row));
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
throw new Error(`Failed to search evidences: ${error instanceof Error ? error.message : String(error)}`);
|
|
312
|
-
}
|
|
1780
|
+
});
|
|
313
1781
|
}
|
|
314
1782
|
/**
|
|
315
1783
|
* Deletes evidence by ID
|
|
@@ -317,14 +1785,11 @@ export class EvidenceDatabase {
|
|
|
317
1785
|
* @returns true if deleted, false if not found
|
|
318
1786
|
*/
|
|
319
1787
|
delete(id) {
|
|
320
|
-
|
|
1788
|
+
return this.dbOp("delete evidence", () => {
|
|
321
1789
|
const stmt = this.db.prepare(`DELETE FROM evidences WHERE id = ?`);
|
|
322
1790
|
const result = stmt.run(id);
|
|
323
1791
|
return result.changes > 0;
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
throw new Error(`Failed to delete evidence: ${error instanceof Error ? error.message : String(error)}`);
|
|
327
|
-
}
|
|
1792
|
+
});
|
|
328
1793
|
}
|
|
329
1794
|
/**
|
|
330
1795
|
* Deletes multiple evidences by IDs
|
|
@@ -334,7 +1799,7 @@ export class EvidenceDatabase {
|
|
|
334
1799
|
deleteMany(ids) {
|
|
335
1800
|
if (ids.length === 0)
|
|
336
1801
|
return 0;
|
|
337
|
-
|
|
1802
|
+
return this.dbOp("delete evidences", () => {
|
|
338
1803
|
// Batch deletions to stay under SQLite's 999 parameter limit
|
|
339
1804
|
const BATCH_SIZE = 999;
|
|
340
1805
|
let totalDeleted = 0;
|
|
@@ -346,10 +1811,7 @@ export class EvidenceDatabase {
|
|
|
346
1811
|
totalDeleted += result.changes;
|
|
347
1812
|
}
|
|
348
1813
|
return totalDeleted;
|
|
349
|
-
}
|
|
350
|
-
catch (error) {
|
|
351
|
-
throw new Error(`Failed to delete evidences: ${error instanceof Error ? error.message : String(error)}`);
|
|
352
|
-
}
|
|
1814
|
+
});
|
|
353
1815
|
}
|
|
354
1816
|
/**
|
|
355
1817
|
* Updates tags for an evidence (replaces existing tags)
|
|
@@ -358,7 +1820,7 @@ export class EvidenceDatabase {
|
|
|
358
1820
|
* @returns true if updated, false if not found
|
|
359
1821
|
*/
|
|
360
1822
|
updateTags(id, tags) {
|
|
361
|
-
|
|
1823
|
+
return this.dbOp("update tags", () => {
|
|
362
1824
|
const stmt = this.db.prepare(`
|
|
363
1825
|
UPDATE evidences
|
|
364
1826
|
SET tags = ?,
|
|
@@ -367,10 +1829,7 @@ export class EvidenceDatabase {
|
|
|
367
1829
|
`);
|
|
368
1830
|
const result = stmt.run(tags, new Date().toISOString(), id);
|
|
369
1831
|
return result.changes > 0;
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
throw new Error(`Failed to update tags: ${error instanceof Error ? error.message : String(error)}`);
|
|
373
|
-
}
|
|
1832
|
+
});
|
|
374
1833
|
}
|
|
375
1834
|
/**
|
|
376
1835
|
* Renames a tag across all evidences atomically using transaction
|
|
@@ -380,7 +1839,7 @@ export class EvidenceDatabase {
|
|
|
380
1839
|
* @throws Error if transaction fails (no partial updates)
|
|
381
1840
|
*/
|
|
382
1841
|
renameTag(oldTag, newTag) {
|
|
383
|
-
|
|
1842
|
+
return this.dbOp("rename tag", () => {
|
|
384
1843
|
// Wrap in transaction for atomic updates
|
|
385
1844
|
const transaction = this.db.transaction(() => {
|
|
386
1845
|
// Get all evidences with this tag
|
|
@@ -398,10 +1857,7 @@ export class EvidenceDatabase {
|
|
|
398
1857
|
return updatedCount;
|
|
399
1858
|
});
|
|
400
1859
|
return transaction();
|
|
401
|
-
}
|
|
402
|
-
catch (error) {
|
|
403
|
-
throw new Error(`Failed to rename tag: ${error instanceof Error ? error.message : String(error)}`);
|
|
404
|
-
}
|
|
1860
|
+
});
|
|
405
1861
|
}
|
|
406
1862
|
/**
|
|
407
1863
|
* Removes a tag from all evidences atomically using transaction
|
|
@@ -410,7 +1866,7 @@ export class EvidenceDatabase {
|
|
|
410
1866
|
* @throws Error if transaction fails (no partial updates)
|
|
411
1867
|
*/
|
|
412
1868
|
removeTag(tag) {
|
|
413
|
-
|
|
1869
|
+
return this.dbOp("remove tag", () => {
|
|
414
1870
|
// Wrap in transaction for atomic updates
|
|
415
1871
|
const transaction = this.db.transaction(() => {
|
|
416
1872
|
// Get all evidences with this tag
|
|
@@ -431,17 +1887,14 @@ export class EvidenceDatabase {
|
|
|
431
1887
|
return updatedCount;
|
|
432
1888
|
});
|
|
433
1889
|
return transaction();
|
|
434
|
-
}
|
|
435
|
-
catch (error) {
|
|
436
|
-
throw new Error(`Failed to remove tag: ${error instanceof Error ? error.message : String(error)}`);
|
|
437
|
-
}
|
|
1890
|
+
});
|
|
438
1891
|
}
|
|
439
1892
|
/**
|
|
440
1893
|
* Gets all unique tags with their counts
|
|
441
1894
|
* @returns Map of tag to count
|
|
442
1895
|
*/
|
|
443
1896
|
getTagCounts() {
|
|
444
|
-
|
|
1897
|
+
return this.dbOp("get tag counts", () => {
|
|
445
1898
|
const stmt = this.db.prepare(`SELECT tags FROM evidences WHERE tags IS NOT NULL AND tags != ''`);
|
|
446
1899
|
const rows = stmt.all();
|
|
447
1900
|
const tagCounts = new Map();
|
|
@@ -455,10 +1908,7 @@ export class EvidenceDatabase {
|
|
|
455
1908
|
}
|
|
456
1909
|
}
|
|
457
1910
|
return tagCounts;
|
|
458
|
-
}
|
|
459
|
-
catch (error) {
|
|
460
|
-
throw new Error(`Failed to get tag counts: ${error instanceof Error ? error.message : String(error)}`);
|
|
461
|
-
}
|
|
1911
|
+
});
|
|
462
1912
|
}
|
|
463
1913
|
/**
|
|
464
1914
|
* Gets total count of all evidence records
|
|
@@ -470,6 +1920,12 @@ export class EvidenceDatabase {
|
|
|
470
1920
|
.get();
|
|
471
1921
|
return row.count;
|
|
472
1922
|
}
|
|
1923
|
+
getSessionCount() {
|
|
1924
|
+
const row = this.db
|
|
1925
|
+
.prepare("SELECT COUNT(*) as count FROM sessions")
|
|
1926
|
+
.get();
|
|
1927
|
+
return row.count;
|
|
1928
|
+
}
|
|
473
1929
|
/**
|
|
474
1930
|
* Gets count of evidence records matching a search query
|
|
475
1931
|
* @param query - Search text to match against conversationId and tags
|
|
@@ -517,5 +1973,129 @@ export class EvidenceDatabase {
|
|
|
517
1973
|
updatedAt: row.updatedAt,
|
|
518
1974
|
};
|
|
519
1975
|
}
|
|
1976
|
+
rowToSession(row) {
|
|
1977
|
+
return {
|
|
1978
|
+
id: row.id,
|
|
1979
|
+
host: row.host,
|
|
1980
|
+
projectRoot: row.projectRoot,
|
|
1981
|
+
cwd: row.cwd,
|
|
1982
|
+
title: row.title,
|
|
1983
|
+
status: row.status,
|
|
1984
|
+
startedAt: row.startedAt,
|
|
1985
|
+
endedAt: row.endedAt,
|
|
1986
|
+
metadata: row.metadata,
|
|
1987
|
+
createdAt: row.createdAt,
|
|
1988
|
+
updatedAt: row.updatedAt,
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
rowToContext(row) {
|
|
1992
|
+
return {
|
|
1993
|
+
id: row.id,
|
|
1994
|
+
label: row.label,
|
|
1995
|
+
workspaceKey: row.workspaceKey,
|
|
1996
|
+
status: row.status,
|
|
1997
|
+
mergedIntoContextId: row.mergedIntoContextId,
|
|
1998
|
+
metadata: row.metadata,
|
|
1999
|
+
createdAt: row.createdAt,
|
|
2000
|
+
updatedAt: row.updatedAt,
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
rowToContextSessionLink(row) {
|
|
2004
|
+
return {
|
|
2005
|
+
sessionId: row.sessionId,
|
|
2006
|
+
contextId: row.contextId,
|
|
2007
|
+
linkSource: row.linkSource,
|
|
2008
|
+
createdAt: row.createdAt,
|
|
2009
|
+
updatedAt: row.updatedAt,
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
rowToContextLinkRejection(row) {
|
|
2013
|
+
return {
|
|
2014
|
+
sessionId: row.sessionId,
|
|
2015
|
+
contextId: row.contextId,
|
|
2016
|
+
createdAt: row.createdAt,
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
rowToContextWorkspacePreference(row) {
|
|
2020
|
+
return {
|
|
2021
|
+
workspaceKey: row.workspaceKey,
|
|
2022
|
+
contextId: row.contextId,
|
|
2023
|
+
createdAt: row.createdAt,
|
|
2024
|
+
updatedAt: row.updatedAt,
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
rowToMessage(row) {
|
|
2028
|
+
return {
|
|
2029
|
+
id: row.id,
|
|
2030
|
+
sessionId: row.sessionId,
|
|
2031
|
+
seq: row.seq,
|
|
2032
|
+
role: row.role,
|
|
2033
|
+
source: row.source,
|
|
2034
|
+
content: row.content,
|
|
2035
|
+
capturedAt: row.capturedAt,
|
|
2036
|
+
metadata: row.metadata,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
rowToTimelineEvent(row) {
|
|
2040
|
+
return {
|
|
2041
|
+
id: row.id,
|
|
2042
|
+
sessionId: row.sessionId,
|
|
2043
|
+
seq: row.seq,
|
|
2044
|
+
eventType: row.eventType,
|
|
2045
|
+
eventSubType: row.eventSubType,
|
|
2046
|
+
source: row.source,
|
|
2047
|
+
summary: row.summary,
|
|
2048
|
+
payload: row.payload,
|
|
2049
|
+
startedAt: row.startedAt,
|
|
2050
|
+
endedAt: row.endedAt,
|
|
2051
|
+
status: row.status,
|
|
2052
|
+
relatedMessageId: row.relatedMessageId,
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
rowToArtifact(row) {
|
|
2056
|
+
return {
|
|
2057
|
+
id: row.id,
|
|
2058
|
+
sessionId: row.sessionId,
|
|
2059
|
+
eventId: row.eventId,
|
|
2060
|
+
artifactType: row.artifactType,
|
|
2061
|
+
path: row.path,
|
|
2062
|
+
metadata: row.metadata,
|
|
2063
|
+
createdAt: row.createdAt,
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
rowToDecision(row) {
|
|
2067
|
+
return {
|
|
2068
|
+
id: row.id,
|
|
2069
|
+
sessionId: row.sessionId,
|
|
2070
|
+
title: row.title,
|
|
2071
|
+
summary: row.summary,
|
|
2072
|
+
rationale: row.rationale,
|
|
2073
|
+
status: row.status,
|
|
2074
|
+
sourceRefs: row.sourceRefs,
|
|
2075
|
+
createdAt: row.createdAt,
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
rowToNarrative(row) {
|
|
2079
|
+
return {
|
|
2080
|
+
id: row.id,
|
|
2081
|
+
sessionId: row.sessionId,
|
|
2082
|
+
kind: row.kind,
|
|
2083
|
+
content: row.content,
|
|
2084
|
+
sourceRefs: row.sourceRefs,
|
|
2085
|
+
createdAt: row.createdAt,
|
|
2086
|
+
updatedAt: row.updatedAt,
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
rowToIngestionRun(row) {
|
|
2090
|
+
return {
|
|
2091
|
+
id: row.id,
|
|
2092
|
+
sessionId: row.sessionId,
|
|
2093
|
+
stage: row.stage,
|
|
2094
|
+
status: row.status,
|
|
2095
|
+
error: row.error,
|
|
2096
|
+
startedAt: row.startedAt,
|
|
2097
|
+
endedAt: row.endedAt,
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
520
2100
|
}
|
|
521
2101
|
//# sourceMappingURL=database.js.map
|