Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0545defdb | |||
| 1b83b18c39 |
+1
-4
@@ -23,14 +23,11 @@ fn ise_post(content: String) -> Void {
|
|||||||
let ise_url: String = env("SOUL_ISE_URL")
|
let ise_url: String = env("SOUL_ISE_URL")
|
||||||
let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url }
|
let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url }
|
||||||
if str_eq(engram_url, "") {
|
if str_eq(engram_url, "") {
|
||||||
let local_id: String = engram_node_full(
|
let discard: String = engram_node_full(
|
||||||
content, "InternalStateEvent", "state-event",
|
content, "InternalStateEvent", "state-event",
|
||||||
el_from_float(0.3), el_from_float(0.3), el_from_float(0.8),
|
el_from_float(0.3), el_from_float(0.3), el_from_float(0.8),
|
||||||
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
||||||
)
|
)
|
||||||
if str_eq(local_id, "") {
|
|
||||||
println("[awareness] ise_post: local engram_node_full failed — ISE lost")
|
|
||||||
}
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
// Proper JSON string escaping: backslashes first, then quotes, then control chars.
|
// Proper JSON string escaping: backslashes first, then quotes, then control chars.
|
||||||
|
|||||||
@@ -130,14 +130,11 @@ fn conv_history_persist(hist: String) -> Void {
|
|||||||
if str_eq(hist, "[]") { return "" }
|
if str_eq(hist, "[]") { return "" }
|
||||||
let ts: Int = time_now()
|
let ts: Int = time_now()
|
||||||
let tags: String = "[\"conv-history\",\"persistent\"]"
|
let tags: String = "[\"conv-history\",\"persistent\"]"
|
||||||
let node_id: String = engram_node_full(
|
let discard: String = engram_node_full(
|
||||||
hist, "Conversation", "conv:history",
|
hist, "Conversation", "conv:history",
|
||||||
el_from_float(0.7), el_from_float(0.8), el_from_float(0.9),
|
el_from_float(0.7), el_from_float(0.8), el_from_float(0.9),
|
||||||
"Episodic", tags
|
"Episodic", tags
|
||||||
)
|
)
|
||||||
if str_eq(node_id, "") {
|
|
||||||
println("[chat] conv_history_persist: engram_node_full returned empty — history node lost")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// conv_history_load — restore conversation history from engram on first access.
|
// conv_history_load — restore conversation history from engram on first access.
|
||||||
@@ -634,6 +631,17 @@ fn handle_chat_agentic(body: String) -> String {
|
|||||||
return "{\"error\":\"message required\",\"reply\":\"\"}"
|
return "{\"error\":\"message required\",\"reply\":\"\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workspace scope (#23): the desktop UI sends the user-chosen Agent Workspace root
|
||||||
|
// on every agentic request. Persist it to state so agent_workspace_root() — and the
|
||||||
|
// path/command tool guards that read it — confine this turn's file/command tools to
|
||||||
|
// that subtree. The UI is the source of truth per request: empty means unscoped (the
|
||||||
|
// backward-compatible default), and it also lets agent_workspace_root() fall through
|
||||||
|
// to the NEURON_AGENT_ROOT env when no root is sent. FLAGGED FOR REVIEW: setting
|
||||||
|
// state from the body each turn (vs. only-when-nonempty) so clearing the folder in
|
||||||
|
// the UI un-scopes — confirm this is the intended ownership model.
|
||||||
|
let ws_root: String = json_get(body, "agent_workspace_root")
|
||||||
|
state_set("agent_workspace_root", ws_root)
|
||||||
|
|
||||||
let req_model: String = json_get(body, "model")
|
let req_model: String = json_get(body, "model")
|
||||||
let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }
|
let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }
|
||||||
|
|
||||||
@@ -649,7 +657,7 @@ fn handle_chat_agentic(body: String) -> String {
|
|||||||
let session_valid: Bool = if str_eq(req_session, "") {
|
let session_valid: Bool = if str_eq(req_session, "") {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
!str_contains(session_get(req_session), "\"error\"")
|
session_exists(req_session)
|
||||||
}
|
}
|
||||||
if !session_valid {
|
if !session_valid {
|
||||||
return "{\"error\":\"session not found\",\"session_id\":\"" + req_session + "\",\"reply\":\"\"}"
|
return "{\"error\":\"session not found\",\"session_id\":\"" + req_session + "\",\"reply\":\"\"}"
|
||||||
@@ -1072,19 +1080,13 @@ fn handle_dharma_room_turn(body: String) -> String {
|
|||||||
// engram_node(content, "episodic", ...) which wrongly put a TIER into the node_type
|
// engram_node(content, "episodic", ...) which wrongly put a TIER into the node_type
|
||||||
// slot — that's why nodes showed node_type="episodic". Use the full, correct contract.)
|
// slot — that's why nodes showed node_type="episodic". Use the full, correct contract.)
|
||||||
let utterance_tags: String = "[\"soul-utterance\",\"episodic\"]"
|
let utterance_tags: String = "[\"soul-utterance\",\"episodic\"]"
|
||||||
let utterance_id: String = engram_node_full(
|
let discard_id: String = engram_node_full(
|
||||||
clean_response, "Conversation", "soul:utterance",
|
clean_response, "Conversation", "soul:utterance",
|
||||||
el_from_float(0.6), el_from_float(0.6), el_from_float(0.8),
|
el_from_float(0.6), el_from_float(0.6), el_from_float(0.8),
|
||||||
"Episodic", utterance_tags
|
"Episodic", utterance_tags
|
||||||
)
|
)
|
||||||
if str_eq(utterance_id, "") {
|
|
||||||
println("[chat] handle_dharma_room_turn: utterance engram write failed — node lost")
|
|
||||||
}
|
|
||||||
if !str_eq(snap_path, "") {
|
if !str_eq(snap_path, "") {
|
||||||
let save_result: String = engram_save(snap_path)
|
let discard_save: String = engram_save(snap_path)
|
||||||
if str_eq(save_result, "") {
|
|
||||||
println("[chat] handle_dharma_room_turn: engram_save failed for " + snap_path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let safe_response: String = json_safe(clean_response)
|
let safe_response: String = json_safe(clean_response)
|
||||||
@@ -1166,7 +1168,7 @@ fn auto_persist(req: String, resp: String) -> Void {
|
|||||||
+ ",\"label\":\"chat:" + ts_str + "\"}"
|
+ ",\"label\":\"chat:" + ts_str + "\"}"
|
||||||
|
|
||||||
let tags: String = "[\"Conversation\",\"chat\",\"timestamped\"]"
|
let tags: String = "[\"Conversation\",\"chat\",\"timestamped\"]"
|
||||||
let persist_id: String = engram_node_full(
|
engram_node_full(
|
||||||
content,
|
content,
|
||||||
"Conversation",
|
"Conversation",
|
||||||
"chat:" + ts_str,
|
"chat:" + ts_str,
|
||||||
@@ -1176,9 +1178,6 @@ fn auto_persist(req: String, resp: String) -> Void {
|
|||||||
"Episodic",
|
"Episodic",
|
||||||
tags
|
tags
|
||||||
)
|
)
|
||||||
if str_eq(persist_id, "") {
|
|
||||||
println("[chat] auto_persist: engram_node_full returned empty — conversation node lost (ts=" + ts_str + ")")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// strengthen_chat_nodes — strengthen the engram nodes that were activated during a chat.
|
// strengthen_chat_nodes — strengthen the engram nodes that were activated during a chat.
|
||||||
|
|||||||
@@ -46,10 +46,7 @@ fn mem_consolidate() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mem_save(path: String) -> Void {
|
fn mem_save(path: String) -> Void {
|
||||||
let save_result: String = engram_save(path)
|
engram_save(path)
|
||||||
if str_eq(save_result, "") {
|
|
||||||
println("[memory] mem_save: engram_save failed for " + path + " — snapshot may be incomplete")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mem_load(path: String) -> Void {
|
fn mem_load(path: String) -> Void {
|
||||||
@@ -79,14 +76,11 @@ fn mem_boot_count_inc() -> Int {
|
|||||||
let next: Int = current + 1
|
let next: Int = current + 1
|
||||||
let content: String = "soul:boot_count:" + int_to_str(next)
|
let content: String = "soul:boot_count:" + int_to_str(next)
|
||||||
let tags: String = "[\"soul-meta\",\"boot-counter\"]"
|
let tags: String = "[\"soul-meta\",\"boot-counter\"]"
|
||||||
let boot_node_id: String = engram_node_full(
|
let discard: String = engram_node_full(
|
||||||
content, "Memory", "soul:boot_count",
|
content, "Memory", "soul:boot_count",
|
||||||
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
||||||
"Canonical", tags
|
"Canonical", tags
|
||||||
)
|
)
|
||||||
if str_eq(boot_node_id, "") {
|
|
||||||
println("[memory] mem_boot_count_inc: engram write failed — boot counter node lost (count=" + int_to_str(next) + ")")
|
|
||||||
}
|
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-10
@@ -400,7 +400,6 @@ fn handle_api_log_state_event(body: String) -> String {
|
|||||||
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
|
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
|
||||||
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
||||||
"Episodic", tags)
|
"Episodic", tags)
|
||||||
if !api_persisted(id) { return api_not_persisted(id) }
|
|
||||||
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
|
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +452,6 @@ fn handle_api_tune_config(body: String) -> String {
|
|||||||
let id: String = engram_node_full(content, "ConfigEntry", key,
|
let id: String = engram_node_full(content, "ConfigEntry", key,
|
||||||
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
||||||
"Canonical", tags)
|
"Canonical", tags)
|
||||||
if !api_persisted(id) { return api_not_persisted(id) }
|
|
||||||
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
|
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,23 +651,17 @@ fn handle_api_consolidate(body: String) -> String {
|
|||||||
let summary: String = json_get(body, "summary")
|
let summary: String = json_get(body, "summary")
|
||||||
let snap: String = state_get("soul_snapshot_path")
|
let snap: String = state_get("soul_snapshot_path")
|
||||||
if !str_eq(snap, "") {
|
if !str_eq(snap, "") {
|
||||||
let save_result: String = engram_save(snap)
|
engram_save(snap)
|
||||||
if str_eq(save_result, "") {
|
|
||||||
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !str_eq(summary, "") {
|
if !str_eq(summary, "") {
|
||||||
let safe_summary: String = str_replace(summary, "\"", "'")
|
let safe_summary: String = str_replace(summary, "\"", "'")
|
||||||
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
||||||
let summary_id: String = engram_node_full(
|
let discard: String = engram_node_full(
|
||||||
"[session-summary] " + safe_summary,
|
"[session-summary] " + safe_summary,
|
||||||
"SessionSummary", "session:summary",
|
"SessionSummary", "session:summary",
|
||||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
||||||
"Episodic", tags
|
"Episodic", tags
|
||||||
)
|
)
|
||||||
if str_eq(summary_id, "") {
|
|
||||||
println("[api] consolidate: session summary engram write failed — summary node lost")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
||||||
}
|
}
|
||||||
|
|||||||
+97
-1
@@ -36,7 +36,49 @@ fn session_make_content(id: String, title: String, created_at: Int, updated_at:
|
|||||||
+ ",\"updated_at\":" + int_to_str(updated_at) + "}"
|
+ ",\"updated_at\":" + int_to_str(updated_at) + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// session_exists — return true if the given session_id is known in Engram or state.
|
||||||
|
// Used by chat.el to validate a session_id before processing a chat message.
|
||||||
|
// Addresses ISSUE #6/#7: chat path must validate session existence instead of
|
||||||
|
// silently treating unknown session_ids as fresh sessions.
|
||||||
|
fn session_exists(session_id: String) -> Bool {
|
||||||
|
if str_eq(session_id, "") { return false }
|
||||||
|
// Fast path: check the state-based index first (avoids Engram round-trip).
|
||||||
|
let idx: String = state_get("session_index")
|
||||||
|
if !str_eq(idx, "") && !str_eq(idx, "[]") {
|
||||||
|
if str_contains(idx, "\"id\":\"" + session_id + "\"") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Slow path: check Engram directly (survives restarts when index is cold).
|
||||||
|
let results: String = engram_search_json("session:meta " + session_id, 5)
|
||||||
|
if str_eq(results, "") { return false }
|
||||||
|
if str_eq(results, "[]") { return false }
|
||||||
|
let total: Int = json_array_len(results)
|
||||||
|
let found: Bool = false
|
||||||
|
let i: Int = 0
|
||||||
|
while i < total {
|
||||||
|
let node: String = json_array_get(results, i)
|
||||||
|
let label: String = json_get(node, "label")
|
||||||
|
let content: String = json_get(node, "content")
|
||||||
|
let sid: String = json_get(content, "id")
|
||||||
|
let is_match: Bool = str_eq(label, "session:meta") && str_eq(sid, session_id)
|
||||||
|
let found = if is_match { true } else { found }
|
||||||
|
let i = i + 1
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
// session_create — create a new session, return {id, title, created_at}.
|
// session_create — create a new session, return {id, title, created_at}.
|
||||||
|
//
|
||||||
|
// ISSUE #1: Ghost sessions on failed first message.
|
||||||
|
// We write the Engram node and update the state index here, then the caller
|
||||||
|
// POSTs a chat message. If that chat call fails (LLM unavailable, network
|
||||||
|
// error, etc.) the session is stranded with no messages. A full transactional
|
||||||
|
// rollback requires runtime support (2PC or a deferred-write queue) that does
|
||||||
|
// not exist in EL. Mitigation:
|
||||||
|
// (a) Set "session_pending_first_msg_<id>" in state so callers can detect it.
|
||||||
|
// (b) Provide session_create_cleanup() for callers that detect a failure.
|
||||||
|
// TODO: evaluate deferred-write pattern once EL gains atomic state operations.
|
||||||
fn session_create(body: String) -> String {
|
fn session_create(body: String) -> String {
|
||||||
let ts: Int = time_now()
|
let ts: Int = time_now()
|
||||||
let id: String = uuid_v4()
|
let id: String = uuid_v4()
|
||||||
@@ -55,8 +97,13 @@ fn session_create(body: String) -> String {
|
|||||||
}
|
}
|
||||||
// Store the engram node_id mapping so we can look up the node for this session
|
// Store the engram node_id mapping so we can look up the node for this session
|
||||||
state_set("session_node_" + id, node_id)
|
state_set("session_node_" + id, node_id)
|
||||||
|
// Mark as pending first message so stale ghost sessions can be identified
|
||||||
|
// (e.g. if the caller\'s subsequent chat POST fails).
|
||||||
|
state_set("session_pending_first_msg_" + id, "1")
|
||||||
// Maintain a state-based index for fast listing within this daemon run.
|
// Maintain a state-based index for fast listing within this daemon run.
|
||||||
// Newest sessions first (prepend).
|
// Newest sessions first (prepend).
|
||||||
|
// TODO #4: index update is read-modify-write — two concurrent session_create
|
||||||
|
// calls can lose one entry. EL has no CAS primitive; fix requires runtime support.
|
||||||
let existing_idx: String = state_get("session_index")
|
let existing_idx: String = state_get("session_index")
|
||||||
let idx_entry: String = "{\"id\":\"" + id + "\",\"title\":\"" + json_safe(title) + "\",\"folder\":\"" + json_safe(folder) + "\",\"created_at\":" + int_to_str(ts) + ",\"updated_at\":" + int_to_str(ts) + ",\"last_message\":\"\"}"
|
let idx_entry: String = "{\"id\":\"" + id + "\",\"title\":\"" + json_safe(title) + "\",\"folder\":\"" + json_safe(folder) + "\",\"created_at\":" + int_to_str(ts) + ",\"updated_at\":" + int_to_str(ts) + ",\"last_message\":\"\"}"
|
||||||
let new_idx: String = if str_eq(existing_idx, "") {
|
let new_idx: String = if str_eq(existing_idx, "") {
|
||||||
@@ -73,6 +120,20 @@ fn session_create(body: String) -> String {
|
|||||||
+ ",\"created_at\":" + int_to_str(ts) + "}"
|
+ ",\"created_at\":" + int_to_str(ts) + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// session_create_cleanup — undo a session_create when the caller\'s first chat
|
||||||
|
// fails. Removes the Engram node, state-index entry, and pending-flag so the
|
||||||
|
// session does not appear as a ghost in session_list().
|
||||||
|
// Addresses ISSUE #1: cleanup path for ghost sessions.
|
||||||
|
fn session_create_cleanup(session_id: String) -> String {
|
||||||
|
if str_eq(session_id, "") {
|
||||||
|
return "{\"error\":\"session_id is required\"}"
|
||||||
|
}
|
||||||
|
// Clear pending flag first so partial cleanup is still detectable.
|
||||||
|
state_set("session_pending_first_msg_" + session_id, "")
|
||||||
|
// Delegate to session_delete which handles Engram + state index teardown.
|
||||||
|
return session_delete(session_id)
|
||||||
|
}
|
||||||
|
|
||||||
// session_list — list all sessions. Returns [{id, title, last_message, created_at, updated_at}].
|
// session_list — list all sessions. Returns [{id, title, last_message, created_at, updated_at}].
|
||||||
fn session_list() -> String {
|
fn session_list() -> String {
|
||||||
// Fast path: state-based index (rebuilt from session_create calls in this daemon run).
|
// Fast path: state-based index (rebuilt from session_create calls in this daemon run).
|
||||||
@@ -222,13 +283,27 @@ fn session_delete(session_id: String) -> String {
|
|||||||
state_set("session_hist_" + session_id, "")
|
state_set("session_hist_" + session_id, "")
|
||||||
state_set("session_node_" + session_id, "")
|
state_set("session_node_" + session_id, "")
|
||||||
state_set("session_index", "")
|
state_set("session_index", "")
|
||||||
|
// ISSUE #5: clean up bridge blobs and always_allow keys that were never
|
||||||
|
// cleared by agentic_resume (e.g. client abandoned a pending tool call).
|
||||||
|
// Without this, stranded bridge blobs accumulate indefinitely in state.
|
||||||
|
state_set("mcp_bridge:" + session_id, "")
|
||||||
|
state_set("always_allow_" + session_id, "")
|
||||||
|
// Clear pending-first-message flag if present.
|
||||||
|
state_set("session_pending_first_msg_" + session_id, "")
|
||||||
return "{\"ok\":true,\"session_id\":\"" + session_id + "\""
|
return "{\"ok\":true,\"session_id\":\"" + session_id + "\""
|
||||||
+ ",\"deleted_meta\":" + int_to_str(deleted_meta)
|
+ ",\"deleted_meta\":" + int_to_str(deleted_meta)
|
||||||
+ ",\"deleted_msgs\":" + int_to_str(deleted_msgs) + "}"
|
+ ",\"deleted_msgs\":" + int_to_str(deleted_msgs) + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// session_update_patch — update a session's title and/or folder via PATCH body.
|
// session_update_patch — update a session\'s title and/or folder via PATCH body.
|
||||||
// Body may contain "title", "folder", or both. Preserves unmentioned fields.
|
// Body may contain "title", "folder", or both. Preserves unmentioned fields.
|
||||||
|
//
|
||||||
|
// ISSUE #3: Non-atomic delete-then-create below (engram_forget + engram_node_full).
|
||||||
|
// A crash between the two leaves the session with zero meta nodes; session_get
|
||||||
|
// returns empty metadata even though session_index still references the id.
|
||||||
|
// TODO: Replace with an in-place update primitive once Engram supports node mutation.
|
||||||
|
// Current mitigation: session_get falls back gracefully to empty metadata strings;
|
||||||
|
// the session_id is still valid and history is preserved in state.
|
||||||
fn session_update_patch(session_id: String, body: String) -> String {
|
fn session_update_patch(session_id: String, body: String) -> String {
|
||||||
if str_eq(session_id, "") {
|
if str_eq(session_id, "") {
|
||||||
return "{\"error\":\"session_id is required\"}"
|
return "{\"error\":\"session_id is required\"}"
|
||||||
@@ -349,6 +424,9 @@ fn session_hist_load(session_id: String) -> String {
|
|||||||
// session_hist_save — persist message history for a session to state and engram.
|
// session_hist_save — persist message history for a session to state and engram.
|
||||||
fn session_hist_save(session_id: String, hist: String) -> Void {
|
fn session_hist_save(session_id: String, hist: String) -> Void {
|
||||||
state_set("session_hist_" + session_id, hist)
|
state_set("session_hist_" + session_id, hist)
|
||||||
|
// Clear pending-first-message flag: once history is saved, the session
|
||||||
|
// is no longer in the ghost/pending state (ISSUE #1 mitigation).
|
||||||
|
state_set("session_pending_first_msg_" + session_id, "")
|
||||||
// Delete old history node and write fresh one
|
// Delete old history node and write fresh one
|
||||||
let old_results: String = engram_search_json("session:messages:" + session_id, 3)
|
let old_results: String = engram_search_json("session:messages:" + session_id, 3)
|
||||||
let o_total: Int = if str_eq(old_results, "") { 0 } else { json_array_len(old_results) }
|
let o_total: Int = if str_eq(old_results, "") { 0 } else { json_array_len(old_results) }
|
||||||
@@ -371,6 +449,16 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// session_update_meta_timestamp — update the updated_at field in the session:meta node.
|
// session_update_meta_timestamp — update the updated_at field in the session:meta node.
|
||||||
|
//
|
||||||
|
// ISSUE #2: No TTL / idle expiry mechanism. Sessions accumulate indefinitely.
|
||||||
|
// A sweep job (e.g. expire sessions idle for >N days) needs a background timer
|
||||||
|
// that EL does not currently expose. Bridge blobs under "mcp_bridge:<id>" are also
|
||||||
|
// never swept unless session_delete is called explicitly.
|
||||||
|
// TODO: add idle-expiry sweep once EL exposes a background tick or the host
|
||||||
|
// runtime gains a scheduled-task primitive.
|
||||||
|
//
|
||||||
|
// ISSUE #3 applies here too: delete-then-create is non-atomic. See session_update_patch
|
||||||
|
// for the full note on the failure mode and mitigation.
|
||||||
fn session_update_meta_timestamp(session_id: String) -> Void {
|
fn session_update_meta_timestamp(session_id: String) -> Void {
|
||||||
let results: String = engram_search_json("session:meta " + session_id, 10)
|
let results: String = engram_search_json("session:meta " + session_id, 10)
|
||||||
let total: Int = if str_eq(results, "") { 0 } else { json_array_len(results) }
|
let total: Int = if str_eq(results, "") { 0 } else { json_array_len(results) }
|
||||||
@@ -464,6 +552,14 @@ fn session_auto_title(session_id: String, first_message: String) -> Void {
|
|||||||
// action: "allow" | "deny" | "always"
|
// action: "allow" | "deny" | "always"
|
||||||
// Resumes the agentic loop from where it was paused.
|
// Resumes the agentic loop from where it was paused.
|
||||||
//
|
//
|
||||||
|
// ISSUE #8: Reconnect/duplicate resume race. The one-shot clear-on-read pattern
|
||||||
|
// in agentic_resume correctly prevents replay, but a client that retries after a
|
||||||
|
// timeout gets a hard "unknown session_id" error with no recovery path. The
|
||||||
|
// conversation is permanently stuck in that case. Full idempotency (e.g. caching
|
||||||
|
// the last reply keyed by call_id) requires a new state structure.
|
||||||
|
// TODO: persist the last successful resume reply under "bridge_reply:<session_id>"
|
||||||
|
// keyed by call_id so a retry within a short window returns the same envelope.
|
||||||
|
//
|
||||||
// Modern path (agentic_loop / bridge): the loop saves its suspension to
|
// Modern path (agentic_loop / bridge): the loop saves its suspension to
|
||||||
// "mcp_bridge:<session_id>" via bridge_save(). On approval we dispatch_tool()
|
// "mcp_bridge:<session_id>" via bridge_save(). On approval we dispatch_tool()
|
||||||
// if allowed (or build a denial string), then hand the result to agentic_resume()
|
// if allowed (or build a denial string), then hand the result to agentic_resume()
|
||||||
|
|||||||
@@ -212,13 +212,8 @@ fn seed_persona_from_env() -> Void {
|
|||||||
let h: Map = {}
|
let h: Map = {}
|
||||||
map_set(h, "Content-Type", "application/json")
|
map_set(h, "Content-Type", "application/json")
|
||||||
let resp: String = http_post_with_headers(engram_url + "/api/nodes", body, h)
|
let resp: String = http_post_with_headers(engram_url + "/api/nodes", body, h)
|
||||||
// Check for empty response (timeout/network error), explicit error, or missing id.
|
if str_contains(resp, "\"error\"") {
|
||||||
if str_eq(resp, "") {
|
|
||||||
println("[soul] persona HTTP write-back failed: empty response (timeout or network error) — in-memory only this session")
|
|
||||||
} else if str_contains(resp, "\"error\"") {
|
|
||||||
println("[soul] persona HTTP write-back failed (in-memory only this session): " + resp)
|
println("[soul] persona HTTP write-back failed (in-memory only this session): " + resp)
|
||||||
} else if !str_contains(resp, "\"id\"") {
|
|
||||||
println("[soul] persona HTTP write-back: unexpected response (no id field) — in-memory only this session: " + resp)
|
|
||||||
} else {
|
} else {
|
||||||
println("[soul] persona persisted to HTTP engram at " + engram_url)
|
println("[soul] persona persisted to HTTP engram at " + engram_url)
|
||||||
}
|
}
|
||||||
@@ -251,14 +246,11 @@ fn emit_session_start_event() -> Void {
|
|||||||
+ ",\"ts\":" + int_to_str(ts) + "}"
|
+ ",\"ts\":" + int_to_str(ts) + "}"
|
||||||
|
|
||||||
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
|
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
|
||||||
let session_event_id: String = engram_node_full(
|
let discard: String = engram_node_full(
|
||||||
payload, "InternalStateEvent", "session-start",
|
payload, "InternalStateEvent", "session-start",
|
||||||
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
||||||
"Episodic", tags
|
"Episodic", tags
|
||||||
)
|
)
|
||||||
if str_eq(session_event_id, "") {
|
|
||||||
println("[soul] emit_session_start_event: engram write failed — session-start event lost")
|
|
||||||
}
|
|
||||||
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + ")")
|
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user