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 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;AAMhC,eAAO,MAAM,GAAG,kBAKd,CAAC;AAEH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAQzD"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AA8BlD,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;AAED,eAAO,MAAM,cAAc,EAAE,MAslB5B,CAAC"}
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: 0,
1953
- connect_timeout: 30,
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
- await sql`
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
- await sql`
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
- await sql`
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
- await sql`
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
- await sql`
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
- await sql`
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
- const [sessionState] = await sql`
2071
- SELECT context_tokens, input_tokens, output_tokens,
2072
- cache_read_tokens, cache_write_tokens, reasoning_tokens, estimated_cost
2073
- FROM sessions WHERE id = ${sessionID}
2074
- `;
2075
- if (sessionState) {
2076
- await sql`
2077
- INSERT INTO compactions (
2078
- session_id,
2079
- context_tokens_before,
2080
- cumulative_input_tokens,
2081
- cumulative_output_tokens,
2082
- cumulative_cache_read,
2083
- cumulative_cache_write,
2084
- cumulative_reasoning,
2085
- cumulative_cost
2086
- )
2087
- VALUES (
2088
- ${sessionID},
2089
- ${sessionState.context_tokens || 0},
2090
- ${sessionState.input_tokens || 0},
2091
- ${sessionState.output_tokens || 0},
2092
- ${sessionState.cache_read_tokens || 0},
2093
- ${sessionState.cache_write_tokens || 0},
2094
- ${sessionState.reasoning_tokens || 0},
2095
- ${parseFloat(sessionState.estimated_cost || "0")}
2096
- )
2097
- `;
2098
- }
2099
- await sql`
2100
- UPDATE sessions
2101
- SET
2102
- status = 'compacted',
2103
- peak_context_tokens = GREATEST(peak_context_tokens, context_tokens),
2104
- context_tokens = 0,
2105
- compaction_count = compaction_count + 1
2106
- WHERE id = ${sessionID}
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
- await sql`
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
- await sql`
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
- const sessionTokens = tokensCountedBySession.get(info.sessionID) || new Set;
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.add(info.id);
2270
+ sessionTokens.set(info.id, Date.now());
2169
2271
  tokensCountedBySession.set(info.sessionID, sessionTokens);
2170
2272
  const contextSize = inputTokens + cacheRead;
2171
- await sql`
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
- await sql`
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
- await sql`
2313
+ const cost = part.cost;
2314
+ fireAndForget(() => sql`
2209
2315
  UPDATE sessions
2210
- SET estimated_cost = estimated_cost + ${part.cost}
2316
+ SET estimated_cost = estimated_cost + ${cost}
2211
2317
  WHERE id = ${part.sessionID}
2212
- `;
2318
+ `);
2213
2319
  }
2214
- await sql`
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
- await sql`
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
- await sql`
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
- UPDATE message_parts
2244
- SET
2245
- tool_name = COALESCE(${toolName}, tool_name),
2246
- text = ${textContent},
2247
- content = ${sql.json(partAsJson)}
2248
- WHERE id = ${part.id}
2249
- AND (text IS NULL OR LENGTH(text) < LENGTH(${textContent}))
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
- await sql`
2262
- INSERT INTO message_parts (id, message_id, part_type, tool_name, text, content, created_at)
2263
- VALUES (
2264
- ${part.id},
2265
- ${part.messageID},
2266
- ${part.type},
2267
- ${toolName},
2268
- ${textContent},
2269
- ${sql.json(partAsJson)},
2270
- NOW()
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
- await sql`
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
- await sql`
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
- await sql`
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
- await client.app.log({
2327
- body: {
2328
- service: "database",
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
- await sql`
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
- await client.app.log({
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
- await sql`
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
- await client.app.log({
2398
- body: {
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
- await sql`
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 message_parts
2427
- SET content = jsonb_set(
2428
- COALESCE(content, '{"state":{}}'::jsonb),
2429
- '{state,output}',
2430
- ${outputJson}::jsonb
2431
- )
2432
- WHERE id = ${partId}
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
- await sql`
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
- UPDATE message_parts
2465
- SET content = jsonb_set(
2466
- COALESCE(content, '{"state":{}}'::jsonb),
2467
- '{state,output}',
2468
- ${outputJson}::jsonb
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
- await client.app.log({
2478
- body: {
2479
- service: "database",
2480
- level: "error",
2481
- message: "Error recording tool completion",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-database-plugin",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "OpenCode plugin that logs sessions, messages, tool executions, and token usage to PostgreSQL",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",