nexo-brain 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +11 -5
  2. package/package.json +1 -1
  3. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  4. package/src/__pycache__/cognitive.cpython-314.pyc +0 -0
  5. package/src/__pycache__/db.cpython-314.pyc +0 -0
  6. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  7. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  8. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  9. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  10. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  11. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  12. package/src/__pycache__/server.cpython-314.pyc +0 -0
  13. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  14. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  15. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  16. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  17. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  18. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  19. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  20. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  21. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  22. package/src/cognitive.py +63 -14
  23. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  24. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  25. package/src/dashboard/app.py +1 -1
  26. package/src/db.py +29 -13
  27. package/src/evolution_cycle.py +72 -94
  28. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  29. package/src/hooks/session-start.sh +5 -2
  30. package/src/hooks/session-stop.sh +79 -133
  31. package/src/knowledge_graph.py +3 -3
  32. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  33. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  34. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  35. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  36. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  37. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  38. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  39. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  40. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  41. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  42. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  43. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  44. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  45. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  46. package/src/plugins/artifact_registry.py +450 -0
  47. package/src/plugins/cognitive_memory.py +9 -9
  48. package/src/plugins/episodic_memory.py +8 -8
  49. package/src/plugins/evolution.py +4 -4
  50. package/src/plugins/guard.py +26 -2
  51. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  52. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  53. package/src/rules/core-rules 2.json +329 -0
  54. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  55. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  56. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  57. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  58. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  59. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  60. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  61. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  62. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  63. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  64. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  65. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  66. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  67. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  68. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  69. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  70. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  71. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  72. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  73. package/src/scripts/check-context.py +257 -0
  74. package/src/scripts/nexo-brain-activation.sh +140 -0
  75. package/src/scripts/nexo-catchup.py +59 -5
  76. package/src/scripts/nexo-daily-self-audit.py +168 -183
  77. package/src/scripts/nexo-evolution-run.py +23 -31
  78. package/src/scripts/nexo-followup-hygiene.py +107 -0
  79. package/src/scripts/nexo-immune.py +108 -91
  80. package/src/scripts/nexo-learning-validator.py +226 -0
  81. package/src/scripts/nexo-postmortem-consolidator.py +230 -414
  82. package/src/scripts/nexo-pre-commit.py +118 -0
  83. package/src/scripts/nexo-proactive-dashboard.py +342 -0
  84. package/src/scripts/nexo-runtime-preflight.py +270 -0
  85. package/src/scripts/nexo-send-email.py +25 -0
  86. package/src/scripts/nexo-send-reply.py +176 -0
  87. package/src/scripts/nexo-sleep.py +283 -527
  88. package/src/scripts/nexo-snapshot-restore.sh +34 -0
  89. package/src/scripts/nexo-synthesis.py +141 -432
  90. package/src/scripts/nexo-watchdog-smoke.py +114 -0
  91. package/src/server.py +5 -5
  92. package/src/tools_coordination.py +3 -3
  93. package/src/tools_learnings.py +1 -1
  94. package/src/tools_menu.py +31 -49
  95. package/src/tools_reminders_crud.py +1 -1
  96. package/src/tools_sessions.py +12 -12
@@ -1,27 +1,22 @@
1
1
  #!/bin/bash
2
- # NEXO Stop hook (v7 — BLOCKING post-mortem with trivial session detection)
2
+ # NEXO Memory Stop Hook (v7 — BLOCKING post-mortem with trivial session detection)
3
3
  #
4
4
  # v5 bug: used "approve" + systemMessage — AI never processed post-mortem.
5
- # v6 bug: used "block" but deleted flag on approve caused infinite block loop.
6
- # Also had TTL on flag that expired between close attempts.
7
- # v7 fix: trivial sessions (<5 tool calls) approve immediately.
8
- # Non-trivial sessions block until post-mortem is done.
9
- # Flag has NO TTL and is NOT deleted on approve.
10
- # SessionStart hook cleans up the flag for the next session.
5
+ # v6 fix: uses "block" but blocked ALL sessions including trivial ones.
6
+ # v7 fix: detects trivial sessions (<5 tool calls) and approves immediately.
7
+ # Non-trivial sessions get blocked until post-mortem is done.
11
8
  #
12
9
  # Flow:
13
- # Trivial session (quick question, <5 meaningful tool calls):
10
+ # Trivial session (quick question, <5 tool calls):
14
11
  # → APPROVE immediately, no post-mortem needed
