From c36ef7e46bff8d9f4ed1ed129bbdef47f07c2490 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=A7=CE=B3=CF=86=CF=84=20Kompanion?=
 <chigammaphitau@chelydra.at>
Date: Sun, 19 Oct 2025 10:40:33 +0200
Subject: [PATCH] middleware: add snapshot helpers (save/load) backed by PgDal;
 link kom_dal with PIC; CLI: add --snapshot-save/--snapshot-load for easy
 syntax

---
 src/CMakeLists.txt              |  7 +++--
 src/cli/KompanionApp.cpp        | 42 +++++++++++++++++++++++++++++
 src/dal/CMakeLists.txt          |  1 +
 src/middleware/orchestrator.cpp | 47 +++++++++++++++++++++++++++++++++
 src/middleware/orchestrator.h   | 18 ++++++++++++-
 5 files changed, 112 insertions(+), 3 deletions(-)

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 558cabc..73ac13c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,5 +1,8 @@
 # Subdir CMake for src
 
+# Ensure internal libs are available to dependents
+add_subdirectory(dal)
+
 add_library(kompanion_mw SHARED
   middleware/kompanioncontroller.cpp
   middleware/libkiexecutor.cpp
@@ -8,7 +11,7 @@ add_library(kompanion_mw SHARED
   middleware/orchestrator.cpp
 )
 
-find_package(Qt6 REQUIRED COMPONENTS Core DBus)
+find_package(Qt6 REQUIRED COMPONENTS Core DBus Sql)
 
 set(KOMPANION_CONTROLLER_DBUS_XML ${CMAKE_CURRENT_SOURCE_DIR}/../docs/dbus/org.kde.kompanion.controller.xml)
 set(KOMPANION_EXECUTOR_DBUS_XML ${CMAKE_CURRENT_SOURCE_DIR}/../docs/dbus/org.kde.kompanion.executor.xml)
@@ -31,7 +34,7 @@ target_include_directories(kompanion_mw PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/middl
 
 target_sources(kompanion_mw PRIVATE ${KOMPANION_DBUS_ADAPTOR_SRCS} ${KOMPANION_DBUS_INTERFACE_SRCS})
 
-target_link_libraries(kompanion_mw PRIVATE Qt6::Core Qt6::DBus Qt6::Network)
+target_link_libraries(kompanion_mw PRIVATE Qt6::Core Qt6::DBus Qt6::Sql Qt6::Network kom_dal)
 target_compile_definitions(kompanion_mw PRIVATE KOMPANION_MW_LIBRARY)
 
 # Example executable wiring GUI/controller/executor together could be added later.
diff --git a/src/cli/KompanionApp.cpp b/src/cli/KompanionApp.cpp
index 7c60b0f..49f6dcd 100644
--- a/src/cli/KompanionApp.cpp
+++ b/src/cli/KompanionApp.cpp
@@ -867,6 +867,18 @@ int main(int argc, char** argv) {
                                    "path");
   parser.addOption(embFileOption);
 
+  // Snapshot helpers
+  QCommandLineOption snapshotSaveOption(QStringList() << "--snapshot-save",
+                                        "Save a JSON snapshot (content) under a key in --ns. Provide content via -r/--stdin/[payload].");
+  parser.addOption(snapshotSaveOption);
+  QCommandLineOption snapshotLoadOption(QStringList() << "--snapshot-load",
+                                        "Load a JSON snapshot for --ns and --key and print it.");
+  parser.addOption(snapshotLoadOption);
+  QCommandLineOption keyOption(QStringList() << "--key",
+                               "Key for snapshot operations (default 'session:last').",
+                               "key", "session:last");
+  parser.addOption(keyOption);
+
   parser.addPositionalArgument("tool", "Tool name to invoke.");
   parser.addPositionalArgument("payload", "Optional JSON payload or file path (use '-' for stdin).", "[payload]");
 
@@ -880,6 +892,8 @@ int main(int argc, char** argv) {
   const bool interactive = parser.isSet(interactiveOption);
   const bool initRequested = parser.isSet(initOption);
   const bool runMcp = parser.isSet(mcpServeOption);
+  const bool snapSave = parser.isSet(snapshotSaveOption);
+  const bool snapLoad = parser.isSet(snapshotLoadOption);
 
   std::optional<std::string> configDsn = readDsnFromConfig();
   const char* envDsn = std::getenv("PG_DSN");
@@ -932,6 +946,34 @@ int main(int argc, char** argv) {
     return runMcpServer(backend, addr, qerr);
   }
 
+  // Snapshot helpers (exclusive)
+  if (snapSave || snapLoad) {
+    const QString ns = parser.value(nsNameOption);
+    if (ns.isEmpty()) { qerr << "--snapshot-save/load requires --ns <name>\n"; return 1; }
+    const QString key = parser.value(keyOption);
+    if (snapSave) {
+      // Resolve content from CLI
+      std::string raw;
+      QString err;
+      if (!resolveRequestPayload(parser, parser.positionalArguments(), requestOption, stdinOption, raw, &err)) {
+        qerr << (err.isEmpty() ? QStringLiteral("Failed to read snapshot content") : err) << "\n";
+        return 1;
+      }
+      // Wrap into save_context call
+      std::ostringstream req;
+      req << "{\"namespace\":\"" << ns.toStdString() << "\",\"key\":\"" << key.toStdString() << "\",\"content\":" << raw << ",\"tags\":[\"snapshot\"]}";
+      const std::string out = server.dispatch("kom.memory.v1.save_context", req.str());
+      std::cout << out << std::endl;
+      return 0;
+    } else {
+      std::ostringstream req;
+      req << "{\"namespace\":\"" << ns.toStdString() << "\",\"key\":\"" << key.toStdString() << "\"}";
+      const std::string out = server.dispatch("kom.memory.v1.recall_context", req.str());
+      std::cout << out << std::endl;
+      return 0;
+    }
+  }
+
   // DB inspection helpers (exclusive)
   if (parser.isSet(dbNsOption)) {
     return dbListNamespaces(qout) ? 0 : 1;
diff --git a/src/dal/CMakeLists.txt b/src/dal/CMakeLists.txt
index 6dd0c04..19744b8 100644
--- a/src/dal/CMakeLists.txt
+++ b/src/dal/CMakeLists.txt
@@ -6,3 +6,4 @@ target_compile_features(kom_dal PUBLIC cxx_std_20)
 target_include_directories(kom_dal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
 target_link_libraries(kom_dal PUBLIC Qt6::Core Qt6::Sql)
 target_compile_options(kom_dal PRIVATE -fexceptions)
+set_target_properties(kom_dal PROPERTIES POSITION_INDEPENDENT_CODE ON)
diff --git a/src/middleware/orchestrator.cpp b/src/middleware/orchestrator.cpp
index a90cab2..f526ac7 100644
--- a/src/middleware/orchestrator.cpp
+++ b/src/middleware/orchestrator.cpp
@@ -11,6 +11,8 @@
 #include <QTextStream>
 #include <QTimer>
 
+#include "dal/PgDal.hpp"
+
 // ---------- OllamaModelProvider ----------
 OllamaModelProvider::OllamaModelProvider(QObject *parent)
   : QObject(parent)
@@ -170,3 +172,48 @@ void Orchestrator::processPendingTasks() {
     }
   }
 }
+
+bool Orchestrator::saveSnapshot(const QString &nameSpace,
+                                const QString &key,
+                                const QJsonObject &content,
+                                const QStringList &tags)
+{
+  ki::PgDal dal;
+  const QByteArray dsn = qgetenv("PG_DSN");
+  if (!dsn.isEmpty()) dal.connect(dsn.toStdString()); else dal.connect("stub://memory");
+
+  auto nsRow = dal.ensureNamespace(nameSpace.toStdString());
+  if (!nsRow) return false;
+
+  ki::ItemRow row;
+  row.namespace_id = nsRow->id;
+  row.key = key.toStdString();
+  row.tags.reserve(tags.size());
+  for (const auto &t : tags) row.tags.push_back(t.toStdString());
+  row.content_json = QString::fromUtf8(QJsonDocument(content).toJson(QJsonDocument::Compact)).toStdString();
+  row.metadata_json = "{}";
+  row.created_at = std::chrono::system_clock::now();
+
+  const std::string id = dal.upsertItem(row);
+  return !id.empty();
+}
+
+std::optional<QJsonObject> Orchestrator::loadSnapshot(const QString &nameSpace,
+                                                      const QString &key)
+{
+  ki::PgDal dal;
+  const QByteArray dsn = qgetenv("PG_DSN");
+  if (!dsn.isEmpty()) dal.connect(dsn.toStdString()); else dal.connect("stub://memory");
+
+  auto nsRow = dal.findNamespace(nameSpace.toStdString());
+  if (!nsRow) return std::nullopt;
+
+  std::vector<std::string> tags; tags.emplace_back("snapshot");
+  auto rows = dal.fetchContext(nsRow->id, std::optional<std::string>(key.toStdString()), tags, std::nullopt, 1);
+  if (rows.empty()) return std::nullopt;
+  const auto &row = rows.front();
+  if (row.content_json.empty()) return std::nullopt;
+  const auto doc = QJsonDocument::fromJson(QByteArray::fromStdString(row.content_json));
+  if (!doc.isObject()) return std::nullopt;
+  return doc.object();
+}
diff --git a/src/middleware/orchestrator.h b/src/middleware/orchestrator.h
index 43658fe..1da47ee 100644
--- a/src/middleware/orchestrator.h
+++ b/src/middleware/orchestrator.h
@@ -64,6 +64,23 @@ public:
   // One-shot tick (public for tests).
   void processPendingTasks();
 
+  /**
+   * saveSnapshot: persist a JSON snapshot under (namespace,key) as a context item.
+   * - Tags default to {"snapshot"}.
+   * - If PG_DSN is unset, uses an in-memory stub (won't persist across restarts).
+   */
+  bool saveSnapshot(const QString &nameSpace,
+                    const QString &key,
+                    const QJsonObject &content,
+                    const QStringList &tags = {QStringLiteral("snapshot")});
+
+  /**
+   * loadSnapshot: fetch the latest item for (namespace,key) tagged as snapshot.
+   * Returns empty optional if not found or on failure.
+   */
+  std::optional<QJsonObject> loadSnapshot(const QString &nameSpace,
+                                          const QString &key);
+
 signals:
   void taskProcessed(const QString &kind);
 
@@ -88,4 +105,3 @@ private:
   bool continuityDone_ = false;
   IModelProvider *model_ = nullptr; // not owned
 };
-
