@dv.nghiem/flowdeck 0.3.5 → 0.3.6

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.
@@ -2,6 +2,13 @@ import { type Session } from "../services/memory-store";
2
2
  export declare function onSessionCreated(directory: string, contentSessionId: string, prompt?: string): Session;
3
3
  export declare function onToolExecuted(contentSessionId: string, toolName: string, toolInput: Record<string, unknown>, toolResponse: string | null, directory: string): void;
4
4
  export declare function onMessageUpdated(contentSessionId: string, role: string, content: string, directory: string): void;
5
+ /**
6
+ * Called when OpenCode compacts a session (session.compacted event).
7
+ *
8
+ * Previously this silently dropped the summary if the session was not in
9
+ * activeSessions (e.g. after plugin reload or cross-process sessions). Now it
10
+ * falls back to a DB lookup so the summary is never lost.
11
+ */
5
12
  export declare function onSessionCompact(contentSessionId: string, summary: string): void;
6
13
  export declare function onSessionEnd(contentSessionId: string, lastMessage?: string): void;
7
14
  export declare function getSessionContext(directory: string, contentSessionId: string): {
@@ -1 +1 @@
1
- {"version":3,"file":"memory-hook.d.ts","sourceRoot":"","sources":["../../src/hooks/memory-hook.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,OAAO,EACb,MAAM,0BAA0B,CAAA;AAwBjC,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAYtG;AAED,wBAAgB,cAAc,CAC5B,gBAAgB,EAAE,MAAM,EACxB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,SAAS,EAAE,MAAM,GAChB,IAAI,CAsBN;AAED,wBAAgB,gBAAgB,CAC9B,gBAAgB,EAAE,MAAM,EACxB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,IAAI,CAyBN;AAED,wBAAgB,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAKhF;AAED,wBAAgB,YAAY,CAAC,gBAAgB,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CASjF;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG;IAC9E,OAAO,EAAE,MAAM,CAAA;IACf,gBAAgB,EAAE,OAAO,EAAE,CAAA;CAC5B,CAYA;AAED,wBAAgB,YAAY,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,eAAO,MAAM,UAAU;;;;;;;;CAQtB,CAAA"}
1
+ {"version":3,"file":"memory-hook.d.ts","sourceRoot":"","sources":["../../src/hooks/memory-hook.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,OAAO,EAGb,MAAM,0BAA0B,CAAA;AAgGjC,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAYtG;AAED,wBAAgB,cAAc,CAC5B,gBAAgB,EAAE,MAAM,EACxB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,SAAS,EAAE,MAAM,GAChB,IAAI,CA4BN;AAED,wBAAgB,gBAAgB,CAC9B,gBAAgB,EAAE,MAAM,EACxB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,IAAI,CA6BN;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CA+BhF;AAED,wBAAgB,YAAY,CAAC,gBAAgB,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CA2BjF;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG;IAC9E,OAAO,EAAE,MAAM,CAAA;IACf,gBAAgB,EAAE,OAAO,EAAE,CAAA;CAC5B,CAIA;AAED,wBAAgB,YAAY,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,eAAO,MAAM,UAAU;;;;;;;;CAQtB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAkGjD,QAAA,MAAM,MAAM,EAAE,MAqPb,CAAA;AAED,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAkGjD,QAAA,MAAM,MAAM,EAAE,MAsPb,CAAA;AAED,eAAe,MAAM,CAAA"}
package/dist/index.js CHANGED
@@ -1709,23 +1709,34 @@ import { Database } from "bun:sqlite";
1709
1709
  import { existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
1710
1710
  import { join as join15 } from "path";
1711
1711
  import { homedir } from "os";
1712
- var MEMORY_DIR = join15(homedir(), ".flowdeck-memory");
1713
- var DB_PATH = join15(MEMORY_DIR, "memory.db");
1714
- function ensureDir() {
1715
- if (!existsSync15(MEMORY_DIR)) {
1716
- mkdirSync10(MEMORY_DIR, { recursive: true });
1717
- }
1712
+ function resolveMemoryDir() {
1713
+ return process.env.FLOWDECK_MEMORY_DIR ?? join15(homedir(), ".flowdeck-memory");
1718
1714
  }
1715
+ var JS_RETRY_COUNT = 3;
1716
+ var JS_RETRY_BASE_MS = 50;
1719
1717
  var db = null;
1718
+ function debugLog(msg) {
1719
+ if (process.env.FLOWDECK_MEMORY_DEBUG) {
1720
+ console.error(`[FlowDeck Memory] ${msg}`);
1721
+ }
1722
+ }
1720
1723
  function getDb() {
1721
1724
  if (!db) {
1722
- ensureDir();
1723
- db = new Database(DB_PATH);
1725
+ const dir = resolveMemoryDir();
1726
+ if (!existsSync15(dir))
1727
+ mkdirSync10(dir, { recursive: true });
1728
+ const dbPath = join15(dir, "memory.db");
1729
+ db = new Database(dbPath);
1730
+ debugLog(`DB opened: ${dbPath}`);
1724
1731
  initializeSchema(db);
1725
1732
  }
1726
1733
  return db;
1727
1734
  }
1728
1735
  function initializeSchema(database) {
1736
+ database.run("PRAGMA journal_mode = WAL");
1737
+ database.run("PRAGMA busy_timeout = 5000");
1738
+ database.run("PRAGMA synchronous = NORMAL");
1739
+ database.run("PRAGMA wal_autocheckpoint = 1000");
1729
1740
  const schema = `
1730
1741
  CREATE TABLE IF NOT EXISTS sessions (
1731
1742
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1753,6 +1764,7 @@ function initializeSchema(database) {
1753
1764
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1754
1765
  session_id INTEGER NOT NULL UNIQUE,
1755
1766
  content TEXT NOT NULL,
1767
+ metadata TEXT,
1756
1768
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
1757
1769
  FOREIGN KEY (session_id) REFERENCES sessions(id)
1758
1770
  );
@@ -1764,6 +1776,49 @@ function initializeSchema(database) {
1764
1776
  CREATE INDEX IF NOT EXISTS idx_sessions_directory ON sessions(directory);
1765
1777
  `;
1766
1778
  database.run(schema);
1779
+ const summaryColumns = database.prepare("PRAGMA table_info(summaries)").all().map((c) => c.name);
1780
+ if (!summaryColumns.includes("metadata")) {
1781
+ database.run("ALTER TABLE summaries ADD COLUMN metadata TEXT");
1782
+ debugLog("Migrated summaries table: added metadata column");
1783
+ }
1784
+ }
1785
+ function isBusyError(err) {
1786
+ if (!err || typeof err !== "object")
1787
+ return false;
1788
+ const e = err;
1789
+ return e.code === "SQLITE_BUSY" || (e.message?.includes("database is locked") ?? false);
1790
+ }
1791
+ function sleepSync(ms) {
1792
+ try {
1793
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1794
+ } catch {
1795
+ const end = Date.now() + ms;
1796
+ while (Date.now() < end) {}
1797
+ }
1798
+ }
1799
+ function executeWrite(fn, context) {
1800
+ for (let attempt = 0;attempt <= JS_RETRY_COUNT; attempt++) {
1801
+ const start = Date.now();
1802
+ try {
1803
+ const result = fn();
1804
+ const duration = Date.now() - start;
1805
+ if (attempt > 0) {
1806
+ debugLog(`${context}: succeeded after ${attempt} JS retr${attempt === 1 ? "y" : "ies"} (${duration}ms)`);
1807
+ } else {
1808
+ debugLog(`${context}: completed in ${duration}ms`);
1809
+ }
1810
+ return result;
1811
+ } catch (err) {
1812
+ if (isBusyError(err) && attempt < JS_RETRY_COUNT) {
1813
+ const delay = JS_RETRY_BASE_MS * (attempt + 1);
1814
+ debugLog(`${context}: SQLITE_BUSY — JS retry ${attempt + 1}/${JS_RETRY_COUNT} after ${delay}ms`);
1815
+ sleepSync(delay);
1816
+ continue;
1817
+ }
1818
+ throw err;
1819
+ }
1820
+ }
1821
+ throw new Error(`${context}: exhausted all retries`);
1767
1822
  }
1768
1823
  function serializeToolInput(input) {
1769
1824
  if (!input)
@@ -1791,7 +1846,7 @@ function initSession(contentSessionId, project, directory) {
1791
1846
  database.prepare("UPDATE sessions SET last_active_at = ?, prompt_count = prompt_count + 1 WHERE id = ?").run(now, existing.id);
1792
1847
  return { ...existing, last_active_at: now, prompt_count: (existing.prompt_count || 0) + 1 };
1793
1848
  }
1794
- const result = database.prepare("INSERT INTO sessions (content_session_id, project, directory, created_at, last_active_at) VALUES (?, ?, ?, ?, ?)").run(contentSessionId, project, directory, now, now);
1849
+ const result = database.prepare("INSERT INTO sessions (content_session_id, project, directory, created_at, last_active_at, prompt_count) VALUES (?, ?, ?, ?, ?, ?)").run(contentSessionId, project, directory, now, now, 1);
1795
1850
  return {
1796
1851
  id: result.lastInsertRowid,
1797
1852
  content_session_id: contentSessionId,
@@ -1805,27 +1860,38 @@ function initSession(contentSessionId, project, directory) {
1805
1860
  function storeObservation(sessionId, toolName, toolInput, toolResponse, directory) {
1806
1861
  const database = getDb();
1807
1862
  const now = new Date().toISOString();
1808
- const result = database.prepare("INSERT INTO observations (session_id, tool_name, tool_input, tool_response, directory, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, toolName, serializeToolInput(toolInput), toolResponse ? toolResponse.slice(0, 1e4) : null, directory, now);
1809
- database.prepare("UPDATE sessions SET last_active_at = ? WHERE id = ?").run(now, sessionId);
1863
+ const serializedInput = serializeToolInput(toolInput);
1864
+ const truncatedResponse = toolResponse ? toolResponse.slice(0, 1e4) : null;
1865
+ const result = executeWrite(database.transaction(() => {
1866
+ const r = database.prepare("INSERT INTO observations (session_id, tool_name, tool_input, tool_response, directory, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, toolName, serializedInput, truncatedResponse, directory, now);
1867
+ database.prepare("UPDATE sessions SET last_active_at = ? WHERE id = ?").run(now, sessionId);
1868
+ return r;
1869
+ }), `storeObservation(${toolName})`);
1810
1870
  return {
1811
1871
  id: result.lastInsertRowid,
1812
1872
  session_id: sessionId,
1813
1873
  tool_name: toolName,
1814
- tool_input: parseToolInput(serializeToolInput(toolInput)),
1815
- tool_response: toolResponse ? toolResponse.slice(0, 1e4) : null,
1874
+ tool_input: parseToolInput(serializedInput),
1875
+ tool_response: truncatedResponse,
1816
1876
  directory,
1817
1877
  created_at: now
1818
1878
  };
1819
1879
  }
1820
- function storeSummary(sessionId, content) {
1880
+ function storeSummary(sessionId, content, metadata) {
1821
1881
  const database = getDb();
1822
1882
  const now = new Date().toISOString();
1823
- database.prepare("INSERT OR REPLACE INTO summaries (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, now);
1824
- database.prepare("UPDATE sessions SET summary = ? WHERE id = ?").run(content, sessionId);
1883
+ const serializedMetadata = metadata ? JSON.stringify(metadata) : null;
1884
+ const id = executeWrite(database.transaction(() => {
1885
+ database.prepare("INSERT OR REPLACE INTO summaries (session_id, content, metadata, created_at) VALUES (?, ?, ?, ?)").run(sessionId, content, serializedMetadata, now);
1886
+ database.prepare("UPDATE sessions SET summary = ? WHERE id = ?").run(content.slice(0, 2000), sessionId);
1887
+ return database.prepare("SELECT last_insert_rowid() as id").get().id;
1888
+ }), `storeSummary(session=${sessionId})`);
1889
+ debugLog(`storeSummary: wrote ${content.length} chars${metadata ? ` + ${JSON.stringify(metadata).length}B metadata` : ""} for session ${sessionId}`);
1825
1890
  return {
1826
- id: database.prepare("SELECT last_insert_rowid() as id").get().id,
1891
+ id,
1827
1892
  session_id: sessionId,
1828
1893
  content,
1894
+ metadata: metadata ?? null,
1829
1895
  created_at: now
1830
1896
  };
1831
1897
  }
@@ -1844,6 +1910,20 @@ function getObservationsForSession(sessionId) {
1844
1910
  tool_input: parseToolInput(obs.tool_input)
1845
1911
  }));
1846
1912
  }
1913
+ function getSessionSummary(sessionId) {
1914
+ const database = getDb();
1915
+ const row = database.prepare("SELECT * FROM summaries WHERE session_id = ?").get(sessionId);
1916
+ if (!row)
1917
+ return null;
1918
+ return {
1919
+ ...row,
1920
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
1921
+ };
1922
+ }
1923
+ function getSessionByContentSessionId(contentSessionId) {
1924
+ const database = getDb();
1925
+ return database.prepare("SELECT * FROM sessions WHERE content_session_id = ?").get(contentSessionId) || null;
1926
+ }
1847
1927
  function getRecentObservations(directory, limit = 50) {
1848
1928
  const database = getDb();
1849
1929
  const rows = database.prepare(`SELECT o.*, s.project, s.content_session_id, s.created_at as session_created
@@ -1905,11 +1985,15 @@ function getContextForDirectory(directory, maxObservations = 20) {
1905
1985
  lines.push(`Output: ${preview}${observation.tool_response.length > 300 ? "..." : ""}`);
1906
1986
  }
1907
1987
  }
1908
- const summaries = getDb().prepare(`SELECT su.* FROM summaries su
1988
+ const summaryRows = getDb().prepare(`SELECT su.* FROM summaries su
1909
1989
  JOIN sessions s ON su.session_id = s.id
1910
1990
  WHERE s.directory = ?
1911
1991
  ORDER BY su.created_at DESC
1912
1992
  LIMIT 3`).all(directory);
1993
+ const summaries = summaryRows.map((r) => ({
1994
+ ...r,
1995
+ metadata: r.metadata ? JSON.parse(r.metadata) : null
1996
+ }));
1913
1997
  if (summaries.length > 0) {
1914
1998
  lines.push(`
1915
1999
  ## Session Summaries`);
@@ -1917,12 +2001,20 @@ function getContextForDirectory(directory, maxObservations = 20) {
1917
2001
  const date = sum.created_at ? new Date(sum.created_at).toLocaleDateString() : "unknown";
1918
2002
  lines.push(`
1919
2003
  ### [${date}]`);
1920
- lines.push(sum.content.slice(0, 500));
2004
+ lines.push(sum.content.slice(0, 2000));
1921
2005
  }
1922
2006
  }
1923
2007
  return lines.join(`
1924
2008
  `);
1925
2009
  }
2010
+ function getDbSettings() {
2011
+ const database = getDb();
2012
+ const journalMode = database.prepare("PRAGMA journal_mode").get().journal_mode;
2013
+ const busyTimeout = database.prepare("PRAGMA busy_timeout").get().timeout;
2014
+ const synchronous = database.prepare("PRAGMA synchronous").get().synchronous;
2015
+ const walAutocheckpoint = database.prepare("PRAGMA wal_autocheckpoint").get().wal_autocheckpoint;
2016
+ return { journal_mode: journalMode, busy_timeout: busyTimeout, synchronous, wal_autocheckpoint: walAutocheckpoint };
2017
+ }
1926
2018
 
1927
2019
  // src/tools/memory-search.ts
1928
2020
  var memorySearchTool = tool16({
@@ -1942,8 +2034,14 @@ var memorySearchTool = tool16({
1942
2034
  return JSON.stringify({ error: "Session not found", session_id: args.session_id });
1943
2035
  }
1944
2036
  const observations = getObservationsForSession(targetSession.id);
2037
+ const summary = getSessionSummary(targetSession.id);
1945
2038
  return JSON.stringify({
1946
2039
  session: targetSession,
2040
+ summary: summary ? {
2041
+ content: summary.content,
2042
+ metadata: summary.metadata,
2043
+ created_at: summary.created_at
2044
+ } : null,
1947
2045
  observations: observations.map((o) => ({
1948
2046
  tool_name: o.tool_name,
1949
2047
  tool_input: o.tool_input,
@@ -1990,69 +2088,57 @@ var memorySearchTool = tool16({
1990
2088
 
1991
2089
  // src/tools/memory-status.ts
1992
2090
  import { tool as tool17 } from "@opencode-ai/plugin";
1993
- import { Database as Database2 } from "bun:sqlite";
1994
2091
  import { existsSync as existsSync16 } from "fs";
1995
2092
  import { join as join16 } from "path";
1996
2093
  import { homedir as homedir2 } from "os";
1997
- var DB_PATH2 = join16(homedir2(), ".flowdeck-memory", "memory.db");
2094
+ function resolveDbPath() {
2095
+ return join16(process.env.FLOWDECK_MEMORY_DIR ?? join16(homedir2(), ".flowdeck-memory"), "memory.db");
2096
+ }
1998
2097
  var memoryStatusTool = tool17({
1999
2098
  description: "Check FlowDeck memory database status, statistics, and recent sessions",
2000
2099
  args: {},
2001
- async execute(_args, _context) {
2100
+ async execute(_args, context) {
2101
+ const directory = context?.directory ?? process.cwd();
2102
+ const dbPath = resolveDbPath();
2002
2103
  try {
2003
- const exists = existsSync16(DB_PATH2);
2004
- const result = {
2005
- database_exists: exists,
2006
- path: DB_PATH2,
2007
- status: exists ? "ACTIVE" : "NOT_INITIALIZED",
2008
- statistics: null
2009
- };
2010
- if (exists) {
2011
- try {
2012
- const db2 = new Database2(DB_PATH2);
2013
- const sessions = db2.prepare("SELECT COUNT(*) as count FROM sessions").get();
2014
- const observations = db2.prepare("SELECT COUNT(*) as count FROM observations").get();
2015
- const summaries = db2.prepare("SELECT COUNT(*) as count FROM summaries").get();
2016
- const recentSessions = db2.prepare(`
2017
- SELECT
2018
- id,
2019
- content_session_id,
2020
- project,
2021
- directory,
2022
- created_at,
2023
- last_active_at,
2024
- prompt_count
2025
- FROM sessions
2026
- ORDER BY last_active_at DESC
2027
- LIMIT 5
2028
- `).all();
2029
- result.statistics = {
2030
- sessions: sessions.count,
2031
- observations: observations.count,
2032
- summaries: summaries.count,
2033
- recent_sessions: recentSessions.map((s) => {
2034
- const obsCount = db2.prepare("SELECT COUNT(*) as count FROM observations WHERE session_id = ?").get(s.id);
2035
- return {
2036
- project: s.project,
2037
- directory: s.directory,
2038
- observations_in_session: obsCount.count,
2039
- last_active: s.last_active_at,
2040
- prompt_count: s.prompt_count
2041
- };
2042
- })
2043
- };
2044
- db2.close();
2045
- } catch (err) {
2046
- result.status = "ERROR";
2047
- result.statistics = { error: String(err) };
2048
- }
2104
+ const exists = existsSync16(dbPath);
2105
+ if (!exists) {
2106
+ return JSON.stringify({
2107
+ database_exists: false,
2108
+ path: dbPath,
2109
+ status: "NOT_INITIALIZED"
2110
+ }, null, 2);
2049
2111
  }
2050
- return JSON.stringify(result, null, 2);
2112
+ const settings = getDbSettings();
2113
+ const recentSessions = getRecentSessions(directory, 5);
2114
+ const sessionStats = recentSessions.map((s) => {
2115
+ const observations = getObservationsForSession(s.id);
2116
+ const summary = getSessionSummary(s.id);
2117
+ return {
2118
+ project: s.project,
2119
+ directory: s.directory,
2120
+ content_session_id: s.content_session_id,
2121
+ observations_in_session: observations.length,
2122
+ last_active: s.last_active_at,
2123
+ prompt_count: s.prompt_count,
2124
+ has_summary: !!summary,
2125
+ summary_length: summary?.content.length ?? 0,
2126
+ summary_preview: summary?.content.slice(0, 200) ?? null,
2127
+ handoff_metadata: summary?.metadata ?? null
2128
+ };
2129
+ });
2130
+ return JSON.stringify({
2131
+ database_exists: true,
2132
+ path: dbPath,
2133
+ status: "ACTIVE",
2134
+ pragma_settings: settings,
2135
+ recent_sessions_in_directory: sessionStats
2136
+ }, null, 2);
2051
2137
  } catch (err) {
2052
2138
  return JSON.stringify({
2053
2139
  status: "ERROR",
2054
2140
  error: String(err),
2055
- path: DB_PATH2
2141
+ path: dbPath
2056
2142
  }, null, 2);
2057
2143
  }
2058
2144
  }
@@ -2060,7 +2146,7 @@ var memoryStatusTool = tool17({
2060
2146
 
2061
2147
  // src/hooks/memory-hook.ts
2062
2148
  var MAX_TOOL_RESPONSE = 1e4;
2063
- var MAX_PROMPT_LENGTH = 2000;
2149
+ var MAX_SUMMARY_STORAGE = 50000;
2064
2150
  var activeSessions = new Map;
2065
2151
  function extractProjectFromDirectory(directory) {
2066
2152
  const parts = directory.split("/");
@@ -2071,6 +2157,59 @@ function truncate(str, max) {
2071
2157
  return str || "";
2072
2158
  return str.slice(0, max);
2073
2159
  }
2160
+ function buildHandoffMetadata(sessionId, directory, summaryText, observations) {
2161
+ const fileTools = new Set(["edit", "create", "view", "read", "hash-edit", "str-replace-editor"]);
2162
+ const importantFilesSet = new Set;
2163
+ for (const obs of observations) {
2164
+ if (fileTools.has(obs.tool_name) && obs.tool_input) {
2165
+ const path = obs.tool_input.path;
2166
+ if (path)
2167
+ importantFilesSet.add(path);
2168
+ }
2169
+ }
2170
+ const toolNamesUsed = [...new Set(observations.map((o) => o.tool_name).filter((t) => t !== "assistant_message"))];
2171
+ function extractBullets(text) {
2172
+ return text.split(`
2173
+ `).filter((l) => /^\s*[-*]/.test(l)).map((l) => l.replace(/^\s*[-*]\s+/, "").trim()).filter(Boolean);
2174
+ }
2175
+ const sections = {};
2176
+ let currentSection = "";
2177
+ const currentLines = [];
2178
+ for (const line of summaryText.split(`
2179
+ `)) {
2180
+ const header = line.match(/^##\s+\d+\.\s+(.+)/);
2181
+ if (header) {
2182
+ if (currentSection)
2183
+ sections[currentSection] = currentLines.join(`
2184
+ `).trim();
2185
+ currentSection = header[1].trim();
2186
+ currentLines.length = 0;
2187
+ } else {
2188
+ currentLines.push(line);
2189
+ }
2190
+ }
2191
+ if (currentSection)
2192
+ sections[currentSection] = currentLines.join(`
2193
+ `).trim();
2194
+ const completed = extractBullets(sections["Work Completed"] ?? "");
2195
+ const pending = extractBullets(sections["Remaining Tasks"] ?? "");
2196
+ return {
2197
+ workflow_name: extractProjectFromDirectory(directory),
2198
+ current_status: "compacted",
2199
+ current_stage: null,
2200
+ completed_stages: completed,
2201
+ pending_stages: pending,
2202
+ key_decisions: [],
2203
+ blockers: [],
2204
+ important_files: [...importantFilesSet].slice(0, 30),
2205
+ approvals: [],
2206
+ open_questions: [],
2207
+ next_steps: pending.slice(0, 5),
2208
+ tool_names_used: toolNamesUsed.slice(0, 20),
2209
+ observation_count: observations.length,
2210
+ updated_at: new Date().toISOString()
2211
+ };
2212
+ }
2074
2213
  function onSessionCreated(directory, contentSessionId, prompt) {
2075
2214
  const project = extractProjectFromDirectory(directory);
2076
2215
  const session = initSession(contentSessionId, project, directory);
@@ -2095,7 +2234,11 @@ function onToolExecuted(contentSessionId, toolName, toolInput, toolResponse, dir
2095
2234
  };
2096
2235
  activeSessions.set(contentSessionId, ctx);
2097
2236
  }
2098
- storeObservation(ctx.sessionId, truncate(toolName, 200), toolInput, toolResponse ? truncate(toolResponse, MAX_TOOL_RESPONSE) : null, directory);
2237
+ try {
2238
+ storeObservation(ctx.sessionId, truncate(toolName, 200), toolInput, toolResponse ? truncate(toolResponse, MAX_TOOL_RESPONSE) : null, directory);
2239
+ } catch (err) {
2240
+ console.warn(`[FlowDeck Memory] Failed to store observation for tool "${toolName}":`, err);
2241
+ }
2099
2242
  }
2100
2243
  function onMessageUpdated(contentSessionId, role, content, directory) {
2101
2244
  if (role !== "assistant")
@@ -2114,33 +2257,64 @@ function onMessageUpdated(contentSessionId, role, content, directory) {
2114
2257
  };
2115
2258
  activeSessions.set(contentSessionId, ctx);
2116
2259
  }
2117
- storeObservation(ctx.sessionId, "assistant_message", { role }, truncate(content, MAX_TOOL_RESPONSE), directory);
2260
+ try {
2261
+ storeObservation(ctx.sessionId, "assistant_message", { role }, truncate(content, MAX_TOOL_RESPONSE), directory);
2262
+ } catch (err) {
2263
+ console.warn("[FlowDeck Memory] Failed to store assistant message observation:", err);
2264
+ }
2118
2265
  }
2119
2266
  function onSessionCompact(contentSessionId, summary) {
2120
- const ctx = activeSessions.get(contentSessionId);
2121
- if (!ctx)
2122
- return;
2123
- storeSummary(ctx.sessionId, truncate(summary, MAX_PROMPT_LENGTH));
2267
+ let ctx = activeSessions.get(contentSessionId);
2268
+ if (!ctx) {
2269
+ const dbSession = getSessionByContentSessionId(contentSessionId);
2270
+ if (!dbSession) {
2271
+ console.warn(`[FlowDeck Memory] onSessionCompact: no session found for contentSessionId=${contentSessionId} — summary discarded`);
2272
+ return;
2273
+ }
2274
+ ctx = {
2275
+ sessionId: dbSession.id,
2276
+ contentSessionId,
2277
+ project: dbSession.project,
2278
+ directory: dbSession.directory
2279
+ };
2280
+ activeSessions.set(contentSessionId, ctx);
2281
+ }
2282
+ const storedContent = truncate(summary, MAX_SUMMARY_STORAGE);
2283
+ try {
2284
+ const observations = getObservationsForSession(ctx.sessionId);
2285
+ const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, summary, observations);
2286
+ storeSummary(ctx.sessionId, storedContent, metadata);
2287
+ } catch (err) {
2288
+ console.warn(`[FlowDeck Memory] Failed to store compaction summary for session ${ctx.sessionId}:`, err);
2289
+ }
2124
2290
  }
2125
2291
  function onSessionEnd(contentSessionId, lastMessage) {
2126
- const ctx = activeSessions.get(contentSessionId);
2127
- if (!ctx)
2128
- return;
2129
- if (lastMessage && lastMessage.trim()) {
2130
- storeSummary(ctx.sessionId, truncate(lastMessage, MAX_PROMPT_LENGTH));
2292
+ let ctx = activeSessions.get(contentSessionId);
2293
+ if (!ctx) {
2294
+ const dbSession = getSessionByContentSessionId(contentSessionId);
2295
+ if (dbSession) {
2296
+ ctx = {
2297
+ sessionId: dbSession.id,
2298
+ contentSessionId,
2299
+ project: dbSession.project,
2300
+ directory: dbSession.directory
2301
+ };
2302
+ }
2303
+ }
2304
+ if (ctx && lastMessage && lastMessage.trim()) {
2305
+ try {
2306
+ const observations = getObservationsForSession(ctx.sessionId);
2307
+ const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, lastMessage, observations);
2308
+ storeSummary(ctx.sessionId, truncate(lastMessage, MAX_SUMMARY_STORAGE), metadata);
2309
+ } catch (err) {
2310
+ console.warn(`[FlowDeck Memory] Failed to store end-of-session summary for session ${ctx?.sessionId}:`, err);
2311
+ }
2131
2312
  }
2132
2313
  activeSessions.delete(contentSessionId);
2133
2314
  }
2134
2315
  function getSessionContext(directory, contentSessionId) {
2135
2316
  const context = getContextForDirectory(directory, 30);
2136
2317
  const previousSessions = getRecentSessions(directory, 5);
2137
- if (previousSessions.length > 0 && activeSessions.has(contentSessionId)) {
2138
- const ctx = activeSessions.get(contentSessionId);
2139
- for (const prev of previousSessions) {
2140
- if (prev.content_session_id === contentSessionId)
2141
- continue;
2142
- }
2143
- }
2144
2318
  return { context, previousSessions };
2145
2319
  }
2146
2320
  function clearSession(contentSessionId) {
@@ -6423,7 +6597,7 @@ var plugin = async (input, _options) => {
6423
6597
  const delEvent = event?.event ?? event;
6424
6598
  const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
6425
6599
  if (sessionId) {
6426
- memoryHook.clearSession(sessionId);
6600
+ memoryHook.onSessionEnd(sessionId);
6427
6601
  }
6428
6602
  }
6429
6603
  } catch (err) {
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Structured handoff artifact persisted alongside the text summary.
3
+ * Derived from observations + LLM compaction output.
4
+ */
5
+ export interface HandoffMetadata {
6
+ workflow_name: string | null;
7
+ current_status: string;
8
+ current_stage: string | null;
9
+ completed_stages: string[];
10
+ pending_stages: string[];
11
+ key_decisions: string[];
12
+ blockers: string[];
13
+ important_files: string[];
14
+ approvals: string[];
15
+ open_questions: string[];
16
+ next_steps: string[];
17
+ tool_names_used: string[];
18
+ observation_count: number;
19
+ updated_at: string;
20
+ }
1
21
  export interface Observation {
2
22
  id?: number;
3
23
  session_id: number;
@@ -21,6 +41,7 @@ export interface Summary {
21
41
  id?: number;
22
42
  session_id: number;
23
43
  content: string;
44
+ metadata?: HandoffMetadata | null;
24
45
  created_at?: string;
25
46
  }
26
47
  export interface SearchResult {
@@ -29,12 +50,24 @@ export interface SearchResult {
29
50
  }
30
51
  export declare function initSession(contentSessionId: string, project: string, directory: string): Session;
31
52
  export declare function storeObservation(sessionId: number, toolName: string, toolInput: unknown, toolResponse: string | null, directory: string): Observation;
32
- export declare function storeSummary(sessionId: number, content: string): Summary;
53
+ export declare function storeSummary(sessionId: number, content: string, metadata?: HandoffMetadata | null): Summary;
33
54
  export declare function getRecentSessions(directory: string, limit?: number): Session[];
34
55
  export declare function getObservationsForSession(sessionId: number): Observation[];
35
56
  export declare function getSessionSummary(sessionId: number): Summary | null;
57
+ export declare function getSessionByContentSessionId(contentSessionId: string): Session | null;
36
58
  export declare function getRecentObservations(directory: string, limit?: number): SearchResult[];
37
59
  export declare function searchObservations(directory: string, query: string, limit?: number): SearchResult[];
38
60
  export declare function getContextForDirectory(directory: string, maxObservations?: number): string;
39
61
  export declare function closeDatabase(): void;
62
+ /**
63
+ * Returns current connection-level PRAGMA values.
64
+ * Intended for diagnostics and tests — queries via the shared singleton so the
65
+ * values reflect what this module actually set, not a second connection.
66
+ */
67
+ export declare function getDbSettings(): {
68
+ journal_mode: string;
69
+ busy_timeout: number;
70
+ synchronous: number;
71
+ wal_autocheckpoint: number;
72
+ };
40
73
  //# sourceMappingURL=memory-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/services/memory-store.ts"],"names":[],"mappings":"AAkEA,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,kBAAkB,EAAE,MAAM,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;CACjB;AAoBD,wBAAgB,WAAW,CAAC,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CA8BjG;AAED,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,OAAO,EAClB,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,SAAS,EAAE,MAAM,GAChB,WAAW,CAqBb;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAgBxE;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,OAAO,EAAE,CAUzE;AAED,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAU1E;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAGnE;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,YAAY,EAAE,CAyBnF;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,YAAY,EAAE,CA2B/F;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,SAAK,GAAG,MAAM,CAwCtF;AAED,wBAAgB,aAAa,IAAI,IAAI,CAKpC"}
1
+ {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/services/memory-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=memory-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-store.test.d.ts","sourceRoot":"","sources":["../../src/services/memory-store.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"memory-search.d.ts","sourceRoot":"","sources":["../../src/tools/memory-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAG/D,eAAO,MAAM,gBAAgB,EAAE,cAwE7B,CAAA"}
1
+ {"version":3,"file":"memory-search.d.ts","sourceRoot":"","sources":["../../src/tools/memory-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAQ/D,eAAO,MAAM,gBAAgB,EAAE,cAgF7B,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"memory-status.d.ts","sourceRoot":"","sources":["../../src/tools/memory-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAQ/D,eAAO,MAAM,gBAAgB,EAAE,cAoE7B,CAAA"}
1
+ {"version":3,"file":"memory-status.d.ts","sourceRoot":"","sources":["../../src/tools/memory-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAU/D,eAAO,MAAM,gBAAgB,EAAE,cAkE7B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dv.nghiem/flowdeck",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "FlowDeck — structured planning and execution workflows for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",