feat: Add memory persistence functions (save/load-memory-to-disk)
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 2s

- Add save-memory-to-disk and load-memory-from-disk to memory.lisp
- Integrate auto-save into heartbeat (every N intervals)
- Load memory on daemon startup, save on graceful shutdown/SIGINT
- Add exports to package.lisp

NOTE: Hash table serialization requires object walker for complex structures.
      Current implementation fails on load due to unreadable objects.
This commit is contained in:
2026-04-22 15:14:18 -04:00
parent b62b7f1095
commit 620267a8df
5 changed files with 133 additions and 8 deletions

View File

@@ -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. 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 #+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 () (defun start-heartbeat ()
"Starts the background heartbeat thread. Interval is loaded from HEARTBEAT_INTERVAL." "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* (setf *heartbeat-thread*
(bt:make-thread (bt:make-thread
(lambda () (lambda ()
(loop (loop
(sleep interval) (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 is synchronous for heartbeats, preventing accumulation.
(inject-stimulus (list :type :EVENT :payload (list :sensor :heartbeat :unix-time (get-universal-time)))))) (inject-stimulus (list :type :EVENT :payload (list :sensor :heartbeat :unix-time (get-universal-time))))))
:name "opencortex-heartbeat")))) :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. 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 #+begin_src lisp :tangle ../library/loop.lisp
(defvar *shutdown-save-enabled* t
"If non-nil, save memory to disk on graceful shutdown.")
(defun main () (defun main ()
"Entry point for the Skeleton MVP. Handles initialization and graceful shutdown." "Entry point for the Skeleton MVP. Handles initialization and graceful shutdown."
(let* ((home (uiop:getenv "HOME")) (let* ((home (uiop:getenv "HOME"))
(env-file (uiop:merge-pathnames* ".local/share/opencortex/.env" (uiop:ensure-directory-pathname 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))) (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-actuators)
(initialize-all-skills) (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 (sb-sys:enable-interrupt sb-unix:sigint
(lambda (sig code scp) (lambda (sig code scp)
(declare (ignore 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))) (uiop:quit 0)))
(let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600))) (let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600)))
(loop (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)))) (sleep sleep-interval))))
#+end_src #+end_src

View File

@@ -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)))) (harness-log "MEMORY ERROR - Snapshot ~a not found." index))))
#+end_src #+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 ** Lookup Utilities
Basic functions for retrieving objects by ID or type. Basic functions for retrieving objects by ID or type.

View File

@@ -39,24 +39,43 @@
(setf current-signal (list :type :EVENT :depth (1+ depth) :meta meta (setf current-signal (list :type :EVENT :depth (1+ depth) :meta meta
:payload (list :sensor :loop-error :message (format nil "~a" c) :depth depth))))))))))) :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 () (defun start-heartbeat ()
"Starts the background heartbeat thread. Interval is loaded from HEARTBEAT_INTERVAL." "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* (setf *heartbeat-thread*
(bt:make-thread (bt:make-thread
(lambda () (lambda ()
(loop (loop
(sleep interval) (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 is synchronous for heartbeats, preventing accumulation.
(inject-stimulus (list :type :EVENT :payload (list :sensor :heartbeat :unix-time (get-universal-time)))))) (inject-stimulus (list :type :EVENT :payload (list :sensor :heartbeat :unix-time (get-universal-time))))))
:name "opencortex-heartbeat")))) :name "opencortex-heartbeat"))))
(defvar *shutdown-save-enabled* t
"If non-nil, save memory to disk on graceful shutdown.")
(defun main () (defun main ()
"Entry point for the Skeleton MVP. Handles initialization and graceful shutdown." "Entry point for the Skeleton MVP. Handles initialization and graceful shutdown."
(let* ((home (uiop:getenv "HOME")) (let* ((home (uiop:getenv "HOME"))
(env-file (uiop:merge-pathnames* ".local/share/opencortex/.env" (uiop:ensure-directory-pathname 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))) (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-actuators)
(initialize-all-skills) (initialize-all-skills)
@@ -67,10 +86,14 @@
(sb-sys:enable-interrupt sb-unix:sigint (sb-sys:enable-interrupt sb-unix:sigint
(lambda (sig code scp) (lambda (sig code scp)
(declare (ignore 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))) (uiop:quit 0)))
(let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600))) (let ((sleep-interval (or (ignore-errors (parse-integer (uiop:getenv "DAEMON_SLEEP_INTERVAL"))) 3600)))
(loop (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)))) (sleep sleep-interval))))

View File

@@ -79,6 +79,42 @@
(harness-log "MEMORY - Memory rolled back to snapshot ~a" index)) (harness-log "MEMORY - Memory rolled back to snapshot ~a" index))
(harness-log "MEMORY ERROR - Snapshot ~a not found." 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 () (defun org-id-new ()
"Generates a new UUID string for Org-mode identification." "Generates a new UUID string for Org-mode identification."
(string-downcase (format nil "~a" (uuid:make-v4-uuid)))) (string-downcase (format nil "~a" (uuid:make-v4-uuid))))

View File

@@ -40,6 +40,8 @@
#:org-object-hash #:org-object-hash
#:snapshot-memory #:snapshot-memory
#:rollback-memory #:rollback-memory
#:save-memory-to-disk
#:load-memory-from-disk
;; --- Context API (Peripheral Vision) --- ;; --- Context API (Peripheral Vision) ---
#:context-query-store #:context-query-store