nexo-brain 7.1.8 → 7.2.0

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.
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env python3
2
+ # nexo: name=prune-runtime-backups
3
+ # nexo: description=Rotate technical rollback snapshots under runtime/backups by family. Never touches business (shopify-backups) or hourly_db (nexo-backup.sh) artifacts.
4
+ # nexo: category=maintenance
5
+ # nexo: runtime=python
6
+ # nexo: timeout=300
7
+ # nexo: idempotent=true
8
+
9
+ """
10
+ prune_runtime_backups.py — NEXO backup retention by class.
11
+
12
+ Separates *technical* rollback snapshots (throwaway, produced by the installer,
13
+ updater and backfills) from *operational* snapshots (shopify-backups, hourly
14
+ DB dumps, weekly archives) so the former can be rotated without risk to the
15
+ latter.
16
+
17
+ Target: $NEXO_HOME/runtime/backups/ (default ~/.nexo/runtime/backups)
18
+
19
+ Class taxonomy (prefix-based) and retention policy:
20
+
21
+ TECHNICAL (rollback snapshots, produced by installer/updater/backfills):
22
+ Prefixes:
23
+ pre-update-*, pre-autoupdate-*, pre-backfill-owner-*,
24
+ pre-runtime-sync-*, pre-sleep-wrapper-*, pre-obs-clean-*,
25
+ pre-import-user-data-*, pre-backfill-*,
26
+ code-tree-*, runtime-tree-*,
27
+ app-install-*, app-reinstall-*, desktop-local-install-*,
28
+ packaged-code-f06-conflicts-*, legacy-shim-conflicts-*,
29
+ legacy-personal-brain-db-stubs-*, legacy-root-db-stubs-*,
30
+ codex-live-sync-*, layout-loop-cleanup-*,
31
+ aux-launchagents-restore-*, live-sync-*, manual-*,
32
+ personal-script-legacy-prefix-*, plist-f06fix-*,
33
+ retired-personal-scripts-*, retired-personal-skills-*,
34
+ runtime-core-sync-*, pre-freshinstall-*
35
+ Retention (per prefix family): keep last N_RECENT + 1 per month for
36
+ MONTHLY_WINDOW_DAYS. Older than that and outside the 10 most recent
37
+ are eligible for deletion.
38
+
39
+ HOURLY_DB (sqlite dumps, managed by nexo-backup.sh):
40
+ Prefix: nexo-YYYY-MM-DD-HHMM.db in runtime/backups/ root
41
+ These are already rotated by nexo-backup.sh (48h retention). We skip
42
+ them here to avoid double-rotation logic.
43
+
44
+ WEEKLY_DB (weekly/ directory):
45
+ Already rotated by nexo-backup.sh (90d retention). Skip.
46
+
47
+ BUSINESS (shopify-backups/ and similar protected directories):
48
+ Prefix/name: shopify-backups (directory). Never touched.
49
+
50
+ Usage:
51
+ prune_runtime_backups.py # dry-run summary
52
+ prune_runtime_backups.py --apply # actually delete
53
+ prune_runtime_backups.py --json # machine-readable report
54
+ prune_runtime_backups.py --recent 10 # override N_RECENT
55
+ prune_runtime_backups.py --window-days 90
56
+ prune_runtime_backups.py --only pre-backfill-owner # restrict family
57
+
58
+ Exit codes:
59
+ 0 success (or nothing to prune)
60
+ 1 bad arguments or fatal I/O error
61
+ """
62
+ from __future__ import annotations
63
+
64
+ import argparse
65
+ import json
66
+ import os
67
+ import re
68
+ import shutil
69
+ import sys
70
+ import time
71
+ from datetime import datetime, timezone
72
+ from pathlib import Path
73
+ from typing import Iterable
74
+
75
+ # Technical prefixes. Order defines precedence when a name matches several.
76
+ TECHNICAL_PREFIXES = (
77
+ "pre-update-",
78
+ "pre-autoupdate-",
79
+ "pre-backfill-owner-",
80
+ "pre-backfill-",
81
+ "pre-runtime-sync-",
82
+ "pre-sleep-wrapper-",
83
+ "pre-obs-clean-",
84
+ "pre-import-user-data-",
85
+ "pre-freshinstall-",
86
+ "code-tree-",
87
+ "runtime-tree-",
88
+ "app-install-",
89
+ "app-reinstall-",
90
+ "desktop-local-install-",
91
+ "packaged-code-f06-conflicts-",
92
+ "legacy-shim-conflicts-",
93
+ "legacy-personal-brain-db-stubs-",
94
+ "legacy-root-db-stubs-",
95
+ "codex-live-sync-",
96
+ "layout-loop-cleanup-",
97
+ "aux-launchagents-restore-",
98
+ "live-sync-",
99
+ "manual-",
100
+ "personal-script-legacy-prefix-",
101
+ "plist-f06fix-",
102
+ "retired-personal-scripts-",
103
+ "retired-personal-skills-",
104
+ "runtime-core-sync-",
105
+ )
106
+
107
+ # Entries that must never be considered for pruning.
108
+ PROTECTED_NAMES = {"shopify-backups", "weekly"}
109
+ # Hourly DB dumps at the root of runtime/backups — managed by nexo-backup.sh.
110
+ HOURLY_DB_RE = re.compile(r"^nexo-\d{4}-\d{2}-\d{2}-\d{4}\.db$")
111
+ # Big ad-hoc DB files at the root — rare, include for reporting but never auto-prune.
112
+ ROOT_DB_RE = re.compile(r"^(pre-obs-clean|pre-sleep-wrapper-apply|pre-.*)-\d{4}-\d{2}-\d{2}-\d{4}\.db$")
113
+
114
+ # Timestamp patterns embedded in directory names.
115
+ TS_PATTERNS = (
116
+ # e.g. 2026-04-20-0427 or 2026-04-20-042733
117
+ re.compile(r"(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})?$"),
118
+ # e.g. 20260420-083106
119
+ re.compile(r"(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})?$"),
120
+ )
121
+
122
+
123
+ def default_nexo_home() -> Path:
124
+ return Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
125
+
126
+
127
+ def parse_timestamp(name: str) -> datetime | None:
128
+ for pat in TS_PATTERNS:
129
+ m = pat.search(name)
130
+ if not m:
131
+ continue
132
+ parts = [int(x) for x in m.groups() if x is not None]
133
+ # year, month, day, hour, minute, [second]
134
+ try:
135
+ if len(parts) == 5:
136
+ y, mo, d, h, mi = parts
137
+ s = 0
138
+ else:
139
+ y, mo, d, h, mi, s = parts
140
+ return datetime(y, mo, d, h, mi, s, tzinfo=timezone.utc)
141
+ except ValueError:
142
+ return None
143
+ return None
144
+
145
+
146
+ def classify(name: str) -> tuple[str, str] | None:
147
+ """Return (class, family) or None if the entry should be ignored."""
148
+ if name in PROTECTED_NAMES:
149
+ return ("BUSINESS", name)
150
+ if HOURLY_DB_RE.match(name):
151
+ return ("HOURLY_DB", "nexo-db")
152
+ if ROOT_DB_RE.match(name):
153
+ return ("ROOT_DB", "root-db")
154
+ for pref in TECHNICAL_PREFIXES:
155
+ if name.startswith(pref):
156
+ return ("TECHNICAL", pref.rstrip("-"))
157
+ # Unknown: report but never touch.
158
+ return ("UNKNOWN", "unknown")
159
+
160
+
161
+ def dir_size_bytes(path: Path) -> int:
162
+ total = 0
163
+ try:
164
+ for root, _dirs, files in os.walk(path, onerror=lambda _e: None):
165
+ for fn in files:
166
+ fp = Path(root) / fn
167
+ try:
168
+ total += fp.stat().st_size
169
+ except OSError:
170
+ pass
171
+ except OSError:
172
+ pass
173
+ return total
174
+
175
+
176
+ def human_size(n: int) -> str:
177
+ for unit in ("B", "K", "M", "G", "T"):
178
+ if n < 1024:
179
+ return f"{n:.1f}{unit}"
180
+ n /= 1024
181
+ return f"{n:.1f}P"
182
+
183
+
184
+ def gather_entries(backups_root: Path) -> list[dict]:
185
+ items: list[dict] = []
186
+ for entry in backups_root.iterdir():
187
+ name = entry.name
188
+ cls = classify(name)
189
+ if cls is None:
190
+ continue
191
+ klass, family = cls
192
+ ts = parse_timestamp(name)
193
+ if ts is None:
194
+ try:
195
+ ts = datetime.fromtimestamp(entry.stat().st_mtime, tz=timezone.utc)
196
+ except OSError:
197
+ ts = None
198
+ size = dir_size_bytes(entry) if entry.is_dir() else entry.stat().st_size
199
+ items.append({
200
+ "name": name,
201
+ "path": str(entry),
202
+ "class": klass,
203
+ "family": family,
204
+ "ts": ts,
205
+ "size": size,
206
+ "is_dir": entry.is_dir(),
207
+ })
208
+ return items
209
+
210
+
211
+ def plan_prunes(
212
+ items: list[dict],
213
+ *,
214
+ n_recent: int,
215
+ window_days: int,
216
+ only: str | None,
217
+ ) -> tuple[list[dict], list[dict]]:
218
+ """Return (to_delete, to_keep) among TECHNICAL items only."""
219
+ now = datetime.now(tz=timezone.utc)
220
+ to_delete: list[dict] = []
221
+ to_keep: list[dict] = []
222
+ by_family: dict[str, list[dict]] = {}
223
+ for it in items:
224
+ if it["class"] != "TECHNICAL":
225
+ continue
226
+ if only and it["family"] != only:
227
+ continue
228
+ by_family.setdefault(it["family"], []).append(it)
229
+
230
+ for family, group in by_family.items():
231
+ group.sort(key=lambda x: (x["ts"] or datetime.min.replace(tzinfo=timezone.utc)), reverse=True)
232
+ # Keep the N_RECENT most recent unconditionally.
233
+ keep_recent = group[:n_recent]
234
+ older = group[n_recent:]
235
+ recent_ts = {id(x) for x in keep_recent}
236
+ # From older, keep one per (year, month) if within window_days. The
237
+ # rest are pruned.
238
+ seen_months: set[tuple[int, int]] = set()
239
+ for it in older:
240
+ ts = it["ts"]
241
+ age_days = (now - ts).days if ts else 10_000
242
+ if age_days <= window_days and ts is not None:
243
+ ym = (ts.year, ts.month)
244
+ if ym not in seen_months:
245
+ seen_months.add(ym)
246
+ to_keep.append(it)
247
+ continue
248
+ to_delete.append(it)
249
+ to_keep.extend(keep_recent)
250
+ return to_delete, to_keep
251
+
252
+
253
+ def run(args: argparse.Namespace) -> int:
254
+ backups_root = Path(args.root or (default_nexo_home() / "runtime" / "backups"))
255
+ if not backups_root.is_dir():
256
+ print(f"ERROR: backups root not found: {backups_root}", file=sys.stderr)
257
+ return 1
258
+ items = gather_entries(backups_root)
259
+ tech_items = [i for i in items if i["class"] == "TECHNICAL"]
260
+ biz_items = [i for i in items if i["class"] == "BUSINESS"]
261
+ hourly_items = [i for i in items if i["class"] == "HOURLY_DB"]
262
+ root_db_items = [i for i in items if i["class"] == "ROOT_DB"]
263
+ unknown_items = [i for i in items if i["class"] == "UNKNOWN"]
264
+
265
+ to_delete, to_keep = plan_prunes(
266
+ items,
267
+ n_recent=args.recent,
268
+ window_days=args.window_days,
269
+ only=args.only,
270
+ )
271
+
272
+ total_all = sum(i["size"] for i in items)
273
+ total_del = sum(i["size"] for i in to_delete)
274
+
275
+ report = {
276
+ "root": str(backups_root),
277
+ "now_utc": datetime.now(tz=timezone.utc).isoformat(),
278
+ "policy": {
279
+ "n_recent": args.recent,
280
+ "window_days": args.window_days,
281
+ "only": args.only,
282
+ },
283
+ "totals": {
284
+ "all_bytes": total_all,
285
+ "all_human": human_size(total_all),
286
+ "delete_bytes": total_del,
287
+ "delete_human": human_size(total_del),
288
+ "delete_count": len(to_delete),
289
+ },
290
+ "counts_by_class": {
291
+ "technical": len(tech_items),
292
+ "business": len(biz_items),
293
+ "hourly_db": len(hourly_items),
294
+ "root_db": len(root_db_items),
295
+ "unknown": len(unknown_items),
296
+ },
297
+ "delete": [
298
+ {"name": i["name"], "family": i["family"], "size": i["size"],
299
+ "ts": i["ts"].isoformat() if i["ts"] else None}
300
+ for i in sorted(to_delete, key=lambda x: x["size"], reverse=True)
301
+ ],
302
+ "keep_sample": [
303
+ {"name": i["name"], "family": i["family"], "size": i["size"],
304
+ "ts": i["ts"].isoformat() if i["ts"] else None}
305
+ for i in sorted(to_keep, key=lambda x: (x["family"], x["ts"] or datetime.min.replace(tzinfo=timezone.utc)), reverse=True)[:30]
306
+ ],
307
+ "unknown": [i["name"] for i in unknown_items],
308
+ }
309
+
310
+ if args.json:
311
+ print(json.dumps(report, indent=2))
312
+ else:
313
+ print(f"NEXO backup prune — root: {backups_root}")
314
+ print(f" total on disk: {human_size(total_all)} ({len(items)} entries)")
315
+ print(f" technical: {len(tech_items)}")
316
+ print(f" business: {len(biz_items)} (protected)")
317
+ print(f" hourly_db: {len(hourly_items)} (managed by nexo-backup.sh)")
318
+ print(f" root_db: {len(root_db_items)} (never auto-pruned)")
319
+ print(f" unknown: {len(unknown_items)}")
320
+ print(f" policy: keep {args.recent} most-recent + 1 per month within {args.window_days}d")
321
+ if args.only:
322
+ print(f" restricted to family: {args.only}")
323
+ print()
324
+ print(f" would free: {human_size(total_del)} ({len(to_delete)} entries)")
325
+ if to_delete:
326
+ print("\nTOP 20 candidates:")
327
+ for it in sorted(to_delete, key=lambda x: x["size"], reverse=True)[:20]:
328
+ ts = it["ts"].strftime("%Y-%m-%d %H:%M") if it["ts"] else "?"
329
+ print(f" - {human_size(it['size']):>8} {ts} {it['name']}")
330
+ if unknown_items:
331
+ print("\nUNKNOWN entries (never pruned — review manually):")
332
+ for it in unknown_items[:20]:
333
+ print(f" ? {it['name']}")
334
+
335
+ if not args.apply:
336
+ if not args.json:
337
+ print("\n(dry-run: pass --apply to delete)")
338
+ return 0
339
+
340
+ deleted = 0
341
+ failed = 0
342
+ freed = 0
343
+ for it in to_delete:
344
+ p = Path(it["path"])
345
+ try:
346
+ if p.is_dir():
347
+ shutil.rmtree(p)
348
+ else:
349
+ p.unlink()
350
+ deleted += 1
351
+ freed += it["size"]
352
+ except OSError as e:
353
+ failed += 1
354
+ print(f"WARN: failed to delete {p}: {e}", file=sys.stderr)
355
+ print(f"\nDELETED {deleted} entries, freed {human_size(freed)}, failures: {failed}")
356
+ return 0 if failed == 0 else 1
357
+
358
+
359
+ def main() -> int:
360
+ ap = argparse.ArgumentParser(description="NEXO runtime backups prune (technical rollback tiers).")
361
+ ap.add_argument("--root", help="override runtime/backups path")
362
+ ap.add_argument("--apply", action="store_true", help="actually delete (default is dry-run)")
363
+ ap.add_argument("--json", action="store_true", help="machine-readable report")
364
+ ap.add_argument("--recent", type=int, default=10, help="N most recent per family to always keep (default: 10)")
365
+ ap.add_argument("--window-days", type=int, default=90, help="month-spaced retention window (default: 90)")
366
+ ap.add_argument("--only", help="restrict to one technical family (e.g. 'pre-backfill-owner')")
367
+ args = ap.parse_args()
368
+ try:
369
+ return run(args)
370
+ except KeyboardInterrupt:
371
+ print("interrupted", file=sys.stderr)
372
+ return 1
373
+
374
+
375
+ if __name__ == "__main__":
376
+ sys.exit(main())
@@ -10,7 +10,9 @@ Use this before claiming a release/publication is closed when you need a live ch
10
10
 
