opencode-database-plugin 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db.d.ts +3 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +316 -218
- package/package.json +1 -1
package/dist/db.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import postgres from "postgres";
|
|
2
2
|
export declare const sql: postgres.Sql<{}>;
|
|
3
|
+
export declare function isDatabaseHealthy(): boolean;
|
|
3
4
|
export declare function ensureConnection(): Promise<boolean>;
|
|
5
|
+
export declare function safeQuery<T>(queryFn: () => Promise<T>, timeoutMs?: number): Promise<T | undefined>;
|
|
6
|
+
export declare function fireAndForget(queryFn: () => Promise<unknown>, onError?: (error: unknown) => void): void;
|
|
4
7
|
//# sourceMappingURL=db.d.ts.map
|
package/dist/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAWhC,eAAO,MAAM,GAAG,kBAMd,CAAC;AAOH,wBAAgB,iBAAiB,IAAI,OAAO,CAY3C;AAYD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAUzD;AAED,wBAAsB,SAAS,CAAC,CAAC,EAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,SAAS,GAAE,MAAsB,GAChC,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CA2BxB;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAC/B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,IAAI,CAMN"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AA0E/D,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC3C,CAAC;CACH;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE;YACL,KAAK,CAAC,EAAE,MAAM,CAAC;YACf,GAAG,CAAC,EAAE,MAAM,CAAC;SACd,CAAC;KACH,CAAC;IACF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC3C,CAAC;CACH;AAmBD,eAAO,MAAM,cAAc,EAAE,MAyoB5B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1947,30 +1947,125 @@ function osUsername() {
|
|
|
1947
1947
|
|
|
1948
1948
|
// db.ts
|
|
1949
1949
|
var DATABASE_URL = process.env.OPENCODE_DATABASE_URL || "postgres://opencode:opencode@postgres:5432/opencode";
|
|
1950
|
+
var QUERY_TIMEOUT = parseInt(process.env.OPENCODE_DB_QUERY_TIMEOUT || "10000", 10);
|
|
1950
1951
|
var sql = src_default(DATABASE_URL, {
|
|
1951
1952
|
max: 10,
|
|
1952
|
-
idle_timeout:
|
|
1953
|
-
connect_timeout:
|
|
1953
|
+
idle_timeout: 30,
|
|
1954
|
+
connect_timeout: 10,
|
|
1955
|
+
max_lifetime: 60 * 30,
|
|
1954
1956
|
onnotice: () => {}
|
|
1955
1957
|
});
|
|
1958
|
+
var consecutiveFailures = 0;
|
|
1959
|
+
var lastFailureTime = 0;
|
|
1960
|
+
var MAX_BACKOFF_MS = 60000;
|
|
1961
|
+
var BASE_BACKOFF_MS = 1000;
|
|
1962
|
+
function isDatabaseHealthy() {
|
|
1963
|
+
if (consecutiveFailures === 0) {
|
|
1964
|
+
return true;
|
|
1965
|
+
}
|
|
1966
|
+
const backoffMs = Math.min(BASE_BACKOFF_MS * Math.pow(2, consecutiveFailures - 1), MAX_BACKOFF_MS);
|
|
1967
|
+
const timeSinceFailure = Date.now() - lastFailureTime;
|
|
1968
|
+
return timeSinceFailure >= backoffMs;
|
|
1969
|
+
}
|
|
1970
|
+
function markHealthy() {
|
|
1971
|
+
consecutiveFailures = 0;
|
|
1972
|
+
lastFailureTime = 0;
|
|
1973
|
+
}
|
|
1974
|
+
function markUnhealthy() {
|
|
1975
|
+
consecutiveFailures++;
|
|
1976
|
+
lastFailureTime = Date.now();
|
|
1977
|
+
}
|
|
1956
1978
|
async function ensureConnection() {
|
|
1957
1979
|
try {
|
|
1958
1980
|
await sql`SELECT 1`;
|
|
1981
|
+
markHealthy();
|
|
1959
1982
|
return true;
|
|
1960
1983
|
} catch (error) {
|
|
1961
1984
|
console.error("[database] Database connection failed:", error);
|
|
1985
|
+
markUnhealthy();
|
|
1962
1986
|
return false;
|
|
1963
1987
|
}
|
|
1964
1988
|
}
|
|
1989
|
+
async function safeQuery(queryFn, timeoutMs = QUERY_TIMEOUT) {
|
|
1990
|
+
if (!isDatabaseHealthy()) {
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
try {
|
|
1994
|
+
const result = await Promise.race([
|
|
1995
|
+
queryFn(),
|
|
1996
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Query timeout")), timeoutMs))
|
|
1997
|
+
]);
|
|
1998
|
+
markHealthy();
|
|
1999
|
+
return result;
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
if (error instanceof Error && (error.message.includes("connection") || error.message.includes("timeout") || error.message.includes("ECONNREFUSED") || error.message.includes("ENOTFOUND") || error.message.includes("ETIMEDOUT"))) {
|
|
2002
|
+
markUnhealthy();
|
|
2003
|
+
}
|
|
2004
|
+
throw error;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
function fireAndForget(queryFn, onError) {
|
|
2008
|
+
safeQuery(queryFn).catch((error) => {
|
|
2009
|
+
if (onError) {
|
|
2010
|
+
onError(error);
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
1965
2014
|
|
|
1966
2015
|
// index.ts
|
|
2016
|
+
var STALE_ENTRY_TIMEOUT_MS = 15 * 60 * 1000;
|
|
2017
|
+
var CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
1967
2018
|
var pendingExecutions = new Map;
|
|
1968
2019
|
var callIdToPartId = new Map;
|
|
1969
2020
|
var pendingUserMessages = new Map;
|
|
1970
2021
|
var tokensCountedBySession = new Map;
|
|
2022
|
+
var callIdTimestamps = new Map;
|
|
2023
|
+
function cleanupStaleMaps() {
|
|
2024
|
+
const now = Date.now();
|
|
2025
|
+
for (const [key, value] of pendingExecutions) {
|
|
2026
|
+
if (now - value.startedAt.getTime() > STALE_ENTRY_TIMEOUT_MS) {
|
|
2027
|
+
pendingExecutions.delete(key);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
for (const [key, value] of pendingUserMessages) {
|
|
2031
|
+
if (now - value.timestamp > STALE_ENTRY_TIMEOUT_MS) {
|
|
2032
|
+
pendingUserMessages.delete(key);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
for (const [key, timestamp] of callIdTimestamps) {
|
|
2036
|
+
if (now - timestamp > STALE_ENTRY_TIMEOUT_MS) {
|
|
2037
|
+
callIdToPartId.delete(key);
|
|
2038
|
+
callIdTimestamps.delete(key);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
for (const [sessionId, messageTimestamps] of tokensCountedBySession) {
|
|
2042
|
+
for (const [messageId, timestamp] of messageTimestamps) {
|
|
2043
|
+
if (now - timestamp > STALE_ENTRY_TIMEOUT_MS) {
|
|
2044
|
+
messageTimestamps.delete(messageId);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
if (messageTimestamps.size === 0) {
|
|
2048
|
+
tokensCountedBySession.delete(sessionId);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
var cleanupInterval = setInterval(cleanupStaleMaps, CLEANUP_INTERVAL_MS);
|
|
2053
|
+
if (cleanupInterval.unref) {
|
|
2054
|
+
cleanupInterval.unref();
|
|
2055
|
+
}
|
|
1971
2056
|
function generateCorrelationId() {
|
|
1972
2057
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
1973
2058
|
}
|
|
2059
|
+
function logError(client, message, extra) {
|
|
2060
|
+
Promise.resolve(client.app.log({
|
|
2061
|
+
body: {
|
|
2062
|
+
service: "database",
|
|
2063
|
+
level: "error",
|
|
2064
|
+
message,
|
|
2065
|
+
extra
|
|
2066
|
+
}
|
|
2067
|
+
})).catch(() => {});
|
|
2068
|
+
}
|
|
1974
2069
|
var DatabasePlugin = async ({ client }) => {
|
|
1975
2070
|
const connected = await ensureConnection();
|
|
1976
2071
|
if (!connected) {
|
|
@@ -1990,7 +2085,7 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
1990
2085
|
switch (event.type) {
|
|
1991
2086
|
case "session.created": {
|
|
1992
2087
|
const info = props.info;
|
|
1993
|
-
|
|
2088
|
+
fireAndForget(() => sql`
|
|
1994
2089
|
INSERT INTO sessions (id, title, parent_id, project_id, directory, status, created_at)
|
|
1995
2090
|
VALUES (
|
|
1996
2091
|
${info.id},
|
|
@@ -2006,43 +2101,52 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2006
2101
|
parent_id = COALESCE(${info.parentID || null}, sessions.parent_id),
|
|
2007
2102
|
project_id = COALESCE(${info.projectID || null}, sessions.project_id),
|
|
2008
2103
|
directory = COALESCE(${info.directory || null}, sessions.directory)
|
|
2009
|
-
|
|
2104
|
+
`, (error) => logError(client, "Error in session.created", {
|
|
2105
|
+
error: String(error)
|
|
2106
|
+
}));
|
|
2010
2107
|
break;
|
|
2011
2108
|
}
|
|
2012
2109
|
case "session.updated": {
|
|
2013
2110
|
const info = props.info;
|
|
2014
|
-
|
|
2111
|
+
fireAndForget(() => sql`
|
|
2015
2112
|
UPDATE sessions
|
|
2016
2113
|
SET title = COALESCE(${info.title || null}, title),
|
|
2017
2114
|
share_url = COALESCE(${info.share?.url || null}, share_url)
|
|
2018
2115
|
WHERE id = ${info.id}
|
|
2019
|
-
|
|
2116
|
+
`, (error) => logError(client, "Error in session.updated", {
|
|
2117
|
+
error: String(error)
|
|
2118
|
+
}));
|
|
2020
2119
|
break;
|
|
2021
2120
|
}
|
|
2022
2121
|
case "session.deleted": {
|
|
2023
2122
|
const info = props.info;
|
|
2024
|
-
|
|
2123
|
+
fireAndForget(() => sql`
|
|
2025
2124
|
UPDATE sessions
|
|
2026
2125
|
SET deleted_at = NOW(), status = 'deleted'
|
|
2027
2126
|
WHERE id = ${info.id}
|
|
2028
|
-
|
|
2127
|
+
`, (error) => logError(client, "Error in session.deleted", {
|
|
2128
|
+
error: String(error)
|
|
2129
|
+
}));
|
|
2029
2130
|
tokensCountedBySession.delete(info.id);
|
|
2030
2131
|
break;
|
|
2031
2132
|
}
|
|
2032
2133
|
case "session.idle": {
|
|
2033
2134
|
const sessionID = props.sessionID;
|
|
2034
|
-
|
|
2135
|
+
fireAndForget(() => sql`
|
|
2035
2136
|
UPDATE sessions
|
|
2036
2137
|
SET status = 'idle', updated_at = NOW()
|
|
2037
2138
|
WHERE id = ${sessionID}
|
|
2038
|
-
|
|
2139
|
+
`, (error) => logError(client, "Error in session.idle", {
|
|
2140
|
+
error: String(error)
|
|
2141
|
+
}));
|
|
2039
2142
|
break;
|
|
2040
2143
|
}
|
|
2041
2144
|
case "session.error": {
|
|
2042
2145
|
const sessionID = props.sessionID;
|
|
2043
2146
|
const error = props.error;
|
|
2044
2147
|
if (sessionID) {
|
|
2045
|
-
|
|
2148
|
+
fireAndForget(async () => {
|
|
2149
|
+
await sql`
|
|
2046
2150
|
INSERT INTO session_errors (session_id, error_type, error_message, error_data)
|
|
2047
2151
|
VALUES (
|
|
2048
2152
|
${sessionID},
|
|
@@ -2051,70 +2155,66 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2051
2155
|
${error ? sql.json(error) : null}
|
|
2052
2156
|
)
|
|
2053
2157
|
`;
|
|
2054
|
-
|
|
2158
|
+
await sql`
|
|
2055
2159
|
UPDATE sessions SET status = 'error' WHERE id = ${sessionID}
|
|
2056
2160
|
`;
|
|
2057
|
-
await client.app.log({
|
|
2058
|
-
body: {
|
|
2059
|
-
service: "database",
|
|
2060
|
-
level: "info",
|
|
2061
|
-
message: "Session error",
|
|
2062
|
-
extra: { sessionID, errorMessage: error?.data?.message }
|
|
2063
|
-
}
|
|
2064
2161
|
});
|
|
2065
2162
|
}
|
|
2066
2163
|
break;
|
|
2067
2164
|
}
|
|
2068
2165
|
case "session.compacted": {
|
|
2069
2166
|
const sessionID = props.sessionID;
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2167
|
+
try {
|
|
2168
|
+
const result = await safeQuery(() => sql`
|
|
2169
|
+
SELECT context_tokens, input_tokens, output_tokens,
|
|
2170
|
+
cache_read_tokens, cache_write_tokens, reasoning_tokens, estimated_cost
|
|
2171
|
+
FROM sessions WHERE id = ${sessionID}
|
|
2172
|
+
`);
|
|
2173
|
+
const sessionState = result?.[0];
|
|
2174
|
+
if (sessionState) {
|
|
2175
|
+
fireAndForget(() => sql`
|
|
2176
|
+
INSERT INTO compactions (
|
|
2177
|
+
session_id,
|
|
2178
|
+
context_tokens_before,
|
|
2179
|
+
cumulative_input_tokens,
|
|
2180
|
+
cumulative_output_tokens,
|
|
2181
|
+
cumulative_cache_read,
|
|
2182
|
+
cumulative_cache_write,
|
|
2183
|
+
cumulative_reasoning,
|
|
2184
|
+
cumulative_cost
|
|
2185
|
+
)
|
|
2186
|
+
VALUES (
|
|
2187
|
+
${sessionID},
|
|
2188
|
+
${sessionState.context_tokens || 0},
|
|
2189
|
+
${sessionState.input_tokens || 0},
|
|
2190
|
+
${sessionState.output_tokens || 0},
|
|
2191
|
+
${sessionState.cache_read_tokens || 0},
|
|
2192
|
+
${sessionState.cache_write_tokens || 0},
|
|
2193
|
+
${sessionState.reasoning_tokens || 0},
|
|
2194
|
+
${parseFloat(sessionState.estimated_cost || "0")}
|
|
2195
|
+
)
|
|
2196
|
+
`);
|
|
2197
|
+
}
|
|
2198
|
+
fireAndForget(() => sql`
|
|
2199
|
+
UPDATE sessions
|
|
2200
|
+
SET
|
|
2201
|
+
status = 'compacted',
|
|
2202
|
+
peak_context_tokens = GREATEST(peak_context_tokens, context_tokens),
|
|
2203
|
+
context_tokens = 0,
|
|
2204
|
+
compaction_count = compaction_count + 1
|
|
2205
|
+
WHERE id = ${sessionID}
|
|
2206
|
+
`);
|
|
2207
|
+
} catch {}
|
|
2108
2208
|
tokensCountedBySession.delete(sessionID);
|
|
2109
2209
|
break;
|
|
2110
2210
|
}
|
|
2111
2211
|
case "message.updated": {
|
|
2112
2212
|
const info = props.info;
|
|
2113
|
-
|
|
2213
|
+
fireAndForget(() => sql`
|
|
2114
2214
|
INSERT INTO sessions (id, status, created_at, updated_at)
|
|
2115
2215
|
VALUES (${info.sessionID}, 'active', NOW(), NOW())
|
|
2116
2216
|
ON CONFLICT (id) DO UPDATE SET updated_at = NOW()
|
|
2117
|
-
|
|
2217
|
+
`);
|
|
2118
2218
|
let messageContent = info.parts;
|
|
2119
2219
|
let textContent = null;
|
|
2120
2220
|
let systemPrompt = info.system || null;
|
|
@@ -2134,7 +2234,7 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2134
2234
|
}
|
|
2135
2235
|
const modelProvider = info.providerID || info.model?.providerID || null;
|
|
2136
2236
|
const modelId = info.modelID || info.model?.modelID || null;
|
|
2137
|
-
|
|
2237
|
+
fireAndForget(() => sql`
|
|
2138
2238
|
INSERT INTO messages (id, session_id, role, model_provider, model_id, text, summary, content, system_prompt, created_at)
|
|
2139
2239
|
VALUES (
|
|
2140
2240
|
${info.id},
|
|
@@ -2156,8 +2256,10 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2156
2256
|
summary = COALESCE(${info.summary?.title || null}, messages.summary),
|
|
2157
2257
|
content = COALESCE(${messageContent ? sql.json(messageContent) : null}, messages.content),
|
|
2158
2258
|
system_prompt = COALESCE(${systemPrompt}, messages.system_prompt)
|
|
2159
|
-
|
|
2160
|
-
|
|
2259
|
+
`, (error) => logError(client, "Error in message.updated", {
|
|
2260
|
+
error: String(error)
|
|
2261
|
+
}));
|
|
2262
|
+
const sessionTokens = tokensCountedBySession.get(info.sessionID) || new Map;
|
|
2161
2263
|
if (info.role === "assistant" && info.tokens && !sessionTokens.has(info.id)) {
|
|
2162
2264
|
const inputTokens = info.tokens.input ?? 0;
|
|
2163
2265
|
const outputTokens = info.tokens.output ?? 0;
|
|
@@ -2165,10 +2267,10 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2165
2267
|
const cacheRead = info.tokens.cache?.read ?? 0;
|
|
2166
2268
|
const cacheWrite = info.tokens.cache?.write ?? 0;
|
|
2167
2269
|
if (inputTokens > 0 || outputTokens > 0) {
|
|
2168
|
-
sessionTokens.
|
|
2270
|
+
sessionTokens.set(info.id, Date.now());
|
|
2169
2271
|
tokensCountedBySession.set(info.sessionID, sessionTokens);
|
|
2170
2272
|
const contextSize = inputTokens + cacheRead;
|
|
2171
|
-
|
|
2273
|
+
fireAndForget(() => sql`
|
|
2172
2274
|
UPDATE sessions
|
|
2173
2275
|
SET
|
|
2174
2276
|
input_tokens = input_tokens + ${inputTokens},
|
|
@@ -2181,16 +2283,18 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2181
2283
|
model_provider = COALESCE(${modelProvider}, model_provider),
|
|
2182
2284
|
model_id = COALESCE(${modelId}, model_id)
|
|
2183
2285
|
WHERE id = ${info.sessionID}
|
|
2184
|
-
|
|
2286
|
+
`, (error) => logError(client, "Error updating session tokens", {
|
|
2287
|
+
error: String(error)
|
|
2288
|
+
}));
|
|
2185
2289
|
}
|
|
2186
2290
|
}
|
|
2187
2291
|
break;
|
|
2188
2292
|
}
|
|
2189
2293
|
case "message.removed": {
|
|
2190
2294
|
const messageID = props.messageID;
|
|
2191
|
-
|
|
2295
|
+
fireAndForget(() => sql`
|
|
2192
2296
|
DELETE FROM messages WHERE id = ${messageID}
|
|
2193
|
-
|
|
2297
|
+
`);
|
|
2194
2298
|
break;
|
|
2195
2299
|
}
|
|
2196
2300
|
case "message.part.updated": {
|
|
@@ -2199,56 +2303,60 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2199
2303
|
const textContent = part.text || null;
|
|
2200
2304
|
if (part.type === "tool" && part.callID) {
|
|
2201
2305
|
callIdToPartId.set(part.callID, part.id);
|
|
2306
|
+
callIdTimestamps.set(part.callID, Date.now());
|
|
2202
2307
|
const pending = pendingExecutions.get(part.callID);
|
|
2203
2308
|
if (pending) {
|
|
2204
2309
|
pending.partId = part.id;
|
|
2205
2310
|
}
|
|
2206
2311
|
}
|
|
2207
2312
|
if (part.type === "step-finish" && part.cost !== undefined) {
|
|
2208
|
-
|
|
2313
|
+
const cost = part.cost;
|
|
2314
|
+
fireAndForget(() => sql`
|
|
2209
2315
|
UPDATE sessions
|
|
2210
|
-
SET estimated_cost = estimated_cost + ${
|
|
2316
|
+
SET estimated_cost = estimated_cost + ${cost}
|
|
2211
2317
|
WHERE id = ${part.sessionID}
|
|
2212
|
-
|
|
2318
|
+
`);
|
|
2213
2319
|
}
|
|
2214
|
-
|
|
2320
|
+
fireAndForget(() => sql`
|
|
2215
2321
|
INSERT INTO messages (id, session_id, role, created_at)
|
|
2216
2322
|
VALUES (${part.messageID}, ${part.sessionID}, 'assistant', NOW())
|
|
2217
2323
|
ON CONFLICT (id) DO UPDATE SET
|
|
2218
2324
|
role = COALESCE(messages.role, 'assistant')
|
|
2219
|
-
|
|
2220
|
-
|
|
2325
|
+
`);
|
|
2326
|
+
fireAndForget(() => sql`
|
|
2221
2327
|
INSERT INTO sessions (id, status, created_at)
|
|
2222
2328
|
VALUES (${part.sessionID}, 'active', NOW())
|
|
2223
2329
|
ON CONFLICT (id) DO NOTHING
|
|
2224
|
-
|
|
2330
|
+
`);
|
|
2225
2331
|
const isStreamingTextPart = part.type === "text" || part.type === "reasoning";
|
|
2226
2332
|
const partAsJson = { ...part };
|
|
2227
2333
|
if (isStreamingTextPart) {
|
|
2228
|
-
|
|
2229
|
-
INSERT INTO message_parts (id, message_id, part_type, tool_name, text, content, created_at)
|
|
2230
|
-
VALUES (
|
|
2231
|
-
${part.id},
|
|
2232
|
-
${part.messageID},
|
|
2233
|
-
${part.type},
|
|
2234
|
-
${toolName},
|
|
2235
|
-
${textContent},
|
|
2236
|
-
${sql.json(partAsJson)},
|
|
2237
|
-
NOW()
|
|
2238
|
-
)
|
|
2239
|
-
ON CONFLICT (id) DO NOTHING
|
|
2240
|
-
`;
|
|
2241
|
-
if (textContent) {
|
|
2334
|
+
fireAndForget(async () => {
|
|
2242
2335
|
await sql`
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2336
|
+
INSERT INTO message_parts (id, message_id, part_type, tool_name, text, content, created_at)
|
|
2337
|
+
VALUES (
|
|
2338
|
+
${part.id},
|
|
2339
|
+
${part.messageID},
|
|
2340
|
+
${part.type},
|
|
2341
|
+
${toolName},
|
|
2342
|
+
${textContent},
|
|
2343
|
+
${sql.json(partAsJson)},
|
|
2344
|
+
NOW()
|
|
2345
|
+
)
|
|
2346
|
+
ON CONFLICT (id) DO NOTHING
|
|
2250
2347
|
`;
|
|
2251
|
-
|
|
2348
|
+
if (textContent) {
|
|
2349
|
+
await sql`
|
|
2350
|
+
UPDATE message_parts
|
|
2351
|
+
SET
|
|
2352
|
+
tool_name = COALESCE(${toolName}, tool_name),
|
|
2353
|
+
text = ${textContent},
|
|
2354
|
+
content = ${sql.json(partAsJson)}
|
|
2355
|
+
WHERE id = ${part.id}
|
|
2356
|
+
AND (text IS NULL OR LENGTH(text) < LENGTH(${textContent}))
|
|
2357
|
+
`;
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2252
2360
|
} else {
|
|
2253
2361
|
const statusPriority = {
|
|
2254
2362
|
pending: 1,
|
|
@@ -2258,59 +2366,61 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2258
2366
|
};
|
|
2259
2367
|
const currentStatus = part.state?.status || "pending";
|
|
2260
2368
|
const currentPriority = statusPriority[currentStatus] || 0;
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
ON CONFLICT (id) DO NOTHING
|
|
2273
|
-
`;
|
|
2274
|
-
await sql`
|
|
2275
|
-
UPDATE message_parts
|
|
2276
|
-
SET
|
|
2277
|
-
tool_name = COALESCE(${toolName}, tool_name),
|
|
2278
|
-
text = COALESCE(${textContent}, text),
|
|
2279
|
-
content = ${sql.json(partAsJson)}
|
|
2280
|
-
WHERE id = ${part.id}
|
|
2281
|
-
AND ${currentPriority} >= COALESCE(
|
|
2282
|
-
CASE (content->'state'->>'status')
|
|
2283
|
-
WHEN 'pending' THEN 1
|
|
2284
|
-
WHEN 'running' THEN 2
|
|
2285
|
-
WHEN 'completed' THEN 3
|
|
2286
|
-
WHEN 'error' THEN 3
|
|
2287
|
-
ELSE 0
|
|
2288
|
-
END, 0
|
|
2369
|
+
fireAndForget(async () => {
|
|
2370
|
+
await sql`
|
|
2371
|
+
INSERT INTO message_parts (id, message_id, part_type, tool_name, text, content, created_at)
|
|
2372
|
+
VALUES (
|
|
2373
|
+
${part.id},
|
|
2374
|
+
${part.messageID},
|
|
2375
|
+
${part.type},
|
|
2376
|
+
${toolName},
|
|
2377
|
+
${textContent},
|
|
2378
|
+
${sql.json(partAsJson)},
|
|
2379
|
+
NOW()
|
|
2289
2380
|
)
|
|
2290
|
-
|
|
2381
|
+
ON CONFLICT (id) DO NOTHING
|
|
2382
|
+
`;
|
|
2383
|
+
await sql`
|
|
2384
|
+
UPDATE message_parts
|
|
2385
|
+
SET
|
|
2386
|
+
tool_name = COALESCE(${toolName}, tool_name),
|
|
2387
|
+
text = COALESCE(${textContent}, text),
|
|
2388
|
+
content = ${sql.json(partAsJson)}
|
|
2389
|
+
WHERE id = ${part.id}
|
|
2390
|
+
AND ${currentPriority} >= COALESCE(
|
|
2391
|
+
CASE (content->'state'->>'status')
|
|
2392
|
+
WHEN 'pending' THEN 1
|
|
2393
|
+
WHEN 'running' THEN 2
|
|
2394
|
+
WHEN 'completed' THEN 3
|
|
2395
|
+
WHEN 'error' THEN 3
|
|
2396
|
+
ELSE 0
|
|
2397
|
+
END, 0
|
|
2398
|
+
)
|
|
2399
|
+
`;
|
|
2400
|
+
});
|
|
2291
2401
|
}
|
|
2292
2402
|
if (part.type === "text" && textContent) {
|
|
2293
|
-
|
|
2403
|
+
fireAndForget(() => sql`
|
|
2294
2404
|
UPDATE messages
|
|
2295
2405
|
SET text = ${textContent}
|
|
2296
2406
|
WHERE id = ${part.messageID}
|
|
2297
2407
|
AND (text IS NULL OR LENGTH(text) < LENGTH(${textContent}))
|
|
2298
|
-
|
|
2408
|
+
`);
|
|
2299
2409
|
}
|
|
2300
2410
|
break;
|
|
2301
2411
|
}
|
|
2302
2412
|
case "message.part.removed": {
|
|
2303
2413
|
const partID = props.partID;
|
|
2304
|
-
|
|
2414
|
+
fireAndForget(() => sql`
|
|
2305
2415
|
DELETE FROM message_parts WHERE id = ${partID}
|
|
2306
|
-
|
|
2416
|
+
`);
|
|
2307
2417
|
break;
|
|
2308
2418
|
}
|
|
2309
2419
|
case "command.executed": {
|
|
2310
2420
|
const name = props.name;
|
|
2311
2421
|
const sessionID = props.sessionID;
|
|
2312
2422
|
const args = props.arguments;
|
|
2313
|
-
|
|
2423
|
+
fireAndForget(() => sql`
|
|
2314
2424
|
INSERT INTO commands (session_id, command_name, command_args, created_at)
|
|
2315
2425
|
VALUES (
|
|
2316
2426
|
${sessionID},
|
|
@@ -2318,27 +2428,23 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2318
2428
|
${args || null},
|
|
2319
2429
|
NOW()
|
|
2320
2430
|
)
|
|
2321
|
-
|
|
2431
|
+
`);
|
|
2322
2432
|
break;
|
|
2323
2433
|
}
|
|
2324
2434
|
}
|
|
2325
2435
|
} catch (error) {
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
level: "error",
|
|
2330
|
-
message: "Error recording event",
|
|
2331
|
-
extra: { eventType: event.type, error: String(error) }
|
|
2332
|
-
}
|
|
2436
|
+
logError(client, "Error recording event", {
|
|
2437
|
+
eventType: event.type,
|
|
2438
|
+
error: String(error)
|
|
2333
2439
|
});
|
|
2334
2440
|
}
|
|
2335
2441
|
},
|
|
2336
2442
|
"chat.message": async (input, output) => {
|
|
2337
2443
|
try {
|
|
2338
|
-
|
|
2444
|
+
fireAndForget(() => sql`
|
|
2339
2445
|
UPDATE sessions SET status = 'active', updated_at = NOW()
|
|
2340
2446
|
WHERE id = ${input.sessionID}
|
|
2341
|
-
|
|
2447
|
+
`);
|
|
2342
2448
|
const systemPrompt = output.message?.system;
|
|
2343
2449
|
if (output.parts && output.parts.length > 0) {
|
|
2344
2450
|
pendingUserMessages.set(input.sessionID, {
|
|
@@ -2354,14 +2460,7 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2354
2460
|
});
|
|
2355
2461
|
}
|
|
2356
2462
|
} catch (error) {
|
|
2357
|
-
|
|
2358
|
-
body: {
|
|
2359
|
-
service: "database",
|
|
2360
|
-
level: "error",
|
|
2361
|
-
message: "Error in chat.message",
|
|
2362
|
-
extra: { error: String(error) }
|
|
2363
|
-
}
|
|
2364
|
-
});
|
|
2463
|
+
logError(client, "Error in chat.message", { error: String(error) });
|
|
2365
2464
|
}
|
|
2366
2465
|
},
|
|
2367
2466
|
"tool.execute.before": async (input, output) => {
|
|
@@ -2375,7 +2474,7 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2375
2474
|
args: output.args || {},
|
|
2376
2475
|
startedAt
|
|
2377
2476
|
});
|
|
2378
|
-
|
|
2477
|
+
fireAndForget(() => sql`
|
|
2379
2478
|
INSERT INTO tool_executions (
|
|
2380
2479
|
correlation_id,
|
|
2381
2480
|
session_id,
|
|
@@ -2392,15 +2491,12 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2392
2491
|
${startedAt},
|
|
2393
2492
|
NOW()
|
|
2394
2493
|
)
|
|
2395
|
-
|
|
2494
|
+
`, (error) => logError(client, "Error recording tool start", {
|
|
2495
|
+
error: String(error)
|
|
2496
|
+
}));
|
|
2396
2497
|
} catch (error) {
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
service: "database",
|
|
2400
|
-
level: "error",
|
|
2401
|
-
message: "Error recording tool start",
|
|
2402
|
-
extra: { error: String(error) }
|
|
2403
|
-
}
|
|
2498
|
+
logError(client, "Error in tool.execute.before", {
|
|
2499
|
+
error: String(error)
|
|
2404
2500
|
});
|
|
2405
2501
|
}
|
|
2406
2502
|
},
|
|
@@ -2408,79 +2504,81 @@ var DatabasePlugin = async ({ client }) => {
|
|
|
2408
2504
|
try {
|
|
2409
2505
|
const completedAt = new Date;
|
|
2410
2506
|
const pending = pendingExecutions.get(input.callID);
|
|
2507
|
+
const partId = pending?.partId || callIdToPartId.get(input.callID) || null;
|
|
2411
2508
|
if (pending) {
|
|
2412
2509
|
const durationMs = completedAt.getTime() - pending.startedAt.getTime();
|
|
2413
|
-
|
|
2414
|
-
UPDATE tool_executions
|
|
2415
|
-
SET
|
|
2416
|
-
result = ${output.output ?? null},
|
|
2417
|
-
completed_at = ${completedAt},
|
|
2418
|
-
duration_ms = ${durationMs},
|
|
2419
|
-
success = true
|
|
2420
|
-
WHERE correlation_id = ${pending.correlationId}
|
|
2421
|
-
`;
|
|
2422
|
-
const partId = pending.partId || callIdToPartId.get(input.callID);
|
|
2423
|
-
if (partId && output.output) {
|
|
2424
|
-
const outputJson = JSON.stringify(output.output);
|
|
2510
|
+
fireAndForget(async () => {
|
|
2425
2511
|
await sql`
|
|
2426
|
-
UPDATE
|
|
2427
|
-
SET
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
${
|
|
2431
|
-
|
|
2432
|
-
WHERE
|
|
2433
|
-
AND (content->'state'->>'output') IS NULL
|
|
2512
|
+
UPDATE tool_executions
|
|
2513
|
+
SET
|
|
2514
|
+
result = ${output.output ?? null},
|
|
2515
|
+
completed_at = ${completedAt},
|
|
2516
|
+
duration_ms = ${durationMs},
|
|
2517
|
+
success = true
|
|
2518
|
+
WHERE correlation_id = ${pending.correlationId}
|
|
2434
2519
|
`;
|
|
2435
|
-
|
|
2520
|
+
if (partId && output.output) {
|
|
2521
|
+
const outputJson = JSON.stringify(output.output);
|
|
2522
|
+
await sql`
|
|
2523
|
+
UPDATE message_parts
|
|
2524
|
+
SET content = jsonb_set(
|
|
2525
|
+
COALESCE(content, '{"state":{}}'::jsonb),
|
|
2526
|
+
'{state,output}',
|
|
2527
|
+
${outputJson}::jsonb
|
|
2528
|
+
)
|
|
2529
|
+
WHERE id = ${partId}
|
|
2530
|
+
AND (content->'state'->>'output') IS NULL
|
|
2531
|
+
`;
|
|
2532
|
+
}
|
|
2533
|
+
});
|
|
2436
2534
|
pendingExecutions.delete(input.callID);
|
|
2437
2535
|
} else {
|
|
2438
|
-
|
|
2439
|
-
INSERT INTO tool_executions (
|
|
2440
|
-
correlation_id,
|
|
2441
|
-
session_id,
|
|
2442
|
-
tool_name,
|
|
2443
|
-
args,
|
|
2444
|
-
result,
|
|
2445
|
-
completed_at,
|
|
2446
|
-
success,
|
|
2447
|
-
created_at
|
|
2448
|
-
)
|
|
2449
|
-
VALUES (
|
|
2450
|
-
${generateCorrelationId()},
|
|
2451
|
-
${input.sessionID},
|
|
2452
|
-
${input.tool},
|
|
2453
|
-
${output.metadata ?? null},
|
|
2454
|
-
${output.output ?? null},
|
|
2455
|
-
${completedAt},
|
|
2456
|
-
true,
|
|
2457
|
-
NOW()
|
|
2458
|
-
)
|
|
2459
|
-
`;
|
|
2460
|
-
const partId = callIdToPartId.get(input.callID);
|
|
2461
|
-
if (partId && output.output) {
|
|
2462
|
-
const outputJson = JSON.stringify(output.output);
|
|
2536
|
+
fireAndForget(async () => {
|
|
2463
2537
|
await sql`
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2538
|
+
INSERT INTO tool_executions (
|
|
2539
|
+
correlation_id,
|
|
2540
|
+
session_id,
|
|
2541
|
+
tool_name,
|
|
2542
|
+
args,
|
|
2543
|
+
result,
|
|
2544
|
+
completed_at,
|
|
2545
|
+
success,
|
|
2546
|
+
created_at
|
|
2547
|
+
)
|
|
2548
|
+
VALUES (
|
|
2549
|
+
${generateCorrelationId()},
|
|
2550
|
+
${input.sessionID},
|
|
2551
|
+
${input.tool},
|
|
2552
|
+
${output.metadata ?? null},
|
|
2553
|
+
${output.output ?? null},
|
|
2554
|
+
${completedAt},
|
|
2555
|
+
true,
|
|
2556
|
+
NOW()
|
|
2469
2557
|
)
|
|
2470
|
-
WHERE id = ${partId}
|
|
2471
|
-
AND (content->'state'->>'output') IS NULL
|
|
2472
2558
|
`;
|
|
2473
|
-
|
|
2559
|
+
if (partId && output.output) {
|
|
2560
|
+
const outputJson = JSON.stringify(output.output);
|
|
2561
|
+
await sql`
|
|
2562
|
+
UPDATE message_parts
|
|
2563
|
+
SET content = jsonb_set(
|
|
2564
|
+
COALESCE(content, '{"state":{}}'::jsonb),
|
|
2565
|
+
'{state,output}',
|
|
2566
|
+
${outputJson}::jsonb
|
|
2567
|
+
)
|
|
2568
|
+
WHERE id = ${partId}
|
|
2569
|
+
AND (content->'state'->>'output') IS NULL
|
|
2570
|
+
`;
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2474
2573
|
}
|
|
2475
2574
|
callIdToPartId.delete(input.callID);
|
|
2575
|
+
callIdTimestamps.delete(input.callID);
|
|
2476
2576
|
} catch (error) {
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
extra: { error: String(error) }
|
|
2483
|
-
}
|
|
2577
|
+
pendingExecutions.delete(input.callID);
|
|
2578
|
+
callIdToPartId.delete(input.callID);
|
|
2579
|
+
callIdTimestamps.delete(input.callID);
|
|
2580
|
+
logError(client, "Error recording tool completion", {
|
|
2581
|
+
error: String(error)
|
|
2484
2582
|
});
|
|
2485
2583
|
}
|
|
2486
2584
|
}
|
package/package.json
CHANGED