15
12
  #
16
13
  # Non-trivial session:
17
14
  # 1. User closes → hook checks flag → not found → BLOCK
18
15
  # 2. AI executes post-mortem → creates flag
19
16
  # 3. User closes again → hook sees flag → APPROVE
20
- # 4. Next session start → SessionStart hook deletes flag
21
17
  set -euo pipefail
22
18
 
23
- NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
24
- NEXO_NAME="${NEXO_NAME:-NEXO}"
19
+ NEXO_HOME="${NEXO_HOME:-$HOME/claude}"
25
20
  FLAG_FILE="$NEXO_HOME/operations/.postmortem-complete"
26
21
  TODAY=$(date +%Y-%m-%d)
27
22
  TOOL_LOG="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
@@ -29,43 +24,58 @@ TOOL_LOG="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
29
24
  # 0. Refresh diary draft with latest changes/decisions (best-effort)
30
25
  python3 -c "
31
26
  import sys, json, os
32
- sys.path.insert(0, os.environ.get('NEXO_HOME', os.path.expanduser('~/.nexo')))
27
+ sys.path.insert(0, os.path.expanduser('~/.nexo/nexo-mcp'))
33
28
  os.environ['NEXO_SKIP_FS_INDEX'] = '1'
34
- try:
35
- from db import init_db, get_db, get_active_sessions, upsert_diary_draft, get_diary_draft
36
- init_db()
37
- conn = get_db()
38
- sessions = get_active_sessions()
39
- for s in sessions:
40
- sid = s['sid']
41
- draft = get_diary_draft(sid)
42
- if not draft:
43
- continue
44
- change_ids = [r[0] for r in conn.execute('SELECT id FROM change_log WHERE session_id = ?', (sid,)).fetchall()]
45
- decision_ids = [r[0] for r in conn.execute('SELECT id FROM decisions WHERE session_id = ?', (sid,)).fetchall()]
46
- upsert_diary_draft(
47
- sid=sid,
48
- tasks_seen=draft['tasks_seen'],
49
- change_ids=json.dumps(change_ids),
50
- decision_ids=json.dumps(decision_ids),
51
- last_context_hint=draft['last_context_hint'],
52
- heartbeat_count=draft['heartbeat_count'],
53
- summary_draft=draft['summary_draft'],
54
- )
55
- except Exception:
56
- pass
29
+ from db import init_db, get_db, get_active_sessions, upsert_diary_draft, get_diary_draft
30
+ init_db()
31
+ conn = get_db()
32
+ sessions = get_active_sessions()
33
+ for s in sessions:
34
+ sid = s['sid']
35
+ draft = get_diary_draft(sid)
36
+ if not draft:
37
+ continue
38
+ change_ids = [r[0] for r in conn.execute('SELECT id FROM change_log WHERE session_id = ?', (sid,)).fetchall()]
39
+ decision_ids = [r[0] for r in conn.execute('SELECT id FROM decisions WHERE session_id = ?', (sid,)).fetchall()]
40
+ upsert_diary_draft(
41
+ sid=sid,
42
+ tasks_seen=draft['tasks_seen'],
43
+ change_ids=json.dumps(change_ids),
44
+ decision_ids=json.dumps(decision_ids),
45
+ last_context_hint=draft['last_context_hint'],
46
+ heartbeat_count=draft['heartbeat_count'],
47
+ summary_draft=draft['summary_draft'],
48
+ )
57
49
  " 2>/dev/null || true
58
50
 
59
- # 1. Detect trivial session — count meaningful tool calls from today's log
60
- # A session with <5 tool calls (excluding Read/Grep/Glob/Bash/ToolSearch) is trivial
51
+ # 1. Detect trivial session — count meaningful tool calls from THIS SESSION only
52
+ # Uses .session-start-ts written by SessionStart hook
53
+ # A session with <5 tool calls (excluding Read/Grep/Glob/Bash) is trivial
54
+ SESSION_START_TS="$NEXO_HOME/operations/.session-start-ts"
55
+ SESSION_START=0
56
+ if [ -f "$SESSION_START_TS" ]; then
57
+ SESSION_START=$(cat "$SESSION_START_TS" 2>/dev/null || echo "0")
58
+ fi
59
+
61
60
  TOOL_COUNT=0
