n2n-nexus 0.4.2
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/CHANGELOG.md +358 -0
- package/LICENSE +201 -0
- package/README.md +286 -0
- package/build/client/nexus-client.js +71 -0
- package/build/config/cli.js +11 -0
- package/build/config/index.js +16 -0
- package/build/config/paths.js +38 -0
- package/build/constants.js +22 -0
- package/build/daemon/index.js +41 -0
- package/build/daemon/server.js +791 -0
- package/build/index.js +47 -0
- package/build/server/nexus.js +98 -0
- package/build/storage/docs.js +74 -0
- package/build/storage/index.js +105 -0
- package/build/storage/logs.js +60 -0
- package/build/storage/meetings.js +276 -0
- package/build/storage/paths.js +26 -0
- package/build/storage/projects.js +75 -0
- package/build/storage/registry.js +230 -0
- package/build/storage/sqlite-meeting.js +311 -0
- package/build/storage/sqlite.js +141 -0
- package/build/storage/store.js +153 -0
- package/build/storage/tasks.js +212 -0
- package/build/types.js +1 -0
- package/build/utils/async-mutex.js +36 -0
- package/docs/ARCHITECTURE.md +205 -0
- package/docs/ARCHITECTURE_zh.md +205 -0
- package/docs/ASSISTANT_GUIDE.md +120 -0
- package/docs/README_zh.md +285 -0
- package/llms.txt +46 -0
- package/package.json +90 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { NexusPaths } from "./paths.js";
|
|
4
|
+
let db = null;
|
|
5
|
+
/**
|
|
6
|
+
* SQLite Database Schema for Nexus Meetings
|
|
7
|
+
*/
|
|
8
|
+
const SCHEMA = `
|
|
9
|
+
-- Meetings table
|
|
10
|
+
CREATE TABLE IF NOT EXISTS meetings (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
topic TEXT NOT NULL,
|
|
13
|
+
status TEXT CHECK(status IN ('active', 'closed', 'archived')) DEFAULT 'active',
|
|
14
|
+
initiator TEXT,
|
|
15
|
+
participants TEXT DEFAULT '[]',
|
|
16
|
+
created_at TEXT NOT NULL,
|
|
17
|
+
closed_at TEXT,
|
|
18
|
+
summary TEXT
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- Messages table
|
|
22
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
meeting_id TEXT NOT NULL,
|
|
25
|
+
sender TEXT NOT NULL,
|
|
26
|
+
text TEXT NOT NULL,
|
|
27
|
+
category TEXT CHECK(category IN ('MEETING_START', 'PROPOSAL', 'DECISION', 'UPDATE', 'CHAT')),
|
|
28
|
+
timestamp TEXT NOT NULL,
|
|
29
|
+
FOREIGN KEY (meeting_id) REFERENCES meetings(id)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- Decisions table (extracted from DECISION messages)
|
|
33
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
+
meeting_id TEXT NOT NULL,
|
|
36
|
+
content TEXT NOT NULL,
|
|
37
|
+
timestamp TEXT NOT NULL,
|
|
38
|
+
FOREIGN KEY (meeting_id) REFERENCES meetings(id)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
-- Meeting state (Key-Value store)
|
|
42
|
+
CREATE TABLE IF NOT EXISTS meeting_state (
|
|
43
|
+
key TEXT PRIMARY KEY,
|
|
44
|
+
value TEXT NOT NULL
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
-- Indexes for performance
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_messages_meeting ON messages(meeting_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_meeting ON decisions(meeting_id);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_meetings_status ON meetings(status);
|
|
52
|
+
|
|
53
|
+
-- Read cursors table (tracks each IDE's last read message per context)
|
|
54
|
+
CREATE TABLE IF NOT EXISTS read_cursors (
|
|
55
|
+
instance_id TEXT NOT NULL,
|
|
56
|
+
context_type TEXT NOT NULL CHECK(context_type IN ('meeting', 'global')),
|
|
57
|
+
context_id TEXT,
|
|
58
|
+
last_read_id INTEGER DEFAULT 0,
|
|
59
|
+
updated_at TEXT,
|
|
60
|
+
PRIMARY KEY (instance_id, context_type, context_id)
|
|
61
|
+
);
|
|
62
|
+
`;
|
|
63
|
+
/**
|
|
64
|
+
* Get the database file path
|
|
65
|
+
*/
|
|
66
|
+
export function getDbPath() {
|
|
67
|
+
return NexusPaths.dbFile;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Initialize the SQLite database with WAL mode
|
|
71
|
+
*/
|
|
72
|
+
export function initDatabase() {
|
|
73
|
+
if (db)
|
|
74
|
+
return db;
|
|
75
|
+
const dbPath = getDbPath();
|
|
76
|
+
db = new Database(dbPath);
|
|
77
|
+
// Enable WAL mode for better concurrent access
|
|
78
|
+
db.pragma("journal_mode = WAL");
|
|
79
|
+
// Initialize schema
|
|
80
|
+
db.exec(SCHEMA);
|
|
81
|
+
// Migration: Add initiator column if it doesn't exist (Upgrade from v0.1.7)
|
|
82
|
+
try {
|
|
83
|
+
const columns = db.prepare("PRAGMA table_info(meetings)").all();
|
|
84
|
+
const hasInitiator = columns.some(c => c.name === "initiator");
|
|
85
|
+
if (!hasInitiator) {
|
|
86
|
+
console.error("[Nexus] Migrating database: Adding 'initiator' column to 'meetings' table.");
|
|
87
|
+
db.exec("ALTER TABLE meetings ADD COLUMN initiator TEXT");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
console.error("[Nexus] Migration check failed:", e);
|
|
92
|
+
}
|
|
93
|
+
// Initialize default state if not exists
|
|
94
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO meeting_state (key, value) VALUES (?, ?)");
|
|
95
|
+
stmt.run("active_meetings", "[]");
|
|
96
|
+
stmt.run("default_meeting", "");
|
|
97
|
+
console.error("[Nexus] SQLite database initialized at:", dbPath);
|
|
98
|
+
return db;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get the database instance
|
|
102
|
+
*/
|
|
103
|
+
export function getDatabase() {
|
|
104
|
+
if (!db) {
|
|
105
|
+
return initDatabase();
|
|
106
|
+
}
|
|
107
|
+
return db;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Close the database connection
|
|
111
|
+
*/
|
|
112
|
+
export function closeDatabase() {
|
|
113
|
+
if (db) {
|
|
114
|
+
db.close();
|
|
115
|
+
db = null;
|
|
116
|
+
console.error("[Nexus] SQLite database closed");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function isSqliteAvailable() {
|
|
120
|
+
const require = createRequire(import.meta.url);
|
|
121
|
+
try {
|
|
122
|
+
// Try to load better-sqlite3
|
|
123
|
+
require("better-sqlite3");
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Cleanup on process exit
|
|
131
|
+
process.on("exit", () => {
|
|
132
|
+
closeDatabase();
|
|
133
|
+
});
|
|
134
|
+
process.on("SIGINT", () => {
|
|
135
|
+
closeDatabase();
|
|
136
|
+
process.exit(0);
|
|
137
|
+
});
|
|
138
|
+
process.on("SIGTERM", () => {
|
|
139
|
+
closeDatabase();
|
|
140
|
+
process.exit(0);
|
|
141
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting Store Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified interface that automatically selects between:
|
|
5
|
+
* - SQLite backend (preferred, for concurrent access safety)
|
|
6
|
+
* - JSON backend (fallback, for environments without native module support)
|
|
7
|
+
*/
|
|
8
|
+
// Lazy-loaded store implementation
|
|
9
|
+
let storeType = null;
|
|
10
|
+
let SqliteStore = null;
|
|
11
|
+
let JsonStore = null;
|
|
12
|
+
/**
|
|
13
|
+
* Detect and initialize the appropriate store
|
|
14
|
+
*/
|
|
15
|
+
async function getStore() {
|
|
16
|
+
if (storeType === "sqlite" && SqliteStore) {
|
|
17
|
+
return { type: "sqlite", store: SqliteStore };
|
|
18
|
+
}
|
|
19
|
+
if (storeType === "json" && JsonStore) {
|
|
20
|
+
return { type: "json", store: JsonStore };
|
|
21
|
+
}
|
|
22
|
+
// Try SQLite first
|
|
23
|
+
try {
|
|
24
|
+
// Dynamic import to avoid bundling issues
|
|
25
|
+
const sqliteModule = await import("./sqlite-meeting.js");
|
|
26
|
+
SqliteStore = sqliteModule.SqliteMeetingStore;
|
|
27
|
+
SqliteStore.init();
|
|
28
|
+
storeType = "sqlite";
|
|
29
|
+
console.error("[Nexus MeetingStore] Using SQLite backend");
|
|
30
|
+
return { type: "sqlite", store: SqliteStore };
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.error("[Nexus MeetingStore] SQLite unavailable:", e.message);
|
|
34
|
+
console.error("[Nexus MeetingStore] Falling back to JSON backend");
|
|
35
|
+
console.warn("[Nexus MeetingStore] ⚠️ JSON mode is single-process only. For multi-IDE environments, install better-sqlite3.");
|
|
36
|
+
// Fall back to JSON
|
|
37
|
+
const jsonModule = await import("./meetings.js");
|
|
38
|
+
JsonStore = jsonModule.MeetingStore;
|
|
39
|
+
storeType = "json";
|
|
40
|
+
return { type: "json", store: JsonStore };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Unified Meeting Store Interface
|
|
45
|
+
*/
|
|
46
|
+
export const UnifiedMeetingStore = {
|
|
47
|
+
/**
|
|
48
|
+
* Start a new meeting
|
|
49
|
+
*/
|
|
50
|
+
async startMeeting(topic, initiator) {
|
|
51
|
+
const { store } = await getStore();
|
|
52
|
+
return store.startMeeting(topic, initiator);
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Get a meeting by ID
|
|
56
|
+
*/
|
|
57
|
+
async getMeeting(id) {
|
|
58
|
+
const { store } = await getStore();
|
|
59
|
+
return store.getMeeting(id);
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* Add a message to a meeting
|
|
63
|
+
*/
|
|
64
|
+
async addMessage(meetingId, message) {
|
|
65
|
+
const { store } = await getStore();
|
|
66
|
+
return store.addMessage(meetingId, message);
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* End a meeting
|
|
70
|
+
*/
|
|
71
|
+
async endMeeting(meetingId, summary, callerId) {
|
|
72
|
+
const { store } = await getStore();
|
|
73
|
+
return store.endMeeting(meetingId, summary, callerId);
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Archive a meeting
|
|
77
|
+
*/
|
|
78
|
+
async archiveMeeting(meetingId, callerId) {
|
|
79
|
+
const { store } = await getStore();
|
|
80
|
+
return store.archiveMeeting(meetingId, callerId);
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Reopen a meeting
|
|
84
|
+
*/
|
|
85
|
+
async reopenMeeting(meetingId, callerId) {
|
|
86
|
+
const { store } = await getStore();
|
|
87
|
+
return store.reopenMeeting(meetingId, callerId);
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* List meetings
|
|
91
|
+
*/
|
|
92
|
+
async listMeetings(status) {
|
|
93
|
+
const { store } = await getStore();
|
|
94
|
+
return store.listMeetings(status);
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Get the current active meeting
|
|
98
|
+
*/
|
|
99
|
+
async getActiveMeeting() {
|
|
100
|
+
const { store } = await getStore();
|
|
101
|
+
return store.getActiveMeeting();
|
|
102
|
+
},
|
|
103
|
+
/**
|
|
104
|
+
* Get recent messages (incremental for SQLite with instanceId)
|
|
105
|
+
*/
|
|
106
|
+
async getRecentMessages(count, meetingId, instanceId) {
|
|
107
|
+
const { type, store } = await getStore();
|
|
108
|
+
if (type === "sqlite") {
|
|
109
|
+
// SQLite supports incremental reads with cursor tracking
|
|
110
|
+
return store.getRecentMessages(count || 10, meetingId, instanceId);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// JSON fallback - no incremental support
|
|
114
|
+
return store.getRecentMessages(count || 10, meetingId);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* Get the current backend type
|
|
119
|
+
*/
|
|
120
|
+
async getBackendType() {
|
|
121
|
+
const { type } = await getStore();
|
|
122
|
+
return type;
|
|
123
|
+
},
|
|
124
|
+
/**
|
|
125
|
+
* Get meeting state (SQLite only, returns empty for JSON)
|
|
126
|
+
*/
|
|
127
|
+
async getState() {
|
|
128
|
+
const { type, store } = await getStore();
|
|
129
|
+
if (type === "sqlite") {
|
|
130
|
+
const sqliteStore = store;
|
|
131
|
+
const activeMeetings = JSON.parse(sqliteStore.getState("active_meetings") || "[]");
|
|
132
|
+
const defaultMeetingId = sqliteStore.getState("default_meeting") || null;
|
|
133
|
+
return { activeMeetings, defaultMeetingId };
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// JSON backend
|
|
137
|
+
const jsonStore = store;
|
|
138
|
+
const state = await jsonStore.getState();
|
|
139
|
+
return state;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Get storage info for status display
|
|
144
|
+
* @returns storage_mode and is_degraded flag
|
|
145
|
+
*/
|
|
146
|
+
async getStorageInfo() {
|
|
147
|
+
const { type } = await getStore();
|
|
148
|
+
return {
|
|
149
|
+
storage_mode: type,
|
|
150
|
+
is_degraded: type === "json" // JSON mode is considered degraded
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskService - Phase 2: Async Task Management
|
|
3
|
+
*
|
|
4
|
+
* Manages long-running operations with progress tracking,
|
|
5
|
+
* meeting traceability, and MCP-compatible status reporting.
|
|
6
|
+
*/
|
|
7
|
+
import { getDatabase } from "./sqlite.js";
|
|
8
|
+
import { TASK_CLEANUP_MAX_AGE_MS } from "../constants.js";
|
|
9
|
+
// Generate unique task ID
|
|
10
|
+
function generateTaskId() {
|
|
11
|
+
const timestamp = new Date().toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
|
|
12
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
13
|
+
return `task_${timestamp}_${random}`;
|
|
14
|
+
}
|
|
15
|
+
let initialized = false;
|
|
16
|
+
/**
|
|
17
|
+
* Initialize the tasks table (run migrations)
|
|
18
|
+
*/
|
|
19
|
+
export function initTasksTable() {
|
|
20
|
+
if (initialized)
|
|
21
|
+
return;
|
|
22
|
+
const db = getDatabase();
|
|
23
|
+
const TASKS_SCHEMA = `
|
|
24
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed', 'cancelled')) DEFAULT 'pending',
|
|
27
|
+
progress REAL DEFAULT 0.0 CHECK(progress >= 0.0 AND progress <= 1.0),
|
|
28
|
+
source_meeting_id TEXT,
|
|
29
|
+
metadata TEXT,
|
|
30
|
+
result_uri TEXT,
|
|
31
|
+
error_message TEXT,
|
|
32
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
33
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
34
|
+
ttl INTEGER,
|
|
35
|
+
FOREIGN KEY (source_meeting_id) REFERENCES meetings(id) ON DELETE SET NULL
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_meeting ON tasks(source_meeting_id);
|
|
40
|
+
`;
|
|
41
|
+
db.exec(TASKS_SCHEMA);
|
|
42
|
+
// Add trigger for auto-updating updated_at (separate exec to handle IF NOT EXISTS)
|
|
43
|
+
try {
|
|
44
|
+
db.exec(`
|
|
45
|
+
CREATE TRIGGER IF NOT EXISTS tasks_updated_at
|
|
46
|
+
AFTER UPDATE ON tasks
|
|
47
|
+
FOR EACH ROW
|
|
48
|
+
BEGIN
|
|
49
|
+
UPDATE tasks SET updated_at = datetime('now') WHERE id = OLD.id;
|
|
50
|
+
END;
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Trigger may already exist in older SQLite versions without IF NOT EXISTS support
|
|
55
|
+
}
|
|
56
|
+
console.error("[Nexus] Tasks table initialized");
|
|
57
|
+
initialized = true;
|
|
58
|
+
}
|
|
59
|
+
export function resetTasksInit() {
|
|
60
|
+
initialized = false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a new task
|
|
64
|
+
*/
|
|
65
|
+
export function createTask(input = {}) {
|
|
66
|
+
const db = getDatabase();
|
|
67
|
+
const id = input.id || generateTaskId();
|
|
68
|
+
const now = new Date().toISOString();
|
|
69
|
+
const stmt = db.prepare(`
|
|
70
|
+
INSERT INTO tasks (id, status, progress, source_meeting_id, metadata, created_at, updated_at, ttl)
|
|
71
|
+
VALUES (?, 'pending', 0.0, ?, ?, ?, ?, ?)
|
|
72
|
+
`);
|
|
73
|
+
stmt.run(id, input.source_meeting_id || null, input.metadata ? JSON.stringify(input.metadata) : null, now, now, input.ttl || null);
|
|
74
|
+
return getTask(id);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get a task by ID
|
|
78
|
+
*/
|
|
79
|
+
export function getTask(id) {
|
|
80
|
+
const db = getDatabase();
|
|
81
|
+
const row = db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
82
|
+
if (!row)
|
|
83
|
+
return null;
|
|
84
|
+
return {
|
|
85
|
+
id: row.id,
|
|
86
|
+
status: row.status,
|
|
87
|
+
progress: row.progress,
|
|
88
|
+
source_meeting_id: row.source_meeting_id || null,
|
|
89
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
90
|
+
result_uri: row.result_uri || null,
|
|
91
|
+
error_message: row.error_message || null,
|
|
92
|
+
created_at: row.created_at,
|
|
93
|
+
updated_at: row.updated_at,
|
|
94
|
+
ttl: row.ttl || null
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Update task status and progress
|
|
99
|
+
*/
|
|
100
|
+
export function updateTask(id, update) {
|
|
101
|
+
const db = getDatabase();
|
|
102
|
+
const now = new Date().toISOString();
|
|
103
|
+
const sets = ["updated_at = ?"];
|
|
104
|
+
const values = [now];
|
|
105
|
+
if (update.status !== undefined) {
|
|
106
|
+
sets.push("status = ?");
|
|
107
|
+
values.push(update.status);
|
|
108
|
+
}
|
|
109
|
+
if (update.progress !== undefined) {
|
|
110
|
+
sets.push("progress = ?");
|
|
111
|
+
values.push(Math.max(0, Math.min(1, update.progress)));
|
|
112
|
+
}
|
|
113
|
+
if (update.result_uri !== undefined) {
|
|
114
|
+
sets.push("result_uri = ?");
|
|
115
|
+
values.push(update.result_uri);
|
|
116
|
+
}
|
|
117
|
+
if (update.error_message !== undefined) {
|
|
118
|
+
sets.push("error_message = ?");
|
|
119
|
+
values.push(update.error_message);
|
|
120
|
+
}
|
|
121
|
+
if (update.metadata !== undefined) {
|
|
122
|
+
sets.push("metadata = ?");
|
|
123
|
+
values.push(JSON.stringify(update.metadata));
|
|
124
|
+
}
|
|
125
|
+
values.push(id);
|
|
126
|
+
const sql = `UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`;
|
|
127
|
+
db.prepare(sql).run(...values);
|
|
128
|
+
return getTask(id);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* List tasks with optional status filter
|
|
132
|
+
*/
|
|
133
|
+
export function listTasks(status, limit = 50) {
|
|
134
|
+
const db = getDatabase();
|
|
135
|
+
let sql = "SELECT * FROM tasks";
|
|
136
|
+
const params = [];
|
|
137
|
+
if (status) {
|
|
138
|
+
sql += " WHERE status = ?";
|
|
139
|
+
params.push(status);
|
|
140
|
+
}
|
|
141
|
+
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
142
|
+
params.push(limit);
|
|
143
|
+
const rows = db.prepare(sql).all(...params);
|
|
144
|
+
return rows.map(row => ({
|
|
145
|
+
id: row.id,
|
|
146
|
+
status: row.status,
|
|
147
|
+
progress: row.progress,
|
|
148
|
+
source_meeting_id: row.source_meeting_id || null,
|
|
149
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
150
|
+
result_uri: row.result_uri || null,
|
|
151
|
+
error_message: row.error_message || null,
|
|
152
|
+
created_at: row.created_at,
|
|
153
|
+
updated_at: row.updated_at,
|
|
154
|
+
ttl: row.ttl || null
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get tasks by meeting ID
|
|
159
|
+
*/
|
|
160
|
+
export function getTasksByMeeting(meetingId) {
|
|
161
|
+
const db = getDatabase();
|
|
162
|
+
const rows = db.prepare(`
|
|
163
|
+
SELECT * FROM tasks WHERE source_meeting_id = ? ORDER BY created_at DESC
|
|
164
|
+
`).all(meetingId);
|
|
165
|
+
return rows.map(row => ({
|
|
166
|
+
id: row.id,
|
|
167
|
+
status: row.status,
|
|
168
|
+
progress: row.progress,
|
|
169
|
+
source_meeting_id: row.source_meeting_id || null,
|
|
170
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
171
|
+
result_uri: row.result_uri || null,
|
|
172
|
+
error_message: row.error_message || null,
|
|
173
|
+
created_at: row.created_at,
|
|
174
|
+
updated_at: row.updated_at,
|
|
175
|
+
ttl: row.ttl || null
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Cancel a pending or running task
|
|
180
|
+
*/
|
|
181
|
+
export function cancelTask(id) {
|
|
182
|
+
const task = getTask(id);
|
|
183
|
+
if (!task)
|
|
184
|
+
return false;
|
|
185
|
+
if (task.status !== "pending" && task.status !== "running")
|
|
186
|
+
return false;
|
|
187
|
+
updateTask(id, { status: "cancelled" });
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Delete completed/failed/cancelled tasks older than specified age
|
|
192
|
+
*/
|
|
193
|
+
export function cleanupTasks(maxAgeMs = TASK_CLEANUP_MAX_AGE_MS) {
|
|
194
|
+
const db = getDatabase();
|
|
195
|
+
const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
|
|
196
|
+
const result = db.prepare(`
|
|
197
|
+
DELETE FROM tasks
|
|
198
|
+
WHERE status IN ('completed', 'failed', 'cancelled')
|
|
199
|
+
AND updated_at < ?
|
|
200
|
+
`).run(cutoff);
|
|
201
|
+
return result.changes;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get active (pending/running) task count
|
|
205
|
+
*/
|
|
206
|
+
export function getActiveTaskCount() {
|
|
207
|
+
const db = getDatabase();
|
|
208
|
+
const row = db.prepare(`
|
|
209
|
+
SELECT COUNT(*) as count FROM tasks WHERE status IN ('pending', 'running')
|
|
210
|
+
`).get();
|
|
211
|
+
return row.count;
|
|
212
|
+
}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple mutex lock for preventing concurrent file writes.
|
|
3
|
+
* Ensures that only one write operation can happen at a time.
|
|
4
|
+
*/
|
|
5
|
+
export class AsyncMutex {
|
|
6
|
+
locked = false;
|
|
7
|
+
queue = [];
|
|
8
|
+
async acquire() {
|
|
9
|
+
if (!this.locked) {
|
|
10
|
+
this.locked = true;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
this.queue.push(resolve);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
release() {
|
|
18
|
+
if (this.queue.length > 0) {
|
|
19
|
+
const next = this.queue.shift();
|
|
20
|
+
if (next)
|
|
21
|
+
next();
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.locked = false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async withLock(fn) {
|
|
28
|
+
await this.acquire();
|
|
29
|
+
try {
|
|
30
|
+
return await fn();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
this.release();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|