opencode-database-plugin 1.0.8 → 1.0.10

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,MAopB5B,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,61 @@ 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`
2114
- INSERT INTO sessions (id, status, created_at, updated_at)
2115
- VALUES (${info.sessionID}, 'active', NOW(), NOW())
2116
- ON CONFLICT (id) DO UPDATE SET updated_at = NOW()
2117
- `;
2118
2213
  let messageContent = info.parts;
2119
2214
  let textContent = null;
2120
2215
  let systemPrompt = info.system || null;
@@ -2134,30 +2229,39 @@ var DatabasePlugin = async ({ client }) => {
2134
2229
  }
2135
2230
  const modelProvider = info.providerID || info.model?.providerID || null;
2136
2231
  const modelId = info.modelID || info.model?.modelID || null;
2137
- await sql`
2138
- INSERT INTO messages (id, session_id, role, model_provider, model_id, text, summary, content, system_prompt, created_at)
2139
- VALUES (
2140
- ${info.id},
2141
- ${info.sessionID},
2142
- ${info.role},
2143
- ${modelProvider},
2144
- ${modelId},
2145
- ${textContent},
2146
- ${info.summary?.title || null},
2147
- ${messageContent ? sql.json(messageContent) : null},
2148
- ${systemPrompt},
2149
- NOW()
2150
- )
2151
- ON CONFLICT (id) DO UPDATE SET
2152
- role = ${info.role},
2153
- model_provider = COALESCE(EXCLUDED.model_provider, messages.model_provider),
2154
- model_id = COALESCE(EXCLUDED.model_id, messages.model_id),
2155
- text = COALESCE(${textContent}, messages.text),
2156
- summary = COALESCE(${info.summary?.title || null}, messages.summary),
2157
- content = COALESCE(${messageContent ? sql.json(messageContent) : null}, messages.content),
2158
- system_prompt = COALESCE(${systemPrompt}, messages.system_prompt)
2159
- `;
2160
- const sessionTokens = tokensCountedBySession.get(info.sessionID) || new Set;
2232
+ fireAndForget(async () => {
2233
+ await sql`
2234
+ INSERT INTO sessions (id, status, created_at, updated_at)
2235
+ VALUES (${info.sessionID}, 'active', NOW(), NOW())
2236
+ ON CONFLICT (id) DO UPDATE SET updated_at = NOW()
2237
+ `;
2238
+ await sql`
2239
+ INSERT INTO messages (id, session_id, role, model_provider, model_id, text, summary, content, system_prompt, created_at)
2240
+ VALUES (
2241
+ ${info.id},
2242
+ ${info.sessionID},
2243
+ ${info.role},
2244
+ ${modelProvider},
2245
+ ${modelId},
2246
+ ${textContent},
2247
+ ${info.summary?.title || null},
2248
+ ${messageContent ? sql.json(messageContent) : null},
2249
+ ${systemPrompt},
2250
+ NOW()
2251
+ )
2252
+ ON CONFLICT (id) DO UPDATE SET
2253
+ role = ${info.role},
2254
+ model_provider = COALESCE(EXCLUDED.model_provider, messages.model_provider),
2255
+ model_id = COALESCE(EXCLUDED.model_id, messages.model_id),
2256
+ text = COALESCE(${textContent}, messages.text),
2257
+ summary = COALESCE(${info.summary?.title || null}, messages.summary),
2258
+ content = COALESCE(${messageContent ? sql.json(messageContent) : null}, messages.content),
2259
+ system_prompt = COALESCE(${systemPrompt}, messages.system_prompt)
2260
+ `;
2261
+ }, (error) => logError(client, "Error in message.updated", {
2262
+ error: String(error)
2263
+ }));
2264
+ const sessionTokens = tokensCountedBySession.get(info.sessionID) || new Map;
2161
2265
  if (info.role === "assistant" && info.tokens && !sessionTokens.has(info.id)) {
2162
2266
  const inputTokens = info.tokens.input ?? 0;
2163
2267
  const outputTokens = info.tokens.output ?? 0;
@@ -2165,10 +2269,10 @@ var DatabasePlugin = async ({ client }) => {
2165
2269
  const cacheRead = info.tokens.cache?.read ?? 0;
2166
2270
  const cacheWrite = info.tokens.cache?.write ?? 0;
2167
2271
  if (inputTokens > 0 || outputTokens > 0) {
2168
- sessionTokens.add(info.id);
2272
+ sessionTokens.set(info.id, Date.now());
2169
2273
  tokensCountedBySession.set(info.sessionID, sessionTokens);
2170
2274
  const contextSize = inputTokens + cacheRead;
2171
- await sql`
2275
+ fireAndForget(() => sql`
2172
2276
  UPDATE sessions
2173
2277
  SET
2174
2278
  input_tokens = input_tokens + ${inputTokens},
@@ -2181,16 +2285,18 @@ var DatabasePlugin = async ({ client }) => {
2181
2285
  model_provider = COALESCE(${modelProvider}, model_provider),
2182
2286
  model_id = COALESCE(${modelId}, model_id)
2183
2287
  WHERE id = ${info.sessionID}
2184
- `;
2288
+ `, (error) => logError(client, "Error updating session tokens", {
2289
+ error: String(error)
2290
+ }));
2185
2291
  }
2186
2292
  }
2187
2293
  break;
2188
2294
  }