11
11
  ## Gotchas
12
12
  - A missing auto-resolved contract is a real blocker for the final release audit.
13
- - Smoke is version-line scoped. If no runner exists, the skill reports the skip explicitly instead of pretending it ran.
13
+ - Smoke is version-line scoped. The audit now requires a passing smoke artifact for the current version during final closeout, and the contract can tighten it further with `smoke.required_groups` and `smoke.max_age_hours`.
14
+ - Contracts may declare `critical_surfaces` to open installed/user-facing files or directories and verify markers before publication is considered closed.
15
+ - Contracts may declare `publication.status`, `publication.checklist_complete`, and `publication.blockers`; any open `high`/`critical` blocker now keeps publication blocked.
14
16
  - The script is read-only except for the optional official `nexo update` step during `final_closeout`; it still does not bump versions, tag, publish, or edit website worktrees.
15
17
  - `final_closeout` is intentionally stricter than the repo-only readiness pass: it fails if the release task was not closed with evidence or if its `change_log` row is missing.
16
18
  - If the touched area includes bootstrap, startup, or public claims, finish with the manual watchpoints in `docs/client-parity-checklist.md`.
@@ -245,6 +245,8 @@ def main() -> int:
245
245
  readiness_cmd.append("--require-contract-complete")
246
246
  elif require_contract_complete:
