nexo-brain 2.6.5 → 2.6.7
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 +911 -143
- package/bin/nexo-brain.js +256 -0
- package/package.json +1 -1
- package/src/auto_close_sessions.py +24 -4
- package/src/auto_update.py +45 -6
- package/src/cli.py +136 -3
- package/src/db/_episodic.py +5 -16
- package/src/doctor/providers/runtime.py +5 -0
- package/src/evolution_cycle.py +51 -1
- package/src/plugins/episodic_memory.py +1 -1
- package/src/plugins/personal_plugins.py +135 -0
- package/src/plugins/update.py +25 -3
- package/src/public_contribution.py +396 -0
- package/src/runtime_power.py +416 -0
- package/src/scripts/nexo-evolution-run.py +394 -2
- package/templates/plugin-template.py +36 -0
package/bin/nexo-brain.js
CHANGED
|
@@ -31,6 +31,8 @@ const LAUNCH_AGENTS = path.join(
|
|
|
31
31
|
"Library",
|
|
32
32
|
"LaunchAgents"
|
|
33
33
|
);
|
|
34
|
+
const MACOS_FDA_SETTINGS_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles";
|
|
35
|
+
const PUBLIC_CONTRIBUTION_UPSTREAM = "wazionapps/nexo";
|
|
34
36
|
|
|
35
37
|
function isEphemeralInstall(nexoHome) {
|
|
36
38
|
const homeDir = require("os").homedir();
|
|
@@ -114,6 +116,139 @@ function logMacPermissionsNotice(nexoHome, pythonPath = "") {
|
|
|
114
116
|
log(" System Settings → Privacy & Security → Full Disk Access");
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
function getRuntimePythonTargets(pythonPath = "") {
|
|
120
|
+
const candidates = [];
|
|
121
|
+
const venvPy = path.join(NEXO_HOME, ".venv", "bin", "python3");
|
|
122
|
+
if (fs.existsSync(venvPy)) candidates.push(venvPy);
|
|
123
|
+
if (pythonPath) candidates.push(pythonPath);
|
|
124
|
+
const discovered = run("which python3") || run("which python") || "";
|
|
125
|
+
if (discovered) candidates.push(discovered);
|
|
126
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function detectFullDiskAccessReasons(nexoHome) {
|
|
130
|
+
if (process.platform !== "darwin") return [];
|
|
131
|
+
const reasons = [];
|
|
132
|
+
if (isProtectedMacPath(nexoHome)) {
|
|
133
|
+
reasons.push(`NEXO_HOME is inside a protected macOS folder: ${nexoHome}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const logsDir = path.join(nexoHome, "logs");
|
|
137
|
+
if (fs.existsSync(logsDir)) {
|
|
138
|
+
const candidates = fs.readdirSync(logsDir).filter((name) => name.endsWith("-stderr.log"));
|
|
139
|
+
for (const name of candidates) {
|
|
140
|
+
try {
|
|
141
|
+
const text = fs.readFileSync(path.join(logsDir, name), "utf8");
|
|
142
|
+
if (text.includes("Operation not permitted")) {
|
|
143
|
+
reasons.push(`Recent background job stderr hit 'Operation not permitted' (${name})`);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
} catch {}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return reasons;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function probeFullDiskAccess(nexoHome) {
|
|
153
|
+
if (process.platform !== "darwin") {
|
|
154
|
+
return { checked: false, granted: null, probePath: "", message: "macOS-only" };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const candidates = [
|
|
158
|
+
path.join(require("os").homedir(), "Library", "Application Support", "com.apple.TCC", "TCC.db"),
|
|
159
|
+
path.join(require("os").homedir(), "Library", "Mail"),
|
|
160
|
+
path.join(require("os").homedir(), "Library", "Messages"),
|
|
161
|
+
path.join(require("os").homedir(), "Library", "Safari"),
|
|
162
|
+
path.join(require("os").homedir(), "Library", "Application Support", "AddressBook"),
|
|
163
|
+
].filter((item) => fs.existsSync(item));
|
|
164
|
+
|
|
165
|
+
if (isProtectedMacPath(nexoHome)) candidates.push(nexoHome);
|
|
166
|
+
if (!candidates.length) {
|
|
167
|
+
return { checked: false, granted: null, probePath: "", message: "No probe path available." };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const seen = new Set();
|
|
171
|
+
for (const candidate of candidates) {
|
|
172
|
+
if (!candidate || seen.has(candidate)) continue;
|
|
173
|
+
seen.add(candidate);
|
|
174
|
+
const result = spawnSync("/bin/bash", [
|
|
175
|
+
"-lc",
|
|
176
|
+
'TARGET="$1"; if [ -d "$TARGET" ]; then ls "$TARGET" >/dev/null 2>&1; else head -c 1 "$TARGET" >/dev/null 2>&1; fi',
|
|
177
|
+
"_",
|
|
178
|
+
candidate,
|
|
179
|
+
], { encoding: "utf8" });
|
|
180
|
+
if (result.status === 0) {
|
|
181
|
+
return { checked: true, granted: true, probePath: candidate, message: "" };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { checked: true, granted: false, probePath: candidates[0], message: "Could not verify Full Disk Access yet." };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function maybeConfigureFullDiskAccess(schedule, useDefaults, pythonPath = "") {
|
|
188
|
+
const current = String((schedule && schedule.full_disk_access_status) || "unset").toLowerCase();
|
|
189
|
+
schedule.full_disk_access_status_version = 1;
|
|
190
|
+
const reasons = detectFullDiskAccessReasons(NEXO_HOME);
|
|
191
|
+
schedule.full_disk_access_reasons = reasons;
|
|
192
|
+
|
|
193
|
+
if (process.platform !== "darwin" || !reasons.length) {
|
|
194
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
195
|
+
return schedule;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (current === "granted") {
|
|
199
|
+
const probe = probeFullDiskAccess(NEXO_HOME);
|
|
200
|
+
if (probe.granted) {
|
|
201
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
202
|
+
return schedule;
|
|
203
|
+
}
|
|
204
|
+
schedule.full_disk_access_status = "later";
|
|
205
|
+
} else if (current === "declined") {
|
|
206
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
207
|
+
return schedule;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (useDefaults || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
211
|
+
schedule.full_disk_access_status = current === "granted" ? "later" : current || "unset";
|
|
212
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
213
|
+
return schedule;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log("");
|
|
217
|
+
log("Optional macOS Full Disk Access guidance:");
|
|
218
|
+
log("macOS does not allow granting this automatically. NEXO can only open the correct System Settings screen and verify best effort.");
|
|
219
|
+
log("Reason(s) detected:");
|
|
220
|
+
reasons.forEach((item) => log(` - ${item}`));
|
|
221
|
+
log("If you proceed, add your terminal app and, if needed for background jobs, these binaries:");
|
|
222
|
+
log(" - /bin/bash");
|
|
223
|
+
getRuntimePythonTargets(pythonPath).forEach((item) => log(` - ${item}`));
|
|
224
|
+
|
|
225
|
+
const answer = (await ask(" Open Full Disk Access setup now? [y/N/later]: ")).trim().toLowerCase();
|
|
226
|
+
if (answer === "y" || answer === "yes") {
|
|
227
|
+
spawnSync("open", [MACOS_FDA_SETTINGS_URL], { stdio: "ignore" });
|
|
228
|
+
log("Opened System Settings → Privacy & Security → Full Disk Access.");
|
|
229
|
+
const followUp = (await ask(" Press Enter after granting it, or type later to skip for now: ")).trim().toLowerCase();
|
|
230
|
+
if (followUp === "later" || followUp === "l") {
|
|
231
|
+
schedule.full_disk_access_status = "later";
|
|
232
|
+
} else {
|
|
233
|
+
const probe = probeFullDiskAccess(NEXO_HOME);
|
|
234
|
+
if (probe.granted) {
|
|
235
|
+
schedule.full_disk_access_status = "granted";
|
|
236
|
+
log(`Full Disk Access verified via ${probe.probePath}.`);
|
|
237
|
+
} else {
|
|
238
|
+
schedule.full_disk_access_status = "later";
|
|
239
|
+
log("Could not verify Full Disk Access yet. NEXO will remind you later if background jobs still hit TCC.");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} else if (answer === "later" || answer === "l" || answer === "") {
|
|
243
|
+
schedule.full_disk_access_status = "later";
|
|
244
|
+
} else {
|
|
245
|
+
schedule.full_disk_access_status = "declined";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
249
|
+
return schedule;
|
|
250
|
+
}
|
|
251
|
+
|
|
117
252
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
118
253
|
// CORE PROCESS & HOOK DEFINITIONS
|
|
119
254
|
// All core nightly/periodic processes and all 8 core hooks that make NEXO functional.
|
|
@@ -296,6 +431,25 @@ function getDefaultSchedule(timezone) {
|
|
|
296
431
|
auto_update: true,
|
|
297
432
|
power_policy: "unset",
|
|
298
433
|
power_policy_version: 2,
|
|
434
|
+
full_disk_access_status: "unset",
|
|
435
|
+
full_disk_access_status_version: 1,
|
|
436
|
+
full_disk_access_reasons: [],
|
|
437
|
+
public_contribution: {
|
|
438
|
+
enabled: false,
|
|
439
|
+
mode: "unset",
|
|
440
|
+
consent_version: 1,
|
|
441
|
+
github_user: "",
|
|
442
|
+
upstream_repo: PUBLIC_CONTRIBUTION_UPSTREAM,
|
|
443
|
+
fork_repo: "",
|
|
444
|
+
machine_id: crypto.createHash("sha1").update(require("os").hostname()).digest("hex").slice(0, 12),
|
|
445
|
+
active_pr_url: "",
|
|
446
|
+
active_pr_number: null,
|
|
447
|
+
active_branch: "",
|
|
448
|
+
status: "unset",
|
|
449
|
+
cooldown_until: "",
|
|
450
|
+
last_run_at: "",
|
|
451
|
+
last_result: "",
|
|
452
|
+
},
|
|
299
453
|
processes: {
|
|
300
454
|
"cognitive-decay": { hour: 3, minute: 0 },
|
|
301
455
|
"postmortem": { hour: 23, minute: 30 },
|
|
@@ -308,6 +462,23 @@ function getDefaultSchedule(timezone) {
|
|
|
308
462
|
};
|
|
309
463
|
}
|
|
310
464
|
|
|
465
|
+
function normalizePublicContributionConfig(config = {}) {
|
|
466
|
+
const base = getDefaultSchedule().public_contribution;
|
|
467
|
+
const merged = { ...base, ...(config || {}) };
|
|
468
|
+
merged.enabled = Boolean(merged.enabled);
|
|
469
|
+
merged.mode = String(merged.mode || "unset").toLowerCase();
|
|
470
|
+
merged.status = String(merged.status || "unset").toLowerCase();
|
|
471
|
+
merged.github_user = String(merged.github_user || "").trim();
|
|
472
|
+
merged.fork_repo = String(merged.fork_repo || "").trim();
|
|
473
|
+
merged.upstream_repo = String(merged.upstream_repo || PUBLIC_CONTRIBUTION_UPSTREAM).trim() || PUBLIC_CONTRIBUTION_UPSTREAM;
|
|
474
|
+
merged.active_pr_url = String(merged.active_pr_url || "").trim();
|
|
475
|
+
merged.active_branch = String(merged.active_branch || "").trim();
|
|
476
|
+
merged.cooldown_until = String(merged.cooldown_until || "").trim();
|
|
477
|
+
merged.last_run_at = String(merged.last_run_at || "").trim();
|
|
478
|
+
merged.last_result = String(merged.last_result || "").trim();
|
|
479
|
+
return merged;
|
|
480
|
+
}
|
|
481
|
+
|
|
311
482
|
async function maybeConfigurePowerPolicy(schedule, useDefaults) {
|
|
312
483
|
const current = String((schedule && schedule.power_policy) || "unset").toLowerCase();
|
|
313
484
|
if (current && current !== "unset") {
|
|
@@ -340,6 +511,84 @@ async function maybeConfigurePowerPolicy(schedule, useDefaults) {
|
|
|
340
511
|
return schedule;
|
|
341
512
|
}
|
|
342
513
|
|
|
514
|
+
function ghLogin() {
|
|
515
|
+
const login = run("gh api user --jq .login 2>/dev/null");
|
|
516
|
+
return (login || "").trim();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function ensureFork(login) {
|
|
520
|
+
if (!login) return { ok: false, message: "Missing GitHub login.", forkRepo: "" };
|
|
521
|
+
const forkRepo = `${login}/nexo`;
|
|
522
|
+
const existing = run(`gh repo view "${forkRepo}" --json nameWithOwner 2>/dev/null`);
|
|
523
|
+
if (existing) return { ok: true, message: "", forkRepo };
|
|
524
|
+
const created = run(`gh repo fork "${PUBLIC_CONTRIBUTION_UPSTREAM}" --clone=false --remote=false 2>/dev/null`);
|
|
525
|
+
if (created !== null) return { ok: true, message: "", forkRepo };
|
|
526
|
+
return { ok: false, message: `Could not ensure fork ${forkRepo}.`, forkRepo: "" };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function maybeConfigurePublicContribution(schedule, useDefaults) {
|
|
530
|
+
const current = normalizePublicContributionConfig((schedule && schedule.public_contribution) || {});
|
|
531
|
+
if (current.mode && current.mode !== "unset") {
|
|
532
|
+
schedule.public_contribution = current;
|
|
533
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
534
|
+
return schedule;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (useDefaults || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
538
|
+
schedule.public_contribution = current;
|
|
539
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
540
|
+
return schedule;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
console.log("");
|
|
544
|
+
log("Optional public contribution mode:");
|
|
545
|
+
log("If enabled, this machine may prepare core NEXO improvements from an isolated checkout and open a Draft PR to the public repository.");
|
|
546
|
+
log("NEXO never auto-merges, and it pauses public evolution on this machine while that Draft PR stays open.");
|
|
547
|
+
log("Public contribution must never publish personal scripts, runtime data, local prompts, logs, or secrets.");
|
|
548
|
+
const answer = (await ask(" Enable public contribution via Draft PRs on this machine? [y/N/later]: ")).trim().toLowerCase();
|
|
549
|
+
if (answer === "y" || answer === "yes") {
|
|
550
|
+
const login = ghLogin();
|
|
551
|
+
if (!login) {
|
|
552
|
+
current.enabled = false;
|
|
553
|
+
current.mode = "pending_auth";
|
|
554
|
+
current.status = "pending_auth";
|
|
555
|
+
current.github_user = "";
|
|
556
|
+
current.fork_repo = "";
|
|
557
|
+
log("GitHub CLI authentication is missing. Contributor mode is pending until 'gh auth login' succeeds.");
|
|
558
|
+
} else {
|
|
559
|
+
const fork = ensureFork(login);
|
|
560
|
+
if (!fork.ok) {
|
|
561
|
+
current.enabled = false;
|
|
562
|
+
current.mode = "pending_auth";
|
|
563
|
+
current.status = "pending_auth";
|
|
564
|
+
current.github_user = login;
|
|
565
|
+
current.fork_repo = "";
|
|
566
|
+
log(fork.message || "Could not ensure a GitHub fork.");
|
|
567
|
+
} else {
|
|
568
|
+
current.enabled = true;
|
|
569
|
+
current.mode = "draft_prs";
|
|
570
|
+
current.status = "active";
|
|
571
|
+
current.github_user = login;
|
|
572
|
+
current.fork_repo = fork.forkRepo;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
} else if (answer === "later" || answer === "l" || answer === "") {
|
|
576
|
+
current.enabled = false;
|
|
577
|
+
current.mode = "unset";
|
|
578
|
+
current.status = "unset";
|
|
579
|
+
} else {
|
|
580
|
+
current.enabled = false;
|
|
581
|
+
current.mode = "off";
|
|
582
|
+
current.status = "off";
|
|
583
|
+
current.github_user = "";
|
|
584
|
+
current.fork_repo = "";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
schedule.public_contribution = current;
|
|
588
|
+
fs.writeFileSync(path.join(NEXO_HOME, "config", "schedule.json"), JSON.stringify(schedule, null, 2));
|
|
589
|
+
return schedule;
|
|
590
|
+
}
|
|
591
|
+
|
|
343
592
|
/**
|
|
344
593
|
* Resolve the venv python path for an existing NEXO_HOME installation.
|
|
345
594
|
*/
|
|
@@ -843,7 +1092,9 @@ async function main() {
|
|
|
843
1092
|
// Regenerate all core LaunchAgents / systemd timers
|
|
844
1093
|
let migSchedule = loadOrCreateSchedule(NEXO_HOME);
|
|
845
1094
|
migSchedule = await maybeConfigurePowerPolicy(migSchedule, useDefaults);
|
|
1095
|
+
migSchedule = await maybeConfigurePublicContribution(migSchedule, useDefaults);
|
|
846
1096
|
const migPython = findVenvPython(NEXO_HOME) || "python3";
|
|
1097
|
+
migSchedule = await maybeConfigureFullDiskAccess(migSchedule, useDefaults, migPython);
|
|
847
1098
|
let migOptionals = {};
|
|
848
1099
|
try {
|
|
849
1100
|
const optFile = path.join(NEXO_HOME, "config", "optionals.json");
|
|
@@ -1478,6 +1729,7 @@ async function main() {
|
|
|
1478
1729
|
);
|
|
1479
1730
|
|
|
1480
1731
|
// Copy source files
|
|
1732
|
+
log("Copying core runtime files...");
|
|
1481
1733
|
const srcDir = path.join(__dirname, "..", "src");
|
|
1482
1734
|
const pluginsSrcDir = path.join(srcDir, "plugins");
|
|
1483
1735
|
const scriptsSrcDir = path.join(srcDir, "scripts");
|
|
@@ -1556,6 +1808,7 @@ async function main() {
|
|
|
1556
1808
|
fs.writeFileSync(runtimeCliPath, runtimeCli);
|
|
1557
1809
|
fs.chmodSync(runtimeCliPath, 0o755);
|
|
1558
1810
|
|
|
1811
|
+
log("Copying core packages...");
|
|
1559
1812
|
// Core packages (directories with __init__.py)
|
|
1560
1813
|
["db", "cognitive", "doctor"].forEach(pkg => {
|
|
1561
1814
|
const pkgSrc = path.join(srcDir, pkg);
|
|
@@ -1564,6 +1817,7 @@ async function main() {
|
|
|
1564
1817
|
}
|
|
1565
1818
|
});
|
|
1566
1819
|
|
|
1820
|
+
log("Copying plugins, scripts, and templates...");
|
|
1567
1821
|
// Plugins (all .py files in plugins/)
|
|
1568
1822
|
fs.mkdirSync(path.join(NEXO_HOME, "plugins"), { recursive: true });
|
|
1569
1823
|
if (fs.existsSync(pluginsSrcDir)) {
|
|
@@ -2116,6 +2370,8 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
2116
2370
|
log("Setting up automated processes...");
|
|
2117
2371
|
let schedule = loadOrCreateSchedule(NEXO_HOME);
|
|
2118
2372
|
schedule = await maybeConfigurePowerPolicy(schedule, useDefaults);
|
|
2373
|
+
schedule = await maybeConfigurePublicContribution(schedule, useDefaults);
|
|
2374
|
+
schedule = await maybeConfigureFullDiskAccess(schedule, useDefaults, python);
|
|
2119
2375
|
const enabledOptionals = { dashboard: doDashboard };
|
|
2120
2376
|
if (isEphemeralInstall(NEXO_HOME)) {
|
|
2121
2377
|
log("Ephemeral HOME/NEXO_HOME detected — skipping LaunchAgents installation.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.7",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — local cognitive runtime for Claude Code. Persistent memory, overnight learning, recovery-aware crons, personal scripts, doctor diagnostics, startup preflight, and optional power helper.",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@ os.environ["NEXO_SKIP_FS_INDEX"] = "1" # Skip FTS rebuild on import
|
|
|
17
17
|
|
|
18
18
|
from db import (
|
|
19
19
|
init_db, get_db, get_diary_draft, delete_diary_draft,
|
|
20
|
-
get_orphan_sessions, write_session_diary, now_epoch,
|
|
20
|
+
get_orphan_sessions, read_checkpoint, write_session_diary, now_epoch,
|
|
21
21
|
SESSION_STALE_SECONDS,
|
|
22
22
|
)
|
|
23
23
|
|
|
@@ -67,9 +67,18 @@ def promote_draft_to_diary(sid: str, draft: dict, task: str = ""):
|
|
|
67
67
|
context_hint = draft.get("last_context_hint", "")
|
|
68
68
|
hb_count = draft.get("heartbeat_count", 0)
|
|
69
69
|
|
|
70
|
+
checkpoint = read_checkpoint(sid) or {}
|
|
70
71
|
summary_parts = []
|
|
71
72
|
if draft.get("summary_draft"):
|
|
72
73
|
summary_parts.append(draft["summary_draft"])
|
|
74
|
+
if task and task not in " ".join(summary_parts):
|
|
75
|
+
summary_parts.append(f"Final task: {task}")
|
|
76
|
+
if context_hint:
|
|
77
|
+
summary_parts.append(f"Latest context: {context_hint[:300]}")
|
|
78
|
+
if checkpoint.get("current_goal"):
|
|
79
|
+
summary_parts.append(f"Current goal: {str(checkpoint['current_goal'])[:300]}")
|
|
80
|
+
if checkpoint.get("next_step"):
|
|
81
|
+
summary_parts.append(f"Next step was: {str(checkpoint['next_step'])[:240]}")
|
|
73
82
|
|
|
74
83
|
tool_summary = get_tool_log_summary(sid)
|
|
75
84
|
if tool_summary:
|
|
@@ -98,6 +107,10 @@ def promote_draft_to_diary(sid: str, draft: dict, task: str = ""):
|
|
|
98
107
|
context_next = f"Last topic: {context_hint}"
|
|
99
108
|
if tasks:
|
|
100
109
|
context_next += f" | Tasks: {', '.join(tasks[-5:])}"
|
|
110
|
+
if checkpoint.get("reasoning_thread"):
|
|
111
|
+
context_next += f" | Reasoning: {str(checkpoint['reasoning_thread'])[:240]}"
|
|
112
|
+
if checkpoint.get("active_files"):
|
|
113
|
+
context_next += f" | Active files: {str(checkpoint['active_files'])[:180]}"
|
|
101
114
|
|
|
102
115
|
write_session_diary(
|
|
103
116
|
session_id=sid,
|
|
@@ -131,12 +144,19 @@ def main():
|
|
|
131
144
|
if draft:
|
|
132
145
|
promote_draft_to_diary(sid, draft, task=session.get("task", ""))
|
|
133
146
|
else:
|
|
147
|
+
checkpoint = read_checkpoint(sid) or {}
|
|
148
|
+
tool_summary = get_tool_log_summary(sid)
|
|
149
|
+
summary_parts = [f"Auto-closed session. Task: {session.get('task', 'unknown')}"]
|
|
150
|
+
if checkpoint.get("current_goal"):
|
|
151
|
+
summary_parts.append(f"Current goal: {str(checkpoint['current_goal'])[:300]}")
|
|
152
|
+
if tool_summary:
|
|
153
|
+
summary_parts.append(tool_summary)
|
|
134
154
|
write_session_diary(
|
|
135
155
|
session_id=sid,
|
|
136
156
|
decisions="No decisions logged",
|
|
137
|
-
summary=
|
|
138
|
-
context_next="",
|
|
139
|
-
mental_state="[auto-close] No draft available.
|
|
157
|
+
summary=" | ".join(summary_parts),
|
|
158
|
+
context_next=str(checkpoint.get("next_step") or ""),
|
|
159
|
+
mental_state="[auto-close] No draft available. Diary reconstructed from task/checkpoint/tool logs.",
|
|
140
160
|
self_critique="[auto-close] Session terminated without diary or draft.",
|
|
141
161
|
source="auto-close",
|
|
142
162
|
)
|
package/src/auto_update.py
CHANGED
|
@@ -1236,7 +1236,7 @@ def _restore_runtime_tree(backup_dir: str, dest: Path = NEXO_HOME) -> None:
|
|
|
1236
1236
|
shutil.copy2(str(item), str(target))
|
|
1237
1237
|
|
|
1238
1238
|
|
|
1239
|
-
def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_HOME) -> dict:
|
|
1239
|
+
def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_HOME, progress_fn=None) -> dict:
|
|
1240
1240
|
import shutil
|
|
1241
1241
|
|
|
1242
1242
|
packages = ["db", "cognitive", "doctor", "dashboard", "rules", "crons", "hooks"]
|
|
@@ -1253,6 +1253,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1253
1253
|
copied_packages = 0
|
|
1254
1254
|
copied_files = 0
|
|
1255
1255
|
|
|
1256
|
+
_emit_progress(progress_fn, "Copying core packages...")
|
|
1256
1257
|
for pkg in packages:
|
|
1257
1258
|
pkg_src = src_dir / pkg
|
|
1258
1259
|
pkg_dest = dest / pkg
|
|
@@ -1266,12 +1267,14 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1266
1267
|
)
|
|
1267
1268
|
copied_packages += 1
|
|
1268
1269
|
|
|
1270
|
+
_emit_progress(progress_fn, "Copying core modules...")
|
|
1269
1271
|
for name in flat_files:
|
|
1270
1272
|
src_file = src_dir / name
|
|
1271
1273
|
if src_file.is_file():
|
|
1272
1274
|
shutil.copy2(str(src_file), str(dest / name))
|
|
1273
1275
|
copied_files += 1
|
|
1274
1276
|
|
|
1277
|
+
_emit_progress(progress_fn, "Copying plugin modules...")
|
|
1275
1278
|
plugins_src = src_dir / "plugins"
|
|
1276
1279
|
plugins_dest = dest / "plugins"
|
|
1277
1280
|
if plugins_src.is_dir():
|
|
@@ -1280,6 +1283,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1280
1283
|
if item.is_file() and item.suffix == ".py":
|
|
1281
1284
|
shutil.copy2(str(item), str(plugins_dest / item.name))
|
|
1282
1285
|
|
|
1286
|
+
_emit_progress(progress_fn, "Copying scripts...")
|
|
1283
1287
|
scripts_src = src_dir / "scripts"
|
|
1284
1288
|
scripts_dest = dest / "scripts"
|
|
1285
1289
|
if scripts_src.is_dir():
|
|
@@ -1297,6 +1301,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1297
1301
|
if item.suffix == ".sh":
|
|
1298
1302
|
dst.chmod(0o755)
|
|
1299
1303
|
|
|
1304
|
+
_emit_progress(progress_fn, "Copying templates and version metadata...")
|
|
1300
1305
|
templates_src = repo_dir / "templates"
|
|
1301
1306
|
templates_dest = dest / "templates"
|
|
1302
1307
|
if templates_src.is_dir():
|
|
@@ -1317,6 +1322,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1317
1322
|
except Exception:
|
|
1318
1323
|
pass
|
|
1319
1324
|
|
|
1325
|
+
_emit_progress(progress_fn, "Copying core skills and runtime wrapper...")
|
|
1320
1326
|
skills_src = src_dir / "skills"
|
|
1321
1327
|
skills_dest = dest / "skills-core"
|
|
1322
1328
|
if skills_src.is_dir():
|
|
@@ -1365,10 +1371,11 @@ def _reinstall_runtime_pip_deps(runtime_root: Path = NEXO_HOME) -> bool:
|
|
|
1365
1371
|
return False
|
|
1366
1372
|
|
|
1367
1373
|
|
|
1368
|
-
def _run_runtime_post_sync(dest: Path = NEXO_HOME) -> tuple[bool, list[str]]:
|
|
1374
|
+
def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bool, list[str]]:
|
|
1369
1375
|
actions: list[str] = []
|
|
1370
1376
|
env = {**os.environ, "NEXO_HOME": str(dest), "NEXO_CODE": str(dest)}
|
|
1371
1377
|
try:
|
|
1378
|
+
_emit_progress(progress_fn, "Initializing database and reconciling personal schedules...")
|
|
1372
1379
|
init_result = subprocess.run(
|
|
1373
1380
|
[
|
|
1374
1381
|
sys.executable,
|
|
@@ -1394,6 +1401,7 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME) -> tuple[bool, list[str]]:
|
|
|
1394
1401
|
except Exception as e:
|
|
1395
1402
|
return False, [f"runtime init error: {e}"]
|
|
1396
1403
|
|
|
1404
|
+
_emit_progress(progress_fn, "Reconciling Python dependencies...")
|
|
1397
1405
|
if _reinstall_runtime_pip_deps(dest):
|
|
1398
1406
|
actions.append("pip-deps")
|
|
1399
1407
|
else:
|
|
@@ -1402,6 +1410,7 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME) -> tuple[bool, list[str]]:
|
|
|
1402
1410
|
sync_path = dest / "crons" / "sync.py"
|
|
1403
1411
|
if sync_path.is_file():
|
|
1404
1412
|
try:
|
|
1413
|
+
_emit_progress(progress_fn, "Syncing core cron definitions...")
|
|
1405
1414
|
sync_result = subprocess.run(
|
|
1406
1415
|
[sys.executable, str(sync_path)],
|
|
1407
1416
|
cwd=str(dest),
|
|
@@ -1418,10 +1427,12 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME) -> tuple[bool, list[str]]:
|
|
|
1418
1427
|
|
|
1419
1428
|
from runtime_power import apply_power_policy
|
|
1420
1429
|
|
|
1430
|
+
_emit_progress(progress_fn, "Refreshing runtime power helper...")
|
|
1421
1431
|
power_result = apply_power_policy()
|
|
1422
1432
|
if power_result.get("ok"):
|
|
1423
1433
|
actions.append(f"power:{power_result.get('action')}")
|
|
1424
1434
|
|
|
1435
|
+
_emit_progress(progress_fn, "Verifying runtime imports...")
|
|
1425
1436
|
verify = subprocess.run(
|
|
1426
1437
|
[sys.executable, "-c", "import server"],
|
|
1427
1438
|
cwd=str(dest),
|
|
@@ -1460,11 +1471,20 @@ def _write_update_summary(summary: dict):
|
|
|
1460
1471
|
_log(f"Failed to write update summary: {e}")
|
|
1461
1472
|
|
|
1462
1473
|
|
|
1463
|
-
def
|
|
1474
|
+
def _emit_progress(progress_fn, message: str) -> None:
|
|
1475
|
+
if callable(progress_fn):
|
|
1476
|
+
try:
|
|
1477
|
+
progress_fn(message)
|
|
1478
|
+
except Exception:
|
|
1479
|
+
pass
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def manual_sync_update(*, interactive: bool = False, allow_source_pull: bool = True, progress_fn=None) -> dict:
|
|
1464
1483
|
src_dir, repo_dir = _resolve_sync_source()
|
|
1465
1484
|
if src_dir is None or repo_dir is None:
|
|
1466
1485
|
return {"ok": False, "mode": "sync", "error": "No source repo recorded for this runtime."}
|
|
1467
1486
|
|
|
1487
|
+
_emit_progress(progress_fn, "Checking recorded source repository...")
|
|
1468
1488
|
source_status = _source_repo_status(repo_dir)
|
|
1469
1489
|
pulled = False
|
|
1470
1490
|
old_head = source_status.get("local_head")
|
|
@@ -1474,17 +1494,21 @@ def manual_sync_update(*, interactive: bool = False, allow_source_pull: bool = T
|
|
|
1474
1494
|
elif source_status.get("diverged"):
|
|
1475
1495
|
_log("Source repo diverged; syncing local tree without remote pull.")
|
|
1476
1496
|
elif source_status.get("behind"):
|
|
1497
|
+
_emit_progress(progress_fn, "Pulling latest source changes...")
|
|
1477
1498
|
rc, _, pull_err = _git_in_repo(repo_dir, "pull", "--ff-only", timeout=60)
|
|
1478
1499
|
if rc != 0:
|
|
1479
1500
|
return {"ok": False, "mode": "sync", "error": pull_err or "git pull failed"}
|
|
1480
1501
|
pulled = True
|
|
1481
1502
|
|
|
1503
|
+
_emit_progress(progress_fn, "Creating runtime backups...")
|
|
1482
1504
|
db_backup_dir = _backup_dbs()
|
|
1483
1505
|
tree_backup_dir = _backup_runtime_tree(NEXO_HOME)
|
|
1484
1506
|
sync_result = {"ok": False, "mode": "sync", "pulled_source": pulled, "backup_dir": db_backup_dir, "tree_backup": tree_backup_dir}
|
|
1485
1507
|
try:
|
|
1486
|
-
|
|
1487
|
-
|
|
1508
|
+
_emit_progress(progress_fn, "Syncing runtime files...")
|
|
1509
|
+
copy_stats = _copy_runtime_from_source(src_dir, repo_dir, NEXO_HOME, progress_fn=progress_fn)
|
|
1510
|
+
_emit_progress(progress_fn, "Reconciling runtime state...")
|
|
1511
|
+
ok, actions = _run_runtime_post_sync(NEXO_HOME, progress_fn=progress_fn)
|
|
1488
1512
|
if not ok:
|
|
1489
1513
|
raise RuntimeError("; ".join(actions))
|
|
1490
1514
|
sync_result.update({
|
|
@@ -1496,7 +1520,9 @@ def manual_sync_update(*, interactive: bool = False, allow_source_pull: bool = T
|
|
|
1496
1520
|
"source": copy_stats["source"],
|
|
1497
1521
|
"repo": copy_stats["repo"],
|
|
1498
1522
|
})
|
|
1523
|
+
_emit_progress(progress_fn, "Runtime update completed.")
|
|
1499
1524
|
except Exception as e:
|
|
1525
|
+
_emit_progress(progress_fn, "Update failed; restoring previous runtime state...")
|
|
1500
1526
|
_restore_runtime_tree(tree_backup_dir, NEXO_HOME)
|
|
1501
1527
|
if db_backup_dir:
|
|
1502
1528
|
_restore_dbs(db_backup_dir)
|
|
@@ -1522,15 +1548,26 @@ def startup_preflight(*, entrypoint: str, interactive: bool = False) -> dict:
|
|
|
1522
1548
|
"migrations": [],
|
|
1523
1549
|
"power_policy": None,
|
|
1524
1550
|
"power_message": None,
|
|
1551
|
+
"full_disk_access_status": None,
|
|
1552
|
+
"full_disk_access_message": None,
|
|
1525
1553
|
"error": None,
|
|
1526
1554
|
}
|
|
1527
1555
|
|
|
1528
|
-
from runtime_power import
|
|
1556
|
+
from runtime_power import (
|
|
1557
|
+
apply_power_policy,
|
|
1558
|
+
ensure_power_policy_choice,
|
|
1559
|
+
get_power_policy,
|
|
1560
|
+
ensure_full_disk_access_choice,
|
|
1561
|
+
get_full_disk_access_status,
|
|
1562
|
+
)
|
|
1529
1563
|
|
|
1530
1564
|
choice = ensure_power_policy_choice(interactive=interactive, reason=entrypoint)
|
|
1531
1565
|
power_result = apply_power_policy(choice.get("policy"))
|
|
1566
|
+
fda_choice = ensure_full_disk_access_choice(interactive=interactive, reason=entrypoint)
|
|
1532
1567
|
result["power_policy"] = choice.get("policy") or get_power_policy()
|
|
1533
1568
|
result["power_message"] = power_result.get("message")
|
|
1569
|
+
result["full_disk_access_status"] = fda_choice.get("status") or get_full_disk_access_status()
|
|
1570
|
+
result["full_disk_access_message"] = fda_choice.get("message")
|
|
1534
1571
|
if power_result.get("ok"):
|
|
1535
1572
|
result["actions"].append(f"power:{power_result.get('action')}")
|
|
1536
1573
|
|
|
@@ -1604,6 +1641,8 @@ def startup_preflight(*, entrypoint: str, interactive: bool = False) -> dict:
|
|
|
1604
1641
|
result["entrypoint"] = entrypoint
|
|
1605
1642
|
result["power_policy"] = choice.get("policy") or get_power_policy()
|
|
1606
1643
|
result["power_message"] = power_result.get("message")
|
|
1644
|
+
result["full_disk_access_status"] = fda_choice.get("status") or get_full_disk_access_status()
|
|
1645
|
+
result["full_disk_access_message"] = fda_choice.get("message")
|
|
1607
1646
|
if power_result.get("ok"):
|
|
1608
1647
|
actions = result.setdefault("actions", [])
|
|
1609
1648
|
actions.append(f"power:{power_result.get('action')}")
|