2189
2295
  case "message.removed": {
2190
2296
  const messageID = props.messageID;
2191
- await sql`
2297
+ fireAndForget(() => sql`
2192
2298
  DELETE FROM messages WHERE id = ${messageID}
2193
- `;
2299
+ `);
2194
2300
  break;
2195
2301
  }
2196
2302
  case "message.part.updated": {
@@ -2199,56 +2305,60 @@ var DatabasePlugin = async ({ client }) => {
2199
2305
  const textContent = part.text || null;
2200
2306
  if (part.type === "tool" && part.callID) {
2201
2307
  callIdToPartId.set(part.callID, part.id);
2308
+ callIdTimestamps.set(part.callID, Date.now());
2202
2309
  const pending = pendingExecutions.get(part.callID);
2203
2310
  if (pending) {
2204
2311
  pending.partId = part.id;
2205
2312
  }
2206
2313
  }
2207
2314
  if (part.type === "step-finish" && part.cost !== undefined) {
2208
- await sql`
2315
+ const cost = part.cost;
2316
+ fireAndForget(() => sql`
2209
2317
  UPDATE sessions
2210
- SET estimated_cost = estimated_cost + ${part.cost}
2318
+ SET estimated_cost = estimated_cost + ${cost}
2211
2319
  WHERE id = ${part.sessionID}
2212
- `;
2320
+ `);
2213
2321
  }
2214
- await sql`
2215
- INSERT INTO messages (id, session_id, role, created_at)
2216
- VALUES (${part.messageID}, ${part.sessionID}, 'assistant', NOW())
2217
- ON CONFLICT (id) DO UPDATE SET
2218
- role = COALESCE(messages.role, 'assistant')
2219
- `;
2220
- await sql`
2221
- INSERT INTO sessions (id, status, created_at)
2222
- VALUES (${part.sessionID}, 'active', NOW())
2223
- ON CONFLICT (id) DO NOTHING
2224
- `;
2225
2322
  const isStreamingTextPart = part.type === "text" || part.type === "reasoning";
2226
2323
  const partAsJson = { ...part };
2227
2324
  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) {
2325
+ fireAndForget(async () => {
2242
2326
  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}))
2327
+ INSERT INTO sessions (id, status, created_at)
2328
+ VALUES (${part.sessionID}, 'active', NOW())
2329
+ ON CONFLICT (id) DO NOTHING
2250
2330
  `;
2251
- }
2331
+ await sql`
2332
+ INSERT INTO messages (id, session_id, role, created_at)
2333
+ VALUES (${part.messageID}, ${part.sessionID}, 'assistant', NOW())
2334
+ ON CONFLICT (id) DO UPDATE SET
2335
+ role = COALESCE(messages.role, 'assistant')
2336
+ `;
2337
+ await sql`
2338
+ INSERT INTO message_parts (id, message_id, part_type, tool_name, text, content, created_at)
2339
+ VALUES (
2340
+ ${part.id},
2341
+ ${part.messageID},
2342
+ ${part.type},
2343
+ ${toolName},
2344
+ ${textContent},
2345
+ ${sql.json(partAsJson)},
2346
+ NOW()
2347
+ )
2348
+ ON CONFLICT (id) DO NOTHING
2349
+ `;
2350
+ if (textContent) {
2351
+ await sql`
2352
+ UPDATE message_parts
2353
+ SET
2354
+ tool_name = COALESCE(${toolName}, tool_name),
2355
+ text = ${textContent},
2356
+ content = ${sql.json(partAsJson)}
2357
+ WHERE id = ${part.id}
2358
+ AND (text IS NULL OR LENGTH(text) < LENGTH(${textContent}))
2359
+ `;
2360
+ }
2361
+ });
2252
2362
  } else {
2253
2363
  const statusPriority = {
2254
2364
  pending: 1,
@@ -2258,59 +2368,72 @@ var DatabasePlugin = async ({ client }) => {
2258
2368
  };
2259
2369
  const currentStatus = part.state?.status || "pending";
2260
2370
  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
2371
+ fireAndForget(async () => {
2372
+ await sql`
2373
+ INSERT INTO sessions (id, status, created_at)
2374
+ VALUES (${part.sessionID}, 'active', NOW())
2375
+ ON CONFLICT (id) DO NOTHING
2376
+ `;
2377
+ await sql`
2378
+ INSERT INTO messages (id, session_id, role, created_at)
2379
+ VALUES (${part.messageID}, ${part.sessionID}, 'assistant', NOW())
2380
+ ON CONFLICT (id) DO UPDATE SET
2381
+ role = COALESCE(messages.role, 'assistant')
2382
+ `;
2383
+ await sql`
2384
+ INSERT INTO message_parts (id, message_id, part_type, tool_name, text, content, created_at)
2385
+ VALUES (
2386
+ ${part.id},
2387
+ ${part.messageID},
2388
+ ${part.type},
2389
+ ${toolName},
2390
+ ${textContent},
2391
+ ${sql.json(partAsJson)},
2392
+ NOW()
2289
2393
  )
