@makefinks/daemon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/cli.js +22 -0
- package/package.json +79 -0
- package/src/ai/agent-turn-runner.ts +130 -0
- package/src/ai/daemon-ai.ts +403 -0
- package/src/ai/exa-client.ts +21 -0
- package/src/ai/exa-fetch-cache.ts +104 -0
- package/src/ai/model-config.ts +99 -0
- package/src/ai/sanitize-messages.ts +83 -0
- package/src/ai/system-prompt.ts +363 -0
- package/src/ai/tools/fetch-urls.ts +187 -0
- package/src/ai/tools/grounding-manager.ts +94 -0
- package/src/ai/tools/index.ts +52 -0
- package/src/ai/tools/read-file.ts +100 -0
- package/src/ai/tools/render-url.ts +275 -0
- package/src/ai/tools/run-bash.ts +224 -0
- package/src/ai/tools/subagents.ts +195 -0
- package/src/ai/tools/todo-manager.ts +150 -0
- package/src/ai/tools/web-search.ts +91 -0
- package/src/app/App.tsx +711 -0
- package/src/app/components/AppOverlays.tsx +131 -0
- package/src/app/components/AvatarLayer.tsx +51 -0
- package/src/app/components/ConversationPane.tsx +476 -0
- package/src/avatar/DaemonAvatarRenderable.ts +343 -0
- package/src/avatar/daemon-avatar-rig.ts +1165 -0
- package/src/avatar-preview.ts +186 -0
- package/src/cli.ts +26 -0
- package/src/components/ApiKeyInput.tsx +99 -0
- package/src/components/ApiKeyStep.tsx +95 -0
- package/src/components/ApprovalPicker.tsx +109 -0
- package/src/components/ContentBlockView.tsx +141 -0
- package/src/components/DaemonText.tsx +34 -0
- package/src/components/DeviceMenu.tsx +166 -0
- package/src/components/GroundingBadge.tsx +21 -0
- package/src/components/GroundingMenu.tsx +310 -0
- package/src/components/HotkeysPane.tsx +115 -0
- package/src/components/InlineStatusIndicator.tsx +106 -0
- package/src/components/ModelMenu.tsx +411 -0
- package/src/components/OnboardingOverlay.tsx +446 -0
- package/src/components/ProviderMenu.tsx +177 -0
- package/src/components/SessionMenu.tsx +297 -0
- package/src/components/SettingsMenu.tsx +291 -0
- package/src/components/StatusBar.tsx +126 -0
- package/src/components/TokenUsageDisplay.tsx +92 -0
- package/src/components/ToolCallView.tsx +113 -0
- package/src/components/TypingInputBar.tsx +131 -0
- package/src/components/tool-layouts/components.tsx +120 -0
- package/src/components/tool-layouts/defaults.ts +9 -0
- package/src/components/tool-layouts/index.ts +22 -0
- package/src/components/tool-layouts/layouts/bash.ts +110 -0
- package/src/components/tool-layouts/layouts/grounding.tsx +98 -0
- package/src/components/tool-layouts/layouts/index.ts +8 -0
- package/src/components/tool-layouts/layouts/read-file.ts +59 -0
- package/src/components/tool-layouts/layouts/subagent.tsx +118 -0
- package/src/components/tool-layouts/layouts/system-info.ts +8 -0
- package/src/components/tool-layouts/layouts/todo.tsx +139 -0
- package/src/components/tool-layouts/layouts/url-tools.ts +220 -0
- package/src/components/tool-layouts/layouts/web-search.ts +110 -0
- package/src/components/tool-layouts/registry.ts +17 -0
- package/src/components/tool-layouts/types.ts +94 -0
- package/src/hooks/daemon-event-handlers.ts +944 -0
- package/src/hooks/keyboard-handlers.ts +399 -0
- package/src/hooks/menu-navigation.ts +147 -0
- package/src/hooks/use-app-audio-devices-loader.ts +71 -0
- package/src/hooks/use-app-callbacks.ts +202 -0
- package/src/hooks/use-app-context-builder.ts +159 -0
- package/src/hooks/use-app-display-state.ts +162 -0
- package/src/hooks/use-app-menus.ts +51 -0
- package/src/hooks/use-app-model-pricing-loader.ts +45 -0
- package/src/hooks/use-app-model.ts +123 -0
- package/src/hooks/use-app-openrouter-models-loader.ts +44 -0
- package/src/hooks/use-app-openrouter-provider-loader.ts +35 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +212 -0
- package/src/hooks/use-app-sessions.ts +105 -0
- package/src/hooks/use-app-settings.ts +62 -0
- package/src/hooks/use-conversation-manager.ts +163 -0
- package/src/hooks/use-copy-on-select.ts +50 -0
- package/src/hooks/use-daemon-events.ts +396 -0
- package/src/hooks/use-daemon-keyboard.ts +397 -0
- package/src/hooks/use-grounding.ts +46 -0
- package/src/hooks/use-input-history.ts +92 -0
- package/src/hooks/use-menu-keyboard.ts +93 -0
- package/src/hooks/use-playwright-notification.ts +23 -0
- package/src/hooks/use-reasoning-animation.ts +97 -0
- package/src/hooks/use-response-timer.ts +55 -0
- package/src/hooks/use-tool-approval.tsx +202 -0
- package/src/hooks/use-typing-mode.ts +137 -0
- package/src/hooks/use-voice-dependencies-notification.ts +37 -0
- package/src/index.tsx +48 -0
- package/src/scripts/setup-browsers.ts +42 -0
- package/src/state/app-context.tsx +160 -0
- package/src/state/daemon-events.ts +67 -0
- package/src/state/daemon-state.ts +493 -0
- package/src/state/migrations/001-init.ts +33 -0
- package/src/state/migrations/index.ts +8 -0
- package/src/state/model-history-store.ts +45 -0
- package/src/state/runtime-context.ts +21 -0
- package/src/state/session-store.ts +359 -0
- package/src/types/index.ts +405 -0
- package/src/types/theme.ts +52 -0
- package/src/ui/constants.ts +157 -0
- package/src/utils/clipboard.ts +89 -0
- package/src/utils/debug-logger.ts +69 -0
- package/src/utils/formatters.ts +242 -0
- package/src/utils/js-rendering.ts +77 -0
- package/src/utils/markdown-tables.ts +234 -0
- package/src/utils/model-metadata.ts +191 -0
- package/src/utils/openrouter-endpoints.ts +212 -0
- package/src/utils/openrouter-models.ts +205 -0
- package/src/utils/openrouter-pricing.ts +59 -0
- package/src/utils/openrouter-reported-cost.ts +16 -0
- package/src/utils/paste.ts +33 -0
- package/src/utils/preferences.ts +289 -0
- package/src/utils/text-fragment.ts +39 -0
- package/src/utils/tool-output-preview.ts +250 -0
- package/src/utils/voice-dependencies.ts +107 -0
- package/src/utils/workspace-manager.ts +85 -0
- package/src/voice/audio-recorder.ts +579 -0
- package/src/voice/mic-level.ts +35 -0
- package/src/voice/tts/openai-tts-stream.ts +222 -0
- package/src/voice/tts/speech-controller.ts +64 -0
- package/src/voice/tts/tts-player.ts +257 -0
- package/src/voice/voice-input-controller.ts +96 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
export function createMigration001Init(defaultUsageJson: string): (db: Database) => void {
|
|
4
|
+
return (db) => {
|
|
5
|
+
db.exec(`
|
|
6
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
title TEXT NOT NULL,
|
|
9
|
+
created_at TEXT NOT NULL,
|
|
10
|
+
updated_at TEXT NOT NULL,
|
|
11
|
+
history_json TEXT NOT NULL DEFAULT '[]',
|
|
12
|
+
usage_json TEXT NOT NULL DEFAULT '${defaultUsageJson}'
|
|
13
|
+
);
|
|
14
|
+
`);
|
|
15
|
+
db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS grounding_maps (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
session_id TEXT NOT NULL,
|
|
19
|
+
message_id INTEGER NOT NULL,
|
|
20
|
+
created_at TEXT NOT NULL,
|
|
21
|
+
items_json TEXT NOT NULL
|
|
22
|
+
);
|
|
23
|
+
`);
|
|
24
|
+
db.exec(`
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_grounding_maps_session_created
|
|
26
|
+
ON grounding_maps(session_id, created_at DESC);
|
|
27
|
+
`);
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_grounding_maps_session_message
|
|
30
|
+
ON grounding_maps(session_id, message_id);
|
|
31
|
+
`);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { createMigration001Init } from "./001-init";
|
|
3
|
+
|
|
4
|
+
export type SessionMigration = (db: Database) => void;
|
|
5
|
+
|
|
6
|
+
export function getSessionMigrations(defaultUsageJson: string): SessionMigration[] {
|
|
7
|
+
return [createMigration001Init(defaultUsageJson)];
|
|
8
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ModelMessage } from "../types";
|
|
2
|
+
|
|
3
|
+
export class ModelHistoryStore {
|
|
4
|
+
private history: ModelMessage[] = [];
|
|
5
|
+
|
|
6
|
+
get(): ModelMessage[] {
|
|
7
|
+
return [...this.history];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
set(history: ModelMessage[]): void {
|
|
11
|
+
this.history = [...history];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
clear(): void {
|
|
15
|
+
this.history = [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
appendTurn(userText: string, responseMessages: ModelMessage[]): void {
|
|
19
|
+
this.history.push({ role: "user", content: userText }, ...responseMessages);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Undo the last turn (user message + assistant response) from the model history.
|
|
24
|
+
* Returns the number of messages removed, or 0 if nothing to undo.
|
|
25
|
+
*/
|
|
26
|
+
undoLastTurn(): number {
|
|
27
|
+
if (this.history.length === 0) return 0;
|
|
28
|
+
|
|
29
|
+
// Find the last user message and remove everything from there onwards.
|
|
30
|
+
// This handles multi-message assistant responses (tool calls, etc.).
|
|
31
|
+
let lastUserIndex = -1;
|
|
32
|
+
for (let i = this.history.length - 1; i >= 0; i--) {
|
|
33
|
+
if (this.history[i]?.role === "user") {
|
|
34
|
+
lastUserIndex = i;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (lastUserIndex === -1) return 0;
|
|
40
|
+
|
|
41
|
+
const removedCount = this.history.length - lastUserIndex;
|
|
42
|
+
this.history = this.history.slice(0, lastUserIndex);
|
|
43
|
+
return removedCount;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface RuntimeContext {
|
|
2
|
+
sessionId: string | null;
|
|
3
|
+
messageId: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
let context: RuntimeContext = {
|
|
7
|
+
sessionId: null,
|
|
8
|
+
messageId: 0,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function setRuntimeContext(sessionId: string | null, messageId: number): void {
|
|
12
|
+
context = { sessionId, messageId };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getRuntimeContext(): RuntimeContext {
|
|
16
|
+
return { ...context };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clearRuntimeContext(): void {
|
|
20
|
+
context = { sessionId: null, messageId: 0 };
|
|
21
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session persistence using SQLite for conversation history and UI state.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { promises as fs } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import type {
|
|
9
|
+
ConversationMessage,
|
|
10
|
+
GroundedStatement,
|
|
11
|
+
GroundingMap,
|
|
12
|
+
ModelMessage,
|
|
13
|
+
SessionInfo,
|
|
14
|
+
SessionSnapshot,
|
|
15
|
+
TokenUsage,
|
|
16
|
+
} from "../types";
|
|
17
|
+
import { debug } from "../utils/debug-logger";
|
|
18
|
+
import { getAppConfigDir } from "../utils/preferences";
|
|
19
|
+
import { ensureWorkspaceExists, deleteWorkspace } from "../utils/workspace-manager";
|
|
20
|
+
import { getSessionMigrations } from "./migrations";
|
|
21
|
+
|
|
22
|
+
const SESSION_DB_FILE = "sessions.sqlite";
|
|
23
|
+
const SESSION_DB_PATH_ENV = "DAEMON_SESSIONS_DB_PATH";
|
|
24
|
+
|
|
25
|
+
const DEFAULT_SESSION_USAGE: TokenUsage = {
|
|
26
|
+
promptTokens: 0,
|
|
27
|
+
completionTokens: 0,
|
|
28
|
+
totalTokens: 0,
|
|
29
|
+
subagentTotalTokens: 0,
|
|
30
|
+
subagentPromptTokens: 0,
|
|
31
|
+
subagentCompletionTokens: 0,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SCHEMA_VERSION = 1;
|
|
35
|
+
|
|
36
|
+
let db: Database | null = null;
|
|
37
|
+
|
|
38
|
+
function getSessionDbPath(): string {
|
|
39
|
+
const override = process.env[SESSION_DB_PATH_ENV]?.trim();
|
|
40
|
+
if (override) return override;
|
|
41
|
+
const dir = getAppConfigDir();
|
|
42
|
+
return path.join(dir, SESSION_DB_FILE);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getUserVersion(database: Database): number {
|
|
46
|
+
const row = database.prepare("PRAGMA user_version").get() as { user_version?: number } | undefined;
|
|
47
|
+
return typeof row?.user_version === "number" ? row.user_version : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setUserVersion(database: Database, version: number): void {
|
|
51
|
+
database.exec(`PRAGMA user_version = ${version}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runMigrations(database: Database): void {
|
|
55
|
+
const migrations = getSessionMigrations(JSON.stringify(DEFAULT_SESSION_USAGE));
|
|
56
|
+
|
|
57
|
+
let currentVersion = getUserVersion(database);
|
|
58
|
+
if (currentVersion > SCHEMA_VERSION) {
|
|
59
|
+
debug.error("session-schema-version-mismatch", {
|
|
60
|
+
message: `Database schema version ${currentVersion} is newer than supported ${SCHEMA_VERSION}`,
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (currentVersion === SCHEMA_VERSION) return;
|
|
66
|
+
|
|
67
|
+
const run = database.transaction(() => {
|
|
68
|
+
for (let version = currentVersion; version < SCHEMA_VERSION; version += 1) {
|
|
69
|
+
const migration = migrations[version];
|
|
70
|
+
if (!migration) {
|
|
71
|
+
throw new Error(`Missing migration for version ${version + 1}`);
|
|
72
|
+
}
|
|
73
|
+
migration(database);
|
|
74
|
+
}
|
|
75
|
+
setUserVersion(database, SCHEMA_VERSION);
|
|
76
|
+
});
|
|
77
|
+
run();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function getDb(): Promise<Database> {
|
|
81
|
+
if (db) return db;
|
|
82
|
+
const dbPath = getSessionDbPath();
|
|
83
|
+
if (dbPath !== ":memory:") {
|
|
84
|
+
await fs.mkdir(path.dirname(dbPath), { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
db = new Database(dbPath);
|
|
87
|
+
db.exec("PRAGMA journal_mode=WAL;");
|
|
88
|
+
db.exec("PRAGMA foreign_keys=ON;");
|
|
89
|
+
runMigrations(db);
|
|
90
|
+
return db;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function closeSessionStore(): void {
|
|
94
|
+
if (!db) return;
|
|
95
|
+
try {
|
|
96
|
+
db.close();
|
|
97
|
+
} catch {
|
|
98
|
+
} finally {
|
|
99
|
+
db = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatSessionTitle(timestamp: string): string {
|
|
104
|
+
const base = timestamp.replace("T", " ").slice(0, 16);
|
|
105
|
+
return `Session ${base}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseConversationHistory(raw: string): ConversationMessage[] {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
111
|
+
if (!Array.isArray(parsed)) return [];
|
|
112
|
+
return parsed as ConversationMessage[];
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseSessionUsage(raw: string): TokenUsage {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(raw) as Partial<TokenUsage>;
|
|
121
|
+
if (!parsed || typeof parsed !== "object") return { ...DEFAULT_SESSION_USAGE };
|
|
122
|
+
return {
|
|
123
|
+
promptTokens: typeof parsed.promptTokens === "number" ? parsed.promptTokens : 0,
|
|
124
|
+
completionTokens: typeof parsed.completionTokens === "number" ? parsed.completionTokens : 0,
|
|
125
|
+
totalTokens: typeof parsed.totalTokens === "number" ? parsed.totalTokens : 0,
|
|
126
|
+
reasoningTokens: typeof parsed.reasoningTokens === "number" ? parsed.reasoningTokens : undefined,
|
|
127
|
+
cachedInputTokens: typeof parsed.cachedInputTokens === "number" ? parsed.cachedInputTokens : undefined,
|
|
128
|
+
cost: typeof parsed.cost === "number" ? parsed.cost : undefined,
|
|
129
|
+
subagentTotalTokens: typeof parsed.subagentTotalTokens === "number" ? parsed.subagentTotalTokens : 0,
|
|
130
|
+
subagentPromptTokens: typeof parsed.subagentPromptTokens === "number" ? parsed.subagentPromptTokens : 0,
|
|
131
|
+
subagentCompletionTokens:
|
|
132
|
+
typeof parsed.subagentCompletionTokens === "number" ? parsed.subagentCompletionTokens : 0,
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
return { ...DEFAULT_SESSION_USAGE };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildModelHistoryFromConversation(
|
|
140
|
+
conversationHistory: ConversationMessage[]
|
|
141
|
+
): ModelMessage[] {
|
|
142
|
+
const modelHistory: ModelMessage[] = [];
|
|
143
|
+
for (const message of conversationHistory) {
|
|
144
|
+
if (Array.isArray(message.messages)) {
|
|
145
|
+
modelHistory.push(...message.messages);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return modelHistory;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function listSessions(): Promise<SessionInfo[]> {
|
|
152
|
+
try {
|
|
153
|
+
const database = await getDb();
|
|
154
|
+
const rows = database
|
|
155
|
+
.prepare("SELECT id, title, created_at, updated_at FROM sessions ORDER BY updated_at DESC")
|
|
156
|
+
.all() as Array<{
|
|
157
|
+
id: string;
|
|
158
|
+
title: string | null;
|
|
159
|
+
created_at: string;
|
|
160
|
+
updated_at: string;
|
|
161
|
+
}>;
|
|
162
|
+
return rows.map((row) => ({
|
|
163
|
+
id: row.id,
|
|
164
|
+
title: row.title && row.title.trim() ? row.title : formatSessionTitle(row.created_at),
|
|
165
|
+
createdAt: row.created_at,
|
|
166
|
+
updatedAt: row.updated_at,
|
|
167
|
+
}));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
170
|
+
debug.error("session-list-failed", { message: err.message });
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function createSession(title?: string): Promise<SessionInfo> {
|
|
176
|
+
const database = await getDb();
|
|
177
|
+
const now = new Date().toISOString();
|
|
178
|
+
const sessionId = crypto.randomUUID();
|
|
179
|
+
const sessionTitle = title?.trim() || formatSessionTitle(now);
|
|
180
|
+
database
|
|
181
|
+
.prepare(
|
|
182
|
+
"INSERT INTO sessions (id, title, created_at, updated_at, history_json, usage_json) VALUES (?, ?, ?, ?, ?, ?)"
|
|
183
|
+
)
|
|
184
|
+
.run(sessionId, sessionTitle, now, now, JSON.stringify([]), JSON.stringify(DEFAULT_SESSION_USAGE));
|
|
185
|
+
|
|
186
|
+
await ensureWorkspaceExists(sessionId);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
id: sessionId,
|
|
190
|
+
title: sessionTitle,
|
|
191
|
+
createdAt: now,
|
|
192
|
+
updatedAt: now,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function loadSessionSnapshot(sessionId: string): Promise<SessionSnapshot | null> {
|
|
197
|
+
try {
|
|
198
|
+
const database = await getDb();
|
|
199
|
+
const row = database
|
|
200
|
+
.prepare("SELECT history_json, usage_json FROM sessions WHERE id = ?")
|
|
201
|
+
.get(sessionId) as { history_json: string; usage_json: string } | undefined;
|
|
202
|
+
if (!row) return null;
|
|
203
|
+
const conversationHistory = parseConversationHistory(row.history_json);
|
|
204
|
+
const sessionUsage = parseSessionUsage(row.usage_json);
|
|
205
|
+
return { conversationHistory, sessionUsage };
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
208
|
+
debug.error("session-load-failed", { message: err.message });
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function saveSessionSnapshot(snapshot: SessionSnapshot, sessionId: string): Promise<void> {
|
|
214
|
+
try {
|
|
215
|
+
const database = await getDb();
|
|
216
|
+
const now = new Date().toISOString();
|
|
217
|
+
const existing = database.prepare("SELECT created_at, title FROM sessions WHERE id = ?").get(sessionId) as
|
|
218
|
+
| { created_at?: string }
|
|
219
|
+
| undefined;
|
|
220
|
+
const createdAt = existing?.created_at ?? now;
|
|
221
|
+
const title =
|
|
222
|
+
(existing as { title?: string } | undefined)?.title?.trim() || formatSessionTitle(createdAt);
|
|
223
|
+
const historyJson = JSON.stringify(snapshot.conversationHistory);
|
|
224
|
+
const usageJson = JSON.stringify(snapshot.sessionUsage);
|
|
225
|
+
database
|
|
226
|
+
.prepare(
|
|
227
|
+
"INSERT INTO sessions (id, title, created_at, updated_at, history_json, usage_json) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET title = excluded.title, updated_at = excluded.updated_at, history_json = excluded.history_json, usage_json = excluded.usage_json"
|
|
228
|
+
)
|
|
229
|
+
.run(sessionId, title, createdAt, now, historyJson, usageJson);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
232
|
+
debug.error("session-save-failed", { message: err.message });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function clearSessionSnapshot(sessionId: string): Promise<void> {
|
|
237
|
+
try {
|
|
238
|
+
const database = await getDb();
|
|
239
|
+
database.prepare("DELETE FROM grounding_maps WHERE session_id = ?").run(sessionId);
|
|
240
|
+
database.prepare("DELETE FROM sessions WHERE id = ?").run(sessionId);
|
|
241
|
+
await deleteWorkspace(sessionId);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
244
|
+
debug.error("session-clear-failed", { message: err.message });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function deleteSession(sessionId: string): Promise<void> {
|
|
249
|
+
// Alias for clarity at call sites (e.g. UI actions).
|
|
250
|
+
return await clearSessionSnapshot(sessionId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function updateSessionTitle(sessionId: string, title: string): Promise<void> {
|
|
254
|
+
try {
|
|
255
|
+
const database = await getDb();
|
|
256
|
+
const now = new Date().toISOString();
|
|
257
|
+
database.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?").run(title, now, sessionId);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
260
|
+
debug.error("session-title-update-failed", { message: err.message });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function parseGroundedStatements(raw: string): GroundedStatement[] {
|
|
265
|
+
try {
|
|
266
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
267
|
+
if (!Array.isArray(parsed)) return [];
|
|
268
|
+
return parsed as GroundedStatement[];
|
|
269
|
+
} catch {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function saveGroundingMap(
|
|
275
|
+
sessionId: string,
|
|
276
|
+
messageId: number,
|
|
277
|
+
items: GroundedStatement[]
|
|
278
|
+
): Promise<GroundingMap> {
|
|
279
|
+
const database = await getDb();
|
|
280
|
+
const now = new Date().toISOString();
|
|
281
|
+
const mapId = crypto.randomUUID();
|
|
282
|
+
const itemsJson = JSON.stringify(items);
|
|
283
|
+
|
|
284
|
+
database
|
|
285
|
+
.prepare(
|
|
286
|
+
"INSERT INTO grounding_maps (id, session_id, message_id, created_at, items_json) VALUES (?, ?, ?, ?, ?)"
|
|
287
|
+
)
|
|
288
|
+
.run(mapId, sessionId, messageId, now, itemsJson);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
id: mapId,
|
|
292
|
+
sessionId,
|
|
293
|
+
messageId,
|
|
294
|
+
createdAt: now,
|
|
295
|
+
items,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function listGroundingMaps(sessionId: string): Promise<GroundingMap[]> {
|
|
300
|
+
try {
|
|
301
|
+
const database = await getDb();
|
|
302
|
+
const rows = database
|
|
303
|
+
.prepare(
|
|
304
|
+
"SELECT id, session_id, message_id, created_at, items_json FROM grounding_maps WHERE session_id = ? ORDER BY created_at DESC"
|
|
305
|
+
)
|
|
306
|
+
.all(sessionId) as Array<{
|
|
307
|
+
id: string;
|
|
308
|
+
session_id: string;
|
|
309
|
+
message_id: number;
|
|
310
|
+
created_at: string;
|
|
311
|
+
items_json: string;
|
|
312
|
+
}>;
|
|
313
|
+
|
|
314
|
+
return rows.map((row) => ({
|
|
315
|
+
id: row.id,
|
|
316
|
+
sessionId: row.session_id,
|
|
317
|
+
messageId: row.message_id,
|
|
318
|
+
createdAt: row.created_at,
|
|
319
|
+
items: parseGroundedStatements(row.items_json),
|
|
320
|
+
}));
|
|
321
|
+
} catch (error) {
|
|
322
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
323
|
+
debug.error("grounding-list-failed", { message: err.message });
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function loadLatestGroundingMap(sessionId: string): Promise<GroundingMap | null> {
|
|
329
|
+
try {
|
|
330
|
+
const database = await getDb();
|
|
331
|
+
const row = database
|
|
332
|
+
.prepare(
|
|
333
|
+
"SELECT id, session_id, message_id, created_at, items_json FROM grounding_maps WHERE session_id = ? ORDER BY created_at DESC LIMIT 1"
|
|
334
|
+
)
|
|
335
|
+
.get(sessionId) as
|
|
336
|
+
| {
|
|
337
|
+
id: string;
|
|
338
|
+
session_id: string;
|
|
339
|
+
message_id: number;
|
|
340
|
+
created_at: string;
|
|
341
|
+
items_json: string;
|
|
342
|
+
}
|
|
343
|
+
| undefined;
|
|
344
|
+
|
|
345
|
+
if (!row) return null;
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
id: row.id,
|
|
349
|
+
sessionId: row.session_id,
|
|
350
|
+
messageId: row.message_id,
|
|
351
|
+
createdAt: row.created_at,
|
|
352
|
+
items: parseGroundedStatements(row.items_json),
|
|
353
|
+
};
|
|
354
|
+
} catch (error) {
|
|
355
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
356
|
+
debug.error("grounding-load-failed", { message: err.message });
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|