abmind 0.2.6 → 0.2.7-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SUPPORTED-SURFACE.md +20 -2
- package/dist/cli/abmind-hook-recall.js +17 -40
- package/dist/cli/abmind-hook-recall.js.map +1 -1
- package/dist/cli/abmind-hook-store.js +10 -31
- package/dist/cli/abmind-hook-store.js.map +1 -1
- package/dist/cli/abmind-hook-wakeup.js +17 -3
- package/dist/cli/abmind-hook-wakeup.js.map +1 -1
- package/dist/cli/abmind-install.js +19 -19
- package/dist/cli/abmind-install.js.map +1 -1
- package/dist/cli/abmind-restore.js +10 -0
- package/dist/cli/abmind-restore.js.map +1 -1
- package/dist/cli/abmind-sleep.d.ts +9 -5
- package/dist/cli/abmind-sleep.d.ts.map +1 -1
- package/dist/cli/abmind-sleep.js +61 -14
- package/dist/cli/abmind-sleep.js.map +1 -1
- package/dist/cli/hook-lifecycle-adapter.d.ts +11 -0
- package/dist/cli/hook-lifecycle-adapter.d.ts.map +1 -0
- package/dist/cli/hook-lifecycle-adapter.js +33 -0
- package/dist/cli/hook-lifecycle-adapter.js.map +1 -0
- package/dist/src/context-engine.d.ts +13 -3
- package/dist/src/context-engine.d.ts.map +1 -1
- package/dist/src/context-engine.js +22 -5
- package/dist/src/context-engine.js.map +1 -1
- package/dist/src/context-orchestrator.d.ts +20 -2
- package/dist/src/context-orchestrator.d.ts.map +1 -1
- package/dist/src/context-orchestrator.js +11 -3
- package/dist/src/context-orchestrator.js.map +1 -1
- package/dist/src/context-tier-renderer.d.ts +8 -1
- package/dist/src/context-tier-renderer.d.ts.map +1 -1
- package/dist/src/context-tier-renderer.js +7 -2
- package/dist/src/context-tier-renderer.js.map +1 -1
- package/dist/src/ensure-initialized.d.ts +5 -0
- package/dist/src/ensure-initialized.d.ts.map +1 -1
- package/dist/src/ensure-initialized.js +19 -28
- package/dist/src/ensure-initialized.js.map +1 -1
- package/dist/src/host-integration/identity.d.ts +10 -0
- package/dist/src/host-integration/identity.d.ts.map +1 -0
- package/dist/src/host-integration/identity.js +66 -0
- package/dist/src/host-integration/identity.js.map +1 -0
- package/dist/src/host-integration/index.d.ts +5 -0
- package/dist/src/host-integration/index.d.ts.map +1 -0
- package/dist/src/host-integration/index.js +4 -0
- package/dist/src/host-integration/index.js.map +1 -0
- package/dist/src/host-integration/lifecycle.d.ts +14 -0
- package/dist/src/host-integration/lifecycle.d.ts.map +1 -0
- package/dist/src/host-integration/lifecycle.js +192 -0
- package/dist/src/host-integration/lifecycle.js.map +1 -0
- package/dist/src/host-integration/render.d.ts +5 -0
- package/dist/src/host-integration/render.d.ts.map +1 -0
- package/dist/src/host-integration/render.js +20 -0
- package/dist/src/host-integration/render.js.map +1 -0
- package/dist/src/host-integration/types.d.ts +107 -0
- package/dist/src/host-integration/types.d.ts.map +1 -0
- package/dist/src/host-integration/types.js +2 -0
- package/dist/src/host-integration/types.js.map +1 -0
- package/dist/src/imemory-system.d.ts +1 -1
- package/dist/src/imemory-system.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/maintenance-service.d.ts +11 -0
- package/dist/src/maintenance-service.d.ts.map +1 -1
- package/dist/src/maintenance-service.js +43 -0
- package/dist/src/maintenance-service.js.map +1 -1
- package/dist/src/memory-manager.d.ts +6 -2
- package/dist/src/memory-manager.d.ts.map +1 -1
- package/dist/src/memory-manager.js +9 -5
- package/dist/src/memory-manager.js.map +1 -1
- package/dist/src/message-store.d.ts +15 -3
- package/dist/src/message-store.d.ts.map +1 -1
- package/dist/src/message-store.js +28 -9
- package/dist/src/message-store.js.map +1 -1
- package/dist/src/openclaw-plugin/index.d.ts.map +1 -1
- package/dist/src/openclaw-plugin/index.js +3 -2
- package/dist/src/openclaw-plugin/index.js.map +1 -1
- package/dist/src/pi-plugin/abtars-client.d.ts +22 -0
- package/dist/src/pi-plugin/abtars-client.d.ts.map +1 -0
- package/dist/src/pi-plugin/abtars-client.js +153 -0
- package/dist/src/pi-plugin/abtars-client.js.map +1 -0
- package/dist/src/pi-plugin/abtars-tools.d.ts +8 -0
- package/dist/src/pi-plugin/abtars-tools.d.ts.map +1 -0
- package/dist/src/pi-plugin/abtars-tools.js +317 -0
- package/dist/src/pi-plugin/abtars-tools.js.map +1 -0
- package/dist/src/pi-plugin/identity.d.ts +8 -0
- package/dist/src/pi-plugin/identity.d.ts.map +1 -0
- package/dist/src/pi-plugin/identity.js +37 -0
- package/dist/src/pi-plugin/identity.js.map +1 -0
- package/dist/src/pi-plugin/index.d.ts +3 -0
- package/dist/src/pi-plugin/index.d.ts.map +1 -0
- package/dist/src/pi-plugin/index.js +139 -0
- package/dist/src/pi-plugin/index.js.map +1 -0
- package/dist/src/pi-plugin/messages.d.ts +4 -0
- package/dist/src/pi-plugin/messages.d.ts.map +1 -0
- package/dist/src/pi-plugin/messages.js +36 -0
- package/dist/src/pi-plugin/messages.js.map +1 -0
- package/dist/src/pi-plugin/pi-types.d.ts +126 -0
- package/dist/src/pi-plugin/pi-types.d.ts.map +1 -0
- package/dist/src/pi-plugin/pi-types.js +15 -0
- package/dist/src/pi-plugin/pi-types.js.map +1 -0
- package/dist/src/pi-plugin/runtime.d.ts +21 -0
- package/dist/src/pi-plugin/runtime.d.ts.map +1 -0
- package/dist/src/pi-plugin/runtime.js +60 -0
- package/dist/src/pi-plugin/runtime.js.map +1 -0
- package/dist/src/pi-plugin/tools.d.ts +11 -0
- package/dist/src/pi-plugin/tools.d.ts.map +1 -0
- package/dist/src/pi-plugin/tools.js +136 -0
- package/dist/src/pi-plugin/tools.js.map +1 -0
- package/dist/src/reconcile.d.ts +2 -0
- package/dist/src/reconcile.d.ts.map +1 -1
- package/dist/src/reconcile.js +21 -4
- package/dist/src/reconcile.js.map +1 -1
- package/dist/src/session-context.d.ts +1 -0
- package/dist/src/session-context.d.ts.map +1 -1
- package/dist/src/session-context.js +42 -23
- package/dist/src/session-context.js.map +1 -1
- package/dist/src/sleep/basic.d.ts +1 -1
- package/dist/src/sleep/basic.d.ts.map +1 -1
- package/dist/src/sleep/basic.js +5 -2
- package/dist/src/sleep/basic.js.map +1 -1
- package/dist/src/sleep/catchup.d.ts +3 -5
- package/dist/src/sleep/catchup.d.ts.map +1 -1
- package/dist/src/sleep/catchup.js +37 -55
- package/dist/src/sleep/catchup.js.map +1 -1
- package/dist/src/sleep/contracts.d.ts +112 -0
- package/dist/src/sleep/contracts.d.ts.map +1 -0
- package/dist/src/sleep/contracts.js +25 -0
- package/dist/src/sleep/contracts.js.map +1 -0
- package/dist/src/sleep/llm-budget.d.ts +34 -14
- package/dist/src/sleep/llm-budget.d.ts.map +1 -1
- package/dist/src/sleep/llm-budget.js +66 -52
- package/dist/src/sleep/llm-budget.js.map +1 -1
- package/dist/src/sleep/orchestrator.d.ts +19 -69
- package/dist/src/sleep/orchestrator.d.ts.map +1 -1
- package/dist/src/sleep/orchestrator.js +286 -270
- package/dist/src/sleep/orchestrator.js.map +1 -1
- package/dist/src/sleep/state.d.ts +7 -19
- package/dist/src/sleep/state.d.ts.map +1 -1
- package/dist/src/sleep/state.js +5 -38
- package/dist/src/sleep/state.js.map +1 -1
- package/dist/src/sleep/test-harness.d.ts +3 -1
- package/dist/src/sleep/test-harness.d.ts.map +1 -1
- package/dist/src/sleep/test-harness.js +3 -2
- package/dist/src/sleep/test-harness.js.map +1 -1
- package/dist/src/soul-seeder.d.ts +36 -0
- package/dist/src/soul-seeder.d.ts.map +1 -0
- package/dist/src/soul-seeder.js +69 -0
- package/dist/src/soul-seeder.js.map +1 -0
- package/dist/src/wake-up-builder.d.ts +3 -1
- package/dist/src/wake-up-builder.d.ts.map +1 -1
- package/dist/src/wake-up-builder.js +29 -5
- package/dist/src/wake-up-builder.js.map +1 -1
- package/package.json +20 -2
- package/dist/cli/abmind-rollback.d.ts +0 -8
- package/dist/cli/abmind-rollback.d.ts.map +0 -1
- package/dist/cli/abmind-rollback.js +0 -78
- package/dist/cli/abmind-rollback.js.map +0 -1
- package/dist/src/native-loader.d.ts +0 -7
- package/dist/src/native-loader.d.ts.map +0 -1
- package/dist/src/native-loader.js +0 -21
- package/dist/src/native-loader.js.map +0 -1
- package/dist/src/recall-benchmark.d.ts +0 -13
- package/dist/src/recall-benchmark.d.ts.map +0 -1
- package/dist/src/recall-benchmark.js +0 -260
- package/dist/src/recall-benchmark.js.map +0 -1
- package/dist/src/sleep/runtime.d.ts +0 -15
- package/dist/src/sleep/runtime.d.ts.map +0 -1
- package/dist/src/sleep/runtime.js +0 -2
- package/dist/src/sleep/runtime.js.map +0 -1
- package/dist/src/wake-up-renderer.d.ts +0 -24
- package/dist/src/wake-up-renderer.d.ts.map +0 -1
- package/dist/src/wake-up-renderer.js +0 -148
- package/dist/src/wake-up-renderer.js.map +0 -1
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* abmind sleep orchestrator —
|
|
3
|
+
* abmind sleep orchestrator — host-neutral memory-maintenance engine (#1353).
|
|
4
4
|
*
|
|
5
|
-
* Called via runSleepCycle(
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Called via runSleepCycle(options). Gathers system state, runs through a
|
|
6
|
+
* pipeline of prompt-driven steps (gc-noise, daily-summary, extract-memories,
|
|
7
|
+
* retrospective, retro-derive, etc.), persists audit log, returns a structured
|
|
8
|
+
* SleepRunResult.
|
|
9
|
+
*
|
|
10
|
+
* abmind owns: step ordering, shared variables/outputs, wired memory
|
|
11
|
+
* maintenance, essential-step/continuation rules, LLM-call budget, durable
|
|
12
|
+
* checkpoints/resume/catch-up/watermark, and the final domain result.
|
|
13
|
+
*
|
|
14
|
+
* The embedding host owns: scheduling, admission, model/provider transport,
|
|
15
|
+
* agent/session lifecycle, cancellation on shutdown, and delivery. The host
|
|
16
|
+
* never reads sleep_*.lock — SleepRunResult is the only supported way to
|
|
17
|
+
* learn what happened.
|
|
8
18
|
*
|
|
9
19
|
* Library-only — no CLI entry point here. Standalone entry lives in
|
|
10
20
|
* cli/abmind-sleep.ts.
|
|
11
|
-
*
|
|
12
|
-
* Flags (passed via RunOpts.flags):
|
|
13
|
-
* --dry-run Gather state + build prompts, print to stdout, skip LLM calls
|
|
14
|
-
* --verbose Detailed logging at each orchestration step
|
|
15
|
-
* --force Run housekeeping even if no messages since last sleep
|
|
16
21
|
*/
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
17
23
|
import { localISO } from "../local-time.js";
|
|
18
24
|
import { getAbmindEnv } from "../env-schema.js";
|
|
19
25
|
import { join, dirname } from "node:path";
|
|
@@ -25,81 +31,79 @@ import { SleepStateGatherer } from "../sleep-state-gatherer.js";
|
|
|
25
31
|
import { loadSleepSteps, buildSleepVars, substituteVars } from "../sleep-pipeline.js";
|
|
26
32
|
import { buildDailySummary, writeDailyFile, LLMUnavailableError } from "../sleep-pipeline.js";
|
|
27
33
|
import { extractFromDaily } from "../sleep-pipeline.js";
|
|
28
|
-
import { logInfo, logWarn
|
|
34
|
+
import { logInfo, logWarn } from "../mem-logger.js";
|
|
29
35
|
import { localDate } from "../local-time.js";
|
|
30
36
|
import { parseLevel, DEFAULT_LEVEL } from "./levels.js";
|
|
31
|
-
import { readStateFile, writeStateFile, runWiredPreTasks, formatWiredResults
|
|
37
|
+
import { readStateFile, writeStateFile, runWiredPreTasks, formatWiredResults } from "./state.js";
|
|
32
38
|
import { buildSnapshotSummary, writeAuditLog } from "./audit.js";
|
|
33
39
|
import { toDateStr, toIsoDate, scanPreviousLocks } from "./locks.js";
|
|
34
40
|
import { redactSecrets } from "../redact-secrets.js";
|
|
35
|
-
import {
|
|
36
|
-
import { runCatchUp } from "./catchup.js";
|
|
41
|
+
import { TransportUnavailableError, LlmBudget, sendToRuntime, MAX_DOMAIN_RETRIES } from "./llm-budget.js";
|
|
42
|
+
import { ESSENTIAL_STEPS, failedEssentials, runCatchUp } from "./catchup.js";
|
|
43
|
+
import { emitSleepEvent } from "./contracts.js";
|
|
37
44
|
const TAG = "abmind-sleep";
|
|
38
45
|
/** Steps whose failure blocks watermark advance. Public so tests can derive reject targets. */
|
|
39
|
-
// Re-exported from catchup.ts — kept here for backward compat of any direct orchestrator imports.
|
|
40
46
|
export { ESSENTIAL_STEPS } from "./catchup.js";
|
|
41
47
|
/** Thrown by runSleepCycle when memory layer fails to initialize. */
|
|
42
48
|
export class SleepInitError extends Error {
|
|
43
49
|
constructor(message) { super(message); this.name = "SleepInitError"; }
|
|
44
50
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const parsed = { dryRun: false, verbose: false, force: false };
|
|
52
|
-
for (const arg of args) {
|
|
53
|
-
switch (arg) {
|
|
54
|
-
case "--dry-run":
|
|
55
|
-
parsed.dryRun = true;
|
|
56
|
-
break;
|
|
57
|
-
case "--verbose":
|
|
58
|
-
parsed.verbose = true;
|
|
59
|
-
break;
|
|
60
|
-
case "--force":
|
|
61
|
-
parsed.force = true;
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return parsed;
|
|
66
|
-
}
|
|
67
|
-
// ── Subagent invocation ─────────────────────────────────────────────────────
|
|
68
|
-
/** LLM call entry for sleep steps — caller provides the runtime via RunOpts. */
|
|
69
|
-
// ── Transport ───────────────────────────────────────────────────────────────
|
|
70
|
-
// ── Main orchestration ──────────────────────────────────────────────────────
|
|
51
|
+
// ── In-process concurrency guard (#1353) ────────────────────────────────────
|
|
52
|
+
// The durable lock file protects against another OS process using the same
|
|
53
|
+
// abmind home. This guards a second invocation within THIS process (e.g. two
|
|
54
|
+
// overlapping calls from a host that forgot to serialize). A key is the
|
|
55
|
+
// resolved memory directory — one run per home, per process.
|
|
56
|
+
const activeRunsByMemoryDir = new Set();
|
|
71
57
|
/**
|
|
72
|
-
* Run the full sleep cycle
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* - Returns { ok, failCount } for observable outcomes
|
|
76
|
-
*
|
|
77
|
-
* Default args preserve current main() behavior exactly.
|
|
58
|
+
* Run the full sleep cycle against a host-injected runtime. Returns a
|
|
59
|
+
* structured SleepRunResult projected from the authoritative on-disk state —
|
|
60
|
+
* hosts never need to read the lock file themselves.
|
|
78
61
|
*/
|
|
79
|
-
export async function runSleepCycle(
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
62
|
+
export async function runSleepCycle(options) {
|
|
63
|
+
const now = options.now ?? Date.now;
|
|
64
|
+
const timeoutMs = options.timeoutMs ?? getAbmindEnv().sleepTimeoutMin * 60 * 1000;
|
|
65
|
+
const domainRetryDelayMs = options.domainRetryDelayMs ?? 6000;
|
|
66
|
+
const betweenStepBackoffMs = options.betweenStepBackoffMs ?? ((n) => [10, 30, 60][Math.min(n, 2)] * 1000);
|
|
67
|
+
const runtime = options.runtime;
|
|
68
|
+
const startedAt = now();
|
|
69
|
+
// ── Cancellation: combine caller signal + wall-clock timeout ──
|
|
70
|
+
const internalController = new AbortController();
|
|
71
|
+
const timeoutHandle = setTimeout(() => internalController.abort(new SleepTimeoutReason()), timeoutMs);
|
|
72
|
+
const onCallerAbort = () => internalController.abort(options.signal?.reason);
|
|
73
|
+
if (options.signal) {
|
|
74
|
+
if (options.signal.aborted)
|
|
75
|
+
internalController.abort(options.signal.reason);
|
|
76
|
+
else
|
|
77
|
+
options.signal.addEventListener("abort", onCallerAbort);
|
|
78
|
+
}
|
|
79
|
+
const signal = internalController.signal;
|
|
80
|
+
const cleanupCancellation = () => {
|
|
81
|
+
clearTimeout(timeoutHandle);
|
|
82
|
+
options.signal?.removeEventListener("abort", onCallerAbort);
|
|
83
|
+
};
|
|
84
|
+
const memoryConfig = { ...loadMemoryConfig(), ...options.memoryConfigOverride };
|
|
85
|
+
// #1353: in-process concurrency guard — claimed synchronously, before any
|
|
86
|
+
// await, so two overlapping calls in the same process cannot both pass the
|
|
87
|
+
// check. This protects a single process invoking runSleepCycle twice; the
|
|
88
|
+
// durable PID-based lock (below) protects against a second OS process.
|
|
89
|
+
const memoryDirKey = memoryConfig.memoryDir;
|
|
90
|
+
if (activeRunsByMemoryDir.has(memoryDirKey)) {
|
|
91
|
+
cleanupCancellation();
|
|
92
|
+
return alreadyRunningResult(startedAt, now());
|
|
90
93
|
}
|
|
91
|
-
|
|
94
|
+
activeRunsByMemoryDir.add(memoryDirKey);
|
|
92
95
|
const memory = new MemoryManager(memoryConfig);
|
|
93
96
|
try {
|
|
94
97
|
await memory.initialize();
|
|
95
98
|
}
|
|
96
99
|
catch (err) {
|
|
100
|
+
activeRunsByMemoryDir.delete(memoryDirKey);
|
|
101
|
+
cleanupCancellation();
|
|
97
102
|
throw new SleepInitError(`Failed to initialize MemoryManager: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
103
|
}
|
|
99
|
-
//
|
|
104
|
+
// #1353: async, library-native preflight — no execSync, no CLI subprocess.
|
|
100
105
|
try {
|
|
101
|
-
|
|
102
|
-
execSync("abmind doctor --fix", { timeout: 30_000, stdio: "ignore" });
|
|
106
|
+
await memory.maintenance.runPreflight();
|
|
103
107
|
}
|
|
104
108
|
catch { /* non-fatal — proceed with sleep */ }
|
|
105
109
|
try {
|
|
@@ -111,14 +115,13 @@ export async function runSleepCycle(opts) {
|
|
|
111
115
|
}
|
|
112
116
|
catch { /* */ }
|
|
113
117
|
const { metaSet, metaIncrement, metaGetInt } = await import("../meta-store.js");
|
|
114
|
-
// Record attempt
|
|
115
118
|
metaSet(db, "sleep_last_attempt_ts", Date.now());
|
|
116
119
|
metaIncrement(db, "sleep_total_runs");
|
|
117
|
-
// State file path — use opts.now for deterministic today derivation
|
|
118
120
|
const dateStr = toDateStr(now());
|
|
119
121
|
const statePath = join(memoryConfig.memoryDir, "sleep", `sleep_${dateStr}.lock`);
|
|
120
122
|
const existingState = readStateFile(statePath);
|
|
121
|
-
// #518: PID guard —
|
|
123
|
+
// #518 + #1353: durable PID guard — protects against another OS process
|
|
124
|
+
// using the same abmind home.
|
|
122
125
|
if (existingState?.status === "ongoing") {
|
|
123
126
|
let alive = false;
|
|
124
127
|
try {
|
|
@@ -128,42 +131,42 @@ export async function runSleepCycle(opts) {
|
|
|
128
131
|
catch { }
|
|
129
132
|
if (alive) {
|
|
130
133
|
logInfo(TAG, `[SLEEP] Already running (pid ${existingState.pid}) — skipping`);
|
|
131
|
-
return
|
|
134
|
+
return alreadyRunningResult(startedAt, now());
|
|
132
135
|
}
|
|
133
136
|
logWarn(TAG, `[SLEEP] Stale lock (pid ${existingState.pid} dead) — claiming`);
|
|
134
137
|
}
|
|
135
|
-
//
|
|
136
|
-
const isResume = !
|
|
138
|
+
// Fresh cycle discards prior state (budget + steps)
|
|
139
|
+
const isResume = !options.fresh && existingState !== null && Object.values(existingState.steps).some(s => s.status === "ok");
|
|
140
|
+
const priorRunId = existingState?.runId;
|
|
141
|
+
const runId = randomUUID();
|
|
142
|
+
const totalStepsForEvent = loadSleepSteps().length;
|
|
143
|
+
emitSleepEvent(options.onEvent, { type: "cycle_started", runId, totalSteps: totalStepsForEvent, resumed: isResume });
|
|
137
144
|
// Gather state
|
|
138
|
-
|
|
139
|
-
const cronFn = undefined;
|
|
140
|
-
const gatherer = new SleepStateGatherer(memory, memoryConfig, cronFn);
|
|
145
|
+
const gatherer = new SleepStateGatherer(memory, memoryConfig, undefined);
|
|
141
146
|
const snapshot = await gatherer.gather();
|
|
142
|
-
if (
|
|
143
|
-
logInfo(TAG, `State gathered: ${buildSnapshotSummary(snapshot)}`);
|
|
144
|
-
// Guardrail: skip if no messages since last sleep (unless --force or resuming)
|
|
147
|
+
// Guardrail: skip if no messages since last sleep (unless resuming)
|
|
145
148
|
const msgCount = snapshot.dbStats.messagesSinceLastSleep;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
// #1353: manual runs (e.g. "/sleep now") still run housekeeping even with
|
|
150
|
+
// zero new messages — matches the previous flags.force escape hatch.
|
|
151
|
+
const forceHousekeeping = options.mode === "manual";
|
|
152
|
+
if (msgCount === 0 && !isResume && !forceHousekeeping) {
|
|
153
|
+
logInfo(TAG, `[SLEEP] No messages since last sleep — nothing to process.`);
|
|
149
154
|
const sleepDir = join(memoryConfig.memoryDir, "sleep");
|
|
150
155
|
mkdirSync(sleepDir, { recursive: true });
|
|
151
|
-
const
|
|
156
|
+
const noWorkDateStr = localDate().replace(/-/g, "");
|
|
152
157
|
const timeStr = new Date().toTimeString().slice(0, 5).replace(/:/g, "");
|
|
153
|
-
writeFileSync(join(sleepDir, `sleep_${
|
|
154
|
-
|
|
158
|
+
writeFileSync(join(sleepDir, `sleep_${noWorkDateStr}_${timeStr}.md`), `# Sleep Audit Log\n\n## No work — 0 messages since last sleep\n`, "utf-8");
|
|
159
|
+
const result = noWorkResult(runId, startedAt, now());
|
|
160
|
+
emitSleepEvent(options.onEvent, { type: "cycle_finished", runId, result });
|
|
161
|
+
return result;
|
|
155
162
|
}
|
|
156
|
-
// Wired pre-tasks (always run — fast, idempotent)
|
|
163
|
+
// Wired pre-tasks (always run — fast, idempotent, abmind-owned only)
|
|
157
164
|
logInfo(TAG, `[SLEEP] Running wired pre-tasks${isResume ? " (resume)" : ""}...`);
|
|
158
165
|
const wiredResults = await runWiredPreTasks(sleepData, memoryConfig.memoryDir, memory);
|
|
159
166
|
logInfo(TAG, `[SLEEP] Wired: ${formatWiredResults(wiredResults)}`);
|
|
160
|
-
// Build candidate lists for conditional prompts
|
|
161
167
|
const candidates = sleepData.buildSleepCandidates(getAbmindEnv().sleepModelName ?? "unknown");
|
|
162
|
-
logInfo(TAG, `[SLEEP] Candidates: topics=${candidates.untaggedMemories ? "yes" : "none"}, promote=${candidates.promotionCandidates ? "yes" : "none"}, contradict=${candidates.contradictions ? "yes" : "none"}, merge=${candidates.mergeCandidates ? "yes" : "none"}, translate=${candidates.translationIssues ? "yes" : "none"}, emotion-ctx=${candidates.emotionContextGaps ? "yes" : "none"}, feedback=${candidates.recallFeedback ? "yes" : "none"}`);
|
|
163
|
-
// Load step files + build vars
|
|
164
168
|
const vars = buildSleepVars(snapshot);
|
|
165
169
|
vars.WIRED_RESULTS = formatWiredResults(wiredResults);
|
|
166
|
-
// Inject candidate lists as template variables
|
|
167
170
|
vars.UNTAGGED_MEMORIES = candidates.untaggedMemories || "No untagged memories found.";
|
|
168
171
|
vars.PROMOTION_CANDIDATES = candidates.promotionCandidates || "No promotion candidates found.";
|
|
169
172
|
vars.CONTRADICTION_WARNINGS = candidates.contradictions || "";
|
|
@@ -171,7 +174,6 @@ export async function runSleepCycle(opts) {
|
|
|
171
174
|
vars.TRANSLATION_ISSUES = candidates.translationIssues || "No translation issues found.";
|
|
172
175
|
vars.EMOTION_CONTEXT_GAPS = candidates.emotionContextGaps || "No emotion context gaps found.";
|
|
173
176
|
vars.RECALL_FEEDBACK = candidates.recallFeedback || "No recalls happened today.";
|
|
174
|
-
// Skill dedup candidates (for step 10 skill-review)
|
|
175
177
|
{
|
|
176
178
|
const { detectSkillDuplicates, formatDedupCandidates } = await import("./skill-dedup.js");
|
|
177
179
|
const abtarsHome = process.env["ABTARS_HOME"] ?? join(process.env["HOME"] ?? "", ".abtars");
|
|
@@ -180,11 +182,9 @@ export async function runSleepCycle(opts) {
|
|
|
180
182
|
const dedupCandidates = detectSkillDuplicates(coreSkillsDir, selfSkillsDir);
|
|
181
183
|
vars.DEDUP_CANDIDATES = formatDedupCandidates(dedupCandidates) || "No skill duplicates or overlaps detected.";
|
|
182
184
|
}
|
|
183
|
-
vars.WIRED_RESULTS = formatWiredResults(wiredResults);
|
|
184
185
|
vars.RESUME_CONTEXT = isResume
|
|
185
186
|
? `This is a RESUMED sleep cycle. Steps already completed: ${Object.entries(existingState.steps).filter(([, s]) => s.status === "ok" || s.status === "skipped").map(([k]) => k).join(", ")}. Only pending/failed steps will run.`
|
|
186
187
|
: "Fresh sleep cycle — all steps will run.";
|
|
187
|
-
// Pre-query messages for retro (watermark-scoped, noise-stripped)
|
|
188
188
|
const lastSleepTs = snapshot.lastSleepTimestamp ?? 0;
|
|
189
189
|
try {
|
|
190
190
|
const garbagePath = join(memoryConfig.memoryDir, "garbage.json");
|
|
@@ -210,10 +210,9 @@ export async function runSleepCycle(opts) {
|
|
|
210
210
|
catch {
|
|
211
211
|
vars.CLEAN_MESSAGES = "Error loading messages — use abmind recall to search.";
|
|
212
212
|
}
|
|
213
|
-
|
|
214
|
-
vars.MESSAGES_SINCE_WATERMARK = vars.CLEAN_MESSAGES; // same data, different name for gc-noise
|
|
213
|
+
vars.MESSAGES_SINCE_WATERMARK = vars.CLEAN_MESSAGES;
|
|
215
214
|
vars.RETRO_PATH = join(memoryConfig.memoryDir, "daily", `daily_${toIsoDate(now())}.md`);
|
|
216
|
-
vars.DAILY_PATH = vars.RETRO_PATH;
|
|
215
|
+
vars.DAILY_PATH = vars.RETRO_PATH;
|
|
217
216
|
try {
|
|
218
217
|
const { getLatestConsolidationFile } = await import("../consolidation-search.js");
|
|
219
218
|
const latest = getLatestConsolidationFile(memoryConfig.memoryDir, "weekly");
|
|
@@ -222,68 +221,41 @@ export async function runSleepCycle(opts) {
|
|
|
222
221
|
catch {
|
|
223
222
|
vars.CONSOLIDATION_PATH = "No consolidation files yet.";
|
|
224
223
|
}
|
|
225
|
-
|
|
226
|
-
const todayIso = new Date(now()).toISOString().slice(0, 10); // YYYY-MM-DD
|
|
224
|
+
const todayIso = new Date(now()).toISOString().slice(0, 10);
|
|
227
225
|
const weeklyDir = join(memoryConfig.memoryDir, "weekly");
|
|
228
226
|
const quarterlyDir = join(memoryConfig.memoryDir, "quarterly");
|
|
229
227
|
mkdirSync(weeklyDir, { recursive: true });
|
|
230
228
|
mkdirSync(quarterlyDir, { recursive: true });
|
|
231
|
-
const month = new Date(now()).getMonth();
|
|
229
|
+
const month = new Date(now()).getMonth();
|
|
232
230
|
const isQuarterBoundary = month % 3 === 0 && new Date(now()).getDate() <= 7;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
else {
|
|
237
|
-
vars.CONSOLIDATION_OUTPUT_PATH = join(weeklyDir, `weekly_${todayIso}.md`);
|
|
238
|
-
}
|
|
231
|
+
vars.CONSOLIDATION_OUTPUT_PATH = isQuarterBoundary
|
|
232
|
+
? join(quarterlyDir, `quarterly_${todayIso}.md`)
|
|
233
|
+
: join(weeklyDir, `weekly_${todayIso}.md`);
|
|
239
234
|
const steps = loadSleepSteps();
|
|
240
|
-
// Merge snapshot vars + bridge vars into one map for JIT substitution
|
|
241
235
|
const snapshotVars = buildSleepVars(snapshot);
|
|
242
236
|
for (const [k, v] of Object.entries(snapshotVars))
|
|
243
237
|
vars[k] = vars[k] ?? v;
|
|
244
|
-
// #895: Fire onCycleStart once before the first step — hosts use this to
|
|
245
|
-
// create a stepped card or reset per-night state. fireOnStep() already
|
|
246
|
-
// swallows handler errors so a misbehaving host can never break the cycle.
|
|
247
|
-
try {
|
|
248
|
-
opts.onCycleStart?.({ totalSteps: steps.length });
|
|
249
|
-
}
|
|
250
|
-
catch { /* host display only */ }
|
|
251
|
-
// Progress protocol — emit PROGRESS:<pct>:<label> on stdout
|
|
252
238
|
const totalSteps = steps.length;
|
|
253
239
|
let stepIndex = 0;
|
|
254
|
-
const emitProgress = (label) => {
|
|
255
|
-
const pct = Math.round((stepIndex / totalSteps) * 100);
|
|
256
|
-
process.stdout.write(`PROGRESS:${pct}:${label}\n`);
|
|
257
|
-
};
|
|
258
|
-
if (flags.dryRun) {
|
|
259
|
-
for (const step of steps)
|
|
260
|
-
process.stdout.write(`\n--- ${step.filename} ---\n${substituteVars(step.rawPrompt, vars)}\n`);
|
|
261
|
-
return { ok: true, failCount: 0 };
|
|
262
|
-
}
|
|
263
240
|
// Skip logic — candidate-driven (empty = skip)
|
|
264
241
|
const skipSet = new Set();
|
|
265
|
-
|
|
266
|
-
// Precedence: opts.level > SLEEP_QUALITY env (legacy bridge path) > DEFAULT_LEVEL.
|
|
267
|
-
const quality = opts.level ?? (getAbmindEnv().sleepQuality ? parseLevel(getAbmindEnv().sleepQuality) : DEFAULT_LEVEL);
|
|
242
|
+
const quality = options.level ?? (getAbmindEnv().sleepQuality ? parseLevel(getAbmindEnv().sleepQuality) : DEFAULT_LEVEL);
|
|
268
243
|
const curationDay = getAbmindEnv().sleepCurationDay;
|
|
269
244
|
const today = new Date(now()).toLocaleDateString("en", { weekday: "long" }).toLowerCase();
|
|
270
245
|
const isCurationDay = today === curationDay;
|
|
271
246
|
const BUDGET_ONLY = new Set(["gc-noise", "daily-summary", "extract-memories"]);
|
|
272
247
|
const BUDGET_CURATION = new Set([...BUDGET_ONLY, "retrospective", "retro-derive"]);
|
|
273
|
-
const WEEKLY_ONLY = new Set(["memory-maintenance", "translation",
|
|
274
|
-
"skill-review", "consolidation", "rem-synthesis"]);
|
|
248
|
+
const WEEKLY_ONLY = new Set(["memory-maintenance", "translation", "skill-review", "consolidation", "rem-synthesis"]);
|
|
275
249
|
if (quality === "budget" && !isCurationDay) {
|
|
276
|
-
for (const step of steps)
|
|
250
|
+
for (const step of steps)
|
|
277
251
|
if (!BUDGET_ONLY.has(step.name))
|
|
278
252
|
skipSet.add(step.name);
|
|
279
|
-
}
|
|
280
253
|
logInfo(TAG, `[SLEEP] Quality=budget — only essential extraction`);
|
|
281
254
|
}
|
|
282
255
|
else if (quality === "budget" && isCurationDay) {
|
|
283
|
-
for (const step of steps)
|
|
256
|
+
for (const step of steps)
|
|
284
257
|
if (!BUDGET_CURATION.has(step.name))
|
|
285
258
|
skipSet.add(step.name);
|
|
286
|
-
}
|
|
287
259
|
logInfo(TAG, `[SLEEP] Quality=budget (curation day) — adds retro + derive`);
|
|
288
260
|
}
|
|
289
261
|
else if (quality === "normal" && !isCurationDay) {
|
|
@@ -295,19 +267,14 @@ export async function runSleepCycle(opts) {
|
|
|
295
267
|
logInfo(TAG, `[SLEEP] Quality=normal (curation day) — all steps`);
|
|
296
268
|
}
|
|
297
269
|
else {
|
|
298
|
-
// ultimate: all steps every night
|
|
299
270
|
logInfo(TAG, `[SLEEP] Quality=${quality}${isCurationDay ? " (curation day)" : ""} — all eligible`);
|
|
300
271
|
}
|
|
301
|
-
// Candidate-driven skips (empty = nothing to do)
|
|
302
272
|
if (!candidates.recallFeedback)
|
|
303
273
|
skipSet.add("feedback");
|
|
304
|
-
// memory-maintenance: skip if ALL three inputs are empty
|
|
305
274
|
if (!candidates.untaggedMemories && !candidates.mergeCandidates && !candidates.emotionContextGaps)
|
|
306
275
|
skipSet.add("memory-maintenance");
|
|
307
|
-
// promotion candidates are optional input to retro-derive — don't skip the step for it
|
|
308
276
|
if (!candidates.translationIssues)
|
|
309
277
|
skipSet.add("translation");
|
|
310
|
-
// Legacy skip names (old prompt files)
|
|
311
278
|
if (snapshot.topicFiles.length === 0)
|
|
312
279
|
skipSet.add("topic-reorg");
|
|
313
280
|
if (snapshot.dbStats.extractedMemoryCount < 10) {
|
|
@@ -327,10 +294,11 @@ export async function runSleepCycle(opts) {
|
|
|
327
294
|
skipSet.add("gc-noise");
|
|
328
295
|
}
|
|
329
296
|
catch { /* */ }
|
|
330
|
-
// Initialize state file
|
|
297
|
+
// Initialize state file with the new run identity.
|
|
331
298
|
const state = existingState ?? {
|
|
332
299
|
status: "ongoing",
|
|
333
300
|
pid: process.pid,
|
|
301
|
+
runId,
|
|
334
302
|
startedAt: now(),
|
|
335
303
|
llmCalls: 0,
|
|
336
304
|
wiredResults,
|
|
@@ -338,30 +306,37 @@ export async function runSleepCycle(opts) {
|
|
|
338
306
|
};
|
|
339
307
|
state.status = "ongoing";
|
|
340
308
|
state.pid = process.pid;
|
|
309
|
+
state.runId = runId;
|
|
310
|
+
if (priorRunId)
|
|
311
|
+
state.priorRunId = priorRunId;
|
|
341
312
|
state.wiredResults = wiredResults;
|
|
342
|
-
// 20-min wall-clock timeout
|
|
343
|
-
const timeoutHandle = setTimeout(() => {
|
|
344
|
-
logError(TAG, `[SLEEP] ⏰ ${Math.round(timeoutMs / 60000)}-minute timeout reached — aborting`);
|
|
345
|
-
throw new SleepTimeoutError(`Sleep cycle timeout after ${Math.round(timeoutMs / 60000)} minutes`);
|
|
346
|
-
}, timeoutMs);
|
|
347
|
-
// Resolve model name for logging. Bridge wraps and passes via env; library default is "unknown".
|
|
348
313
|
const modelUsed = getAbmindEnv().sleepModelName;
|
|
349
314
|
let dreamySucceeded = true;
|
|
350
315
|
let dailySummaryPath = null;
|
|
316
|
+
let cancelled = false;
|
|
317
|
+
/** Persist a checkpoint-safe cancellation marker and record the reason. */
|
|
318
|
+
const persistCancelled = () => {
|
|
319
|
+
state.status = "suspended";
|
|
320
|
+
writeStateFile(statePath, state);
|
|
321
|
+
cancelled = true;
|
|
322
|
+
};
|
|
351
323
|
try {
|
|
352
|
-
// ── LLM call budget (hard safety limit) ──
|
|
353
|
-
// On resume: reset llmCalls to completed step count (don't carry stale counter)
|
|
354
324
|
if (isResume) {
|
|
355
325
|
const completedCount = Object.values(state.steps).filter(s => s.status === "ok").length;
|
|
356
326
|
state.llmCalls = completedCount;
|
|
357
327
|
}
|
|
358
328
|
const budget = new LlmBudget(state, statePath);
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
329
|
+
// Checkpoint boundary: before preflight/wired maintenance already ran above.
|
|
330
|
+
if (signal.aborted) {
|
|
331
|
+
persistCancelled();
|
|
332
|
+
}
|
|
333
|
+
if (!cancelled) {
|
|
334
|
+
const sleepDir = join(memoryConfig.memoryDir, "sleep");
|
|
335
|
+
const previousLocks = scanPreviousLocks(sleepDir, dateStr);
|
|
336
|
+
if (previousLocks.length > 0) {
|
|
337
|
+
logInfo(TAG, `[CATCH-UP] Found ${previousLocks.length} previous lock(s)`);
|
|
338
|
+
await runCatchUp(previousLocks, sleepData, memoryConfig, steps, runtime, runId, signal, budget, 6000, options.onEvent);
|
|
339
|
+
}
|
|
365
340
|
}
|
|
366
341
|
// Housekeeping: move misplaced daily/consolidation_* to weekly/ (#640)
|
|
367
342
|
try {
|
|
@@ -370,9 +345,9 @@ export async function runSleepCycle(opts) {
|
|
|
370
345
|
for (const f of readdirSync(dailyDir).filter(fn => fn.startsWith("consolidation_"))) {
|
|
371
346
|
const m = f.match(/consolidation_(\d{4})-(\d{2})-week(\d)/);
|
|
372
347
|
if (m) {
|
|
373
|
-
const [, year,
|
|
348
|
+
const [, year, month2, week] = m;
|
|
374
349
|
const day = (parseInt(week) - 1) * 7 + 1;
|
|
375
|
-
const approxDate = `${year}-${
|
|
350
|
+
const approxDate = `${year}-${month2}-${String(Math.min(day, 28)).padStart(2, "0")}`;
|
|
376
351
|
const dest = join(weeklyDir, `weekly_${approxDate}.md`);
|
|
377
352
|
if (!existsSync(dest)) {
|
|
378
353
|
const { renameSync } = await import("node:fs");
|
|
@@ -386,52 +361,50 @@ export async function runSleepCycle(opts) {
|
|
|
386
361
|
catch (err) {
|
|
387
362
|
logWarn(TAG, `[HOUSEKEEPING] consolidation migration failed: ${err}`);
|
|
388
363
|
}
|
|
389
|
-
emitProgress("starting");
|
|
390
364
|
let consecutiveFailures = 0;
|
|
391
|
-
|
|
392
|
-
const stepLogDir = join(sleepDir, dateStr);
|
|
365
|
+
const stepLogDir = join(sleepDir(memoryConfig.memoryDir), dateStr);
|
|
393
366
|
mkdirSync(stepLogDir, { recursive: true });
|
|
394
|
-
// Load Dreamy identity (context injection, prepended to first step only)
|
|
395
367
|
const userSoul = join(memoryConfig.memoryDir, "..", "prompts", "sleep", "SOUL-Dreamy.md");
|
|
396
368
|
const pkgSoul = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "prompts", "sleep", "SOUL-Dreamy.md");
|
|
397
369
|
const soulPath = existsSync(userSoul) ? userSoul : pkgSoul;
|
|
398
370
|
let soulPrefix = existsSync(soulPath) ? readFileSync(soulPath, "utf-8") + "\n\n---\n\n" : "";
|
|
399
371
|
for (const step of steps) {
|
|
400
|
-
|
|
372
|
+
if (cancelled)
|
|
373
|
+
break;
|
|
374
|
+
// Checkpoint boundary: before each step's model/mutation work.
|
|
375
|
+
if (signal.aborted) {
|
|
376
|
+
persistCancelled();
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
401
379
|
if (budget.exhausted) {
|
|
402
380
|
logWarn(TAG, `[BUDGET] Suspending sleep — ${budget.calls}/${getAbmindEnv().sleepMaxLlmCalls} LLM calls used`);
|
|
403
381
|
state.status = "suspended";
|
|
404
382
|
writeStateFile(statePath, state);
|
|
405
383
|
break;
|
|
406
384
|
}
|
|
407
|
-
emitProgress(step.name);
|
|
408
385
|
stepIndex++;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// best-effort — a throwing handler never breaks the cycle.
|
|
412
|
-
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "start" });
|
|
413
|
-
// Resume: skip already completed steps
|
|
386
|
+
const essential = ESSENTIAL_STEPS.has(step.name);
|
|
387
|
+
emitSleepEvent(options.onEvent, { type: "step_started", runId, stepId: step.name, index: stepIndex, total: totalSteps });
|
|
414
388
|
if (isResume && existingState?.steps[step.name]?.status === "ok") {
|
|
415
389
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — already done (resume)`);
|
|
416
|
-
|
|
390
|
+
emitSleepEvent(options.onEvent, { type: "step_skipped", runId, step: toSummary(step.name, "skipped", essential, existingState.steps[step.name]) });
|
|
417
391
|
continue;
|
|
418
392
|
}
|
|
419
393
|
if (isResume && existingState?.steps[step.name]?.status === "skipped") {
|
|
420
394
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — skipped (resume)`);
|
|
421
|
-
|
|
395
|
+
emitSleepEvent(options.onEvent, { type: "step_skipped", runId, step: toSummary(step.name, "skipped", essential, existingState.steps[step.name]) });
|
|
422
396
|
continue;
|
|
423
397
|
}
|
|
424
|
-
// Skip logic (essential steps always run)
|
|
425
398
|
if (step.skippable && skipSet.has(step.name)) {
|
|
426
399
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — skipped`);
|
|
427
|
-
state.steps[step.name] = { status: "skipped" };
|
|
400
|
+
state.steps[step.name] = { status: "skipped", essential };
|
|
428
401
|
writeStateFile(statePath, state);
|
|
429
|
-
|
|
402
|
+
emitSleepEvent(options.onEvent, { type: "step_skipped", runId, step: toSummary(step.name, "skipped", essential, state.steps[step.name]) });
|
|
430
403
|
continue;
|
|
431
404
|
}
|
|
432
405
|
const start = Date.now();
|
|
433
406
|
logInfo(TAG, `[SLEEP] → ${step.name}`);
|
|
434
|
-
state.steps[step.name] = { status: "pending" };
|
|
407
|
+
state.steps[step.name] = { status: "pending", essential };
|
|
435
408
|
writeStateFile(statePath, state);
|
|
436
409
|
// Code-driven steps
|
|
437
410
|
if (step.name === "daily-summary") {
|
|
@@ -439,57 +412,44 @@ export async function runSleepCycle(opts) {
|
|
|
439
412
|
const ctxWindow = getAbmindEnv().sleepCtxWindow;
|
|
440
413
|
const userId = sleepData.getPrimaryUserId();
|
|
441
414
|
const watermarkTs = sleepData.getExtractionWatermark(userId);
|
|
442
|
-
// Determine target date from first unprocessed message
|
|
443
415
|
const firstMsgTs = sleepData.getFirstMessageAfter(userId, watermarkTs);
|
|
444
416
|
const firstMsgDate = firstMsgTs ? new Date(firstMsgTs) : new Date(now());
|
|
445
417
|
const targetDate = `${firstMsgDate.getFullYear()}-${String(firstMsgDate.getMonth() + 1).padStart(2, "0")}-${String(firstMsgDate.getDate()).padStart(2, "0")}`;
|
|
446
|
-
const summary = await buildDailySummary(sleepData.getDb(), (p) =>
|
|
418
|
+
const summary = await buildDailySummary(sleepData.getDb(), (p) => sendToRuntime(runtime, p, "daily-summary", runId, signal, budget, domainRetryDelayMs).then(r => { if (r === null)
|
|
447
419
|
throw new LLMUnavailableError(); return r; }), {
|
|
448
420
|
ctxWindow, memoryDir: memoryConfig.memoryDir, userId, watermarkTs,
|
|
449
421
|
});
|
|
450
422
|
if (summary) {
|
|
451
423
|
dailySummaryPath = writeDailyFile(memoryConfig.memoryDir, targetDate, summary);
|
|
452
|
-
state.steps[step.name] = { status: "ok", duration: Math.round((Date.now() - start) / 100) / 10, path: dailySummaryPath };
|
|
424
|
+
state.steps[step.name] = { status: "ok", essential, duration: Math.round((Date.now() - start) / 100) / 10, path: dailySummaryPath };
|
|
453
425
|
writeFileSync(join(stepLogDir, `${String(stepIndex).padStart(2, "0")}-${step.name}.md`), redactSecrets(summary), "utf-8");
|
|
454
426
|
}
|
|
455
427
|
else {
|
|
456
|
-
state.steps[step.name] = { status: "skipped" };
|
|
428
|
+
state.steps[step.name] = { status: "skipped", essential };
|
|
457
429
|
}
|
|
458
430
|
}
|
|
459
431
|
catch (err) {
|
|
460
|
-
logWarn(TAG, `[SLEEP]
|
|
461
|
-
state.steps[step.name] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
432
|
+
logWarn(TAG, `[SLEEP] daily-summary failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
433
|
+
state.steps[step.name] = { status: "failed", essential, duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
462
434
|
dreamySucceeded = false;
|
|
463
435
|
writeStateFile(statePath, state);
|
|
464
|
-
|
|
465
|
-
if (err instanceof
|
|
466
|
-
logWarn(TAG, `[SLEEP] ${step.name} —
|
|
436
|
+
emitSleepEvent(options.onEvent, { type: "step_failed", runId, step: toSummary(step.name, "failed", essential, state.steps[step.name]) });
|
|
437
|
+
if (err instanceof TransportUnavailableError) {
|
|
438
|
+
logWarn(TAG, `[SLEEP] ${step.name} — runtime rejected, stopping cycle (not advancing to next phase)`);
|
|
467
439
|
break;
|
|
468
440
|
}
|
|
469
441
|
}
|
|
470
442
|
if (state.steps[step.name]?.status !== "failed") {
|
|
471
|
-
// success/skip path: persist state + log + fire event
|
|
472
443
|
writeStateFile(statePath, state);
|
|
444
|
+
const s = state.steps[step.name];
|
|
445
|
+
emitSleepEvent(options.onEvent, s.status === "ok"
|
|
446
|
+
? { type: "step_completed", runId, step: toSummary(step.name, "completed", essential, s) }
|
|
447
|
+
: { type: "step_skipped", runId, step: toSummary(step.name, "skipped", essential, s) });
|
|
473
448
|
}
|
|
474
449
|
logInfo(TAG, `[SLEEP] ${state.steps[step.name]?.status === "ok" ? "✓" : "✗"} ${step.name} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
475
|
-
// #895: emit terminal event for code-driven daily-summary (failure already fired in catch)
|
|
476
|
-
if (state.steps[step.name]?.status !== "failed") {
|
|
477
|
-
fireOnStep(opts.onStep, {
|
|
478
|
-
name: step.name,
|
|
479
|
-
filename: step.filename,
|
|
480
|
-
index: stepIndex,
|
|
481
|
-
total: totalSteps,
|
|
482
|
-
phase: state.steps[step.name]?.status === "ok" ? "done"
|
|
483
|
-
: state.steps[step.name]?.status === "skipped" ? "skipped"
|
|
484
|
-
: "failed",
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
450
|
continue;
|
|
488
451
|
}
|
|
489
452
|
if (step.name === "extract-memories") {
|
|
490
|
-
// Resume path: if daily-summary already completed in a prior run, the
|
|
491
|
-
// in-memory dailySummaryPath is null. Recover it from the lock's
|
|
492
|
-
// recorded path so extract-memories can still run. #181.
|
|
493
453
|
if (!dailySummaryPath) {
|
|
494
454
|
const priorPath = state.steps["daily-summary"]?.path;
|
|
495
455
|
if (priorPath && existsSync(priorPath)) {
|
|
@@ -498,47 +458,38 @@ export async function runSleepCycle(opts) {
|
|
|
498
458
|
}
|
|
499
459
|
}
|
|
500
460
|
if (!dailySummaryPath) {
|
|
501
|
-
state.steps[step.name] = { status: "skipped" };
|
|
461
|
+
state.steps[step.name] = { status: "skipped", essential };
|
|
502
462
|
writeStateFile(statePath, state);
|
|
503
463
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — no daily summary`);
|
|
504
|
-
|
|
505
|
-
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
464
|
+
emitSleepEvent(options.onEvent, { type: "step_skipped", runId, step: toSummary(step.name, "skipped", essential, state.steps[step.name]) });
|
|
506
465
|
continue;
|
|
507
466
|
}
|
|
508
467
|
try {
|
|
509
468
|
const userId = sleepData.getPrimaryUserId();
|
|
510
|
-
const result = await extractFromDaily(dailySummaryPath, userId, (p) =>
|
|
469
|
+
const result = await extractFromDaily(dailySummaryPath, userId, (p) => sendToRuntime(runtime, p, "extract-memories", runId, signal, budget, domainRetryDelayMs).then(r => { if (r === null)
|
|
511
470
|
throw new LLMUnavailableError(); return r; }));
|
|
512
|
-
state.steps[step.name] = { status: "ok", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
471
|
+
state.steps[step.name] = { status: "ok", essential, duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
513
472
|
writeFileSync(join(stepLogDir, `${String(stepIndex).padStart(2, "0")}-${step.name}.md`), redactSecrets(result), "utf-8");
|
|
514
473
|
logInfo(TAG, `[SLEEP] ✓ ${step.name} (${((Date.now() - start) / 1000).toFixed(1)}s) — ${result.slice(0, 80)}`);
|
|
515
474
|
}
|
|
516
475
|
catch (err) {
|
|
517
|
-
logWarn(TAG, `[SLEEP]
|
|
518
|
-
state.steps[step.name] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
476
|
+
logWarn(TAG, `[SLEEP] extract-memories failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
477
|
+
state.steps[step.name] = { status: "failed", essential, duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
519
478
|
dreamySucceeded = false;
|
|
520
479
|
writeStateFile(statePath, state);
|
|
521
|
-
|
|
522
|
-
if (err instanceof
|
|
523
|
-
logWarn(TAG, `[SLEEP] ${step.name} —
|
|
480
|
+
emitSleepEvent(options.onEvent, { type: "step_failed", runId, step: toSummary(step.name, "failed", essential, state.steps[step.name]) });
|
|
481
|
+
if (err instanceof TransportUnavailableError) {
|
|
482
|
+
logWarn(TAG, `[SLEEP] ${step.name} — runtime rejected, stopping cycle (not advancing to next phase)`);
|
|
524
483
|
break;
|
|
525
484
|
}
|
|
526
485
|
}
|
|
527
486
|
if (state.steps[step.name]?.status !== "failed") {
|
|
528
487
|
writeStateFile(statePath, state);
|
|
529
|
-
|
|
530
|
-
fireOnStep(opts.onStep, {
|
|
531
|
-
name: step.name,
|
|
532
|
-
filename: step.filename,
|
|
533
|
-
index: stepIndex,
|
|
534
|
-
total: totalSteps,
|
|
535
|
-
phase: state.steps[step.name]?.status === "ok" ? "done" : "failed",
|
|
536
|
-
});
|
|
488
|
+
emitSleepEvent(options.onEvent, { type: "step_completed", runId, step: toSummary(step.name, "completed", essential, state.steps[step.name]) });
|
|
537
489
|
}
|
|
538
490
|
continue;
|
|
539
491
|
}
|
|
540
492
|
// Standard prompt-driven step — JIT substitution
|
|
541
|
-
// Populate contradiction+graph vars JIT (needs today's extractions to exist)
|
|
542
493
|
if (step.name === "contradiction-and-graph") {
|
|
543
494
|
try {
|
|
544
495
|
const todayStart = new Date(now());
|
|
@@ -546,15 +497,13 @@ export async function runSleepCycle(opts) {
|
|
|
546
497
|
const memDb = memory.getDatabase();
|
|
547
498
|
const newRows = (memDb?.prepare(`SELECT id, content_en, memory_type, topic, trust FROM extracted_memories WHERE created_at >= ? AND memory_type != 'observation' ORDER BY created_at DESC LIMIT 30`).all(todayStart.getTime()) ?? []);
|
|
548
499
|
if (newRows.length === 0) {
|
|
549
|
-
state.steps[step.name] = { status: "skipped" };
|
|
500
|
+
state.steps[step.name] = { status: "skipped", essential };
|
|
550
501
|
writeStateFile(statePath, state);
|
|
551
502
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — no new extractions today`);
|
|
552
|
-
|
|
553
|
-
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
503
|
+
emitSleepEvent(options.onEvent, { type: "step_skipped", runId, step: toSummary(step.name, "skipped", essential, state.steps[step.name]) });
|
|
554
504
|
continue;
|
|
555
505
|
}
|
|
556
506
|
vars.NEW_EXTRACTIONS = newRows.map(r => `[id=${r.id}] (${r.memory_type}, trust=${r.trust}) ${r.content_en}`).join("\n");
|
|
557
|
-
// Find candidates: FTS5 top-5 per new memory, capped at 20 total
|
|
558
507
|
const candidateIds = new Set();
|
|
559
508
|
const candidateRows = [];
|
|
560
509
|
for (const nr of newRows.slice(0, 5)) {
|
|
@@ -578,67 +527,64 @@ export async function runSleepCycle(opts) {
|
|
|
578
527
|
}
|
|
579
528
|
catch (err) {
|
|
580
529
|
logWarn(TAG, `[SLEEP] contradiction-and-graph var prep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
581
|
-
state.steps[step.name] = { status: "skipped" };
|
|
530
|
+
state.steps[step.name] = { status: "skipped", essential };
|
|
582
531
|
writeStateFile(statePath, state);
|
|
583
|
-
|
|
584
|
-
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
532
|
+
emitSleepEvent(options.onEvent, { type: "step_skipped", runId, step: toSummary(step.name, "skipped", essential, state.steps[step.name]) });
|
|
585
533
|
continue;
|
|
586
534
|
}
|
|
587
535
|
}
|
|
588
|
-
// Populate REM sample vars JIT
|
|
589
536
|
if (step.name === "rem-synthesis") {
|
|
590
537
|
try {
|
|
591
538
|
const memDb = memory.getDatabase();
|
|
592
539
|
const sample = memDb?.prepare(`SELECT id, content_en, memory_type, created_at FROM extracted_memories WHERE trust >= 2 AND memory_type != 'observation' AND valid_to IS NULL ORDER BY RANDOM() LIMIT 10`).all() ?? [];
|
|
593
540
|
if (sample.length < 5) {
|
|
594
|
-
state.steps[step.name] = { status: "skipped" };
|
|
541
|
+
state.steps[step.name] = { status: "skipped", essential };
|
|
595
542
|
writeStateFile(statePath, state);
|
|
596
543
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — not enough memories for REM`);
|
|
597
|
-
|
|
598
|
-
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
544
|
+
emitSleepEvent(options.onEvent, { type: "step_skipped", runId, step: toSummary(step.name, "skipped", essential, state.steps[step.name]) });
|
|
599
545
|
continue;
|
|
600
546
|
}
|
|
601
547
|
vars.REM_SAMPLE = sample.map(r => `[${r.memory_type}, ${new Date(r.created_at).toISOString().slice(0, 10)}] ${r.content_en}`).join("\n");
|
|
602
548
|
}
|
|
603
549
|
catch {
|
|
604
|
-
state.steps[step.name] = { status: "skipped" };
|
|
550
|
+
state.steps[step.name] = { status: "skipped", essential };
|
|
605
551
|
writeStateFile(statePath, state);
|
|
606
|
-
|
|
607
|
-
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
552
|
+
emitSleepEvent(options.onEvent, { type: "step_skipped", runId, step: toSummary(step.name, "skipped", essential, state.steps[step.name]) });
|
|
608
553
|
continue;
|
|
609
554
|
}
|
|
610
555
|
}
|
|
611
556
|
const prompt = substituteVars(step.rawPrompt, vars);
|
|
612
557
|
const fullPrompt = soulPrefix + prompt;
|
|
613
558
|
if (soulPrefix)
|
|
614
|
-
soulPrefix = "";
|
|
615
|
-
const ctxBefore = -1;
|
|
559
|
+
soulPrefix = "";
|
|
616
560
|
let response;
|
|
617
561
|
try {
|
|
618
|
-
response = await
|
|
562
|
+
response = await sendToRuntime(runtime, fullPrompt, step.name, runId, signal, budget, domainRetryDelayMs);
|
|
619
563
|
}
|
|
620
564
|
catch (err) {
|
|
621
|
-
if (err instanceof
|
|
565
|
+
if (err instanceof TransportUnavailableError) {
|
|
622
566
|
const duration = Date.now() - start;
|
|
623
|
-
logWarn(TAG, `[SLEEP] ${step.name} —
|
|
624
|
-
state.steps[step.name] = { status: "failed", duration: Math.round(duration / 100) / 10
|
|
567
|
+
logWarn(TAG, `[SLEEP] ${step.name} — runtime rejected, stopping cycle (not advancing to next phase)`);
|
|
568
|
+
state.steps[step.name] = { status: "failed", essential, duration: Math.round(duration / 100) / 10 };
|
|
625
569
|
dreamySucceeded = false;
|
|
626
570
|
writeStateFile(statePath, state);
|
|
627
|
-
|
|
571
|
+
emitSleepEvent(options.onEvent, { type: "step_failed", runId, step: toSummary(step.name, "failed", essential, state.steps[step.name]) });
|
|
628
572
|
break;
|
|
629
573
|
}
|
|
630
574
|
throw err;
|
|
631
575
|
}
|
|
632
|
-
const ctxAfter = -1;
|
|
633
576
|
const duration = Date.now() - start;
|
|
577
|
+
// Checkpoint boundary: after the awaited call, before applying its output.
|
|
578
|
+
if (signal.aborted) {
|
|
579
|
+
persistCancelled();
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
634
582
|
if (response) {
|
|
635
|
-
state.steps[step.name] = { status: "ok", duration: Math.round(duration / 100) / 10
|
|
583
|
+
state.steps[step.name] = { status: "ok", essential, duration: Math.round(duration / 100) / 10 };
|
|
636
584
|
writeFileSync(join(stepLogDir, `${String(stepIndex).padStart(2, "0")}-${step.name}.md`), redactSecrets(response), "utf-8");
|
|
637
|
-
// Generic output chaining + explicit aliases
|
|
638
585
|
vars[step.name.toUpperCase().replace(/-/g, "_") + "_OUTPUT"] = response;
|
|
639
586
|
if (step.name === "retrospective")
|
|
640
587
|
vars.RETRO_CONTENT = response;
|
|
641
|
-
// #515: parse CONTRADICT directives from step 15 and invalidate old memories
|
|
642
588
|
if (step.name === "contradiction-and-graph") {
|
|
643
589
|
const memDb = memory.getDatabase();
|
|
644
590
|
if (memDb) {
|
|
@@ -650,24 +596,22 @@ export async function runSleepCycle(opts) {
|
|
|
650
596
|
if (changes > 0)
|
|
651
597
|
logInfo(TAG, `[SLEEP] Invalidated memory #${oldId} (contradicted)`);
|
|
652
598
|
}
|
|
653
|
-
// #520: parse RELATION directives → entity_graph
|
|
654
599
|
const relationRe = /RELATION\s+entity_a="([^"]+)"\s+entity_b="([^"]+)"\s+rel="([^"]+)"/g;
|
|
655
600
|
let rm;
|
|
656
601
|
while ((rm = relationRe.exec(response)) !== null) {
|
|
657
602
|
const [, a, b, rel] = rm;
|
|
658
603
|
memDb.prepare(`INSERT INTO entity_graph (entity_a, entity_b, relation, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(entity_a, entity_b, relation) DO UPDATE SET last_seen_at = ?`).run(a, b, rel, Date.now(), Date.now(), Date.now());
|
|
659
604
|
}
|
|
660
|
-
// #529: age out stale event memories via decay (recall_count / age_days < threshold)
|
|
661
605
|
const EVENT_MIN_AGE_DAYS = 7;
|
|
662
|
-
const DECAY_THRESHOLD = 0.1;
|
|
663
|
-
const
|
|
664
|
-
const
|
|
606
|
+
const DECAY_THRESHOLD = 0.1;
|
|
607
|
+
const nowMs = Date.now();
|
|
608
|
+
const decayCandidates = memDb.prepare(`SELECT id, recall_count, created_at FROM extracted_memories WHERE memory_type = 'event' AND valid_to IS NULL AND created_at < ?`).all(nowMs - EVENT_MIN_AGE_DAYS * 86400_000);
|
|
665
609
|
let agedCount = 0;
|
|
666
|
-
for (const m of
|
|
667
|
-
const ageDays = (
|
|
610
|
+
for (const m of decayCandidates) {
|
|
611
|
+
const ageDays = (nowMs - m.created_at) / 86400_000;
|
|
668
612
|
const score = m.recall_count / ageDays;
|
|
669
613
|
if (score < DECAY_THRESHOLD) {
|
|
670
|
-
memDb.prepare("UPDATE extracted_memories SET valid_to = ? WHERE id = ?").run(
|
|
614
|
+
memDb.prepare("UPDATE extracted_memories SET valid_to = ? WHERE id = ?").run(nowMs, m.id);
|
|
671
615
|
agedCount++;
|
|
672
616
|
}
|
|
673
617
|
}
|
|
@@ -677,57 +621,61 @@ export async function runSleepCycle(opts) {
|
|
|
677
621
|
}
|
|
678
622
|
}
|
|
679
623
|
else {
|
|
680
|
-
state.steps[step.name] = { status: "failed", duration: Math.round(duration / 100) / 10, attempts:
|
|
624
|
+
state.steps[step.name] = { status: "failed", essential, duration: Math.round(duration / 100) / 10, attempts: MAX_DOMAIN_RETRIES };
|
|
681
625
|
dreamySucceeded = false;
|
|
682
626
|
}
|
|
683
627
|
writeStateFile(statePath, state);
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
filename: step.filename,
|
|
688
|
-
index: stepIndex,
|
|
689
|
-
total: totalSteps,
|
|
690
|
-
phase: response ? "done" : "failed",
|
|
691
|
-
});
|
|
628
|
+
emitSleepEvent(options.onEvent, response
|
|
629
|
+
? { type: "step_completed", runId, step: toSummary(step.name, "completed", essential, state.steps[step.name]) }
|
|
630
|
+
: { type: "step_failed", runId, step: toSummary(step.name, "failed", essential, state.steps[step.name]) });
|
|
692
631
|
logInfo(TAG, `[SLEEP] ${response ? "✓" : "✗"} ${step.name} (${(duration / 1000).toFixed(1)}s, ${response?.length ?? 0} chars)`);
|
|
693
|
-
// Backoff between steps: 10s → 30s → 60s on consecutive failures, reset on success
|
|
694
632
|
if (response) {
|
|
695
633
|
consecutiveFailures = 0;
|
|
696
634
|
}
|
|
697
635
|
else {
|
|
698
636
|
consecutiveFailures++;
|
|
699
637
|
}
|
|
700
|
-
const isEssential =
|
|
638
|
+
const isEssential = essential;
|
|
701
639
|
if (!isEssential) {
|
|
702
|
-
const delayMs =
|
|
703
|
-
if (delayMs > 0) {
|
|
640
|
+
const delayMs = betweenStepBackoffMs(consecutiveFailures);
|
|
641
|
+
if (delayMs > 0 && consecutiveFailures > 0) {
|
|
704
642
|
logInfo(TAG, `[SLEEP] Waiting ${Math.round(delayMs / 1000)}s before next step`);
|
|
705
643
|
await new Promise(r => setTimeout(r, delayMs));
|
|
706
644
|
}
|
|
707
645
|
}
|
|
646
|
+
// Checkpoint boundary: between step mutations.
|
|
647
|
+
if (signal.aborted) {
|
|
648
|
+
persistCancelled();
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
708
651
|
}
|
|
709
652
|
}
|
|
710
653
|
finally {
|
|
711
|
-
|
|
712
|
-
|
|
654
|
+
cleanupCancellation();
|
|
655
|
+
}
|
|
656
|
+
if (cancelled) {
|
|
657
|
+
const result = projectResult(runId, "cancelled", startedAt, now(), state, /* watermarkAdvanced */ false, /* resumable */ true);
|
|
658
|
+
emitSleepEvent(options.onEvent, { type: "cycle_finished", runId, result });
|
|
659
|
+
return result;
|
|
713
660
|
}
|
|
714
661
|
// Set final status
|
|
715
662
|
if (state.status === "ongoing") {
|
|
716
663
|
state.status = dreamySucceeded ? "completed" : "failed";
|
|
717
664
|
writeStateFile(statePath, state);
|
|
718
665
|
}
|
|
719
|
-
//
|
|
720
|
-
|
|
666
|
+
// Checkpoint boundary: before watermark advance.
|
|
667
|
+
let watermarkAdvanced = false;
|
|
668
|
+
if (dreamySucceeded && !signal.aborted) {
|
|
721
669
|
try {
|
|
722
670
|
const count = sleepData.advanceExtractionWatermarks();
|
|
671
|
+
watermarkAdvanced = count > 0;
|
|
723
672
|
logInfo(TAG, `[SLEEP] Extraction watermark advanced for ${count} chat(s)`);
|
|
724
673
|
}
|
|
725
674
|
catch { /* non-fatal */ }
|
|
726
675
|
}
|
|
727
|
-
else {
|
|
676
|
+
else if (!dreamySucceeded) {
|
|
728
677
|
logWarn(TAG, "[SLEEP] Watermark NOT advanced — essential steps failed, messages preserved for catch-up");
|
|
729
678
|
}
|
|
730
|
-
// Write audit
|
|
731
679
|
const stepEntries = Object.entries(state.steps);
|
|
732
680
|
const okCount = stepEntries.filter(([, s]) => s.status === "ok").length;
|
|
733
681
|
const failCount = stepEntries.filter(([, s]) => s.status === "failed" || s.status === "timeout").length;
|
|
@@ -746,10 +694,8 @@ export async function runSleepCycle(opts) {
|
|
|
746
694
|
catch (err) {
|
|
747
695
|
process.stderr.write(`Warning: Failed to write audit — ${err instanceof Error ? err.message : String(err)}\n`);
|
|
748
696
|
}
|
|
749
|
-
// Wired post-task: flush old messages (keep max 500, age out >7 days, garbage 12h)
|
|
750
697
|
if (dreamySucceeded) {
|
|
751
698
|
try {
|
|
752
|
-
// Flush garbage-marked messages
|
|
753
699
|
const garbagePath = join(memoryConfig.memoryDir, "garbage.json");
|
|
754
700
|
if (existsSync(garbagePath)) {
|
|
755
701
|
const raw = JSON.parse(readFileSync(garbagePath, "utf-8"));
|
|
@@ -763,7 +709,6 @@ export async function runSleepCycle(opts) {
|
|
|
763
709
|
writeFileSync(garbagePath, "[]");
|
|
764
710
|
}
|
|
765
711
|
}
|
|
766
|
-
// Age out + cap
|
|
767
712
|
const { agedOut, capped } = sleepData.flushOldMessages({ maxAgeDays: 7, maxCount: 500 });
|
|
768
713
|
if (agedOut > 0)
|
|
769
714
|
logInfo(TAG, `[SLEEP] Flushed ${agedOut} messages >7d`);
|
|
@@ -774,9 +719,7 @@ export async function runSleepCycle(opts) {
|
|
|
774
719
|
logWarn(TAG, `[WIRED] flush failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
775
720
|
}
|
|
776
721
|
}
|
|
777
|
-
emitProgress("done");
|
|
778
722
|
logInfo(TAG, `[SLEEP] 🏁 ${okCount} ok, ${failCount} failed, ${skipCount} skipped | wired: ${formatWiredResults(wiredResults)} | ${totalDuration.toFixed(0)}s total`);
|
|
779
|
-
// Record result in _meta (#447)
|
|
780
723
|
if (failCount === 0) {
|
|
781
724
|
metaSet(db, "sleep_last_success_ts", Date.now());
|
|
782
725
|
metaSet(db, "sleep_consecutive_failures", 0);
|
|
@@ -786,13 +729,86 @@ export async function runSleepCycle(opts) {
|
|
|
786
729
|
metaSet(db, "sleep_consecutive_failures", prev + 1);
|
|
787
730
|
metaSet(db, "sleep_last_fail_reason", `${failCount} step(s) failed`);
|
|
788
731
|
}
|
|
789
|
-
|
|
732
|
+
const terminalStatus = failCount === 0
|
|
733
|
+
? "completed"
|
|
734
|
+
: failedEssentials(state).length > 0
|
|
735
|
+
? (okCount > 0 || skipCount > 0 ? "partial" : "failed")
|
|
736
|
+
: "partial";
|
|
737
|
+
const result = projectResult(runId, terminalStatus, startedAt, now(), state, watermarkAdvanced, failedEssentials(state).length > 0);
|
|
738
|
+
emitSleepEvent(options.onEvent, { type: "cycle_finished", runId, result });
|
|
739
|
+
return result;
|
|
790
740
|
}
|
|
791
741
|
finally {
|
|
742
|
+
activeRunsByMemoryDir.delete(memoryDirKey);
|
|
792
743
|
memory.close();
|
|
793
744
|
}
|
|
794
745
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
746
|
+
function sleepDir(memoryDir) {
|
|
747
|
+
return join(memoryDir, "sleep");
|
|
748
|
+
}
|
|
749
|
+
/** Internal marker for a timeout-triggered abort (never surfaced as a public error type). */
|
|
750
|
+
class SleepTimeoutReason extends Error {
|
|
751
|
+
constructor() { super("sleep cycle timeout"); this.name = "SleepTimeoutReason"; }
|
|
752
|
+
}
|
|
753
|
+
function toSummary(id, status, essential, s) {
|
|
754
|
+
return {
|
|
755
|
+
id,
|
|
756
|
+
status,
|
|
757
|
+
essential,
|
|
758
|
+
attempts: s?.attempts ?? 1,
|
|
759
|
+
durationMs: s?.duration != null ? Math.round(s.duration * 1000) : undefined,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function projectResult(runId, status, startedAt, finishedAt, state, watermarkAdvanced, resumable) {
|
|
763
|
+
const steps = Object.entries(state.steps).map(([id, s]) => toSummary(id, s.status === "ok" ? "completed" : s.status === "timeout" ? "timeout" : s.status === "skipped" ? "skipped" : "failed", s.essential ?? ESSENTIAL_STEPS.has(id), s));
|
|
764
|
+
const essentialFailures = failedEssentials(state);
|
|
765
|
+
const okCount = steps.filter(s => s.status === "completed").length;
|
|
766
|
+
const failCount = steps.filter(s => s.status === "failed" || s.status === "timeout").length;
|
|
767
|
+
const skipCount = steps.filter(s => s.status === "skipped").length;
|
|
768
|
+
const report = `Sleep ${status} — ${okCount} completed, ${failCount} failed, ${skipCount} skipped (of ${steps.length}).`
|
|
769
|
+
+ (essentialFailures.length > 0 ? ` Essential failures: ${essentialFailures.join(", ")}.` : "");
|
|
770
|
+
return {
|
|
771
|
+
runId,
|
|
772
|
+
status,
|
|
773
|
+
startedAt,
|
|
774
|
+
finishedAt,
|
|
775
|
+
llmCalls: state.llmCalls ?? 0,
|
|
776
|
+
steps,
|
|
777
|
+
essentialFailures,
|
|
778
|
+
resumable,
|
|
779
|
+
watermarkAdvanced,
|
|
780
|
+
report,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function alreadyRunningResult(startedAt, finishedAt) {
|
|
784
|
+
return {
|
|
785
|
+
runId: "",
|
|
786
|
+
status: "already_running",
|
|
787
|
+
startedAt,
|
|
788
|
+
finishedAt,
|
|
789
|
+
llmCalls: 0,
|
|
790
|
+
steps: [],
|
|
791
|
+
essentialFailures: [],
|
|
792
|
+
resumable: false,
|
|
793
|
+
watermarkAdvanced: false,
|
|
794
|
+
report: "Sleep cycle already running — no-op.",
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function noWorkResult(runId, startedAt, finishedAt) {
|
|
798
|
+
return {
|
|
799
|
+
runId,
|
|
800
|
+
status: "no_work",
|
|
801
|
+
startedAt,
|
|
802
|
+
finishedAt,
|
|
803
|
+
llmCalls: 0,
|
|
804
|
+
steps: [],
|
|
805
|
+
essentialFailures: [],
|
|
806
|
+
resumable: false,
|
|
807
|
+
watermarkAdvanced: false,
|
|
808
|
+
report: "No messages since last sleep — nothing to process.",
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
// CLI entry + isDirectRun removed — the standalone entry point lives in
|
|
812
|
+
// cli/abmind-sleep.ts. Library consumers call runSleepCycle(options) directly
|
|
813
|
+
// with their own SleepRuntime.
|
|
798
814
|
//# sourceMappingURL=orchestrator.js.map
|