feat: Context Manager skill + org-object→memory-object fix
Some checks failed
Deploy (Gitea) / deploy (push) Has been cancelled
Some checks failed
Deploy (Gitea) / deploy (push) Has been cancelled
- system-context-manager (new skill): stack-based project focusing with push-context/pop-context, path resolution relative to base path, and scope-aware memory queries via context-scoped-query. - core-memory: add :scope slot to memory-object struct (default :memex). - core-memory: ingest-ast accepts &key (scope :memex), propagates to children. - core-context: context-query accepts :scope parameter for filtering. - DEFECT FIX: renamed org-object-* accessors to memory-object-* across core-context, security-dispatcher, tests, and defpackage exports. The struct was renamed but accessor references were never updated — the code referenced nonexistent functions.
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
(in-package :passepartout)
|
||||
|
||||
(defun context-query (&key tag todo-state type)
|
||||
"Filters the Memory based on tags, todo states, or types."
|
||||
(defun context-query (&key tag todo-state type scope)
|
||||
"Filters the Memory based on tags, todo states, or types.
|
||||
Optional SCOPE restricts results to objects with that scope
|
||||
or :memex (global scope always visible)."
|
||||
(let ((results nil))
|
||||
(maphash (lambda (id obj)
|
||||
(declare (ignore id))
|
||||
(let* ((attrs (org-object-attributes obj)) (state (getf attrs :TODO-STATE)) (match t))
|
||||
(when (and type (not (eq (org-object-type obj) type))) (setf match nil))
|
||||
(let* ((attrs (memory-object-attributes obj)) (state (getf attrs :TODO-STATE)) (match t))
|
||||
;; Scope filter: if scope specified, only match :memex (global) or same scope
|
||||
(when (and scope (not (eq (memory-object-scope obj) :memex))
|
||||
(not (eq (memory-object-scope obj) scope)))
|
||||
(setf match nil))
|
||||
(when (and type (not (eq (memory-object-type obj) type))) (setf match nil))
|
||||
(when tag (unless (search tag (format nil "~a" (getf attrs :TAGS)) :test #'string-equal) (setf match nil)))
|
||||
(when (and todo-state (not (equal state todo-state))) (setf match nil))
|
||||
(when match (push obj results))))
|
||||
@@ -15,7 +21,7 @@
|
||||
|
||||
(defun context-active-projects ()
|
||||
"Returns headlines tagged as 'project' that are not yet marked DONE."
|
||||
(remove-if (lambda (obj) (equal (getf (org-object-attributes obj) :TODO-STATE) "DONE"))
|
||||
(remove-if (lambda (obj) (equal (getf (memory-object-attributes obj) :TODO-STATE) "DONE"))
|
||||
(context-query :tag "project" :type :HEADLINE)))
|
||||
|
||||
(defun context-recent-tasks ()
|
||||
@@ -59,13 +65,13 @@ or nil if the heading is not found."
|
||||
|
||||
(defun context-object-render (obj &key (depth 1) (foveal-id nil) semantic-threshold (foveal-vector nil))
|
||||
"Recursively renders an org-object and its children to an Org string using a Foveal-Peripheral Hybrid model."
|
||||
(let* ((id (org-object-id obj))
|
||||
(let* ((id (memory-object-id obj))
|
||||
(is-foveal (equal id foveal-id))
|
||||
(title (or (getf (org-object-attributes obj) :TITLE) "Untitled"))
|
||||
(content (org-object-content obj))
|
||||
(children (org-object-children obj))
|
||||
(title (or (getf (memory-object-attributes obj) :TITLE) "Untitled"))
|
||||
(content (memory-object-content obj))
|
||||
(children (memory-object-children obj))
|
||||
(stars (make-string depth :initial-element #\*))
|
||||
(obj-vector (org-object-vector obj))
|
||||
(obj-vector (memory-object-vector obj))
|
||||
(threshold (or semantic-threshold (ignore-errors (read-from-string (uiop:getenv "CONTEXT_SEMANTIC_THRESHOLD"))) 0.75))
|
||||
(similarity (if (and foveal-vector obj-vector (not is-foveal))
|
||||
(cosine-similarity foveal-vector obj-vector)
|
||||
@@ -84,7 +90,7 @@ or nil if the heading is not found."
|
||||
(setf output (concatenate 'string output content (string #\Newline))))
|
||||
|
||||
(dolist (child-id children)
|
||||
(let ((child-obj (lookup-object child-id)))
|
||||
(let ((child-obj (memory-object-get child-id)))
|
||||
(when child-obj
|
||||
(let ((next-foveal (if is-foveal child-id foveal-id)))
|
||||
(setf output (concatenate 'string output
|
||||
@@ -111,7 +117,7 @@ or nil if the heading is not found."
|
||||
|
||||
(defun context-privacy-filtered-p (obj)
|
||||
"Returns T if an org-object's :TAGS attribute matches bouncer-privacy-tags."
|
||||
(let* ((attrs (org-object-attributes obj))
|
||||
(let* ((attrs (memory-object-attributes obj))
|
||||
(tags (getf attrs :TAGS))
|
||||
(privacy-tags (and (find-package :passepartout.security-dispatcher)
|
||||
(symbol-value
|
||||
|
||||
@@ -26,23 +26,24 @@
|
||||
#:skill-gateway-link
|
||||
#:gateway-manager-main
|
||||
#:ingest-ast
|
||||
#:lookup-object
|
||||
#:memory-object-get
|
||||
#:list-objects-by-type
|
||||
#:org-id-new
|
||||
#:*memory-store*
|
||||
#:*history-store*
|
||||
#:org-object
|
||||
#:make-org-object
|
||||
#:org-object-id
|
||||
#:org-object-type
|
||||
#:org-object-attributes
|
||||
#:org-object-parent-id
|
||||
#:org-object-children
|
||||
#:org-object-version
|
||||
#:org-object-last-sync
|
||||
#:org-object-vector
|
||||
#:org-object-content
|
||||
#:org-object-hash
|
||||
#:memory-object
|
||||
#:make-memory-object
|
||||
#:memory-object-id
|
||||
#:memory-object-type
|
||||
#:memory-object-attributes
|
||||
#:memory-object-parent-id
|
||||
#:memory-object-children
|
||||
#:memory-object-version
|
||||
#:memory-object-last-sync
|
||||
#:memory-object-vector
|
||||
#:memory-object-content
|
||||
#:memory-object-hash
|
||||
#:memory-object-scope
|
||||
#:snapshot-memory
|
||||
#:rollback-memory
|
||||
#:context-query-store
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
(concatenate 'string "id-" (string-downcase (format nil "~a" (uuid:make-v4-uuid)))))
|
||||
|
||||
(defstruct memory-object
|
||||
id type attributes content vector parent-id children version last-sync hash)
|
||||
id type attributes content vector parent-id children version last-sync hash scope)
|
||||
|
||||
(defmethod make-load-form ((obj memory-object) &optional env)
|
||||
(make-load-form-saving-slots obj :environment env))
|
||||
@@ -39,7 +39,8 @@
|
||||
:children (copy-list (memory-object-children obj))
|
||||
:version (memory-object-version obj)
|
||||
:last-sync (memory-object-last-sync obj)
|
||||
:hash (memory-object-hash obj)))
|
||||
:hash (memory-object-hash obj)
|
||||
:scope (memory-object-scope obj)))
|
||||
|
||||
(defun memory-merkle-hash (id type attributes content child-hashes)
|
||||
(let* ((alist (loop for (k v) on attributes by #'cddr collect (cons k v)))
|
||||
@@ -52,7 +53,7 @@
|
||||
(ironclad:update-digest digester (ironclad:ascii-string-to-byte-array data-string))
|
||||
(ironclad:byte-array-to-hex-string (ironclad:produce-digest digester))))
|
||||
|
||||
(defun ingest-ast (ast &optional parent-id)
|
||||
(defun ingest-ast (ast &key parent-id (scope :memex))
|
||||
(let* ((type (getf ast :type))
|
||||
(props (getf ast :properties))
|
||||
(id (or (getf props :ID) (format nil "temp-~a" (get-universal-time))))
|
||||
@@ -62,7 +63,7 @@
|
||||
(child-ids nil) (child-hashes nil))
|
||||
(dolist (child contents)
|
||||
(when (listp child)
|
||||
(let ((child-id (ingest-ast child id)))
|
||||
(let ((child-id (ingest-ast child :parent-id id :scope scope)))
|
||||
(push child-id child-ids)
|
||||
(let ((child-obj (gethash child-id *memory-store*)))
|
||||
(when child-obj (push (memory-object-hash child-obj) child-hashes))))))
|
||||
@@ -75,7 +76,7 @@
|
||||
:id id :type type :attributes props :content raw-content
|
||||
:parent-id parent-id :children child-ids
|
||||
:version (get-universal-time) :last-sync (get-universal-time)
|
||||
:hash hash))))
|
||||
:hash hash :scope scope))))
|
||||
(unless existing-obj (setf (gethash hash *memory-history*) obj))
|
||||
(setf (gethash id *memory-store*) obj)
|
||||
id)))
|
||||
|
||||
@@ -285,16 +285,16 @@ privacy tags, privacy text, shell safety, network exfil, high-impact approval."
|
||||
(let ((approved-nodes (list-objects-with-attribute :TODO "APPROVED"))
|
||||
(found-any nil))
|
||||
(dolist (node approved-nodes)
|
||||
(let* ((attrs (org-object-attributes node))
|
||||
(let* ((attrs (memory-object-attributes node))
|
||||
(tags (getf attrs :TAGS))
|
||||
(action-str (getf attrs :ACTION)))
|
||||
(when (and (member "FLIGHT_PLAN" tags :test #'string-equal) action-str)
|
||||
(log-message "BOUNCER: Found approved flight plan '~a'. Re-injecting..." (org-object-id node))
|
||||
(log-message "BOUNCER: Found approved flight plan '~a'. Re-injecting..." (memory-object-id node))
|
||||
(let ((action (ignore-errors (read-from-string action-str))))
|
||||
(when action
|
||||
(setf (getf action :approved) t)
|
||||
(inject-stimulus action)
|
||||
(setf (getf (org-object-attributes node) :TODO) "DONE")
|
||||
(setf (getf (memory-object-attributes node) :TODO) "DONE")
|
||||
(setq found-any t))))))
|
||||
found-any))
|
||||
|
||||
|
||||
118
lisp/system-context-manager.lisp
Normal file
118
lisp/system-context-manager.lisp
Normal file
@@ -0,0 +1,118 @@
|
||||
(defvar *context-stack* nil
|
||||
"Stack of context plists. Each plist has :project, :base-path, :scope.
|
||||
Top of stack (car) is the current context.")
|
||||
|
||||
(defvar *context-max-depth* 10
|
||||
"Maximum context stack depth. Prevents runaway pushes.")
|
||||
|
||||
(defun current-context ()
|
||||
"Returns the current context plist, or nil if no context is set."
|
||||
(car *context-stack*))
|
||||
|
||||
(defun current-scope ()
|
||||
"Returns the current scope keyword (:memex/:session/:project).
|
||||
Returns :memex when no context is set (defaults to global scope)."
|
||||
(or (getf (current-context) :scope) :memex))
|
||||
|
||||
(defun current-project ()
|
||||
"Returns the current project name, or nil."
|
||||
(getf (current-context) :project))
|
||||
|
||||
(defun current-base-path ()
|
||||
"Returns the current base path for file resolution, or nil."
|
||||
(getf (current-context) :base-path))
|
||||
|
||||
(defun context-stack-depth ()
|
||||
"Returns the current depth of the context stack."
|
||||
(length *context-stack*))
|
||||
|
||||
(defun push-context (&key project base-path (scope :project))
|
||||
"Pushes a new context onto the stack. When focused on a project:
|
||||
- File paths resolve relative to BASE-PATH
|
||||
- Memory queries filter by SCOPE
|
||||
- :memex scope objects remain visible (always global)
|
||||
Returns the new context plist."
|
||||
(when (>= (context-stack-depth) *context-max-depth*)
|
||||
(log-message "CONTEXT: Stack depth limit reached (~d), refusing push" *context-max-depth*)
|
||||
(return-from push-context (current-context)))
|
||||
(let* ((context (list :project project
|
||||
:base-path base-path
|
||||
:scope scope)))
|
||||
(push context *context-stack*)
|
||||
(log-message "CONTEXT: Pushed ~a (depth ~d)" project (context-stack-depth))
|
||||
context))
|
||||
|
||||
(defun pop-context ()
|
||||
"Pops the current context, restoring the previous one.
|
||||
Returns the restored context or nil if stack becomes empty."
|
||||
(if *context-stack*
|
||||
(let ((popped (pop *context-stack*)))
|
||||
(log-message "CONTEXT: Popped ~a (depth ~d)"
|
||||
(getf popped :project) (context-stack-depth))
|
||||
(current-context))
|
||||
(progn
|
||||
(log-message "CONTEXT: Cannot pop — stack is empty")
|
||||
nil)))
|
||||
|
||||
(defmacro with-context ((&key project base-path (scope :project)) &body body)
|
||||
"Executes BODY within a scoped context, then restores the previous context.
|
||||
Example:
|
||||
(with-context (:project \"passepartout\" :base-path \"/home/user/memex/projects/passepartout\")
|
||||
(context-scoped-query :tag \"bug\"))"
|
||||
`(let ((*context-stack* (cons (list :project ,project
|
||||
:base-path ,base-path
|
||||
:scope ,scope)
|
||||
*context-stack*)))
|
||||
,@body))
|
||||
|
||||
(defun resolve-path (path)
|
||||
"Resolves a file path relative to the current context.
|
||||
If PATH is absolute, returns it unchanged.
|
||||
If PATH is relative and a base-path is set, merges them.
|
||||
Otherwise returns PATH unchanged."
|
||||
(let ((base (current-base-path)))
|
||||
(if (and base path (not (uiop:absolute-pathname-p path)))
|
||||
(namestring (merge-pathnames path (uiop:ensure-directory-pathname base)))
|
||||
path)))
|
||||
|
||||
(defun context-scoped-query (&key tag todo-state type)
|
||||
"Like context-query but filtered to the current context's scope.
|
||||
:memex-scoped objects are always visible regardless of current scope."
|
||||
(context-query :tag tag :todo-state todo-state :type type :scope (current-scope)))
|
||||
|
||||
(defun project-objects ()
|
||||
"Returns all objects scoped to the current project.
|
||||
Includes :memex-scoped objects (global knowledge) plus :project-scoped
|
||||
objects matching the current project."
|
||||
(context-scoped-query))
|
||||
|
||||
(defun focus-project (name base-path)
|
||||
"Shortcut: focus on a project by name and base path.
|
||||
Calls push-context with :scope :project."
|
||||
(push-context :project name :base-path base-path :scope :project))
|
||||
|
||||
(defun focus-session ()
|
||||
"Shortcut: enter a session context (ephemeral scope).
|
||||
Objects created in this scope are visible only during the session."
|
||||
(push-context :project "session" :scope :session))
|
||||
|
||||
(defun focus-memex ()
|
||||
"Shortcut: return to global memex scope. Equivalent to pop-context
|
||||
until stack is empty or :memex context is reached."
|
||||
(loop while (and *context-stack*
|
||||
(not (eq (getf (current-context) :scope) :memex)))
|
||||
do (pop-context)))
|
||||
|
||||
(defun unfocus ()
|
||||
"Pop the top context and return to the previous one."
|
||||
(pop-context))
|
||||
|
||||
(defskill :passepartout-system-context-manager
|
||||
:priority 90
|
||||
:trigger (lambda (ctx) (declare (ignore ctx)) nil)
|
||||
:deterministic (lambda (action ctx)
|
||||
(declare (ignore action))
|
||||
(ignore-errors
|
||||
(when (> (context-stack-depth) 0)
|
||||
nil))
|
||||
nil))
|
||||
Reference in New Issue
Block a user