2290
- `;
2394
+ ON CONFLICT (id) DO NOTHING
2395
+ `;
2396
+ await sql`
2397
+ UPDATE message_parts
2398
+ SET
2399
+ tool_name = COALESCE(${toolName}, tool_name),
2400
+ text = COALESCE(${textContent}, text),
2401
+ content = ${sql.json(partAsJson)}
2402
+ WHERE id = ${part.id}
2403
+ AND ${currentPriority} >= COALESCE(
2404
+ CASE (content->'state'->>'status')
2405
+ WHEN 'pending' THEN 1
2406
+ WHEN 'running' THEN 2
2407
+ WHEN 'completed' THEN 3
2408
+ WHEN 'error' THEN 3
2409
+ ELSE 0
2410
+ END, 0
2411
+ )
2412
+ `;
2413
+ });
2291
2414
  }
2292
2415
  if (part.type === "text" && textContent) {
2293
- await sql`
2416
+ fireAndForget(() => sql`
2294
2417
  UPDATE messages
2295
2418
  SET text = ${textContent}
2296
2419
  WHERE id = ${part.messageID}
2297
2420
  AND (text IS NULL OR LENGTH(text) < LENGTH(${textContent}))
2298
- `;
2421
+ `);
2299
2422
  }
2300
2423
  break;
2301
2424
  }
2302
2425
  case "message.part.removed": {
2303
2426
  const partID = props.partID;
2304
- await sql`
2427
+ fireAndForget(() => sql`
2305
2428
  DELETE FROM message_parts WHERE id = ${partID}
2306
- `;
2429
+ `);
2307
2430
  break;
2308
2431
  }
2309
2432
  case "command.executed": {
2310
2433
  const name = props.name;
2311
2434
  const sessionID = props.sessionID;
2312
2435
  const args = props.arguments;
2313
- await sql`
2436
+ fireAndForget(() => sql`
2314
2437
  INSERT INTO commands (session_id, command_name, command_args, created_at)
2315
2438
  VALUES (
2316
2439
  ${sessionID},
@@ -2318,27 +2441,23 @@ var DatabasePlugin = async ({ client }) => {
2318
2441
  ${args || null},
2319
2442
  NOW()
2320
2443
  )
2321
- `;
2444
+ `);
2322
2445
  break;
2323
2446
  }
2324
2447
  }
2325
2448
  } 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
- }
2449
+ logError(client, "Error recording event", {
2450
+ eventType: event.type,
2451
+ error: String(error)
2333
2452
  });
2334
2453
  }
2335
2454
  },
2336
2455
  "chat.message": async (input, output) => {
2337
2456
  try {
2338
- await sql`
2457
+ fireAndForget(() => sql`
2339
2458
  UPDATE sessions SET status = 'active', updated_at = NOW()
2340
2459
  WHERE id = ${input.sessionID}
2341
- `;
2460
+ `);
2342
2461
  const systemPrompt = output.message?.system;
2343
2462
  if (output.parts && output.parts.length > 0) {
2344
2463
  pendingUserMessages.set(input.sessionID, {
@@ -2354,14 +2473,7 @@ var DatabasePlugin = async ({ client }) => {
2354
2473
  });
2355
2474
  }
2356
2475
  } 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
- });
2476
+ logError(client, "Error in chat.message", { error: String(error) });
2365
2477
  }
