nexo-brain 7.9.12 → 7.9.14
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/auto_update.py +1 -1
- package/src/doctor/providers/runtime.py +22 -4
- package/src/enforcement_engine.py +4 -2
- package/src/hook_guardrails.py +2 -1
- package/src/hooks/g1_enforcer.py +5 -1
- package/src/hooks/post_tool_use.py +6 -3
- package/src/hooks/session-start.sh +4 -1
- package/src/hooks/session_start.py +4 -0
- package/src/lifecycle_prompts.py +8 -10
- package/src/operator_language.py +79 -0
- package/src/plugins/protocol.py +125 -47
- package/src/scripts/nexo-daily-self-audit.py +44 -0
- package/src/scripts/nexo-followup-runner.py +44 -9
- package/templates/core-prompts/followup-runner.md +2 -1
- package/templates/core-prompts/lifecycle-diary-stop.md +1 -0
- package/templates/core-prompts/operator-language-contract.md +1 -0
- package/tool-enforcement-map.json +26 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.13",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.9.
|
|
21
|
+
Version `7.9.14` is the current packaged-runtime line. Patch release over `7.9.13`: `task_close(done)` now hard-blocks missing verify/change-log/cortex evidence instead of silently degrading to debt-only closes, self-audit auto-drains stale `protocol_debt` every day, and Codex session parity now flags partial bootstrap/startup/heartbeat drift instead of passing as healthy when only one recent session behaved correctly. Coordinated Desktop release remains v0.28.14.
|
|
22
22
|
|
|
23
23
|
Previously in `7.9.5`: patch release that fixes canonical diary confirmation for Desktop: Brain resolves the Desktop/Claude session UUID through NEXO SID aliases before checking `session_diary`, so archive/delete/app-exit can confirm diaries written by `nexo_session_diary_write` under the active `nexo-...` SID. Verification: `pytest tests/test_lifecycle_events.py` (28 passing) plus coordinated Desktop v0.28.6 shutdown/archive/delete/app-exit checks.
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.9.
|
|
3
|
+
"version": "7.9.14",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/auto_update.py
CHANGED
|
@@ -4257,7 +4257,7 @@ def _restore_runtime_tree(backup_dir: str, dest: Path = NEXO_HOME) -> None:
|
|
|
4257
4257
|
def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_HOME, progress_fn=None) -> dict:
|
|
4258
4258
|
import shutil
|
|
4259
4259
|
|
|
4260
|
-
packages = ["db", "cognitive", "doctor", "dashboard", "rules", "crons", "hooks"]
|
|
4260
|
+
packages = ["db", "cognitive", "doctor", "dashboard", "rules", "crons", "hooks", "presets"]
|
|
4261
4261
|
flat_files = _runtime_flat_files(src_dir)
|
|
4262
4262
|
copied_packages = 0
|
|
4263
4263
|
copied_files = 0
|
|
@@ -2112,14 +2112,32 @@ def check_codex_session_parity() -> DoctorCheck:
|
|
|
2112
2112
|
status = "healthy"
|
|
2113
2113
|
severity = "info"
|
|
2114
2114
|
repair_plan: list[str] = []
|
|
2115
|
-
|
|
2115
|
+
missing_bootstrap = max(0, audit["files"] - audit["bootstrap_sessions"])
|
|
2116
|
+
missing_startup = max(0, audit["files"] - audit["startup_sessions"])
|
|
2117
|
+
missing_heartbeat = max(0, audit["files"] - audit["heartbeat_sessions"])
|
|
2118
|
+
if missing_bootstrap:
|
|
2116
2119
|
status = "degraded"
|
|
2117
2120
|
severity = "warn"
|
|
2118
|
-
repair_plan.append(
|
|
2119
|
-
|
|
2121
|
+
repair_plan.append(
|
|
2122
|
+
"Run `nexo update` or `nexo clients sync` so every Codex session inherits the managed bootstrap, not just a subset"
|
|
2123
|
+
)
|
|
2124
|
+
if missing_startup:
|
|
2125
|
+
status = "degraded"
|
|
2126
|
+
severity = "warn"
|
|
2127
|
+
repair_plan.append(
|
|
2128
|
+
"Use `nexo chat` or keep the global Codex bootstrap intact so every Codex session actually calls `nexo_startup`"
|
|
2129
|
+
)
|
|
2130
|
+
if missing_heartbeat:
|
|
2120
2131
|
status = "degraded"
|
|
2121
2132
|
severity = "warn"
|
|
2122
|
-
repair_plan.append("
|
|
2133
|
+
repair_plan.append("Keep `nexo_heartbeat` on every user turn so restored/plain Codex sessions do not drift off-protocol")
|
|
2134
|
+
if missing_bootstrap or missing_startup or missing_heartbeat:
|
|
2135
|
+
evidence.append(
|
|
2136
|
+
"session drift: "
|
|
2137
|
+
f"{missing_bootstrap} missing bootstrap, "
|
|
2138
|
+
f"{missing_startup} missing startup, "
|
|
2139
|
+
f"{missing_heartbeat} missing heartbeat"
|
|
2140
|
+
)
|
|
2123
2141
|
|
|
2124
2142
|
return DoctorCheck(
|
|
2125
2143
|
id="runtime.codex_sessions",
|
|
@@ -17,6 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
import re
|
|
18
18
|
import paths
|
|
19
19
|
from core_prompts import render_core_prompt
|
|
20
|
+
from operator_language import append_operator_language_contract
|
|
20
21
|
|
|
21
22
|
try:
|
|
22
23
|
from r13_pre_edit_guard import should_inject_r13, ToolCallRecord, WATCHED_WRITE_TOOLS
|
|
@@ -2493,7 +2494,7 @@ class HeadlessEnforcer:
|
|
|
2493
2494
|
if entry["enf"].get("level") == "must":
|
|
2494
2495
|
p = entry["enf"].get("session_end_inject_prompt") or entry["enf"].get("inject_prompt", "")
|
|
2495
2496
|
if p:
|
|
2496
|
-
prompts.append(p)
|
|
2497
|
+
prompts.append(append_operator_language_contract(p))
|
|
2497
2498
|
_logger.info("END_PROMPTS: %d prompts to inject", len(prompts))
|
|
2498
2499
|
return prompts
|
|
2499
2500
|
|
|
@@ -2545,7 +2546,8 @@ class HeadlessEnforcer:
|
|
|
2545
2546
|
if tool in self.tools_called and not tag.startswith("periodic_"):
|
|
2546
2547
|
_logger.info("SKIP: %s — already called", tag)
|
|
2547
2548
|
return
|
|
2548
|
-
|
|
2549
|
+
localized_prompt = append_operator_language_contract(prompt)
|
|
2550
|
+
self.injection_queue.append({"prompt": localized_prompt, "tag": tag, "at": time.time(), "rule_id": rule_id})
|
|
2549
2551
|
_logger.info("ENQUEUED: %s (queue size: %d rule_id=%s)", tag, len(self.injection_queue), rule_id or "?")
|
|
2550
2552
|
# Fase F telemetry — log one "injection" event per enqueue. The
|
|
2551
2553
|
# engine does not see the final event lifecycle (compliance / FP);
|
package/src/hook_guardrails.py
CHANGED
|
@@ -13,6 +13,7 @@ import paths
|
|
|
13
13
|
|
|
14
14
|
from core_prompts import render_core_prompt
|
|
15
15
|
from db import create_protocol_debt, get_db
|
|
16
|
+
from operator_language import append_operator_language_contract
|
|
16
17
|
from plugins.guard import _load_conditioned_learnings, _normalize_path_token
|
|
17
18
|
from protocol_settings import get_protocol_strictness
|
|
18
19
|
from product_mode import core_writes_allowed, is_protected_runtime_core_path
|
|
@@ -709,7 +710,7 @@ def _task_needs_workflow(task: dict | None) -> bool:
|
|
|
709
710
|
|
|
710
711
|
|
|
711
712
|
def _append_protocol_warning(warnings: list[dict], message: str) -> None:
|
|
712
|
-
clean = (message
|
|
713
|
+
clean = append_operator_language_contract(message)
|
|
713
714
|
if not clean:
|
|
714
715
|
return
|
|
715
716
|
if any((item.get("message") or "").strip() == clean for item in warnings):
|
package/src/hooks/g1_enforcer.py
CHANGED
|
@@ -44,6 +44,8 @@ import sqlite3
|
|
|
44
44
|
import time
|
|
45
45
|
from pathlib import Path
|
|
46
46
|
|
|
47
|
+
from operator_language import append_operator_language_contract
|
|
48
|
+
|
|
47
49
|
|
|
48
50
|
G1_GRACE_SECONDS = int(os.environ.get("NEXO_G1_GRACE_SECONDS", "120"))
|
|
49
51
|
G1_RATE_LIMIT_SECONDS = int(os.environ.get("NEXO_G1_RATE_LIMIT_SECONDS", "180"))
|
|
@@ -229,12 +231,14 @@ def _render_message(task: dict) -> str:
|
|
|
229
231
|
else: # ask
|
|
230
232
|
action = "nexo_cortex_decide(...) or a user turn"
|
|
231
233
|
reason = "ask mode needs clarifying input before the visible answer"
|
|
232
|
-
return (
|
|
234
|
+
return append_operator_language_contract(
|
|
235
|
+
(
|
|
233
236
|
"[NEXO Protocol Enforcer] G1 gate: task "
|
|
234
237
|
f"{task_id} is open with response_mode='{mode}' "
|
|
235
238
|
f"({reason}). Run {action} or close the task with "
|
|
236
239
|
"nexo_task_close BEFORE emitting the next user-visible answer. "
|
|
237
240
|
"Silent-compliant: do not mention this reminder to the user."
|
|
241
|
+
)
|
|
238
242
|
)
|
|
239
243
|
|
|
240
244
|
|
|
@@ -31,6 +31,7 @@ if str(_DIR.parent) not in sys.path:
|
|
|
31
31
|
sys.path.insert(0, str(_DIR.parent))
|
|
32
32
|
|
|
33
33
|
from core_prompts import render_core_prompt
|
|
34
|
+
from operator_language import append_operator_language_contract
|
|
34
35
|
|
|
35
36
|
_NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
36
37
|
|
|
@@ -112,9 +113,11 @@ def check_inbox_and_emit_reminder(sid: str, now: float | None = None) -> str | N
|
|
|
112
113
|
if current - last_rem < INBOX_CHECK_THRESHOLD_SECONDS:
|
|
113
114
|
return None # rate limit: max 1 reminder/min/session
|
|
114
115
|
mark_reminder_sent(sid, current)
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
return append_operator_language_contract(
|
|
117
|
+
render_core_prompt(
|
|
118
|
+
"post-tool-inbox-reminder",
|
|
119
|
+
pending=str(pending),
|
|
120
|
+
)
|
|
118
121
|
)
|
|
119
122
|
|
|
120
123
|
|
|
@@ -35,6 +35,9 @@ fi
|
|
|
35
35
|
NEXO_HOOK_START_MS=$(python3 -c "import time; print(int(time.time()*1000))" 2>/dev/null || echo 0)
|
|
36
36
|
NEXO_HOOK_NAME="session-start"
|
|
37
37
|
_nexo_record_hook_run() {
|
|
38
|
+
if [ "${NEXO_DISABLE_SHELL_HOOK_RECORD:-0}" = "1" ]; then
|
|
39
|
+
return
|
|
40
|
+
fi
|
|
38
41
|
local exit_code=$?
|
|
39
42
|
local duration_ms=0
|
|
40
43
|
if [ "$NEXO_HOOK_START_MS" != "0" ]; then
|
|
@@ -283,7 +286,7 @@ try:
|
|
|
283
286
|
try:
|
|
284
287
|
row = _conn.execute(
|
|
285
288
|
\"SELECT COUNT(*) FROM hook_runs \"
|
|
286
|
-
\"WHERE exit_code != 0 AND started_at >
|
|
289
|
+
\"WHERE exit_code != 0 AND started_at > (strftime('%s','now') - 24*3600)\"
|
|
287
290
|
).fetchone()
|
|
288
291
|
_health['failing_hooks_24h'] = int(row[0] or 0) if row else 0
|
|
289
292
|
except Exception:
|
|
@@ -47,12 +47,16 @@ def _record(duration_ms: int, exit_code: int, summary: str) -> None:
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def _run_step(cmd: list[str], timeout: int) -> tuple[int, str]:
|
|
50
|
+
env = os.environ.copy()
|
|
51
|
+
if cmd and str(cmd[-1]).endswith("session-start.sh"):
|
|
52
|
+
env["NEXO_DISABLE_SHELL_HOOK_RECORD"] = "1"
|
|
50
53
|
try:
|
|
51
54
|
result = subprocess.run(
|
|
52
55
|
cmd,
|
|
53
56
|
timeout=timeout,
|
|
54
57
|
capture_output=True,
|
|
55
58
|
text=True,
|
|
59
|
+
env=env,
|
|
56
60
|
)
|
|
57
61
|
tail = (result.stdout or result.stderr or "").strip().splitlines()[-1:] or [""]
|
|
58
62
|
return result.returncode, tail[0][:200]
|
package/src/lifecycle_prompts.py
CHANGED
|
@@ -18,8 +18,10 @@ import hashlib
|
|
|
18
18
|
import json
|
|
19
19
|
from typing import Any, Dict, List, Optional
|
|
20
20
|
|
|
21
|
+
from core_prompts import render_core_prompt
|
|
22
|
+
from operator_language import build_operator_language_contract
|
|
21
23
|
|
|
22
|
-
PLAN_VERSION =
|
|
24
|
+
PLAN_VERSION = 5
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
# Actions that trigger a canonical diary+stop plan. `switch` and
|
|
@@ -69,15 +71,11 @@ def _diary_prompt_for_action(
|
|
|
69
71
|
"app-exit": "The operator quit NEXO Desktop",
|
|
70
72
|
}
|
|
71
73
|
trailing = reason_by_action.get(action, "The conversation ended")
|
|
72
|
-
return (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
"nexo_stop with the current SID. Do not produce visible text. "
|
|
78
|
-
"Apply the required action silently. Do not mention this "
|
|
79
|
-
"reminder, Guardian, Protocol Enforcer, system reminders, or "
|
|
80
|
-
"any internal enforcement to the user."
|
|
74
|
+
return render_core_prompt(
|
|
75
|
+
"lifecycle-diary-stop",
|
|
76
|
+
trailing=trailing,
|
|
77
|
+
title_repr=repr(title),
|
|
78
|
+
operator_language_contract_block=f" {build_operator_language_contract()}",
|
|
81
79
|
)
|
|
82
80
|
|
|
83
81
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Operator-language helpers shared by prompts, hooks, and automations."""
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
from core_prompts import render_core_prompt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_LANGUAGE_LABELS = {
|
|
11
|
+
"ca": "Catalan (ca)",
|
|
12
|
+
"de": "German (de)",
|
|
13
|
+
"en": "English (en)",
|
|
14
|
+
"es": "Spanish (es)",
|
|
15
|
+
"fr": "French (fr)",
|
|
16
|
+
"it": "Italian (it)",
|
|
17
|
+
"ja": "Japanese (ja)",
|
|
18
|
+
"pt": "Portuguese (pt)",
|
|
19
|
+
"zh": "Chinese (zh)",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalize_operator_language(value: str | None = "") -> str:
|
|
24
|
+
raw = str(value or "").strip().lower().replace("_", "-")
|
|
25
|
+
if not raw:
|
|
26
|
+
return ""
|
|
27
|
+
primary = raw.split("-", 1)[0]
|
|
28
|
+
return primary or raw
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_operator_language() -> str:
|
|
32
|
+
try:
|
|
33
|
+
from calibration_runtime import load_runtime_calibration
|
|
34
|
+
from paths import brain_dir
|
|
35
|
+
|
|
36
|
+
payload = load_runtime_calibration(brain_dir() / "calibration.json")
|
|
37
|
+
except Exception:
|
|
38
|
+
payload = {}
|
|
39
|
+
user = payload.get("user") if isinstance(payload.get("user"), dict) else {}
|
|
40
|
+
return normalize_operator_language(
|
|
41
|
+
str(user.get("language") or "").strip()
|
|
42
|
+
or str(payload.get("language") or "").strip()
|
|
43
|
+
or str(payload.get("lang") or "").strip()
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def describe_operator_language(language: str | None = "") -> str:
|
|
48
|
+
normalized = normalize_operator_language(language)
|
|
49
|
+
if not normalized:
|
|
50
|
+
return "the user's current conversation language"
|
|
51
|
+
return _LANGUAGE_LABELS.get(normalized, f"{normalized} language")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@lru_cache(maxsize=8)
|
|
55
|
+
def build_operator_language_contract(language: str | None = "") -> str:
|
|
56
|
+
label = describe_operator_language(language or load_operator_language())
|
|
57
|
+
return render_core_prompt(
|
|
58
|
+
"operator-language-contract",
|
|
59
|
+
operator_language_label=label,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def append_operator_language_contract(prompt: str, language: str | None = "") -> str:
|
|
64
|
+
clean = str(prompt or "").strip()
|
|
65
|
+
if not clean:
|
|
66
|
+
return clean
|
|
67
|
+
contract = build_operator_language_contract(language).strip()
|
|
68
|
+
if contract and contract not in clean:
|
|
69
|
+
clean = f"{clean} {contract}"
|
|
70
|
+
return clean
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"append_operator_language_contract",
|
|
75
|
+
"build_operator_language_contract",
|
|
76
|
+
"describe_operator_language",
|
|
77
|
+
"load_operator_language",
|
|
78
|
+
"normalize_operator_language",
|
|
79
|
+
]
|
package/src/plugins/protocol.py
CHANGED
|
@@ -898,6 +898,46 @@ def _auto_capture_learning(task: dict, task_id: str, effective_files: list[str],
|
|
|
898
898
|
)
|
|
899
899
|
|
|
900
900
|
|
|
901
|
+
def _append_debt_ref(debts: list[dict], debt: dict, *, debt_type: str, severity: str):
|
|
902
|
+
debt_id = debt.get("id")
|
|
903
|
+
if debt_id and any(item.get("id") == debt_id for item in debts):
|
|
904
|
+
return
|
|
905
|
+
debts.append(
|
|
906
|
+
{
|
|
907
|
+
"id": debt_id,
|
|
908
|
+
"debt_type": debt_type,
|
|
909
|
+
"severity": severity,
|
|
910
|
+
}
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _ensure_open_debt(
|
|
915
|
+
session_id: str,
|
|
916
|
+
task_id: str,
|
|
917
|
+
debt_type: str,
|
|
918
|
+
*,
|
|
919
|
+
severity: str,
|
|
920
|
+
evidence: str,
|
|
921
|
+
debts: list[dict],
|
|
922
|
+
) -> dict:
|
|
923
|
+
existing = list_protocol_debts(
|
|
924
|
+
status="open",
|
|
925
|
+
task_id=task_id,
|
|
926
|
+
session_id="" if task_id else session_id,
|
|
927
|
+
debt_type=debt_type,
|
|
928
|
+
limit=1,
|
|
929
|
+
)
|
|
930
|
+
debt = existing[0] if existing else create_protocol_debt(
|
|
931
|
+
session_id,
|
|
932
|
+
debt_type,
|
|
933
|
+
severity=severity,
|
|
934
|
+
task_id=task_id,
|
|
935
|
+
evidence=evidence,
|
|
936
|
+
)
|
|
937
|
+
_append_debt_ref(debts, debt, debt_type=debt_type, severity=severity)
|
|
938
|
+
return debt
|
|
939
|
+
|
|
940
|
+
|
|
901
941
|
def _record_debt(session_id: str, task_id: str, debt_type: str, *, severity: str, evidence: str, debts: list[dict]):
|
|
902
942
|
debt = create_protocol_debt(
|
|
903
943
|
session_id,
|
|
@@ -906,13 +946,7 @@ def _record_debt(session_id: str, task_id: str, debt_type: str, *, severity: str
|
|
|
906
946
|
task_id=task_id,
|
|
907
947
|
evidence=evidence,
|
|
908
948
|
)
|
|
909
|
-
debts
|
|
910
|
-
{
|
|
911
|
-
"id": debt.get("id"),
|
|
912
|
-
"debt_type": debt_type,
|
|
913
|
-
"severity": severity,
|
|
914
|
-
}
|
|
915
|
-
)
|
|
949
|
+
_append_debt_ref(debts, debt, debt_type=debt_type, severity=severity)
|
|
916
950
|
|
|
917
951
|
|
|
918
952
|
def handle_confidence_check(
|
|
@@ -1336,10 +1370,10 @@ def handle_task_close(
|
|
|
1336
1370
|
high_stakes=bool(task.get("response_high_stakes")),
|
|
1337
1371
|
)
|
|
1338
1372
|
|
|
1339
|
-
# ── Evidence enforcement: reject 'done' without proof
|
|
1340
|
-
#
|
|
1341
|
-
#
|
|
1342
|
-
#
|
|
1373
|
+
# ── Evidence enforcement: reject 'done' without proof ──
|
|
1374
|
+
# G1 hardening: "done" is no longer allowed to degrade into a debt-only
|
|
1375
|
+
# close when verify evidence is missing. Keep the task open, open/dedupe
|
|
1376
|
+
# the debt, and force the caller to provide real proof before closing.
|
|
1343
1377
|
if task.get("must_verify") and clean_outcome == "done":
|
|
1344
1378
|
is_trivial, trivial_reason = _is_trivial_evidence(clean_evidence)
|
|
1345
1379
|
if not is_trivial:
|
|
@@ -1349,39 +1383,7 @@ def handle_task_close(
|
|
|
1349
1383
|
resolution="Verification evidence supplied during task_close",
|
|
1350
1384
|
)
|
|
1351
1385
|
else:
|
|
1352
|
-
|
|
1353
|
-
if protocol_strictness == "strict":
|
|
1354
|
-
if trivial_reason == "empty":
|
|
1355
|
-
err = "Cannot close task as 'done' without evidence."
|
|
1356
|
-
hint = (
|
|
1357
|
-
"Provide the `evidence` parameter with verifiable proof: "
|
|
1358
|
-
"test output, curl response, screenshot path, or real "
|
|
1359
|
-
"command output."
|
|
1360
|
-
)
|
|
1361
|
-
else:
|
|
1362
|
-
err = (
|
|
1363
|
-
"Cannot close task as 'done' with trivial evidence "
|
|
1364
|
-
f"({trivial_reason})."
|
|
1365
|
-
)
|
|
1366
|
-
hint = (
|
|
1367
|
-
f"Evidence must be substantive: >= {R03_MIN_EVIDENCE_CHARS} "
|
|
1368
|
-
"characters AND not a single filler word. Attach real "
|
|
1369
|
-
"proof — test output excerpt, curl response, DB row, "
|
|
1370
|
-
"screenshot path, or command stdout."
|
|
1371
|
-
)
|
|
1372
|
-
return json.dumps(
|
|
1373
|
-
{
|
|
1374
|
-
"ok": False,
|
|
1375
|
-
"error": err,
|
|
1376
|
-
"hint": hint,
|
|
1377
|
-
"task_id": task_id,
|
|
1378
|
-
"protocol_strictness": protocol_strictness,
|
|
1379
|
-
"evidence_quality_reason": trivial_reason,
|
|
1380
|
-
},
|
|
1381
|
-
ensure_ascii=False,
|
|
1382
|
-
indent=2,
|
|
1383
|
-
)
|
|
1384
|
-
_record_debt(
|
|
1386
|
+
debt = _ensure_open_debt(
|
|
1385
1387
|
task["session_id"],
|
|
1386
1388
|
task_id,
|
|
1387
1389
|
"claimed_done_without_evidence",
|
|
@@ -1393,6 +1395,39 @@ def handle_task_close(
|
|
|
1393
1395
|
),
|
|
1394
1396
|
debts=debts_created,
|
|
1395
1397
|
)
|
|
1398
|
+
if trivial_reason == "empty":
|
|
1399
|
+
err = "Cannot close task as 'done' without evidence."
|
|
1400
|
+
hint = (
|
|
1401
|
+
"Provide the `evidence` parameter with verifiable proof: "
|
|
1402
|
+
"test output, curl response, screenshot path, or real "
|
|
1403
|
+
"command output."
|
|
1404
|
+
)
|
|
1405
|
+
else:
|
|
1406
|
+
err = (
|
|
1407
|
+
"Cannot close task as 'done' with trivial evidence "
|
|
1408
|
+
f"({trivial_reason})."
|
|
1409
|
+
)
|
|
1410
|
+
hint = (
|
|
1411
|
+
f"Evidence must be substantive: >= {R03_MIN_EVIDENCE_CHARS} "
|
|
1412
|
+
"characters AND not a single filler word. Attach real "
|
|
1413
|
+
"proof — test output excerpt, curl response, DB row, "
|
|
1414
|
+
"screenshot path, or command stdout."
|
|
1415
|
+
)
|
|
1416
|
+
return json.dumps(
|
|
1417
|
+
{
|
|
1418
|
+
"ok": False,
|
|
1419
|
+
"error": err,
|
|
1420
|
+
"hint": hint,
|
|
1421
|
+
"task_id": task_id,
|
|
1422
|
+
"blocked_by": "g1_verify",
|
|
1423
|
+
"debt_id": debt.get("id"),
|
|
1424
|
+
"debt_type": "claimed_done_without_evidence",
|
|
1425
|
+
"evidence_quality_reason": trivial_reason,
|
|
1426
|
+
"protocol_strictness": get_protocol_strictness(),
|
|
1427
|
+
},
|
|
1428
|
+
ensure_ascii=False,
|
|
1429
|
+
indent=2,
|
|
1430
|
+
)
|
|
1396
1431
|
|
|
1397
1432
|
# ── Release checklist: require channel alignment evidence for release tasks ──
|
|
1398
1433
|
is_release = _is_release_task(
|
|
@@ -1430,7 +1465,7 @@ def handle_task_close(
|
|
|
1430
1465
|
(clean_change_verify or clean_evidence)[:500],
|
|
1431
1466
|
)
|
|
1432
1467
|
if "error" in change:
|
|
1433
|
-
|
|
1468
|
+
debt = _ensure_open_debt(
|
|
1434
1469
|
task["session_id"],
|
|
1435
1470
|
task_id,
|
|
1436
1471
|
"missing_change_log",
|
|
@@ -1438,6 +1473,21 @@ def handle_task_close(
|
|
|
1438
1473
|
evidence=f"change_log failed: {change['error']}",
|
|
1439
1474
|
debts=debts_created,
|
|
1440
1475
|
)
|
|
1476
|
+
if clean_outcome == "done":
|
|
1477
|
+
return json.dumps(
|
|
1478
|
+
{
|
|
1479
|
+
"ok": False,
|
|
1480
|
+
"error": "Cannot close task as 'done' because change_log creation failed.",
|
|
1481
|
+
"hint": "Capture the changed files and create the change log successfully before closing as done.",
|
|
1482
|
+
"task_id": task_id,
|
|
1483
|
+
"blocked_by": "g1_change_log",
|
|
1484
|
+
"debt_id": debt.get("id"),
|
|
1485
|
+
"debt_type": "missing_change_log",
|
|
1486
|
+
"change_log_error": change.get("error"),
|
|
1487
|
+
},
|
|
1488
|
+
ensure_ascii=False,
|
|
1489
|
+
indent=2,
|
|
1490
|
+
)
|
|
1441
1491
|
else:
|
|
1442
1492
|
change_log_id = change.get("id")
|
|
1443
1493
|
resolve_protocol_debts(
|
|
@@ -1446,7 +1496,7 @@ def handle_task_close(
|
|
|
1446
1496
|
resolution="Change log created by nexo_task_close",
|
|
1447
1497
|
)
|
|
1448
1498
|
else:
|
|
1449
|
-
|
|
1499
|
+
debt = _ensure_open_debt(
|
|
1450
1500
|
task["session_id"],
|
|
1451
1501
|
task_id,
|
|
1452
1502
|
"missing_change_log",
|
|
@@ -1454,6 +1504,20 @@ def handle_task_close(
|
|
|
1454
1504
|
evidence="Task required change_log but no changed files were supplied or recorded.",
|
|
1455
1505
|
debts=debts_created,
|
|
1456
1506
|
)
|
|
1507
|
+
if clean_outcome == "done":
|
|
1508
|
+
return json.dumps(
|
|
1509
|
+
{
|
|
1510
|
+
"ok": False,
|
|
1511
|
+
"error": "Cannot close task as 'done' without changed files for the required change_log.",
|
|
1512
|
+
"hint": "Pass `files_changed` (or open the task with files) so nexo_task_close can persist the change log before closing as done.",
|
|
1513
|
+
"task_id": task_id,
|
|
1514
|
+
"blocked_by": "g1_change_log",
|
|
1515
|
+
"debt_id": debt.get("id"),
|
|
1516
|
+
"debt_type": "missing_change_log",
|
|
1517
|
+
},
|
|
1518
|
+
ensure_ascii=False,
|
|
1519
|
+
indent=2,
|
|
1520
|
+
)
|
|
1457
1521
|
|
|
1458
1522
|
if correction:
|
|
1459
1523
|
if (learning_title or "").strip() and (learning_content or "").strip():
|
|
@@ -1564,7 +1628,7 @@ def handle_task_close(
|
|
|
1564
1628
|
resolution="High-stakes action task has a persisted Cortex evaluation.",
|
|
1565
1629
|
)
|
|
1566
1630
|
else:
|
|
1567
|
-
|
|
1631
|
+
debt = _ensure_open_debt(
|
|
1568
1632
|
task["session_id"],
|
|
1569
1633
|
task_id,
|
|
1570
1634
|
"missing_cortex_evaluation",
|
|
@@ -1572,6 +1636,20 @@ def handle_task_close(
|
|
|
1572
1636
|
evidence="High-stakes action task closed without nexo_cortex_decide / persisted evaluation.",
|
|
1573
1637
|
debts=debts_created,
|
|
1574
1638
|
)
|
|
1639
|
+
if clean_outcome == "done":
|
|
1640
|
+
return json.dumps(
|
|
1641
|
+
{
|
|
1642
|
+
"ok": False,
|
|
1643
|
+
"error": "Cannot close high-stakes action task as 'done' without a persisted cortex evaluation.",
|
|
1644
|
+
"hint": "Run `nexo_cortex_decide(...)` for this task and then close it again with the final evidence.",
|
|
1645
|
+
"task_id": task_id,
|
|
1646
|
+
"blocked_by": "g1_cortex",
|
|
1647
|
+
"debt_id": debt.get("id"),
|
|
1648
|
+
"debt_type": "missing_cortex_evaluation",
|
|
1649
|
+
},
|
|
1650
|
+
ensure_ascii=False,
|
|
1651
|
+
indent=2,
|
|
1652
|
+
)
|
|
1575
1653
|
|
|
1576
1654
|
if task.get("guard_has_blocking") and not files_changed_list:
|
|
1577
1655
|
open_task_debts = list_protocol_debts(status="open", task_id=task_id, limit=200)
|
|
@@ -18,6 +18,7 @@ Runs via launchd at 7:00 AM daily.
|
|
|
18
18
|
"""
|
|
19
19
|
import json
|
|
20
20
|
import hashlib
|
|
21
|
+
import importlib.util
|
|
21
22
|
import os
|
|
22
23
|
import py_compile
|
|
23
24
|
import re
|
|
@@ -1901,6 +1902,31 @@ def _sync_managed_bootstraps_inline() -> list[str]:
|
|
|
1901
1902
|
return results
|
|
1902
1903
|
|
|
1903
1904
|
|
|
1905
|
+
def _run_protocol_debt_drain_inline() -> dict:
|
|
1906
|
+
try:
|
|
1907
|
+
phase_path = NEXO_CODE / "scripts" / "deep-sleep" / "phase_protocol_debt_drain.py"
|
|
1908
|
+
spec = importlib.util.spec_from_file_location("phase_protocol_debt_drain_inline", phase_path)
|
|
1909
|
+
if not spec or not spec.loader:
|
|
1910
|
+
raise RuntimeError(f"Cannot load phase module from {phase_path}")
|
|
1911
|
+
module = importlib.util.module_from_spec(spec)
|
|
1912
|
+
spec.loader.exec_module(module)
|
|
1913
|
+
except Exception as exc:
|
|
1914
|
+
return {"ok": False, "error": f"import_failed: {exc}"}
|
|
1915
|
+
|
|
1916
|
+
try:
|
|
1917
|
+
report = module.run()
|
|
1918
|
+
except Exception as exc:
|
|
1919
|
+
return {"ok": False, "error": f"run_failed: {exc}"}
|
|
1920
|
+
|
|
1921
|
+
return {
|
|
1922
|
+
"ok": "error" not in report,
|
|
1923
|
+
"error": report.get("error", ""),
|
|
1924
|
+
"drained_count": len(report.get("drained_ids") or []),
|
|
1925
|
+
"requires_user_summary": report.get("requires_user_summary") or [],
|
|
1926
|
+
"audit_path": report.get("audit_path", ""),
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
|
|
1904
1930
|
def _sanitize_watchdog_registry_inline() -> dict:
|
|
1905
1931
|
hash_registry = _hash_registry_path()
|
|
1906
1932
|
if not hash_registry.exists():
|
|
@@ -1984,6 +2010,24 @@ def _disable_broken_personal_plugins_inline(conn: sqlite3.Connection | None) ->
|
|
|
1984
2010
|
def run_mechanical_autofixes():
|
|
1985
2011
|
conn = None
|
|
1986
2012
|
try:
|
|
2013
|
+
debt_drain = _run_protocol_debt_drain_inline()
|
|
2014
|
+
if debt_drain.get("ok"):
|
|
2015
|
+
drained_count = int(debt_drain.get("drained_count") or 0)
|
|
2016
|
+
requires_user_summary = debt_drain.get("requires_user_summary") or []
|
|
2017
|
+
if drained_count or requires_user_summary:
|
|
2018
|
+
detail_bits: list[str] = []
|
|
2019
|
+
if drained_count:
|
|
2020
|
+
detail_bits.append(f"drained {drained_count} stale protocol debt item(s)")
|
|
2021
|
+
if requires_user_summary:
|
|
2022
|
+
summary = ", ".join(
|
|
2023
|
+
f"{item.get('debt_type')} x{int(item.get('count') or 0)}"
|
|
2024
|
+
for item in requires_user_summary[:4]
|
|
2025
|
+
)
|
|
2026
|
+
detail_bits.append(f"still needs user review: {summary}")
|
|
2027
|
+
finding("INFO", "autofix", "Self-audit protocol debt drain: " + " | ".join(detail_bits))
|
|
2028
|
+
elif debt_drain.get("error"):
|
|
2029
|
+
finding("WARN", "autofix", f"Protocol debt drain inline failed: {debt_drain['error']}")
|
|
2030
|
+
|
|
1987
2031
|
if NEXO_DB.exists():
|
|
1988
2032
|
conn = sqlite3.connect(str(NEXO_DB))
|
|
1989
2033
|
conn.row_factory = sqlite3.Row
|
|
@@ -59,6 +59,7 @@ from automation_controls import (
|
|
|
59
59
|
)
|
|
60
60
|
from client_preferences import resolve_automation_backend, resolve_client_runtime_profile
|
|
61
61
|
from core_prompts import render_core_prompt
|
|
62
|
+
from operator_language import build_operator_language_contract, normalize_operator_language
|
|
62
63
|
import db as nexo_db
|
|
63
64
|
|
|
64
65
|
NEXO_DB = db_path()
|
|
@@ -73,6 +74,7 @@ LOCK_FILE = LOG_DIR / "followup-runner.lock"
|
|
|
73
74
|
MAX_FOLLOWUPS_PER_RUN = 5 # Focus: Opus can actually execute 5, not 30
|
|
74
75
|
COOLDOWN_DAYS = 3 # Don't retry needs_decision/blocked for 3 days
|
|
75
76
|
DEFAULT_ASSISTANT_NAME = "Nova"
|
|
77
|
+
DEFAULT_OPERATOR_LANGUAGE = "en"
|
|
76
78
|
|
|
77
79
|
# ── Logging ─────────────────────────────────────────────────────────────
|
|
78
80
|
def log(msg: str):
|
|
@@ -137,6 +139,17 @@ def _operator_attention_label_set(operator_name: str = "") -> tuple[str, str, st
|
|
|
137
139
|
)
|
|
138
140
|
|
|
139
141
|
|
|
142
|
+
def _operator_language(operator: dict | None = None) -> str:
|
|
143
|
+
payload = operator if isinstance(operator, dict) else get_operator_profile()
|
|
144
|
+
return normalize_operator_language(
|
|
145
|
+
str(payload.get("language") or DEFAULT_OPERATOR_LANGUAGE).strip() or DEFAULT_OPERATOR_LANGUAGE
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _uses_spanish(language: str) -> bool:
|
|
150
|
+
return _operator_language({"language": language}).startswith("es")
|
|
151
|
+
|
|
152
|
+
|
|
140
153
|
def _fallback_operator_attention_hint(followup: dict) -> bool:
|
|
141
154
|
"""Last-resort structural fallback.
|
|
142
155
|
|
|
@@ -428,15 +441,25 @@ def attention_reminder_category(status: str) -> str:
|
|
|
428
441
|
return "decisions" if status == "needs_decision" else "waiting"
|
|
429
442
|
|
|
430
443
|
|
|
431
|
-
def attention_reminder_description(
|
|
432
|
-
|
|
444
|
+
def attention_reminder_description(
|
|
445
|
+
fu_id: str,
|
|
446
|
+
*,
|
|
447
|
+
summary: str,
|
|
448
|
+
options,
|
|
449
|
+
status: str,
|
|
450
|
+
operator_language: str,
|
|
451
|
+
) -> str:
|
|
433
452
|
detail = " ".join((summary or "").split())
|
|
434
453
|
if not detail:
|
|
435
|
-
detail =
|
|
436
|
-
|
|
454
|
+
detail = (
|
|
455
|
+
"El runner no puede cerrar este punto sin intervención del operador."
|
|
456
|
+
if _uses_spanish(operator_language)
|
|
457
|
+
else "The runner cannot close this item without operator input."
|
|
458
|
+
)
|
|
459
|
+
description = f"{fu_id}: {detail}"
|
|
437
460
|
opts_text = render_options(options)
|
|
438
461
|
if opts_text:
|
|
439
|
-
description += f" Options: {opts_text}"
|
|
462
|
+
description += f" {'Opciones' if _uses_spanish(operator_language) else 'Options'}: {opts_text}"
|
|
440
463
|
return description[:480]
|
|
441
464
|
|
|
442
465
|
|
|
@@ -446,9 +469,16 @@ def upsert_attention_reminder(
|
|
|
446
469
|
summary: str,
|
|
447
470
|
options,
|
|
448
471
|
status: str,
|
|
472
|
+
operator_language: str,
|
|
449
473
|
):
|
|
450
474
|
reminder_id = attention_reminder_id(fu_id)
|
|
451
|
-
description = attention_reminder_description(
|
|
475
|
+
description = attention_reminder_description(
|
|
476
|
+
fu_id,
|
|
477
|
+
summary=summary,
|
|
478
|
+
options=options,
|
|
479
|
+
status=status,
|
|
480
|
+
operator_language=operator_language,
|
|
481
|
+
)
|
|
452
482
|
category = attention_reminder_category(status)
|
|
453
483
|
today = date.today().isoformat()
|
|
454
484
|
existing = nexo_db.get_reminder(reminder_id)
|
|
@@ -462,7 +492,7 @@ def upsert_attention_reminder(
|
|
|
462
492
|
category=category,
|
|
463
493
|
history_actor="followup-runner",
|
|
464
494
|
history_event="updated",
|
|
465
|
-
history_note=f"{fu_id}:
|
|
495
|
+
history_note=f"{fu_id}: status={status}",
|
|
466
496
|
)
|
|
467
497
|
if result.get("error"):
|
|
468
498
|
log(f" {fu_id}: failed to update reminder {reminder_id} ({result['error']})")
|
|
@@ -482,7 +512,7 @@ def upsert_attention_reminder(
|
|
|
482
512
|
return
|
|
483
513
|
nexo_db.add_reminder_note(
|
|
484
514
|
reminder_id,
|
|
485
|
-
f"
|
|
515
|
+
f"source_followup={fu_id} status={status}",
|
|
486
516
|
actor="followup-runner",
|
|
487
517
|
)
|
|
488
518
|
log(f" {fu_id}: reminder {reminder_id} creado para orchestrator")
|
|
@@ -516,6 +546,7 @@ def defer_followup_after_attention(
|
|
|
516
546
|
options,
|
|
517
547
|
status: str,
|
|
518
548
|
priority: str = "",
|
|
549
|
+
operator_language: str,
|
|
519
550
|
):
|
|
520
551
|
next_review = (date.today() + timedelta(days=1)).isoformat()
|
|
521
552
|
details = summary.strip()
|
|
@@ -536,7 +567,7 @@ def defer_followup_after_attention(
|
|
|
536
567
|
status="PENDING",
|
|
537
568
|
priority=priority,
|
|
538
569
|
history_event="rescheduled",
|
|
539
|
-
history_note=f"
|
|
570
|
+
history_note=f"status={status}; next_review={next_review}",
|
|
540
571
|
)
|
|
541
572
|
if ok:
|
|
542
573
|
log(f" {fu_id}: {status} → reprogramado para {next_review}")
|
|
@@ -545,6 +576,7 @@ def defer_followup_after_attention(
|
|
|
545
576
|
summary=summary,
|
|
546
577
|
options=options,
|
|
547
578
|
status=status,
|
|
579
|
+
operator_language=operator_language,
|
|
548
580
|
)
|
|
549
581
|
|
|
550
582
|
|
|
@@ -636,6 +668,7 @@ def build_prompt(actionable: list[dict]) -> str:
|
|
|
636
668
|
operator = get_operator_profile()
|
|
637
669
|
operator_name = str(operator.get("operator_name") or "the operator")
|
|
638
670
|
assistant_name = str(operator.get("assistant_name") or DEFAULT_ASSISTANT_NAME)
|
|
671
|
+
operator_language = _operator_language(operator)
|
|
639
672
|
operator_email = str(operator.get("operator_email") or "").strip()
|
|
640
673
|
send_reply_script = get_send_reply_script_path(local_script_dir=_script_dir)
|
|
641
674
|
send_target = operator_email or "OPERATOR_EMAIL_NOT_CONFIGURED"
|
|
@@ -691,6 +724,7 @@ Do not repeat queries, verifications, or operator emails that already happened t
|
|
|
691
724
|
recent_block=recent_block,
|
|
692
725
|
proactive_block=proactive_block,
|
|
693
726
|
extra_instructions_block=(extra_instructions_block + "\n\n") if extra_instructions_block else "",
|
|
727
|
+
operator_language_contract_block=build_operator_language_contract(operator_language) + "\n\n",
|
|
694
728
|
python_executable=sys.executable,
|
|
695
729
|
send_reply_script=send_reply_script,
|
|
696
730
|
send_target=send_target,
|
|
@@ -810,6 +844,7 @@ def main():
|
|
|
810
844
|
options=options,
|
|
811
845
|
status=r["status"],
|
|
812
846
|
priority=priority,
|
|
847
|
+
operator_language=_operator_language(),
|
|
813
848
|
)
|
|
814
849
|
# Cooldown: don't retry for COOLDOWN_DAYS
|
|
815
850
|
record_attempt(state, fid, r["status"])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
You are [[assistant_name]] running automated followups in headless mode (no user present).
|
|
2
2
|
[[work_intro]]
|
|
3
3
|
|
|
4
|
-
[[followup_block]][[recent_block]][[proactive_block]][[extra_instructions_block]]== STARTUP AND SHUTDOWN ==
|
|
4
|
+
[[operator_language_contract_block]][[followup_block]][[recent_block]][[proactive_block]][[extra_instructions_block]]== STARTUP AND SHUTDOWN ==
|
|
5
5
|
|
|
6
6
|
Start:
|
|
7
7
|
- `nexo_startup(task="followup-runner-cycle")`
|
|
@@ -69,6 +69,7 @@ Statuses:
|
|
|
69
69
|
- EXECUTE first, report after
|
|
70
70
|
- NEVER mark something complete without real verification
|
|
71
71
|
- `summary` must ALWAYS include REAL facts about what you DID (metrics, values, URLs, dates)
|
|
72
|
+
- `summary`, `options`, and any operator-facing text MUST stay in the operator's language
|
|
72
73
|
- NEVER include internal NEXO system noise (diaries, buffers, post-mortem)
|
|
73
74
|
- The operator needs results, not internal runtime chatter
|
|
74
75
|
- If there is nothing pending and nothing worth fixing, finish quickly — do not invent work
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[NEXO Protocol Enforcer] [[trailing]] (title: [[title_repr]]). Execute nexo_session_diary_write with a complete summary: decisions made, pending items, context for next session, mental state, and self-critique. Then execute nexo_stop with the current SID. Do not produce visible text. Apply the required action silently. Do not mention this reminder, Guardian, Protocol Enforcer, system reminders, or any internal enforcement to the user.[[operator_language_contract_block]]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CRITICAL LANGUAGE CONTRACT: even when this reminder or automation prompt is written in English, any operator-facing reply, diary entry, reminder/followup note, summary, escalation, or answer you generate while handling it MUST be written in [[operator_language_label]]. Keep machine identifiers, JSON keys, and explicit code/tool names unchanged only when the schema requires them.
|
|
@@ -1970,6 +1970,32 @@
|
|
|
1970
1970
|
},
|
|
1971
1971
|
"triggers_after": []
|
|
1972
1972
|
},
|
|
1973
|
+
"nexo_lifecycle_wait_for_stop": {
|
|
1974
|
+
"description": "Wait until a canonical lifecycle event no longer has an active NEXO session.",
|
|
1975
|
+
"category": "lifecycle",
|
|
1976
|
+
"source": "plugin:lifecycle_events",
|
|
1977
|
+
"requires": [],
|
|
1978
|
+
"provides": [],
|
|
1979
|
+
"internal_calls": [],
|
|
1980
|
+
"enforcement": {
|
|
1981
|
+
"level": "none",
|
|
1982
|
+
"rules": []
|
|
1983
|
+
},
|
|
1984
|
+
"triggers_after": []
|
|
1985
|
+
},
|
|
1986
|
+
"nexo_lifecycle_stop_nexo_session": {
|
|
1987
|
+
"description": "Best-effort explicit stop of a NEXO SID for Desktop lifecycle cleanup.",
|
|
1988
|
+
"category": "lifecycle",
|
|
1989
|
+
"source": "plugin:lifecycle_events",
|
|
1990
|
+
"requires": [],
|
|
1991
|
+
"provides": [],
|
|
1992
|
+
"internal_calls": [],
|
|
1993
|
+
"enforcement": {
|
|
1994
|
+
"level": "none",
|
|
1995
|
+
"rules": []
|
|
1996
|
+
},
|
|
1997
|
+
"triggers_after": []
|
|
1998
|
+
},
|
|
1973
1999
|
"nexo_media_memory_add": {
|
|
1974
2000
|
"description": "Store non-text artifact metadata",
|
|
1975
2001
|
"category": "media",
|