247
247
  print("[release-final-audit] require_contract_complete ignored because contract=none")
248
+ if include_smoke or final_closeout:
249
+ readiness_cmd.append("--require-smoke")
248
250
  if final_closeout:
249
251
  readiness_cmd.append("--final-closeout")
250
252
  if protocol_task_id.strip():
@@ -32,8 +32,38 @@ except Exception: # pragma: no cover - optional runtime dependency
32
32
  # Threads are daemon=True so they die when the MCP server process exits.
33
33
 
34
34
  KEEPALIVE_INTERVAL = 600 # 10 min — well inside the 15-min TTL
35
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
36
- SESSION_PORTABILITY_DIR = paths.operations_dir() / "session-portability"
35
+
36
+
37
+ # Path resolution moved to lazy functions (AUDITOR-V700-PASS2 §11, B10 item
38
+ # 3). The prior module-level NEXO_HOME / SESSION_PORTABILITY_DIR constants
39
+ # were evaluated at import time, so tests that monkeypatched NEXO_HOME or
40
+ # paths.operations_dir() after import saw stale values. The ``__getattr__``
41
+ # hook below keeps ``tools_sessions.SESSION_PORTABILITY_DIR`` / ``.NEXO_HOME``
42
+ # working for attribute-style access (re-evaluated on every read). The
43
+ # existing ``monkeypatch.setattr(tools_sessions, "SESSION_PORTABILITY_DIR",
44
+ # ...)`` pattern in tests keeps working because setattr inserts into the
45
+ # module __dict__ and shadows __getattr__.
46
+
47
+
48
+ def _nexo_home() -> Path:
49
+ return Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
50
+
51
+
52
+ def _session_portability_dir() -> Path:
53
+ return paths.operations_dir() / "session-portability"
54
+
55
+
56
+ _LAZY_PATHS = {
57
+ "NEXO_HOME": _nexo_home,
58
+ "SESSION_PORTABILITY_DIR": _session_portability_dir,
59
+ }
60
+
61
+
62
+ def __getattr__(name: str):
63
+ resolver = _LAZY_PATHS.get(name)
64
+ if resolver is None:
65
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
66
+ return resolver()
37
67
 