2366
2478
  },
2367
2479
  "tool.execute.before": async (input, output) => {
@@ -2375,7 +2487,7 @@ var DatabasePlugin = async ({ client }) => {
2375
2487
  args: output.args || {},
2376
2488
  startedAt
2377
2489
  });
2378
- await sql`
2490
+ fireAndForget(() => sql`
2379
2491
  INSERT INTO tool_executions (
2380
2492
  correlation_id,
2381
2493
  session_id,
@@ -2392,15 +2504,12 @@ var DatabasePlugin = async ({ client }) => {
2392
2504
  ${startedAt},
2393
2505
  NOW()
2394
2506
  )
2395
- `;
2507
+ `, (error) => logError(client, "Error recording tool start", {
2508
+ error: String(error)
2509
+ }));
2396
2510
  } 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
- }
2511
+ logError(client, "Error in tool.execute.before", {
2512
+ error: String(error)
2404
2513
  });
2405
2514
  }
2406
2515
  },
@@ -2408,79 +2517,81 @@ var DatabasePlugin = async ({ client }) => {
2408
2517
  try {
2409
2518
  const completedAt = new Date;
2410
2519
  const pending = pendingExecutions.get(input.callID);
2520
+ const partId = pending?.partId || callIdToPartId.get(input.callID) || null;
2411
2521
  if (pending) {
2412
2522
  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);
2523
+ fireAndForget(async () => {
2425
2524
  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
2525
+ UPDATE tool_executions
2526
+ SET
2527
+ result = ${output.output ?? null},
2528
+ completed_at = ${completedAt},
2529
+ duration_ms = ${durationMs},
2530
+ success = true
2531
+ WHERE correlation_id = ${pending.correlationId}
2434
2532
  `;
2435
- }
2533
+ if (partId && output.output) {
2534
+ const outputJson = JSON.stringify(output.output);
2535
+ await sql`
2536
+ UPDATE message_parts
2537
+ SET content = jsonb_set(
2538
+ COALESCE(content, '{"state":{}}'::jsonb),
2539
+ '{state,output}',
2540
+ ${outputJson}::jsonb
2541
+ )
2542
+ WHERE id = ${partId}
2543
+ AND (content->'state'->>'output') IS NULL
2544
+ `;
2545
+ }
2546
+ });
2436
2547
  pendingExecutions.delete(input.callID);
2437
2548
  } 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);
2549
+ fireAndForget(async () => {
2463
2550
  await sql`
2464
- UPDATE message_parts
2465
- SET content = jsonb_set(
2466
- COALESCE(content, '{"state":{}}'::jsonb),
2467
- '{state,output}',
2468
- ${outputJson}::jsonb
2551
+ INSERT INTO tool_executions (
2552
+ correlation_id,
2553
+ session_id,
2554
+ tool_name,
2555
+ args,
2556
+ result,
2557
+ completed_at,
2558
+ success,
2559
+ created_at
2560
+ )
2561
+ VALUES (
2562
+ ${generateCorrelationId()},
2563
+ ${input.sessionID},
2564
+ ${input.tool},
2565
+ ${output.metadata ?? null},
2566
+ ${output.output ?? null},
2567
+ ${completedAt},
2568
+ true,
2569
+ NOW()
2469
2570
  )
2470
- WHERE id = ${partId}
2471
- AND (content->'state'->>'output') IS NULL
2472
2571
  `;
2473
- }
2572
+ if (partId && output.output) {
2573
+ const outputJson = JSON.stringify(output.output);
2574
+ await sql`
2575
+ UPDATE message_parts
2576
+ SET content = jsonb_set(
2577
+ COALESCE(content, '{"state":{}}'::jsonb),
2578
+ '{state,output}',
2579
+ ${outputJson}::jsonb
2580
+ )
2581
+ WHERE id = ${partId}
2582
+ AND (content->'state'->>'output') IS NULL
2583
+ `;
2584
+ }
2585
+ });
2474
2586
  }
2475
2587
  callIdToPartId.delete(input.callID);
2588
+ callIdTimestamps.delete(input.callID);
2476
2589
  } 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
- }
2590
+ pendingExecutions.delete(input.callID);
2591
+ callIdToPartId.delete(input.callID);
2592
+ callIdTimestamps.delete(input.callID);
2593
+ logError(client, "Error recording tool completion", {
2594
+ error: String(error)
2484
2595
  });
2485
2596
  }
2486
2597
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-database-plugin",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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",