feat: Add memory persistence functions (save/load-memory-to-disk)
Some checks failed
Deploy-Agent-V15-Stdin / JOB-V15-STDIN (push) Failing after 2s
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:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user