62
61
  if [ -f "$TOOL_LOG" ]; then
63
62
  TOOL_COUNT=$(python3 -c "
64
- import json, sys
63
+ import json, sys, os
64
+ session_start = float(os.environ.get('SESSION_START', '0'))
65
65
  count = 0
66
66
  for line in open('$TOOL_LOG'):
67
67
  try:
68
68
  d = json.loads(line)
69
+ # Only count tools from THIS session (after session-start-ts)
70
+ ts = d.get('timestamp', '')
71
+ if ts and session_start > 0:
72
+ from datetime import datetime
73
+ try:
74
+ entry_ts = datetime.fromisoformat(ts.replace('Z', '+00:00')).timestamp()
75
+ if entry_ts < session_start:
76
+ continue
77
+ except:
78
+ pass
69
79
  t = d.get('tool_name', '')
70
80
  if t and t not in ('Read', 'Grep', 'Glob', 'Bash', 'ToolSearch'):
71
81
  count += 1
@@ -75,13 +85,8 @@ print(count)
75
85
  " 2>/dev/null || echo "0")
76
86
  fi
77
87
 
78
- # Trivial session → approve immediately, write minimal buffer, skip post-mortem
88
+ # Trivial session → approve immediately, no buffer writing, skip post-mortem
79
89
  if [ "$TOOL_COUNT" -lt 5 ]; then
80
- BUFFER="$NEXO_HOME/brain/session_buffer.jsonl"
81
- TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S")
82
- mkdir -p "$(dirname "$BUFFER")"
83
- echo "{\"ts\":\"$TIMESTAMP\",\"tasks\":[\"trivial session\"],\"decisions\":[],\"user_patterns\":[],\"files_modified\":[],\"errors_resolved\":[],\"self_critique\":\"trivial session — no post-mortem needed\",\"mood\":\"neutral\",\"source\":\"hook-trivial\"}" >> "$BUFFER" 2>/dev/null
84
-
85
90
  cat << 'HOOKEOF'
86
91
  {
87
92
  "decision": "approve"
@@ -92,8 +97,6 @@ fi
92
97
 
93
98
  # 2. Non-trivial session — check if post-mortem was already completed
94
99
  # Flag has NO TTL — it persists until SessionStart cleans it up next session.
95
- # IMPORTANT: do NOT delete flag here — that causes an infinite block loop
96
- # if the session doesn't close immediately after approve.
97
100
  POSTMORTEM_DONE=false
98
101
  if [ -f "$FLAG_FILE" ]; then
99
102
  POSTMORTEM_DONE=true
@@ -101,6 +104,7 @@ fi
101
104
 
102
105
  if [ "$POSTMORTEM_DONE" = true ]; then
103
106
  # Post-mortem was done — allow session to close
107
+ # NOTE: do NOT delete flag here — SessionStart cleans it up next session
104
108
  cat << 'HOOKEOF'
105
109
  {
106
110
  "decision": "approve"
@@ -108,99 +112,41 @@ if [ "$POSTMORTEM_DONE" = true ]; then
108
112
  HOOKEOF
109
113
  else
110
114
  # Post-mortem NOT done — BLOCK session close and inject instructions
111
- cat << HOOKEOF
115
+ cat << 'HOOKEOF'
112
116
  {
113
117
  "decision": "block",
114
- "reason": "STOP HOOK — MANDATORY POST-MORTEM before ending (do NOT ask permission, do NOT skip):\n\n## 1. SELF-CRITIQUE (MANDATORY — write to session diary)\nAnswer these questions in the self_critique field of nexo_session_diary_write:\n- Did the user have to ask me for something I should have detected or done on my own?\n- Did I wait for the user to tell me something I could have verified proactively?\n- Are there systems/states I can check next session without being asked?\n- Did I repeat an error that already had a registered learning?\n- What would I do differently if I repeated this session?\nIf any answer is YES — write the specific rule that would prevent repetition.\nIf the session was flawless, write 'No self-critique — clean session.'\n\n## 2. SESSION BUFFER\nIf the session was NOT trivial, append ONE JSON line to ${NEXO_HOME}/brain/session_buffer.jsonl:\n{\"ts\":\"YYYY-MM-DDTHH:MM:SS\",\"tasks\":[...],\"decisions\":[...],\"user_patterns\":[...],\"files_modified\":[...],\"errors_resolved\":[...],\"self_critique\":\"short summary\",\"mood\":\"focused|impatient|exploratory|frustrated|satisfied|neutral\",\"source\":\"claude\"}\n\n## 3. FOLLOWUPS\nIf there were deploys/cron changes/fixes — nexo_followup_create with verification date.\n\n## 4. PROACTIVE SEEDS\nWhat can I leave prepared so the next session starts doing useful work without the user asking?\n\n## 5. MARK COMPLETE\nWhen ALL of the above is done, run:\nbash -c 'mkdir -p ${NEXO_HOME}/operations && date +%s > ${NEXO_HOME}/operations/.postmortem-complete'\nThe user will close again and the hook will approve.\n\nIMPORTANT: Do NOT say goodbye, do NOT say goodnight or any farewell. Just execute the steps and mark complete."
115
- }
116
- HOOKEOF
117
- fi
118
+ "reason": "STOP HOOK — MANDATORY POST-MORTEM before ending (do NOT ask permission, do NOT skip):
118
119
 
119
- # 3. Direct session buffer fallback (runs for non-trivial sessions)
120
- BUFFER="$NEXO_HOME/brain/session_buffer.jsonl"
121
- TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S")
120
+ ## 1. SELF-CRITIQUE (MANDATORY write to session diary)
121
+ Answer these questions in the self_critique field of nexo_session_diary_write:
122
+ - Did the user have to ask me for something I should have detected or done on my own?
123
+ - Did I wait for the user to tell me something I could have verified proactively?
124
+ - Are there systems/states I can check next session without being asked?
125
+ - Did I repeat an error that already had a registered learning?
126
+ - What would I do differently if I repeated this session?
127
+ If any answer is YES — write the specific rule that would prevent repetition.
128
+ If the session was flawless, write 'No self-critique — clean session.'
122
129
 
123
- SKIP_FALLBACK=false
124
- if [ -f "$BUFFER" ]; then
125
- LAST_SOURCE=$(python3 -c "
126
- import json, sys
127
- from datetime import datetime, timedelta
128
- try:
129
- lines = open('$BUFFER').readlines()
130
- if lines:
131
- d = json.loads(lines[-1])
132
- ts = d.get('ts','')
133
- src = d.get('source','')
134
- entry_dt = datetime.strptime(ts, '%Y-%m-%dT%H:%M:%S')
135
- if datetime.utcnow() - entry_dt < timedelta(seconds=60) and src == 'claude':
136
- print('skip')
137
- else:
138
- print('write')
139
- else:
140
- print('write')
141
- except:
142
- print('write')
143
- " 2>/dev/null || echo "write")
144
- if [ "$LAST_SOURCE" = "skip" ]; then
145
- SKIP_FALLBACK=true
146
- fi
147
- fi
148
-
149
- if [ "$SKIP_FALLBACK" = false ]; then
150
- mkdir -p "$(dirname "$BUFFER")"
151
- ADAPTIVE_MODE="unknown"
152
- ADAPTIVE_FILE="$NEXO_HOME/brain/adaptive_state.json"
153
- if [ -f "$ADAPTIVE_FILE" ]; then
154
- ADAPTIVE_MODE=$(python3 -c "
155
- import json
156
- try:
157
- d = json.load(open('$ADAPTIVE_FILE'))
158
- print(d.get('current_mode', 'unknown').lower())
159
- except:
160
- print('unknown')
161
- " 2>/dev/null || echo "unknown")
162
- fi
163
- echo "{\"ts\":\"$TIMESTAMP\",\"tasks\":[\"session ended\"],\"decisions\":[],\"user_patterns\":[],\"files_modified\":[],\"errors_resolved\":[],\"self_critique\":\"hook-fallback, no self-critique captured\",\"mood\":\"unknown\",\"session_end_mode\":\"$ADAPTIVE_MODE\",\"source\":\"hook-fallback\"}" >> "$BUFFER" 2>/dev/null
164
- fi
130
+ ## 2. SESSION BUFFER
131
+ If the session was NOT trivial, append ONE JSON line to ~/.nexo/brain/session_buffer.jsonl:
132
+ {"ts":"YYYY-MM-DDTHH:MM:SS","tasks":[...],"decisions":[...],"user_patterns":[...],"files_modified":[...],"errors_resolved":[...],"self_critique":"short summary","mood":"focused|impatient|exploratory|frustrated|satisfied|neutral","source":"claude"}
165
133
 
166
- # 4. Intra-day reflection trigger
167
- REFLECTION_SCRIPT="$NEXO_HOME/scripts/nexo-reflection.py"
168
- REFLECTION_STATE="$NEXO_HOME/coordination/reflection-log.json"
169
- TRIGGER_THRESHOLD=3
134
+ ## 3. FOLLOWUPS
135
+ If there were deploys/cron changes/fixes — nexo_followup_create with verification date.
170
136
 
171
- if [ -f "$BUFFER" ] && [ -f "$REFLECTION_SCRIPT" ]; then
172
- LINE_COUNT=$(wc -l < "$BUFFER" | tr -d ' ')
137
+ ## 4. PROACTIVE SEEDS
138
+ What can I leave prepared so the next session starts doing useful work without the user asking?
173
139
 
174
- if [ "$LINE_COUNT" -ge "$TRIGGER_THRESHOLD" ]; then
175
- SHOULD_REFLECT=true
176
- if [ -f "$REFLECTION_STATE" ]; then
177
- LAST_TS=$(python3 -c "
178
- import json
179
- from datetime import datetime, timedelta
180
- try:
181
- log = json.load(open('$REFLECTION_STATE'))
182
- if log:
183
- last = log[-1]['timestamp']
184
- last_dt = datetime.strptime(last, '%Y-%m-%d %H:%M')
185
- if datetime.now() - last_dt < timedelta(hours=4):
186
- print('too_recent')
187
- else:
188
- print('ok')
189
- else:
190
- print('ok')
191
- except:
192
- print('ok')
193
- " 2>/dev/null)
194
- if [ "$LAST_TS" = "too_recent" ]; then
195
- SHOULD_REFLECT=false
196
- fi
197
- fi
140
+ ## 5. MARK COMPLETE
141
+ When ALL of the above is done, run:
142
+ bash -c 'mkdir -p ~/.nexo/operations && date +%s > ~/.nexo/operations/.postmortem-complete'
143
+ The user will close again and the hook will approve.
198
144
 
199
- if [ "$SHOULD_REFLECT" = true ]; then
200
- PYTHON=$(which python3 2>/dev/null || echo "/usr/bin/python3")
201
- nohup "$PYTHON" "$REFLECTION_SCRIPT" \
202
- >> "$NEXO_HOME/logs/reflection-stdout.log" \
203
- 2>> "$NEXO_HOME/logs/reflection-stderr.log" &
204
- fi
205
- fi
145
+ IMPORTANT: Do NOT say goodbye, do NOT say goodnight or any farewell. Just execute the steps and mark complete."
146
+ }
147
+ HOOKEOF
206
148
  fi
149
+
150
+ # 3. Session buffer fallback REMOVED (v8)
151
+ # The old hook-fallback was 86% noise. Session diary (written by Claude during
152
+ # post-mortem) is the only source of truth now. No more buffer writing.
@@ -1,7 +1,7 @@
1
1
  """NEXO Knowledge Graph — Bi-temporal entity-relationship graph on SQLite."""
2
2
 
3
3
  import json
4
- from datetime import datetime
4
+ from datetime import datetime, timezone
5
5
  from typing import Optional
6
6
  import os
7
7
 
@@ -58,7 +58,7 @@ def upsert_edge(source_type: str, source_ref: str, relation: str,
58
58
  source_id = source_node["id"]
59
59
  target_id = target_node["id"]
60
60
  props_json = json.dumps(properties or {})
61
- now = datetime.now(datetime.timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S")
61
+ now = datetime.now(timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S")
62
62
  existing = db.execute(
63
63
  "SELECT id, weight, confidence, properties FROM kg_edges "
64
64
  "WHERE source_id = ? AND target_id = ? AND relation = ? AND valid_until IS NULL",
@@ -90,7 +90,7 @@ def delete_edge(source_type: str, source_ref: str, relation: str,
90
90
  target = get_node(target_type, target_ref)
91
91
  if not source or not target:
92
92
  return False
93
- now = datetime.now(datetime.timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S")
93
+ now = datetime.now(timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S")
94
94
  cursor = db.execute(
95
95
  "UPDATE kg_edges SET valid_until = ? WHERE source_id = ? AND target_id = ? "
96
96
  "AND relation = ? AND valid_until IS NULL",