38
68
  _keepalive_threads: dict[str, threading.Event] = {} # sid → stop_event
39
69
 
@@ -264,7 +294,7 @@ def handle_session_export_bundle(sid: str = "", path: str = "") -> str:
264
294
  return json.dumps(bundle, ensure_ascii=False)
265
295
 
266
296
  session_id = bundle["session"]["sid"]
267
- export_path = Path(path).expanduser() if path else (SESSION_PORTABILITY_DIR / f"{session_id}.json")
297
+ export_path = Path(path).expanduser() if path else (_session_portability_dir() / f"{session_id}.json")
268
298
  export_path.parent.mkdir(parents=True, exist_ok=True)
269
299
  export_path.write_text(json.dumps(bundle, indent=2, ensure_ascii=False) + "\n")
270
300
  return json.dumps(
@@ -556,6 +586,27 @@ def handle_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
556
586
  def _handle_heartbeat_inner(sid: str, task: str, context_hint: str = '') -> str:
557
587
  """Inner body of handle_heartbeat — wrapped by tool_span above."""
558
588
  from db import get_db, update_last_heartbeat_ts
589
+
590
+ mandate_state = None
591
+ if context_hint:
592
+ try:
593
+ from autonomy_mandate import maybe_ingest_from_text
594
+
595
+ mandate_state = maybe_ingest_from_text(
596
+ context_hint,
597
+ session_id=sid,
598
+ source="heartbeat",
599
+ )
600
+ except Exception:
601
+ mandate_state = None
602
+ if mandate_state is None:
603
+ try:
604
+ from autonomy_mandate import load_state
605
+
606
+ mandate_state = load_state()
607
+ except Exception:
608
+ mandate_state = None
609
+
559
610
  update_session(sid, task)
560
611
  # v6.0.1 — stamp last_heartbeat_ts so the PostToolUse hook can
561
612
  # decide whether to surface a pending-inbox reminder on autopilot
@@ -602,6 +653,16 @@ def _handle_heartbeat_inner(sid: str, task: str, context_hint: str = '') -> str:
602
653
  except Exception:
603
654
  pass
604
655
 
656
+ try:
657
+ from autonomy_mandate import format_execution_latch_notice
658
+
659
+ latch_notice = format_execution_latch_notice(sid, state=mandate_state)
660
+ if latch_notice:
661
+ parts.append("")
662
+ parts.append(latch_notice)
663
+ except Exception:
664
+ pass
665
+
605
666
  # Incremental diary draft — accumulate every heartbeat, full UPSERT every 5
606
667
  _hb_count = 0 # Hoisted for Layer 3 DIARY_OVERDUE signal
607
668
  try:
@@ -1 +1 @@
1
- Protocol reminder for [[task_id]]: keep `nexo_heartbeat(...)` current and close it with `nexo_task_close(...)` plus evidence before saying it is resolved.[[change_note]]
1
+ Protocol reminder for [[task_id]]: keep `nexo_heartbeat(...)` current and do not close the conversation optimistically. Finish with `nexo_task_close(...)` plus concrete evidence before saying it is resolved.[[change_note]][[closeout_note]]
@@ -1 +1 @@
1
- R14 post-user-correction: the last user message was classified as a correction (or carried strongly negative valence) and three tool calls have elapsed without a nexo_learning_add. Capture the rule you just learned NOW via nexo_learning_add(category=..., title=..., content=..., reasoning=..., prevention=...). The auto_capture hook fires in parallel, but this reminder stays active until you either call learning_add or acknowledge the correction via nexo_cognitive_trust(event='correction'). Do not produce visible text for this reminder — act on it. After handling the silent action, continue with your previous task; this reminder is silent context, not a new instruction.
1
+ R14 post-user-correction: the last user message was classified as a correction (or carried strongly negative valence) and three tool calls have elapsed without a nexo_learning_add. Capture the rule you just learned NOW via nexo_learning_add(category=..., title=..., content=..., reasoning=..., prevention=...). If the correction led to real edits, close the task with `nexo_task_close(...)` plus concrete evidence and let that closeout capture the `change_log`; if you cannot finish cleanly in this turn, use `followup_needed=true` on the closeout instead of ending the conversation loosely. The auto_capture hook fires in parallel, but this reminder stays active until you either call learning_add or acknowledge the correction via nexo_cognitive_trust(event='correction'). Do not produce visible text for this reminder — act on it. After handling the silent action, continue with your previous task; this reminder is silent context, not a new instruction.