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.
Files changed (173) hide show
  1. package/SUPPORTED-SURFACE.md +20 -2
  2. package/dist/cli/abmind-hook-recall.js +17 -40
  3. package/dist/cli/abmind-hook-recall.js.map +1 -1
  4. package/dist/cli/abmind-hook-store.js +10 -31
  5. package/dist/cli/abmind-hook-store.js.map +1 -1
  6. package/dist/cli/abmind-hook-wakeup.js +17 -3
  7. package/dist/cli/abmind-hook-wakeup.js.map +1 -1
  8. package/dist/cli/abmind-install.js +19 -19
  9. package/dist/cli/abmind-install.js.map +1 -1
  10. package/dist/cli/abmind-restore.js +10 -0
  11. package/dist/cli/abmind-restore.js.map +1 -1
  12. package/dist/cli/abmind-sleep.d.ts +9 -5
  13. package/dist/cli/abmind-sleep.d.ts.map +1 -1
  14. package/dist/cli/abmind-sleep.js +61 -14
  15. package/dist/cli/abmind-sleep.js.map +1 -1
  16. package/dist/cli/hook-lifecycle-adapter.d.ts +11 -0
  17. package/dist/cli/hook-lifecycle-adapter.d.ts.map +1 -0
  18. package/dist/cli/hook-lifecycle-adapter.js +33 -0
  19. package/dist/cli/hook-lifecycle-adapter.js.map +1 -0
  20. package/dist/src/context-engine.d.ts +13 -3
  21. package/dist/src/context-engine.d.ts.map +1 -1
  22. package/dist/src/context-engine.js +22 -5
  23. package/dist/src/context-engine.js.map +1 -1
  24. package/dist/src/context-orchestrator.d.ts +20 -2
  25. package/dist/src/context-orchestrator.d.ts.map +1 -1
  26. package/dist/src/context-orchestrator.js +11 -3
  27. package/dist/src/context-orchestrator.js.map +1 -1
  28. package/dist/src/context-tier-renderer.d.ts +8 -1
  29. package/dist/src/context-tier-renderer.d.ts.map +1 -1
  30. package/dist/src/context-tier-renderer.js +7 -2
  31. package/dist/src/context-tier-renderer.js.map +1 -1
  32. package/dist/src/ensure-initialized.d.ts +5 -0
  33. package/dist/src/ensure-initialized.d.ts.map +1 -1
  34. package/dist/src/ensure-initialized.js +19 -28
  35. package/dist/src/ensure-initialized.js.map +1 -1
  36. package/dist/src/host-integration/identity.d.ts +10 -0
  37. package/dist/src/host-integration/identity.d.ts.map +1 -0
  38. package/dist/src/host-integration/identity.js +66 -0
  39. package/dist/src/host-integration/identity.js.map +1 -0
  40. package/dist/src/host-integration/index.d.ts +5 -0
  41. package/dist/src/host-integration/index.d.ts.map +1 -0
  42. package/dist/src/host-integration/index.js +4 -0
  43. package/dist/src/host-integration/index.js.map +1 -0
  44. package/dist/src/host-integration/lifecycle.d.ts +14 -0
  45. package/dist/src/host-integration/lifecycle.d.ts.map +1 -0
  46. package/dist/src/host-integration/lifecycle.js +192 -0
  47. package/dist/src/host-integration/lifecycle.js.map +1 -0
  48. package/dist/src/host-integration/render.d.ts +5 -0
  49. package/dist/src/host-integration/render.d.ts.map +1 -0
  50. package/dist/src/host-integration/render.js +20 -0
  51. package/dist/src/host-integration/render.js.map +1 -0
  52. package/dist/src/host-integration/types.d.ts +107 -0
  53. package/dist/src/host-integration/types.d.ts.map +1 -0
  54. package/dist/src/host-integration/types.js +2 -0
  55. package/dist/src/host-integration/types.js.map +1 -0
  56. package/dist/src/imemory-system.d.ts +1 -1
  57. package/dist/src/imemory-system.d.ts.map +1 -1
  58. package/dist/src/index.d.ts +6 -4
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/index.js +3 -1
  61. package/dist/src/index.js.map +1 -1
  62. package/dist/src/maintenance-service.d.ts +11 -0
  63. package/dist/src/maintenance-service.d.ts.map +1 -1
  64. package/dist/src/maintenance-service.js +43 -0
  65. package/dist/src/maintenance-service.js.map +1 -1
  66. package/dist/src/memory-manager.d.ts +6 -2
  67. package/dist/src/memory-manager.d.ts.map +1 -1
  68. package/dist/src/memory-manager.js +9 -5
  69. package/dist/src/memory-manager.js.map +1 -1
  70. package/dist/src/message-store.d.ts +15 -3
  71. package/dist/src/message-store.d.ts.map +1 -1
  72. package/dist/src/message-store.js +28 -9
  73. package/dist/src/message-store.js.map +1 -1
  74. package/dist/src/openclaw-plugin/index.d.ts.map +1 -1
  75. package/dist/src/openclaw-plugin/index.js +3 -2
  76. package/dist/src/openclaw-plugin/index.js.map +1 -1
  77. package/dist/src/pi-plugin/abtars-client.d.ts +22 -0
  78. package/dist/src/pi-plugin/abtars-client.d.ts.map +1 -0
  79. package/dist/src/pi-plugin/abtars-client.js +153 -0
  80. package/dist/src/pi-plugin/abtars-client.js.map +1 -0
  81. package/dist/src/pi-plugin/abtars-tools.d.ts +8 -0
  82. package/dist/src/pi-plugin/abtars-tools.d.ts.map +1 -0
  83. package/dist/src/pi-plugin/abtars-tools.js +317 -0
  84. package/dist/src/pi-plugin/abtars-tools.js.map +1 -0
  85. package/dist/src/pi-plugin/identity.d.ts +8 -0
  86. package/dist/src/pi-plugin/identity.d.ts.map +1 -0
  87. package/dist/src/pi-plugin/identity.js +37 -0
  88. package/dist/src/pi-plugin/identity.js.map +1 -0
  89. package/dist/src/pi-plugin/index.d.ts +3 -0
  90. package/dist/src/pi-plugin/index.d.ts.map +1 -0
  91. package/dist/src/pi-plugin/index.js +139 -0
  92. package/dist/src/pi-plugin/index.js.map +1 -0
  93. package/dist/src/pi-plugin/messages.d.ts +4 -0
  94. package/dist/src/pi-plugin/messages.d.ts.map +1 -0
  95. package/dist/src/pi-plugin/messages.js +36 -0
  96. package/dist/src/pi-plugin/messages.js.map +1 -0
  97. package/dist/src/pi-plugin/pi-types.d.ts +126 -0
  98. package/dist/src/pi-plugin/pi-types.d.ts.map +1 -0
  99. package/dist/src/pi-plugin/pi-types.js +15 -0
  100. package/dist/src/pi-plugin/pi-types.js.map +1 -0
  101. package/dist/src/pi-plugin/runtime.d.ts +21 -0
  102. package/dist/src/pi-plugin/runtime.d.ts.map +1 -0
  103. package/dist/src/pi-plugin/runtime.js +60 -0
  104. package/dist/src/pi-plugin/runtime.js.map +1 -0
  105. package/dist/src/pi-plugin/tools.d.ts +11 -0
  106. package/dist/src/pi-plugin/tools.d.ts.map +1 -0
  107. package/dist/src/pi-plugin/tools.js +136 -0
  108. package/dist/src/pi-plugin/tools.js.map +1 -0
  109. package/dist/src/reconcile.d.ts +2 -0
  110. package/dist/src/reconcile.d.ts.map +1 -1
  111. package/dist/src/reconcile.js +21 -4
  112. package/dist/src/reconcile.js.map +1 -1
  113. package/dist/src/session-context.d.ts +1 -0
  114. package/dist/src/session-context.d.ts.map +1 -1
  115. package/dist/src/session-context.js +42 -23
  116. package/dist/src/session-context.js.map +1 -1
  117. package/dist/src/sleep/basic.d.ts +1 -1
  118. package/dist/src/sleep/basic.d.ts.map +1 -1
  119. package/dist/src/sleep/basic.js +5 -2
  120. package/dist/src/sleep/basic.js.map +1 -1
  121. package/dist/src/sleep/catchup.d.ts +3 -5
  122. package/dist/src/sleep/catchup.d.ts.map +1 -1
  123. package/dist/src/sleep/catchup.js +37 -55
  124. package/dist/src/sleep/catchup.js.map +1 -1
  125. package/dist/src/sleep/contracts.d.ts +112 -0
  126. package/dist/src/sleep/contracts.d.ts.map +1 -0
  127. package/dist/src/sleep/contracts.js +25 -0
  128. package/dist/src/sleep/contracts.js.map +1 -0
  129. package/dist/src/sleep/llm-budget.d.ts +34 -14
  130. package/dist/src/sleep/llm-budget.d.ts.map +1 -1
  131. package/dist/src/sleep/llm-budget.js +66 -52
  132. package/dist/src/sleep/llm-budget.js.map +1 -1
  133. package/dist/src/sleep/orchestrator.d.ts +19 -69
  134. package/dist/src/sleep/orchestrator.d.ts.map +1 -1
  135. package/dist/src/sleep/orchestrator.js +286 -270
  136. package/dist/src/sleep/orchestrator.js.map +1 -1
  137. package/dist/src/sleep/state.d.ts +7 -19
  138. package/dist/src/sleep/state.d.ts.map +1 -1
  139. package/dist/src/sleep/state.js +5 -38
  140. package/dist/src/sleep/state.js.map +1 -1
  141. package/dist/src/sleep/test-harness.d.ts +3 -1
  142. package/dist/src/sleep/test-harness.d.ts.map +1 -1
  143. package/dist/src/sleep/test-harness.js +3 -2
  144. package/dist/src/sleep/test-harness.js.map +1 -1
  145. package/dist/src/soul-seeder.d.ts +36 -0
  146. package/dist/src/soul-seeder.d.ts.map +1 -0
  147. package/dist/src/soul-seeder.js +69 -0
  148. package/dist/src/soul-seeder.js.map +1 -0
  149. package/dist/src/wake-up-builder.d.ts +3 -1
  150. package/dist/src/wake-up-builder.d.ts.map +1 -1
  151. package/dist/src/wake-up-builder.js +29 -5
  152. package/dist/src/wake-up-builder.js.map +1 -1
  153. package/package.json +20 -2
  154. package/dist/cli/abmind-rollback.d.ts +0 -8
  155. package/dist/cli/abmind-rollback.d.ts.map +0 -1
  156. package/dist/cli/abmind-rollback.js +0 -78
  157. package/dist/cli/abmind-rollback.js.map +0 -1
  158. package/dist/src/native-loader.d.ts +0 -7
  159. package/dist/src/native-loader.d.ts.map +0 -1
  160. package/dist/src/native-loader.js +0 -21
  161. package/dist/src/native-loader.js.map +0 -1
  162. package/dist/src/recall-benchmark.d.ts +0 -13
  163. package/dist/src/recall-benchmark.d.ts.map +0 -1
  164. package/dist/src/recall-benchmark.js +0 -260
  165. package/dist/src/recall-benchmark.js.map +0 -1
  166. package/dist/src/sleep/runtime.d.ts +0 -15
  167. package/dist/src/sleep/runtime.d.ts.map +0 -1
  168. package/dist/src/sleep/runtime.js +0 -2
  169. package/dist/src/sleep/runtime.js.map +0 -1
  170. package/dist/src/wake-up-renderer.d.ts +0 -24
  171. package/dist/src/wake-up-renderer.d.ts.map +0 -1
  172. package/dist/src/wake-up-renderer.js +0 -148
  173. 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 — library function for overnight memory maintenance.
