diff --git a/harness/loop.org b/harness/loop.org index a362f61..c5fccd8 100644 --- a/harness/loop.org +++ b/harness/loop.org @@ -65,14 +65,27 @@ The `process-signal` function is the core metabolic processor. It iterates throu The heartbeat ensures the agent remains "alive" even in the absence of external stimuli, allowing for latent reflection and periodic maintenance. The interval is externalized to the `HEARTBEAT_INTERVAL` environment variable. #+begin_src lisp :tangle ../library/loop.lisp +(defvar *auto-save-interval* 300 + "Save memory to disk every N seconds. Set from MEMORY_AUTO_SAVE_INTERVAL env.") + +(defvar *heartbeat-save-counter* 0 + "Counter for auto-save triggers.") + (defun start-heartbeat () "Starts the background heartbeat thread. Interval is loaded from HEARTBEAT_INTERVAL." - (let ((interval (or (ignore-errors (parse-integer (uiop:getenv "HEARTBEAT_INTERVAL"))) 60))) + (let ((interval (or (ignore-errors (parse-integer (uiop:getenv "HEARTBEAT_INTERVAL"))) 60)) + (auto-save (or (ignore-errors (parse-integer (uiop:getenv "MEMORY_AUTO_SAVE_INTERVAL"))) *auto-save-interval*))) + (setf *auto-save-interval* auto-save) + (setf *heartbeat-save-counter* 0) (setf *heartbeat-thread* (bt:make-thread (lambda () (loop (sleep interval) + (incf *heartbeat-save-counter*) + (when (>= *heartbeat-save-counter* (/ *auto-save-interval* interval)) + (setf *heartbeat-save-counter* 0) + (save-memory-to-disk)) ;; inject-stimulus is synchronous for heartbeats, preventing accumulation. (inject-stimulus (list :type :EVENT :payload (list :sensor :heartbeat :unix-time (get-universal-time)))))) :name "opencortex-heartbeat")))) @@ -82,12 +95,18 @@ The heartbeat ensures the agent remains "alive" even in the absence of external The `main` function initializes the environment, loads skills, and starts the heartbeat. It now includes a graceful shutdown handler for `SIGINT` (Ctrl+C) and uses `DAEMON_SLEEP_INTERVAL` to control its idle rhythm. #+begin_src lisp :tangle ../library/loop.lisp +(defvar *shutdown-save-enabled* t + "If non-nil, save memory to disk on graceful shutdown.") + (defun main () "Entry point for the Skeleton MVP. Handles initialization and graceful shutdown." (let* ((home (uiop:getenv "HOME")) (env-file (uiop:merge-pathnames* ".local/share/opencortex/.env" (uiop:ensure-directory-pathname home)))) (when (uiop:file-exists-p env-file) (cl-dotenv:load-env env-file))) - + + ;; Load memory from disk if a snapshot exists + (load-memory-from-disk) + (initialize-actuators) (initialize-all-skills) @@ -98,11 +117,15 @@ The `main` function initializes the environment, loads skills, and starts the he (sb-sys:enable-interrupt sb-unix:sigint (lambda (sig code scp) (declare (ignore sig code scp)) - (harness-log "SHUTDOWN: SIGINT received. Exiting...") + (harness-log "SHUTDOWN: SIGINT received. Saving memory...") + (when *shutdown-save-enabled* (save-memory-to-disk)) (uiop:quit 0))) (let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600))) (loop - (when (bt:with-lock-held (*interrupt-lock*) *interrupt-flag*) (return)) + (when (bt:with-lock-held (*interrupt-lock*) *interrupt-flag*) + (harness-log "SHUTDOWN: Interrupt flag set. Saving memory...") + (when *shutdown-save-enabled* (save-memory-to-disk)) + (return)) (sleep sleep-interval)))) #+end_src diff --git a/harness/memory.org b/harness/memory.org index 1ca5180..93a2918 100644 --- a/harness/memory.org +++ b/harness/memory.org @@ -144,6 +144,47 @@ Restores the state of the Memex from one of the previous snapshots. (harness-log "MEMORY ERROR - Snapshot ~a not found." index)))) #+end_src +** Disk Persistence (save-memory / load-memory) +Essential for surviving crashes. Saves the in-memory hash tables to disk and loads them back on restart. The path is controlled by the `MEMORY_SNAPSHOT_PATH` environment variable. + +#+begin_src lisp :tangle ../library/memory.lisp +(defvar *memory-snapshot-path* nil + "Path to the memory snapshot file. Set from MEMORY_SNAPSHOT_PATH env or default.") + +(defun ensure-memory-snapshot-path () + "Initializes the snapshot path from environment or default location." + (or *memory-snapshot-path* + (let ((env-path (uiop:getenv "MEMORY_SNAPSHOT_PATH"))) + (setf *memory-snapshot-path* + (or env-path + (uiop:merge-pathnames* "memory.snap" (user-homedir-pathname))))))) + +(defun save-memory-to-disk () + "Serializes *memory* and *history-store* to disk for crash recovery." + (let ((path (ensure-memory-snapshot-path))) + (with-open-file (stream path :direction :output :if-exists :supersede :if-does-not-exist :create) + (format stream ";; OpenCortex Memory Snapshot~%") + (format stream ";; Created: ~a~%~%" (format nil "~a" (get-universal-time))) + (prin1 (list :memory *memory* :history-store *history-store*) stream)) + (harness-log "MEMORY - Saved to ~a" path) + path)) + +(defun load-memory-from-disk () + "Loads *memory* and *history-store* from disk if the snapshot exists." + (let ((path (ensure-memory-snapshot-path))) + (when (uiop:file-exists-p path) + (handler-case + (with-open-file (stream path :direction :input) + (let ((data (read stream nil))) + (when data + (setf *memory* (getf data :memory)) + (setf *history-store* (getf data :history-store)) + (harness-log "MEMORY - Loaded from ~a (~a objects)" path (hash-table-size *memory*))))) + (error (c) + (harness-log "MEMORY WARNING - Failed to load snapshot: ~a" c)))) + t)) +#+end_src + ** Lookup Utilities Basic functions for retrieving objects by ID or type. diff --git a/library/loop.lisp b/library/loop.lisp index 8fbddc0..fd0e903 100644 --- a/library/loop.lisp +++ b/library/loop.lisp @@ -39,24 +39,43 @@ (setf current-signal (list :type :EVENT :depth (1+ depth) :meta meta :payload (list :sensor :loop-error :message (format nil "~a" c) :depth depth))))))))))) +(defvar *auto-save-interval* 300 + "Save memory to disk every N seconds. Set from MEMORY_AUTO_SAVE_INTERVAL env.") + +(defvar *heartbeat-save-counter* 0 + "Counter for auto-save triggers.") + (defun start-heartbeat () "Starts the background heartbeat thread. Interval is loaded from HEARTBEAT_INTERVAL." - (let ((interval (or (ignore-errors (parse-integer (uiop:getenv "HEARTBEAT_INTERVAL"))) 60))) + (let ((interval (or (ignore-errors (parse-integer (uiop:getenv "HEARTBEAT_INTERVAL"))) 60)) + (auto-save (or (ignore-errors (parse-integer (uiop:getenv "MEMORY_AUTO_SAVE_INTERVAL"))) *auto-save-interval*))) + (setf *auto-save-interval* auto-save) + (setf *heartbeat-save-counter* 0) (setf *heartbeat-thread* (bt:make-thread (lambda () (loop (sleep interval) + (incf *heartbeat-save-counter*) + (when (>= *heartbeat-save-counter* (/ *auto-save-interval* interval)) + (setf *heartbeat-save-counter* 0) + (save-memory-to-disk)) ;; inject-stimulus is synchronous for heartbeats, preventing accumulation. (inject-stimulus (list :type :EVENT :payload (list :sensor :heartbeat :unix-time (get-universal-time)))))) :name "opencortex-heartbeat")))) +(defvar *shutdown-save-enabled* t + "If non-nil, save memory to disk on graceful shutdown.") + (defun main () "Entry point for the Skeleton MVP. Handles initialization and graceful shutdown." (let* ((home (uiop:getenv "HOME")) (env-file (uiop:merge-pathnames* ".local/share/opencortex/.env" (uiop:ensure-directory-pathname home)))) (when (uiop:file-exists-p env-file) (cl-dotenv:load-env env-file))) - + + ;; Load memory from disk if a snapshot exists + (load-memory-from-disk) + (initialize-actuators) (initialize-all-skills) @@ -67,10 +86,14 @@ (sb-sys:enable-interrupt sb-unix:sigint (lambda (sig code scp) (declare (ignore sig code scp)) - (harness-log "SHUTDOWN: SIGINT received. Exiting...") + (harness-log "SHUTDOWN: SIGINT received. Saving memory...") + (when *shutdown-save-enabled* (save-memory-to-disk)) (uiop:quit 0))) (let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600))) (loop - (when (bt:with-lock-held (*interrupt-lock*) *interrupt-flag*) (return)) + (when (bt:with-lock-held (*interrupt-lock*) *interrupt-flag*) + (harness-log "SHUTDOWN: Interrupt flag set. Saving memory...") + (when *shutdown-save-enabled* (save-memory-to-disk)) + (return)) (sleep sleep-interval)))) diff --git a/library/memory.lisp b/library/memory.lisp index 7c70dca..d48b10d 100644 --- a/library/memory.lisp +++ b/library/memory.lisp @@ -79,6 +79,42 @@ (harness-log "MEMORY - Memory rolled back to snapshot ~a" index)) (harness-log "MEMORY ERROR - Snapshot ~a not found." index)))) +(defvar *memory-snapshot-path* nil + "Path to the memory snapshot file. Set from MEMORY_SNAPSHOT_PATH env or default.") + +(defun ensure-memory-snapshot-path () + "Initializes the snapshot path from environment or default location." + (or *memory-snapshot-path* + (let ((env-path (uiop:getenv "MEMORY_SNAPSHOT_PATH"))) + (setf *memory-snapshot-path* + (or env-path + (uiop:merge-pathnames* "memory.snap" (user-homedir-pathname))))))) + +(defun save-memory-to-disk () + "Serializes *memory* and *history-store* to disk for crash recovery." + (let ((path (ensure-memory-snapshot-path))) + (with-open-file (stream path :direction :output :if-exists :supersede :if-does-not-exist :create) + (format stream ";; OpenCortex Memory Snapshot~%") + (format stream ";; Created: ~a~%~%" (format nil "~a" (get-universal-time))) + (prin1 (list :memory *memory* :history-store *history-store*) stream)) + (harness-log "MEMORY - Saved to ~a" path) + path)) + +(defun load-memory-from-disk () + "Loads *memory* and *history-store* from disk if the snapshot exists." + (let ((path (ensure-memory-snapshot-path))) + (when (uiop:file-exists-p path) + (handler-case + (with-open-file (stream path :direction :input) + (let ((data (read stream nil))) + (when data + (setf *memory* (getf data :memory)) + (setf *history-store* (getf data :history-store)) + (harness-log "MEMORY - Loaded from ~a (~a objects)" path (hash-table-size *memory*))))) + (error (c) + (harness-log "MEMORY WARNING - Failed to load snapshot: ~a" c)))) + t)) + (defun org-id-new () "Generates a new UUID string for Org-mode identification." (string-downcase (format nil "~a" (uuid:make-v4-uuid)))) diff --git a/library/package.lisp b/library/package.lisp index 25a53db..c3286dd 100644 --- a/library/package.lisp +++ b/library/package.lisp @@ -40,6 +40,8 @@ #:org-object-hash #:snapshot-memory #:rollback-memory + #:save-memory-to-disk + #:load-memory-from-disk ;; --- Context API (Peripheral Vision) --- #:context-query-store