@vox-ai-app/storage 1.0.1
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/package.json +32 -0
- package/src/config.js +62 -0
- package/src/db.js +145 -0
- package/src/index.js +4 -0
- package/src/messages.js +213 -0
- package/src/tasks.js +219 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vox-ai-app/storage",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Local message and config persistence for Vox",
|
|
7
|
+
"main": "./src/index.js",
|
|
8
|
+
"private": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./db": "./src/db.js",
|
|
12
|
+
"./messages": "./src/messages.js",
|
|
13
|
+
"./config": "./src/config.js",
|
|
14
|
+
"./tasks": "./src/tasks.js"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/vox-ai-app/vox.git"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"better-sqlite3": "^12.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
function resolveConfigPath(configPath) {
|
|
5
|
+
const normalized = String(configPath || '').trim()
|
|
6
|
+
if (!normalized) {
|
|
7
|
+
throw new Error('A config path is required.')
|
|
8
|
+
}
|
|
9
|
+
return path.resolve(normalized)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readConfigFile(configPath) {
|
|
13
|
+
const resolvedPath = resolveConfigPath(configPath)
|
|
14
|
+
if (!existsSync(resolvedPath)) {
|
|
15
|
+
return {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(resolvedPath, 'utf8')
|
|
20
|
+
if (!raw.trim()) return {}
|
|
21
|
+
const parsed = JSON.parse(raw)
|
|
22
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
23
|
+
return {}
|
|
24
|
+
}
|
|
25
|
+
return parsed
|
|
26
|
+
} catch {
|
|
27
|
+
return {}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeConfigFile(configPath, value) {
|
|
32
|
+
const resolvedPath = resolveConfigPath(configPath)
|
|
33
|
+
mkdirSync(path.dirname(resolvedPath), { recursive: true })
|
|
34
|
+
const tempPath = `${resolvedPath}.tmp`
|
|
35
|
+
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
36
|
+
renameSync(tempPath, resolvedPath)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function configGet(configPath, key) {
|
|
40
|
+
const config = readConfigFile(configPath)
|
|
41
|
+
return config[String(key)]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function configSet(configPath, key, value) {
|
|
45
|
+
const config = readConfigFile(configPath)
|
|
46
|
+
config[String(key)] = value
|
|
47
|
+
writeConfigFile(configPath, config)
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function configDelete(configPath, key) {
|
|
52
|
+
const config = readConfigFile(configPath)
|
|
53
|
+
const normalizedKey = String(key)
|
|
54
|
+
const existed = Object.prototype.hasOwnProperty.call(config, normalizedKey)
|
|
55
|
+
delete config[normalizedKey]
|
|
56
|
+
writeConfigFile(configPath, config)
|
|
57
|
+
return existed
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function configGetAll(configPath) {
|
|
61
|
+
return { ...readConfigFile(configPath) }
|
|
62
|
+
}
|
package/src/db.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import Database from 'better-sqlite3'
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const dbs = new Map()
|
|
6
|
+
|
|
7
|
+
function resolveDbPath(dbPath) {
|
|
8
|
+
const normalized = String(dbPath || '').trim()
|
|
9
|
+
if (!normalized) {
|
|
10
|
+
throw new Error('A database path is required.')
|
|
11
|
+
}
|
|
12
|
+
return path.resolve(normalized)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function prepareDb(db) {
|
|
16
|
+
db.pragma('journal_mode = WAL')
|
|
17
|
+
db.pragma('foreign_keys = ON')
|
|
18
|
+
|
|
19
|
+
db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
created_at TEXT NOT NULL,
|
|
23
|
+
updated_at TEXT NOT NULL
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
conversation_id TEXT NOT NULL,
|
|
29
|
+
role TEXT NOT NULL,
|
|
30
|
+
content TEXT NOT NULL,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id_id
|
|
36
|
+
ON messages (conversation_id, id);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
39
|
+
task_id TEXT PRIMARY KEY,
|
|
40
|
+
instructions TEXT NOT NULL DEFAULT '',
|
|
41
|
+
context TEXT NOT NULL DEFAULT '',
|
|
42
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
43
|
+
created_at TEXT NOT NULL,
|
|
44
|
+
updated_at TEXT NOT NULL,
|
|
45
|
+
current_plan TEXT NOT NULL DEFAULT '',
|
|
46
|
+
message TEXT NOT NULL DEFAULT '',
|
|
47
|
+
result TEXT,
|
|
48
|
+
completed_at TEXT NOT NULL DEFAULT '',
|
|
49
|
+
failed_at TEXT NOT NULL DEFAULT ''
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_created_at
|
|
53
|
+
ON tasks (created_at DESC, task_id DESC);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS task_activity (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
task_id TEXT NOT NULL,
|
|
58
|
+
type TEXT NOT NULL,
|
|
59
|
+
name TEXT,
|
|
60
|
+
raw_result TEXT,
|
|
61
|
+
timestamp TEXT NOT NULL,
|
|
62
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
63
|
+
FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_task_activity_timestamp
|
|
67
|
+
ON task_activity (timestamp ASC, id ASC);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_task_activity_task_id
|
|
70
|
+
ON task_activity (task_id, timestamp ASC, id ASC);
|
|
71
|
+
`)
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN reported INTEGER NOT NULL DEFAULT 0`)
|
|
75
|
+
} catch {
|
|
76
|
+
/* */
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN context_summary TEXT`)
|
|
81
|
+
} catch {
|
|
82
|
+
/* */
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN context_checkpoint_id INTEGER`)
|
|
87
|
+
} catch {
|
|
88
|
+
/* */
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
db.exec(`
|
|
93
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
|
|
94
|
+
task_id UNINDEXED,
|
|
95
|
+
instructions,
|
|
96
|
+
result,
|
|
97
|
+
tokenize = 'unicode61'
|
|
98
|
+
)
|
|
99
|
+
`)
|
|
100
|
+
} catch {
|
|
101
|
+
/* table may already exist */
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
db.exec(`
|
|
106
|
+
CREATE TABLE IF NOT EXISTS knowledge_patterns (
|
|
107
|
+
id TEXT PRIMARY KEY,
|
|
108
|
+
trigger TEXT NOT NULL,
|
|
109
|
+
solution TEXT NOT NULL,
|
|
110
|
+
created_at TEXT NOT NULL
|
|
111
|
+
)
|
|
112
|
+
`)
|
|
113
|
+
db.exec(`
|
|
114
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS patterns_fts USING fts5(
|
|
115
|
+
pattern_id UNINDEXED,
|
|
116
|
+
trigger,
|
|
117
|
+
solution,
|
|
118
|
+
tokenize = 'unicode61'
|
|
119
|
+
)
|
|
120
|
+
`)
|
|
121
|
+
} catch {
|
|
122
|
+
/* tables may already exist */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function openDb(dbPath) {
|
|
127
|
+
const resolvedPath = resolveDbPath(dbPath)
|
|
128
|
+
const existing = dbs.get(resolvedPath)
|
|
129
|
+
if (existing) return existing
|
|
130
|
+
|
|
131
|
+
mkdirSync(path.dirname(resolvedPath), { recursive: true })
|
|
132
|
+
|
|
133
|
+
const db = new Database(resolvedPath)
|
|
134
|
+
prepareDb(db)
|
|
135
|
+
dbs.set(resolvedPath, db)
|
|
136
|
+
return db
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function closeDb(dbPath) {
|
|
140
|
+
const resolvedPath = resolveDbPath(dbPath)
|
|
141
|
+
const db = dbs.get(resolvedPath)
|
|
142
|
+
if (!db) return
|
|
143
|
+
db.close()
|
|
144
|
+
dbs.delete(resolvedPath)
|
|
145
|
+
}
|
package/src/index.js
ADDED
package/src/messages.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
export const DEFAULT_CONVERSATION_ID = 'main'
|
|
2
|
+
|
|
3
|
+
function getConversationId(conversationId) {
|
|
4
|
+
const normalized = String(conversationId || '').trim()
|
|
5
|
+
return normalized || DEFAULT_CONVERSATION_ID
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeLimit(limit) {
|
|
9
|
+
const parsed = Number.parseInt(limit, 10)
|
|
10
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeBeforeId(beforeId) {
|
|
14
|
+
const parsed = Number.parseInt(beforeId, 10)
|
|
15
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mapRow(row) {
|
|
19
|
+
if (!row) return null
|
|
20
|
+
return {
|
|
21
|
+
id: row.id,
|
|
22
|
+
conversationId: row.conversation_id,
|
|
23
|
+
role: row.role,
|
|
24
|
+
content: row.content,
|
|
25
|
+
createdAt: row.created_at
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ensureConversation(db, conversationId = DEFAULT_CONVERSATION_ID) {
|
|
30
|
+
const id = getConversationId(conversationId)
|
|
31
|
+
const now = new Date().toISOString()
|
|
32
|
+
|
|
33
|
+
db.prepare(
|
|
34
|
+
`
|
|
35
|
+
INSERT INTO conversations (id, created_at, updated_at)
|
|
36
|
+
VALUES (?, ?, ?)
|
|
37
|
+
ON CONFLICT(id) DO NOTHING
|
|
38
|
+
`
|
|
39
|
+
).run(id, now, now)
|
|
40
|
+
|
|
41
|
+
return db
|
|
42
|
+
.prepare(
|
|
43
|
+
`
|
|
44
|
+
SELECT id, created_at, updated_at
|
|
45
|
+
FROM conversations
|
|
46
|
+
WHERE id = ?
|
|
47
|
+
`
|
|
48
|
+
)
|
|
49
|
+
.get(id)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function touchConversation(db, conversationId = DEFAULT_CONVERSATION_ID) {
|
|
53
|
+
const id = getConversationId(conversationId)
|
|
54
|
+
const now = new Date().toISOString()
|
|
55
|
+
|
|
56
|
+
db.prepare(
|
|
57
|
+
`
|
|
58
|
+
INSERT INTO conversations (id, created_at, updated_at)
|
|
59
|
+
VALUES (?, ?, ?)
|
|
60
|
+
ON CONFLICT(id) DO UPDATE SET updated_at = excluded.updated_at
|
|
61
|
+
`
|
|
62
|
+
).run(id, now, now)
|
|
63
|
+
|
|
64
|
+
return db
|
|
65
|
+
.prepare(
|
|
66
|
+
`
|
|
67
|
+
SELECT id, created_at, updated_at
|
|
68
|
+
FROM conversations
|
|
69
|
+
WHERE id = ?
|
|
70
|
+
`
|
|
71
|
+
)
|
|
72
|
+
.get(id)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function appendMessage(db, role, content, conversationId = DEFAULT_CONVERSATION_ID) {
|
|
76
|
+
const id = getConversationId(conversationId)
|
|
77
|
+
const now = new Date().toISOString()
|
|
78
|
+
const normalizedRole = String(role || '').trim() || 'user'
|
|
79
|
+
const normalizedContent = String(content ?? '')
|
|
80
|
+
|
|
81
|
+
touchConversation(db, id)
|
|
82
|
+
|
|
83
|
+
const result = db
|
|
84
|
+
.prepare(
|
|
85
|
+
`
|
|
86
|
+
INSERT INTO messages (conversation_id, role, content, created_at)
|
|
87
|
+
VALUES (?, ?, ?, ?)
|
|
88
|
+
`
|
|
89
|
+
)
|
|
90
|
+
.run(id, normalizedRole, normalizedContent, now)
|
|
91
|
+
|
|
92
|
+
return mapRow(
|
|
93
|
+
db
|
|
94
|
+
.prepare(
|
|
95
|
+
`
|
|
96
|
+
SELECT id, conversation_id, role, content, created_at
|
|
97
|
+
FROM messages
|
|
98
|
+
WHERE id = ?
|
|
99
|
+
`
|
|
100
|
+
)
|
|
101
|
+
.get(result.lastInsertRowid)
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getMessages(db, conversationId = DEFAULT_CONVERSATION_ID, limit) {
|
|
106
|
+
const id = getConversationId(conversationId)
|
|
107
|
+
const normalizedLimit = normalizeLimit(limit)
|
|
108
|
+
|
|
109
|
+
if (!normalizedLimit) {
|
|
110
|
+
return db
|
|
111
|
+
.prepare(
|
|
112
|
+
`
|
|
113
|
+
SELECT id, conversation_id, role, content, created_at
|
|
114
|
+
FROM messages
|
|
115
|
+
WHERE conversation_id = ?
|
|
116
|
+
ORDER BY id ASC
|
|
117
|
+
`
|
|
118
|
+
)
|
|
119
|
+
.all(id)
|
|
120
|
+
.map(mapRow)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return db
|
|
124
|
+
.prepare(
|
|
125
|
+
`
|
|
126
|
+
SELECT id, conversation_id, role, content, created_at
|
|
127
|
+
FROM (
|
|
128
|
+
SELECT id, conversation_id, role, content, created_at
|
|
129
|
+
FROM messages
|
|
130
|
+
WHERE conversation_id = ?
|
|
131
|
+
ORDER BY id DESC
|
|
132
|
+
LIMIT ?
|
|
133
|
+
)
|
|
134
|
+
ORDER BY id ASC
|
|
135
|
+
`
|
|
136
|
+
)
|
|
137
|
+
.all(id, normalizedLimit)
|
|
138
|
+
.map(mapRow)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function getMessagesBeforeId(
|
|
142
|
+
db,
|
|
143
|
+
beforeId,
|
|
144
|
+
conversationId = DEFAULT_CONVERSATION_ID,
|
|
145
|
+
limit = 50
|
|
146
|
+
) {
|
|
147
|
+
const id = getConversationId(conversationId)
|
|
148
|
+
const normalizedBeforeId = normalizeBeforeId(beforeId)
|
|
149
|
+
const normalizedLimit = normalizeLimit(limit) || 50
|
|
150
|
+
|
|
151
|
+
if (!normalizedBeforeId) {
|
|
152
|
+
return []
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return db
|
|
156
|
+
.prepare(
|
|
157
|
+
`
|
|
158
|
+
SELECT id, conversation_id, role, content, created_at
|
|
159
|
+
FROM (
|
|
160
|
+
SELECT id, conversation_id, role, content, created_at
|
|
161
|
+
FROM messages
|
|
162
|
+
WHERE conversation_id = ? AND id < ?
|
|
163
|
+
ORDER BY id DESC
|
|
164
|
+
LIMIT ?
|
|
165
|
+
)
|
|
166
|
+
ORDER BY id ASC
|
|
167
|
+
`
|
|
168
|
+
)
|
|
169
|
+
.all(id, normalizedBeforeId, normalizedLimit)
|
|
170
|
+
.map(mapRow)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function clearMessages(db, conversationId = DEFAULT_CONVERSATION_ID) {
|
|
174
|
+
const id = getConversationId(conversationId)
|
|
175
|
+
ensureConversation(db, id)
|
|
176
|
+
return db
|
|
177
|
+
.prepare(
|
|
178
|
+
`
|
|
179
|
+
DELETE FROM messages
|
|
180
|
+
WHERE conversation_id = ?
|
|
181
|
+
`
|
|
182
|
+
)
|
|
183
|
+
.run(id)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function saveSummaryCheckpoint(
|
|
187
|
+
db,
|
|
188
|
+
summary,
|
|
189
|
+
checkpointId,
|
|
190
|
+
conversationId = DEFAULT_CONVERSATION_ID
|
|
191
|
+
) {
|
|
192
|
+
const id = getConversationId(conversationId)
|
|
193
|
+
ensureConversation(db, id)
|
|
194
|
+
db.prepare(
|
|
195
|
+
`UPDATE conversations SET context_summary = ?, context_checkpoint_id = ?, updated_at = ? WHERE id = ?`
|
|
196
|
+
).run(summary, checkpointId, new Date().toISOString(), id)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function loadSummaryCheckpoint(db, conversationId = DEFAULT_CONVERSATION_ID) {
|
|
200
|
+
const id = getConversationId(conversationId)
|
|
201
|
+
const row = db
|
|
202
|
+
.prepare(`SELECT context_summary, context_checkpoint_id FROM conversations WHERE id = ?`)
|
|
203
|
+
.get(id)
|
|
204
|
+
if (!row || !row.context_summary) return null
|
|
205
|
+
return { summary: row.context_summary, checkpointId: row.context_checkpoint_id }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function clearSummaryCheckpoint(db, conversationId = DEFAULT_CONVERSATION_ID) {
|
|
209
|
+
const id = getConversationId(conversationId)
|
|
210
|
+
db.prepare(
|
|
211
|
+
`UPDATE conversations SET context_summary = NULL, context_checkpoint_id = NULL, updated_at = ? WHERE id = ?`
|
|
212
|
+
).run(new Date().toISOString(), id)
|
|
213
|
+
}
|
package/src/tasks.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
function parseJson(value, fallback) {
|
|
2
|
+
if (value === null || value === undefined || value === '') {
|
|
3
|
+
return fallback
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(value)
|
|
8
|
+
} catch {
|
|
9
|
+
return fallback
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stringifyJson(value, fallback = null) {
|
|
14
|
+
if (value === null || value === undefined) {
|
|
15
|
+
return fallback
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(value)
|
|
20
|
+
} catch {
|
|
21
|
+
return fallback
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mapTask(row) {
|
|
26
|
+
if (!row) return null
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
taskId: row.task_id,
|
|
30
|
+
instructions: row.instructions,
|
|
31
|
+
context: row.context,
|
|
32
|
+
status: row.status,
|
|
33
|
+
createdAt: row.created_at,
|
|
34
|
+
updatedAt: row.updated_at,
|
|
35
|
+
currentPlan: row.current_plan,
|
|
36
|
+
message: row.message,
|
|
37
|
+
result: row.result,
|
|
38
|
+
completedAt: row.completed_at,
|
|
39
|
+
failedAt: row.failed_at
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function mapActivity(row) {
|
|
44
|
+
if (!row) return null
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
id: row.id,
|
|
48
|
+
taskId: row.task_id,
|
|
49
|
+
type: row.type,
|
|
50
|
+
name: row.name || null,
|
|
51
|
+
rawResult: parseJson(row.raw_result, row.raw_result),
|
|
52
|
+
timestamp: row.timestamp,
|
|
53
|
+
data: parseJson(row.data, {})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function upsertTask(db, task) {
|
|
58
|
+
const taskId = String(task?.taskId || '').trim()
|
|
59
|
+
if (!taskId) {
|
|
60
|
+
throw new Error('taskId is required.')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const createdAt = String(task?.createdAt || new Date().toISOString())
|
|
64
|
+
const updatedAt = String(task?.updatedAt || createdAt)
|
|
65
|
+
|
|
66
|
+
db.prepare(
|
|
67
|
+
`
|
|
68
|
+
INSERT INTO tasks (
|
|
69
|
+
task_id,
|
|
70
|
+
instructions,
|
|
71
|
+
context,
|
|
72
|
+
status,
|
|
73
|
+
created_at,
|
|
74
|
+
updated_at,
|
|
75
|
+
current_plan,
|
|
76
|
+
message,
|
|
77
|
+
result,
|
|
78
|
+
completed_at,
|
|
79
|
+
failed_at
|
|
80
|
+
)
|
|
81
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
82
|
+
ON CONFLICT(task_id) DO UPDATE SET
|
|
83
|
+
instructions = excluded.instructions,
|
|
84
|
+
context = excluded.context,
|
|
85
|
+
status = excluded.status,
|
|
86
|
+
updated_at = excluded.updated_at,
|
|
87
|
+
current_plan = excluded.current_plan,
|
|
88
|
+
message = excluded.message,
|
|
89
|
+
result = excluded.result,
|
|
90
|
+
completed_at = excluded.completed_at,
|
|
91
|
+
failed_at = excluded.failed_at
|
|
92
|
+
`
|
|
93
|
+
).run(
|
|
94
|
+
taskId,
|
|
95
|
+
String(task?.instructions || ''),
|
|
96
|
+
String(task?.context || ''),
|
|
97
|
+
String(task?.status || 'queued'),
|
|
98
|
+
createdAt,
|
|
99
|
+
updatedAt,
|
|
100
|
+
String(task?.currentPlan || ''),
|
|
101
|
+
String(task?.message || ''),
|
|
102
|
+
task?.result === null || task?.result === undefined ? null : String(task.result),
|
|
103
|
+
String(task?.completedAt || ''),
|
|
104
|
+
String(task?.failedAt || '')
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return getTask(db, taskId)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getTask(db, taskId) {
|
|
111
|
+
return mapTask(
|
|
112
|
+
db
|
|
113
|
+
.prepare(
|
|
114
|
+
`
|
|
115
|
+
SELECT
|
|
116
|
+
task_id,
|
|
117
|
+
instructions,
|
|
118
|
+
context,
|
|
119
|
+
status,
|
|
120
|
+
created_at,
|
|
121
|
+
updated_at,
|
|
122
|
+
current_plan,
|
|
123
|
+
message,
|
|
124
|
+
result,
|
|
125
|
+
completed_at,
|
|
126
|
+
failed_at
|
|
127
|
+
FROM tasks
|
|
128
|
+
WHERE task_id = ?
|
|
129
|
+
`
|
|
130
|
+
)
|
|
131
|
+
.get(String(taskId || '').trim())
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function loadTasks(db) {
|
|
136
|
+
return db
|
|
137
|
+
.prepare(
|
|
138
|
+
`
|
|
139
|
+
SELECT
|
|
140
|
+
task_id,
|
|
141
|
+
instructions,
|
|
142
|
+
context,
|
|
143
|
+
status,
|
|
144
|
+
created_at,
|
|
145
|
+
updated_at,
|
|
146
|
+
current_plan,
|
|
147
|
+
message,
|
|
148
|
+
result,
|
|
149
|
+
completed_at,
|
|
150
|
+
failed_at
|
|
151
|
+
FROM tasks
|
|
152
|
+
ORDER BY created_at DESC, task_id DESC
|
|
153
|
+
`
|
|
154
|
+
)
|
|
155
|
+
.all()
|
|
156
|
+
.map(mapTask)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function appendTaskActivity(db, activity) {
|
|
160
|
+
const id = String(activity?.id || '').trim()
|
|
161
|
+
const taskId = String(activity?.taskId || '').trim()
|
|
162
|
+
if (!id || !taskId) {
|
|
163
|
+
throw new Error('Task activity requires id and taskId.')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
db.prepare(
|
|
167
|
+
`
|
|
168
|
+
INSERT INTO task_activity (id, task_id, type, name, raw_result, timestamp, data)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
170
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
171
|
+
task_id = excluded.task_id,
|
|
172
|
+
type = excluded.type,
|
|
173
|
+
name = excluded.name,
|
|
174
|
+
raw_result = excluded.raw_result,
|
|
175
|
+
timestamp = excluded.timestamp,
|
|
176
|
+
data = excluded.data
|
|
177
|
+
`
|
|
178
|
+
).run(
|
|
179
|
+
id,
|
|
180
|
+
taskId,
|
|
181
|
+
String(activity?.type || ''),
|
|
182
|
+
activity?.name ? String(activity.name) : null,
|
|
183
|
+
stringifyJson(
|
|
184
|
+
activity?.rawResult,
|
|
185
|
+
activity?.rawResult === undefined ? null : String(activity.rawResult)
|
|
186
|
+
),
|
|
187
|
+
String(activity?.timestamp || new Date().toISOString()),
|
|
188
|
+
stringifyJson(activity?.data || {}, '{}')
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return id
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function loadTaskActivity(db, taskId) {
|
|
195
|
+
return db
|
|
196
|
+
.prepare(
|
|
197
|
+
`
|
|
198
|
+
SELECT id, task_id, type, name, raw_result, timestamp, data
|
|
199
|
+
FROM task_activity
|
|
200
|
+
WHERE task_id = ?
|
|
201
|
+
ORDER BY timestamp ASC, id ASC
|
|
202
|
+
`
|
|
203
|
+
)
|
|
204
|
+
.all(String(taskId || '').trim())
|
|
205
|
+
.map(mapActivity)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function loadAllTaskActivity(db) {
|
|
209
|
+
return db
|
|
210
|
+
.prepare(
|
|
211
|
+
`
|
|
212
|
+
SELECT id, task_id, type, name, raw_result, timestamp, data
|
|
213
|
+
FROM task_activity
|
|
214
|
+
ORDER BY timestamp ASC, id ASC
|
|
215
|
+
`
|
|
216
|
+
)
|
|
217
|
+
.all()
|
|
218
|
+
.map(mapActivity)
|
|
219
|
+
}
|