3
+ * abmind sleep orchestrator — host-neutral memory-maintenance engine (#1353).
4
4
  *
5
- * Called via runSleepCycle({ runtime, level, ... }). Gathers system state,
6
- * runs through a pipeline of prompt-driven steps (gc-noise, daily-summary,
7
- * extract-memories, retrospective, retro-derive, etc.), persists audit log, returns result.
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, logError } from "../mem-logger.js";
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, fireOnStep } from "./state.js";
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 { ModelUnavailableError, LlmBudget, sendWithRetry, DEFAULT_TRANSPORT_RETRY_WINDOW_MS, MAX_RETRIES } from "./llm-budget.js";
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
- /** Thrown by runSleepCycle when the wall-clock timeout expires before completion. */
46
- export class SleepTimeoutError extends Error {
47
- constructor(message) { super(message); this.name = "SleepTimeoutError"; }
48
- }
49
- export function parseArgs(argv) {
50
- const args = argv.slice(2);
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. Extracted from main() for testability (#175).
73
- * - Deterministic time injection via opts.now for decision sites
74
- * - Throws SleepInitError / SleepTimeoutError instead of process.exit
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(opts) {
80
- const flags = opts.flags ?? parseArgs(process.argv);
81
- const now = opts.now ?? Date.now;
82
- const timeoutMs = opts.timeoutMs ?? getAbmindEnv().sleepTimeoutMin * 60 * 1000;
83
- const backoffMs = opts.backoffMs ?? ((n) => [10, 30, 60][Math.min(n, 2)] * 1000);
84
- const retryDelayMs = opts.retryDelayMs ?? 6000;
85
- const transportBackoffMs = opts.transportBackoffMs ?? ((n) => Math.min(30_000 * Math.pow(2, n - 1), 120_000));
86
- const transportRetryWindowMs = opts.transportRetryWindowMs ?? DEFAULT_TRANSPORT_RETRY_WINDOW_MS;
87
- const runtime = opts.runtime;
88
- if (flags.verbose) {
89
- logInfo(TAG, "Verbose mode enabled");
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
- const memoryConfig = { ...loadMemoryConfig(), ...opts.memoryConfigOverride };
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
- // Step 0: run abmind doctor --fix to repair any FTS corruption before sleep steps
104
+ // #1353: async, library-native preflight no execSync, no CLI subprocess.
100
105
  try {
101
- const { execSync } = await import("node:child_process");
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 — prevent concurrent sleep execution
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 { ok: true, failCount: 0 };
134
+ return alreadyRunningResult(startedAt, now());
132
135
  }
133
136
  logWarn(TAG, `[SLEEP] Stale lock (pid ${existingState.pid} dead) — claiming`);
134
137
  }
135
- // #1082: Fresh cycle discards prior state (budget + steps)
136
- const isResume = !opts.fresh && existingState !== null && Object.values(existingState.steps).some(s => s.status === "ok");
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
- // cron integration is caller-owned (bridge-only). Abmind library runs without cron awareness.
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 (flags.verbose)
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
- if (msgCount === 0 && !flags.force && !isResume) {
147
- logInfo(TAG, `[SLEEP] No messages since last sleep nothing to process. Use --force to run housekeeping anyway.`);
148
- // Write minimal audit marker so hasSleepAuditToday() returns true (prevents bedtime re-trigger)
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 dateStr = localDate().replace(/-/g, "");
156
+ const noWorkDateStr = localDate().replace(/-/g, "");
152
157
  const timeStr = new Date().toTimeString().slice(0, 5).replace(/:/g, "");
153
- writeFileSync(join(sleepDir, `sleep_${dateStr}_${timeStr}.md`), `# Sleep Audit Log\n\n## No work — 0 messages since last sleep\n`, "utf-8");
154
- return { ok: true, failCount: 0 };
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
- // Set remaining missing vars
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; // step 03 appends retro to the daily file
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
- // Output path for consolidation — weekly or quarterly
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(); // 0-based
229
+ const month = new Date(now()).getMonth();
232
230
  const isQuarterBoundary = month % 3 === 0 && new Date(now()).getDate() <= 7;
233
- if (isQuarterBoundary) {
234
- vars.CONSOLIDATION_OUTPUT_PATH = join(quarterlyDir, `quarterly_${todayIso}.md`);
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
- // Level tiering controls which prompts are eligible.
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
- // ── Catch-up: recover failed essentials from previous days ──
360
- const sleepDir = join(memoryConfig.memoryDir, "sleep");
361
- const previousLocks = scanPreviousLocks(sleepDir, dateStr);
362
- if (previousLocks.length > 0) {
363
- logInfo(TAG, `[CATCH-UP] Found ${previousLocks.length} previous lock(s)`);
364
- await runCatchUp(previousLocks, sleepData, memoryConfig, steps, flags, runtime, budget, retryDelayMs, opts.onStep);
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, month, week] = m;
348
+ const [, year, month2, week] = m;
374
349
  const day = (parseInt(week) - 1) * 7 + 1;
375
- const approxDate = `${year}-${month}-${String(Math.min(day, 28)).padStart(2, "0")}`;
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
- // Create day directory for per-step logs
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
- // Hard safety: LLM call budget exhausted → suspend
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
- // #895: "start" fires once per step (before skip checks) so every step
410
- // emits exactly one start + one terminal (done/skipped/failed). Display-only,
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
- fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
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
- fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
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
- fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
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) => sendWithRetry(runtime, p, "daily-summary", flags.verbose, budget, retryDelayMs, transportBackoffMs, transportRetryWindowMs, now).then(r => { if (r === null)
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] 04a failed: ${err instanceof Error ? err.message : String(err)}`);
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
- fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "failed" });
465
- if (err instanceof ModelUnavailableError) {
466
- logWarn(TAG, `[SLEEP] ${step.name} — model unavailable, stopping cycle (not advancing to next phase)`);
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
- // #895: emit skip when there's no daily summary to extract from
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) => sendWithRetry(runtime, p, "04b-extract", flags.verbose, budget, retryDelayMs, transportBackoffMs, transportRetryWindowMs, now).then(r => { if (r === null)
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] 04b failed: ${err instanceof Error ? err.message : String(err)}`);
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
- fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "failed" });
522
- if (err instanceof ModelUnavailableError) {
523
- logWarn(TAG, `[SLEEP] ${step.name} — model unavailable, stopping cycle (not advancing to next phase)`);
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
- // #895: emit terminal event for code-driven extract-memories (failure already fired in catch)
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
- // #895: emit skip when there's no work for contradiction-and-graph
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
- // #895: emit skip on var-prep failure
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
- // #895: emit skip when not enough memories for REM
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
- // #895: emit skip on var-prep failure for rem-synthesis
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 = ""; // only prepend to first step
615
- const ctxBefore = -1;
559
+ soulPrefix = "";
616
560
  let response;
617
561
  try {
618
- response = await sendWithRetry(runtime, fullPrompt, step.name, flags.verbose, budget, retryDelayMs, transportBackoffMs, transportRetryWindowMs, now);
562
+ response = await sendToRuntime(runtime, fullPrompt, step.name, runId, signal, budget, domainRetryDelayMs);
619
563
  }
620
564
  catch (err) {
621
- if (err instanceof ModelUnavailableError) {
565
+ if (err instanceof TransportUnavailableError) {
622
566
  const duration = Date.now() - start;
623
- logWarn(TAG, `[SLEEP] ${step.name} — model unavailable, stopping cycle (not advancing to next phase)`);
624
- state.steps[step.name] = { status: "failed", duration: Math.round(duration / 100) / 10, ctxBefore, ctxAfter: -1 };
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
- fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "failed" });
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, ctxBefore, ctxAfter };
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; // below this score → expire
663
- const now = Date.now();
664
- const candidates = memDb.prepare(`SELECT id, recall_count, created_at FROM extracted_memories WHERE memory_type = 'event' AND valid_to IS NULL AND created_at < ?`).all(now - EVENT_MIN_AGE_DAYS * 86400_000);
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 candidates) {
667
- const ageDays = (now - m.created_at) / 86400_000;
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(now, m.id);
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: MAX_RETRIES, ctxBefore, ctxAfter };
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
- // #895: emit terminal event for the standard prompt-driven step
685
- fireOnStep(opts.onStep, {
686
- name: step.name,
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 = step.name.startsWith("04") || false;
638
+ const isEssential = essential;
701
639
  if (!isEssential) {
702
- const delayMs = backoffMs(consecutiveFailures);
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
- clearTimeout(timeoutHandle);
712
- // Runtime lifecycle is caller-owned — no shutdown call from the library.
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
- // Advance extraction watermark — only when all steps succeeded
720
- if (dreamySucceeded) {
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
- return { ok: failCount === 0, failCount };
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
- // CLI entry + isDirectRun removed — the standalone entry point now lives in
796
- // cli/abmind.ts as the `abmind sleep` subcommand. Library consumers call
797
- // runSleepCycle(opts) directly with their own SleepRuntime.
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