abmind 0.2.5-alpha.0 → 0.2.6
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 +52 -0
- package/dist/cli/abmind-backup.js +1 -1
- package/dist/cli/abmind-backup.js.map +1 -1
- package/dist/cli/abmind-deps.d.ts +8 -0
- package/dist/cli/abmind-deps.d.ts.map +1 -0
- package/dist/cli/abmind-deps.js +55 -0
- package/dist/cli/abmind-deps.js.map +1 -0
- package/dist/cli/abmind-doctor.js +9 -7
- package/dist/cli/abmind-doctor.js.map +1 -1
- package/dist/cli/abmind-embed.js +2 -3
- package/dist/cli/abmind-embed.js.map +1 -1
- package/dist/cli/abmind-expand.d.ts.map +1 -1
- package/dist/cli/abmind-expand.js +2 -3
- package/dist/cli/abmind-expand.js.map +1 -1
- package/dist/cli/abmind-install.js +9 -23
- package/dist/cli/abmind-install.js.map +1 -1
- package/dist/cli/abmind-passwd.js +2 -3
- package/dist/cli/abmind-passwd.js.map +1 -1
- package/dist/cli/abmind-secrets.d.ts.map +1 -1
- package/dist/cli/abmind-secrets.js +2 -3
- package/dist/cli/abmind-secrets.js.map +1 -1
- package/dist/cli/abmind-status-runtime.js +10 -12
- package/dist/cli/abmind-status-runtime.js.map +1 -1
- package/dist/cli/abmind-update-args.d.ts +26 -0
- package/dist/cli/abmind-update-args.d.ts.map +1 -0
- package/dist/cli/abmind-update-args.js +56 -0
- package/dist/cli/abmind-update-args.js.map +1 -0
- package/dist/cli/abmind-update.d.ts +13 -5
- package/dist/cli/abmind-update.d.ts.map +1 -1
- package/dist/cli/abmind-update.js +151 -147
- package/dist/cli/abmind-update.js.map +1 -1
- package/dist/cli/abmind.js +6 -10
- package/dist/cli/abmind.js.map +1 -1
- package/dist/cli/banner.d.ts +6 -0
- package/dist/cli/banner.d.ts.map +1 -1
- package/dist/cli/banner.js +14 -22
- package/dist/cli/banner.js.map +1 -1
- package/dist/cli/lib/native-dep.d.ts +15 -0
- package/dist/cli/lib/native-dep.d.ts.map +1 -0
- package/dist/cli/lib/native-dep.js +25 -0
- package/dist/cli/lib/native-dep.js.map +1 -0
- package/dist/src/context-orchestrator.d.ts +38 -2
- package/dist/src/context-orchestrator.d.ts.map +1 -1
- package/dist/src/context-orchestrator.js +84 -12
- package/dist/src/context-orchestrator.js.map +1 -1
- package/dist/src/fts-utils.d.ts +47 -0
- package/dist/src/fts-utils.d.ts.map +1 -0
- package/dist/src/fts-utils.js +65 -0
- package/dist/src/fts-utils.js.map +1 -0
- package/dist/src/index.d.ts +5 -3
- 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/memory-db.d.ts.map +1 -1
- package/dist/src/memory-db.js +13 -5
- package/dist/src/memory-db.js.map +1 -1
- package/dist/src/memory-index.d.ts +0 -16
- package/dist/src/memory-index.d.ts.map +1 -1
- package/dist/src/memory-index.js +1 -60
- package/dist/src/memory-index.js.map +1 -1
- package/dist/src/memory-manager.d.ts +8 -0
- package/dist/src/memory-manager.d.ts.map +1 -1
- package/dist/src/memory-manager.js +8 -0
- package/dist/src/memory-manager.js.map +1 -1
- package/dist/src/ollama-embed.d.ts.map +1 -1
- package/dist/src/ollama-embed.js +11 -1
- package/dist/src/ollama-embed.js.map +1 -1
- package/dist/src/recall-boosts.d.ts +15 -0
- package/dist/src/recall-boosts.d.ts.map +1 -0
- package/dist/src/recall-boosts.js +159 -0
- package/dist/src/recall-boosts.js.map +1 -0
- package/dist/src/recall-engine.d.ts +0 -2
- package/dist/src/recall-engine.d.ts.map +1 -1
- package/dist/src/recall-engine.js +1 -151
- package/dist/src/recall-engine.js.map +1 -1
- package/dist/src/sleep/audit.d.ts +33 -0
- package/dist/src/sleep/audit.d.ts.map +1 -0
- package/dist/src/sleep/audit.js +160 -0
- package/dist/src/sleep/audit.js.map +1 -0
- package/dist/src/sleep/catchup.d.ts +19 -0
- package/dist/src/sleep/catchup.d.ts.map +1 -0
- package/dist/src/sleep/catchup.js +159 -0
- package/dist/src/sleep/catchup.js.map +1 -0
- package/dist/src/sleep/llm-budget.d.ts +29 -0
- package/dist/src/sleep/llm-budget.d.ts.map +1 -0
- package/dist/src/sleep/llm-budget.js +92 -0
- package/dist/src/sleep/llm-budget.js.map +1 -0
- package/dist/src/sleep/locks.d.ts +19 -0
- package/dist/src/sleep/locks.d.ts.map +1 -0
- package/dist/src/sleep/locks.js +42 -0
- package/dist/src/sleep/locks.js.map +1 -0
- package/dist/src/sleep/orchestrator.d.ts +18 -1
- package/dist/src/sleep/orchestrator.d.ts.map +1 -1
- package/dist/src/sleep/orchestrator.js +104 -420
- package/dist/src/sleep/orchestrator.js.map +1 -1
- package/dist/src/sleep/sleep-prompt-loader.d.ts +0 -5
- package/dist/src/sleep/sleep-prompt-loader.d.ts.map +1 -1
- package/dist/src/sleep/sleep-prompt-loader.js +0 -12
- package/dist/src/sleep/sleep-prompt-loader.js.map +1 -1
- package/dist/src/sleep/state.d.ts +56 -0
- package/dist/src/sleep/state.d.ts.map +1 -0
- package/dist/src/sleep/state.js +83 -0
- package/dist/src/sleep/state.js.map +1 -0
- package/package.json +2 -5
|
@@ -16,10 +16,9 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { localISO } from "../local-time.js";
|
|
18
18
|
import { getAbmindEnv } from "../env-schema.js";
|
|
19
|
-
import { join,
|
|
19
|
+
import { join, dirname } from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
|
-
import {
|
|
22
|
-
import { atomicWriteSync } from "../atomic-write.js";
|
|
21
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
22
|
import { MemoryManager } from "../memory-manager.js";
|
|
24
23
|
import { loadMemoryConfig } from "../memory-config.js";
|
|
25
24
|
import { SleepStateGatherer } from "../sleep-state-gatherer.js";
|
|
@@ -27,23 +26,18 @@ import { loadSleepSteps, buildSleepVars, substituteVars } from "../sleep-pipelin
|
|
|
27
26
|
import { buildDailySummary, writeDailyFile, LLMUnavailableError } from "../sleep-pipeline.js";
|
|
28
27
|
import { extractFromDaily } from "../sleep-pipeline.js";
|
|
29
28
|
import { logInfo, logWarn, logError } from "../mem-logger.js";
|
|
30
|
-
import { redactSecrets } from "../redact-secrets.js";
|
|
31
29
|
import { localDate } from "../local-time.js";
|
|
32
30
|
import { parseLevel, DEFAULT_LEVEL } from "./levels.js";
|
|
31
|
+
import { readStateFile, writeStateFile, runWiredPreTasks, formatWiredResults, fireOnStep } from "./state.js";
|
|
32
|
+
import { buildSnapshotSummary, writeAuditLog } from "./audit.js";
|
|
33
|
+
import { toDateStr, toIsoDate, scanPreviousLocks } from "./locks.js";
|
|
34
|
+
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";
|
|
33
37
|
const TAG = "abmind-sleep";
|
|
34
|
-
/** Format a timestamp as YYYYMMDD (for lock file names). */
|
|
35
|
-
function toDateStr(ts) {
|
|
36
|
-
const d = new Date(ts);
|
|
37
|
-
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}`;
|
|
38
|
-
}
|
|
39
|
-
/** Format a timestamp as YYYY-MM-DD (for daily file paths). */
|
|
40
|
-
function toIsoDate(ts) {
|
|
41
|
-
const d = new Date(ts);
|
|
42
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
43
|
-
}
|
|
44
38
|
/** Steps whose failure blocks watermark advance. Public so tests can derive reject targets. */
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
// Re-exported from catchup.ts — kept here for backward compat of any direct orchestrator imports.
|
|
40
|
+
export { ESSENTIAL_STEPS } from "./catchup.js";
|
|
47
41
|
/** Thrown by runSleepCycle when memory layer fails to initialize. */
|
|
48
42
|
export class SleepInitError extends Error {
|
|
49
43
|
constructor(message) { super(message); this.name = "SleepInitError"; }
|
|
@@ -70,405 +64,9 @@ export function parseArgs(argv) {
|
|
|
70
64
|
}
|
|
71
65
|
return parsed;
|
|
72
66
|
}
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
if (!existsSync(path))
|
|
77
|
-
return null;
|
|
78
|
-
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
79
|
-
if (typeof raw !== "object" || raw === null || !raw.steps)
|
|
80
|
-
return null;
|
|
81
|
-
// Backfill defaults for legacy lock files
|
|
82
|
-
if (!raw.status)
|
|
83
|
-
raw.status = "ongoing";
|
|
84
|
-
if (raw.llmCalls == null)
|
|
85
|
-
raw.llmCalls = 0;
|
|
86
|
-
return raw;
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
function writeStateFile(path, state) {
|
|
93
|
-
atomicWriteSync(path, JSON.stringify(state, null, 2));
|
|
94
|
-
}
|
|
95
|
-
// ── Wired pre-tasks (delegated to abmind MaintenanceService) ────────────────
|
|
96
|
-
async function runWiredPreTasks(sleepData, memoryDir, memory) {
|
|
97
|
-
const r = await memory.maintenance.runPreSleepTasks(memory, sleepData);
|
|
98
|
-
// Bridge-side: log rotation (not memory's concern)
|
|
99
|
-
let logsDeleted = 0;
|
|
100
|
-
try {
|
|
101
|
-
const logsDir = join(memoryDir, "..", "logs");
|
|
102
|
-
if (existsSync(logsDir)) {
|
|
103
|
-
const cutoff = Date.now() - 7 * 86400000;
|
|
104
|
-
for (const f of readdirSync(logsDir)) {
|
|
105
|
-
if (!f.startsWith("bridge-") || !f.endsWith(".log"))
|
|
106
|
-
continue;
|
|
107
|
-
const match = f.match(/bridge-(\d{4}-\d{2}-\d{2})\.log/);
|
|
108
|
-
if (match && new Date(match[1]).getTime() < cutoff) {
|
|
109
|
-
unlinkSync(join(logsDir, f));
|
|
110
|
-
logsDeleted++;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
catch (err) {
|
|
116
|
-
logWarn(TAG, `[WIRED] log rotation: ${err instanceof Error ? err.message : String(err)}`);
|
|
117
|
-
}
|
|
118
|
-
return { purged: r.purged, deduped: r.deduped, embedded: r.embedded, anomaliesFixed: r.anomaliesFixed, walOk: r.walOk, ftsOk: r.ftsOk, logsDeleted };
|
|
119
|
-
}
|
|
120
|
-
function formatWiredResults(r) {
|
|
121
|
-
const parts = [];
|
|
122
|
-
if (r.purged > 0)
|
|
123
|
-
parts.push(`${r.purged} garbage purged`);
|
|
124
|
-
if (r.deduped > 0)
|
|
125
|
-
parts.push(`${r.deduped} dupes deleted`);
|
|
126
|
-
parts.push(`WAL ${r.walOk ? "ok" : "FAILED"}`);
|
|
127
|
-
parts.push(`FTS ${r.ftsOk ? "ok" : "FAILED"}`);
|
|
128
|
-
if (r.embedded > 0)
|
|
129
|
-
parts.push(`${r.embedded} embedded`);
|
|
130
|
-
if (r.anomaliesFixed > 0)
|
|
131
|
-
parts.push(`${r.anomaliesFixed} anomalies fixed`);
|
|
132
|
-
if (r.logsDeleted > 0)
|
|
133
|
-
parts.push(`${r.logsDeleted} old logs deleted`);
|
|
134
|
-
return parts.length > 0 ? parts.join(", ") : "nothing to do";
|
|
135
|
-
}
|
|
67
|
+
// ── Subagent invocation ─────────────────────────────────────────────────────
|
|
68
|
+
/** LLM call entry for sleep steps — caller provides the runtime via RunOpts. */
|
|
136
69
|
// ── Transport ───────────────────────────────────────────────────────────────
|
|
137
|
-
// (SleepRuntime is imported at the top of the file.)
|
|
138
|
-
const MAX_RETRIES = 3;
|
|
139
|
-
/** Budget tracker — shared across all sendWithRetry calls in a sleep cycle. */
|
|
140
|
-
class LlmBudget {
|
|
141
|
-
state;
|
|
142
|
-
statePath;
|
|
143
|
-
exhausted = false;
|
|
144
|
-
constructor(state, statePath) {
|
|
145
|
-
this.state = state;
|
|
146
|
-
this.statePath = statePath;
|
|
147
|
-
}
|
|
148
|
-
/** Increment counter, return false if budget exhausted. */
|
|
149
|
-
consume() {
|
|
150
|
-
this.state.llmCalls = (this.state.llmCalls ?? 0) + 1;
|
|
151
|
-
writeStateFile(this.statePath, this.state);
|
|
152
|
-
if (this.state.llmCalls > getAbmindEnv().sleepMaxLlmCalls) {
|
|
153
|
-
this.exhausted = true;
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
get calls() { return this.state.llmCalls ?? 0; }
|
|
159
|
-
}
|
|
160
|
-
async function sendWithRetry(runtime, prompt, stepName, _verbose, budget, delayMs = 6000) {
|
|
161
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
162
|
-
if (budget && !budget.consume()) {
|
|
163
|
-
logWarn(TAG, `[BUDGET] LLM call limit (${getAbmindEnv().sleepMaxLlmCalls}) reached at step ${stepName} — suspending`);
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
const result = await runtime.complete(prompt);
|
|
168
|
-
if (!result || !result.trim()) {
|
|
169
|
-
logWarn(TAG, `Step ${stepName} attempt ${attempt}/${MAX_RETRIES} returned empty response`);
|
|
170
|
-
if (attempt === MAX_RETRIES) {
|
|
171
|
-
logError(TAG, `Step ${stepName} failed after ${MAX_RETRIES} attempts (empty), skipping`);
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
if (delayMs > 0)
|
|
175
|
-
await new Promise(r => setTimeout(r, delayMs));
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
return result;
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
182
|
-
logWarn(TAG, `Step ${stepName} attempt ${attempt}/${MAX_RETRIES} failed: ${msg}`);
|
|
183
|
-
if (attempt === MAX_RETRIES) {
|
|
184
|
-
logError(TAG, `Step ${stepName} failed after ${MAX_RETRIES} attempts, skipping`);
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
if (delayMs > 0)
|
|
188
|
-
await new Promise(r => setTimeout(r, delayMs));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
// ── Audit trail helpers ─────────────────────────────────────────────────────
|
|
194
|
-
function buildSnapshotSummary(snapshot) {
|
|
195
|
-
return [
|
|
196
|
-
`Working dirs: ${snapshot.workingDirs.length}`,
|
|
197
|
-
`Messages: ${snapshot.dbStats.messageCount}`,
|
|
198
|
-
`Embeddings: ${snapshot.dbStats.embeddingCount}`,
|
|
199
|
-
`Extracted memories: ${snapshot.dbStats.extractedMemoryCount}`,
|
|
200
|
-
`Disk: ${(snapshot.diskUsageBytes / 1024 / 1024).toFixed(1)} MB / ${(snapshot.diskBudgetBytes / 1024 / 1024).toFixed(0)} MB`,
|
|
201
|
-
`Topics: ${snapshot.topicFiles.length}`,
|
|
202
|
-
`FTS5: messages=${snapshot.fts5Health.messages_fts}, extracted=${snapshot.fts5Health.extracted_memories_fts}, original=${snapshot.fts5Health.extracted_memories_original_fts}`,
|
|
203
|
-
].join(", ");
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Parse outcome counts from the subagent's free-form text response.
|
|
207
|
-
*
|
|
208
|
-
* The subagent response is unstructured text, so we use best-effort regex
|
|
209
|
-
* matching for common patterns like "consolidated 3 files", "pruned 42
|
|
210
|
-
* messages", etc. Returns 0 for any count that can't be parsed.
|
|
211
|
-
*/
|
|
212
|
-
function parseOutcomesFromResponse(response) {
|
|
213
|
-
const defaults = {
|
|
214
|
-
filesConsolidated: 0,
|
|
215
|
-
messagesPruned: 0,
|
|
216
|
-
embeddingsRemoved: 0,
|
|
217
|
-
sessionsCleaned: 0,
|
|
218
|
-
topicsMerged: 0,
|
|
219
|
-
topicsDeleted: 0,
|
|
220
|
-
};
|
|
221
|
-
if (!response)
|
|
222
|
-
return defaults;
|
|
223
|
-
const text = response.toLowerCase();
|
|
224
|
-
// Each pattern array contains regexes to try in order for a given outcome.
|
|
225
|
-
// We take the first match found. Patterns cover both "verb N noun" and
|
|
226
|
-
// "N noun verb" orderings that an LLM might produce.
|
|
227
|
-
const patterns = [
|
|
228
|
-
{
|
|
229
|
-
key: "filesConsolidated",
|
|
230
|
-
regexes: [
|
|
231
|
-
/consolidat\w*\s+(\d+)\s+(?:file|dir|working)/i,
|
|
232
|
-
/(\d+)\s+(?:file|dir|working\s*dir)\w*\s+consolidat/i,
|
|
233
|
-
/files?\s+consolidated\s*:\s*(\d+)/i,
|
|
234
|
-
],
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
key: "messagesPruned",
|
|
238
|
-
regexes: [
|
|
239
|
-
/prun\w*\s+(\d+)\s+message/i,
|
|
240
|
-
/(\d+)\s+message\w*\s+prun/i,
|
|
241
|
-
/(?:delet|remov)\w*\s+(\d+)\s+message/i,
|
|
242
|
-
/(\d+)\s+message\w*\s+(?:delet|remov)/i,
|
|
243
|
-
/messages?\s+pruned\s*:\s*(\d+)/i,
|
|
244
|
-
],
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
key: "embeddingsRemoved",
|
|
248
|
-
regexes: [
|
|
249
|
-
/(?:remov|delet|clean)\w*\s+(\d+)\s+embedding/i,
|
|
250
|
-
/(\d+)\s+embedding\w*\s+(?:remov|delet|clean)/i,
|
|
251
|
-
/embeddings?\s+removed\s*:\s*(\d+)/i,
|
|
252
|
-
],
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
key: "sessionsCleaned",
|
|
256
|
-
regexes: [
|
|
257
|
-
/(?:clean|delet|remov)\w*\s+(\d+)\s+session/i,
|
|
258
|
-
/(\d+)\s+session\w*\s+(?:clean|delet|remov)/i,
|
|
259
|
-
/sessions?\s+cleaned\s*:\s*(\d+)/i,
|
|
260
|
-
],
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
key: "topicsMerged",
|
|
264
|
-
regexes: [
|
|
265
|
-
/merg\w*\s+(\d+)\s+topic/i,
|
|
266
|
-
/(\d+)\s+topic\w*\s+merg/i,
|
|
267
|
-
/topics?\s+merged\s*:\s*(\d+)/i,
|
|
268
|
-
],
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
key: "topicsDeleted",
|
|
272
|
-
regexes: [
|
|
273
|
-
/delet\w*\s+(\d+)\s+topic/i,
|
|
274
|
-
/(\d+)\s+topic\w*\s+delet/i,
|
|
275
|
-
/topics?\s+deleted\s*:\s*(\d+)/i,
|
|
276
|
-
],
|
|
277
|
-
},
|
|
278
|
-
];
|
|
279
|
-
for (const { key, regexes } of patterns) {
|
|
280
|
-
for (const regex of regexes) {
|
|
281
|
-
const match = text.match(regex);
|
|
282
|
-
if (match?.[1]) {
|
|
283
|
-
const parsed = parseInt(match[1], 10);
|
|
284
|
-
if (!isNaN(parsed) && parsed >= 0) {
|
|
285
|
-
defaults[key] = parsed;
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return defaults;
|
|
292
|
-
}
|
|
293
|
-
function writeAuditLog(memoryDir, entry) {
|
|
294
|
-
const sleepDir = join(memoryDir, "sleep");
|
|
295
|
-
mkdirSync(sleepDir, { recursive: true });
|
|
296
|
-
const suffix = [
|
|
297
|
-
``,
|
|
298
|
-
`---`,
|
|
299
|
-
``,
|
|
300
|
-
`## CLI Wrapper`,
|
|
301
|
-
``,
|
|
302
|
-
`**Timestamp:** ${entry.timestamp}`,
|
|
303
|
-
`**Model:** ${entry.model}`,
|
|
304
|
-
``,
|
|
305
|
-
`### State Snapshot`,
|
|
306
|
-
`${entry.stateSnapshotSummary}`,
|
|
307
|
-
``,
|
|
308
|
-
`### Outcomes`,
|
|
309
|
-
`- Files consolidated: ${entry.outcomes.filesConsolidated}`,
|
|
310
|
-
`- Messages pruned: ${entry.outcomes.messagesPruned}`,
|
|
311
|
-
`- Embeddings removed: ${entry.outcomes.embeddingsRemoved}`,
|
|
312
|
-
`- Sessions cleaned: ${entry.outcomes.sessionsCleaned}`,
|
|
313
|
-
`- Topics merged: ${entry.outcomes.topicsMerged}`,
|
|
314
|
-
`- Topics deleted: ${entry.outcomes.topicsDeleted}`,
|
|
315
|
-
entry.error ? `\n### Error\n${entry.error}` : "",
|
|
316
|
-
].join("\n");
|
|
317
|
-
// Find the subagent's audit file and append to it
|
|
318
|
-
const today = localDate().replace(/-/g, "");
|
|
319
|
-
try {
|
|
320
|
-
const files = readdirSync(sleepDir)
|
|
321
|
-
.filter(f => f.startsWith(`sleep_${today}`) && f.endsWith(".md"))
|
|
322
|
-
.sort();
|
|
323
|
-
if (files.length > 0) {
|
|
324
|
-
const target = join(sleepDir, files[files.length - 1]);
|
|
325
|
-
const existingLines = readFileSync(target, "utf-8").split("\n").length;
|
|
326
|
-
if (existingLines < 50) {
|
|
327
|
-
logWarn(TAG, `Sleep audit suspiciously short (${existingLines} lines) — subagent may have truncated`);
|
|
328
|
-
}
|
|
329
|
-
appendFileSync(target, redactSecrets(suffix), "utf-8");
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
catch { /* fall through to standalone */ }
|
|
334
|
-
// Fallback: no subagent file found — write standalone
|
|
335
|
-
const now = new Date();
|
|
336
|
-
const dateStr = localDate().replace(/-/g, "");
|
|
337
|
-
const timeStr = now.toTimeString().slice(0, 5).replace(/:/g, "");
|
|
338
|
-
const filename = `sleep_${dateStr}_${timeStr}.md`;
|
|
339
|
-
writeFileSync(join(sleepDir, filename), redactSecrets(`# Sleep Audit Log${suffix}`), "utf-8");
|
|
340
|
-
}
|
|
341
|
-
function scanPreviousLocks(sleepDir, todayStr) {
|
|
342
|
-
if (!existsSync(sleepDir))
|
|
343
|
-
return [];
|
|
344
|
-
const locks = [];
|
|
345
|
-
const todayMs = dateStrToMs(todayStr);
|
|
346
|
-
for (const f of readdirSync(sleepDir)) {
|
|
347
|
-
const m = f.match(/^sleep_(\d{8})\.lock$/);
|
|
348
|
-
if (!m || m[1] === todayStr)
|
|
349
|
-
continue;
|
|
350
|
-
const state = readStateFile(join(sleepDir, f));
|
|
351
|
-
if (!state)
|
|
352
|
-
continue;
|
|
353
|
-
const ageDays = Math.round((todayMs - dateStrToMs(m[1])) / 86400000);
|
|
354
|
-
if (ageDays > 0)
|
|
355
|
-
locks.push({ path: join(sleepDir, f), dateStr: m[1], state, ageDays });
|
|
356
|
-
}
|
|
357
|
-
return locks.sort((a, b) => b.dateStr.localeCompare(a.dateStr)); // newest first
|
|
358
|
-
}
|
|
359
|
-
function dateStrToMs(ds) {
|
|
360
|
-
return new Date(`${ds.slice(0, 4)}-${ds.slice(4, 6)}-${ds.slice(6, 8)}T00:00:00`).getTime();
|
|
361
|
-
}
|
|
362
|
-
function dateStrToFormatted(ds) {
|
|
363
|
-
return `${ds.slice(0, 4)}-${ds.slice(4, 6)}-${ds.slice(6, 8)}`;
|
|
364
|
-
}
|
|
365
|
-
function failedEssentials(state) {
|
|
366
|
-
const failed = [];
|
|
367
|
-
for (const name of ESSENTIAL_STEPS) {
|
|
368
|
-
const s = state.steps[name];
|
|
369
|
-
if (!s || s.status === "failed" || s.status === "timeout" || s.status === "pending") {
|
|
370
|
-
failed.push(name);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return failed;
|
|
374
|
-
}
|
|
375
|
-
async function runCatchUp(locks, sleepData, memoryConfig, steps, flags, runtime, budget, retryDelayMs = 6000) {
|
|
376
|
-
for (const lock of locks) {
|
|
377
|
-
if (lock.ageDays > CATCHUP_MAX_AGE_DAYS) {
|
|
378
|
-
logError(TAG, `[CATCH-UP] Abandoning stale lock ${basename(lock.path)} — ${lock.ageDays} days old, data unrecoverable`);
|
|
379
|
-
unlinkSync(lock.path);
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
const needed = failedEssentials(lock.state);
|
|
383
|
-
if (needed.length === 0) {
|
|
384
|
-
logInfo(TAG, `[CATCH-UP] Cleaning up completed lock ${basename(lock.path)}`);
|
|
385
|
-
unlinkSync(lock.path);
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
logInfo(TAG, `[CATCH-UP] ${basename(lock.path)} — recovering: ${needed.join(", ")}`);
|
|
389
|
-
// 04a — daily summary with date-range
|
|
390
|
-
if (needed.includes("daily-summary")) {
|
|
391
|
-
const start = Date.now();
|
|
392
|
-
try {
|
|
393
|
-
const ctxWindow = getAbmindEnv().sleepCtxWindow;
|
|
394
|
-
const userId = sleepData.getPrimaryUserId();
|
|
395
|
-
const dayStart = dateStrToMs(lock.dateStr);
|
|
396
|
-
const dayEnd = dayStart + 86400000;
|
|
397
|
-
const summary = await buildDailySummary(sleepData.getDb(), (p) => sendWithRetry(runtime, p, "catch-up-04a", flags.verbose, budget, retryDelayMs).then(r => { if (r === null)
|
|
398
|
-
throw new LLMUnavailableError(); return r; }), {
|
|
399
|
-
ctxWindow, memoryDir: memoryConfig.memoryDir, userId, watermarkTs: 0,
|
|
400
|
-
dateRange: { startTs: dayStart, endTs: dayEnd },
|
|
401
|
-
});
|
|
402
|
-
if (summary) {
|
|
403
|
-
writeDailyFile(memoryConfig.memoryDir, dateStrToFormatted(lock.dateStr), summary);
|
|
404
|
-
lock.state.steps["daily-summary"] = { status: "ok", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
lock.state.steps["daily-summary"] = { status: "skipped" };
|
|
408
|
-
}
|
|
409
|
-
logInfo(TAG, `[CATCH-UP] ✓ 04a-daily-summary for ${lock.dateStr} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
410
|
-
}
|
|
411
|
-
catch (err) {
|
|
412
|
-
logWarn(TAG, `[CATCH-UP] ✗ 04a-daily-summary for ${lock.dateStr}: ${err instanceof Error ? err.message : String(err)}`);
|
|
413
|
-
lock.state.steps["daily-summary"] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
414
|
-
}
|
|
415
|
-
writeStateFile(lock.path, lock.state);
|
|
416
|
-
}
|
|
417
|
-
// 04b — extract memories from daily (needs daily file to exist)
|
|
418
|
-
if (needed.includes("extract-memories")) {
|
|
419
|
-
const dailyPath = join(memoryConfig.memoryDir, "daily", `daily_${dateStrToFormatted(lock.dateStr)}.md`);
|
|
420
|
-
if (!existsSync(dailyPath)) {
|
|
421
|
-
logInfo(TAG, `[CATCH-UP] ⏭ 04b — no daily file for ${lock.dateStr}`);
|
|
422
|
-
lock.state.steps["extract-memories"] = { status: "skipped" };
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
const start = Date.now();
|
|
426
|
-
try {
|
|
427
|
-
const userId = sleepData.getPrimaryUserId();
|
|
428
|
-
const result = await extractFromDaily(dailyPath, userId, (p) => sendWithRetry(runtime, p, "catch-up-04b", flags.verbose, budget, retryDelayMs).then(r => { if (r === null)
|
|
429
|
-
throw new LLMUnavailableError(); return r; }));
|
|
430
|
-
lock.state.steps["extract-memories"] = { status: "ok", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
431
|
-
logInfo(TAG, `[CATCH-UP] ✓ 04b-extract-memories for ${lock.dateStr} (${((Date.now() - start) / 1000).toFixed(1)}s) — ${result.slice(0, 80)}`);
|
|
432
|
-
}
|
|
433
|
-
catch (err) {
|
|
434
|
-
logWarn(TAG, `[CATCH-UP] ✗ 04b for ${lock.dateStr}: ${err instanceof Error ? err.message : String(err)}`);
|
|
435
|
-
lock.state.steps["extract-memories"] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
writeStateFile(lock.path, lock.state);
|
|
439
|
-
}
|
|
440
|
-
// Prompt-driven essentials (retrospective — now includes extraction)
|
|
441
|
-
for (const stepName of ["retrospective"]) {
|
|
442
|
-
if (!needed.includes(stepName))
|
|
443
|
-
continue;
|
|
444
|
-
const step = steps.find(s => s.name === stepName);
|
|
445
|
-
if (!step) {
|
|
446
|
-
logWarn(TAG, `[CATCH-UP] Step file not found: ${stepName}`);
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
const start = Date.now();
|
|
450
|
-
const response = await sendWithRetry(runtime, step.rawPrompt, `catch-up-${stepName}`, flags.verbose, budget, retryDelayMs);
|
|
451
|
-
if (response) {
|
|
452
|
-
lock.state.steps[stepName] = { status: "ok", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
453
|
-
logInfo(TAG, `[CATCH-UP] ✓ ${stepName} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
lock.state.steps[stepName] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
457
|
-
logWarn(TAG, `[CATCH-UP] ✗ ${stepName}`);
|
|
458
|
-
}
|
|
459
|
-
writeStateFile(lock.path, lock.state);
|
|
460
|
-
}
|
|
461
|
-
// Final check: all essentials recovered?
|
|
462
|
-
const stillFailing = failedEssentials(lock.state);
|
|
463
|
-
if (stillFailing.length === 0) {
|
|
464
|
-
logInfo(TAG, `[CATCH-UP] ✅ ${basename(lock.path)} — all essentials recovered, lock deleted`);
|
|
465
|
-
unlinkSync(lock.path);
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
logWarn(TAG, `[CATCH-UP] ${basename(lock.path)} — still failing: ${stillFailing.join(", ")} (failing ${lock.ageDays} day(s))`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
70
|
// ── Main orchestration ──────────────────────────────────────────────────────
|
|
473
71
|
/**
|
|
474
72
|
* Run the full sleep cycle. Extracted from main() for testability (#175).
|
|
@@ -484,6 +82,8 @@ export async function runSleepCycle(opts) {
|
|
|
484
82
|
const timeoutMs = opts.timeoutMs ?? getAbmindEnv().sleepTimeoutMin * 60 * 1000;
|
|
485
83
|
const backoffMs = opts.backoffMs ?? ((n) => [10, 30, 60][Math.min(n, 2)] * 1000);
|
|
486
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;
|
|
487
87
|
const runtime = opts.runtime;
|
|
488
88
|
if (flags.verbose) {
|
|
489
89
|
logInfo(TAG, "Verbose mode enabled");
|
|
@@ -641,6 +241,13 @@ export async function runSleepCycle(opts) {
|
|
|
641
241
|
const snapshotVars = buildSleepVars(snapshot);
|
|
642
242
|
for (const [k, v] of Object.entries(snapshotVars))
|
|
643
243
|
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 */ }
|
|
644
251
|
// Progress protocol — emit PROGRESS:<pct>:<label> on stdout
|
|
645
252
|
const totalSteps = steps.length;
|
|
646
253
|
let stepIndex = 0;
|
|
@@ -754,7 +361,7 @@ export async function runSleepCycle(opts) {
|
|
|
754
361
|
const previousLocks = scanPreviousLocks(sleepDir, dateStr);
|
|
755
362
|
if (previousLocks.length > 0) {
|
|
756
363
|
logInfo(TAG, `[CATCH-UP] Found ${previousLocks.length} previous lock(s)`);
|
|
757
|
-
await runCatchUp(previousLocks, sleepData, memoryConfig, steps, flags, runtime, budget, retryDelayMs);
|
|
364
|
+
await runCatchUp(previousLocks, sleepData, memoryConfig, steps, flags, runtime, budget, retryDelayMs, opts.onStep);
|
|
758
365
|
}
|
|
759
366
|
// Housekeeping: move misplaced daily/consolidation_* to weekly/ (#640)
|
|
760
367
|
try {
|
|
@@ -799,13 +406,19 @@ export async function runSleepCycle(opts) {
|
|
|
799
406
|
}
|
|
800
407
|
emitProgress(step.name);
|
|
801
408
|
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" });
|
|
802
413
|
// Resume: skip already completed steps
|
|
803
414
|
if (isResume && existingState?.steps[step.name]?.status === "ok") {
|
|
804
415
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — already done (resume)`);
|
|
416
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
805
417
|
continue;
|
|
806
418
|
}
|
|
807
419
|
if (isResume && existingState?.steps[step.name]?.status === "skipped") {
|
|
808
420
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — skipped (resume)`);
|
|
421
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
809
422
|
continue;
|
|
810
423
|
}
|
|
811
424
|
// Skip logic (essential steps always run)
|
|
@@ -813,6 +426,7 @@ export async function runSleepCycle(opts) {
|
|
|
813
426
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — skipped`);
|
|
814
427
|
state.steps[step.name] = { status: "skipped" };
|
|
815
428
|
writeStateFile(statePath, state);
|
|
429
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
816
430
|
continue;
|
|
817
431
|
}
|
|
818
432
|
const start = Date.now();
|
|
@@ -829,7 +443,7 @@ export async function runSleepCycle(opts) {
|
|
|
829
443
|
const firstMsgTs = sleepData.getFirstMessageAfter(userId, watermarkTs);
|
|
830
444
|
const firstMsgDate = firstMsgTs ? new Date(firstMsgTs) : new Date(now());
|
|
831
445
|
const targetDate = `${firstMsgDate.getFullYear()}-${String(firstMsgDate.getMonth() + 1).padStart(2, "0")}-${String(firstMsgDate.getDate()).padStart(2, "0")}`;
|
|
832
|
-
const summary = await buildDailySummary(sleepData.getDb(), (p) => sendWithRetry(runtime, p, "daily-summary", flags.verbose, budget, retryDelayMs).then(r => { if (r === null)
|
|
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)
|
|
833
447
|
throw new LLMUnavailableError(); return r; }), {
|
|
834
448
|
ctxWindow, memoryDir: memoryConfig.memoryDir, userId, watermarkTs,
|
|
835
449
|
});
|
|
@@ -846,9 +460,30 @@ export async function runSleepCycle(opts) {
|
|
|
846
460
|
logWarn(TAG, `[SLEEP] 04a failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
847
461
|
state.steps[step.name] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
848
462
|
dreamySucceeded = false;
|
|
463
|
+
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)`);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (state.steps[step.name]?.status !== "failed") {
|
|
471
|
+
// success/skip path: persist state + log + fire event
|
|
472
|
+
writeStateFile(statePath, state);
|
|
849
473
|
}
|
|
850
|
-
writeStateFile(statePath, state);
|
|
851
474
|
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
|
+
}
|
|
852
487
|
continue;
|
|
853
488
|
}
|
|
854
489
|
if (step.name === "extract-memories") {
|
|
@@ -866,11 +501,13 @@ export async function runSleepCycle(opts) {
|
|
|
866
501
|
state.steps[step.name] = { status: "skipped" };
|
|
867
502
|
writeStateFile(statePath, state);
|
|
868
503
|
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" });
|
|
869
506
|
continue;
|
|
870
507
|
}
|
|
871
508
|
try {
|
|
872
509
|
const userId = sleepData.getPrimaryUserId();
|
|
873
|
-
const result = await extractFromDaily(dailySummaryPath, userId, (p) => sendWithRetry(runtime, p, "04b-extract", flags.verbose, budget, retryDelayMs).then(r => { if (r === null)
|
|
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)
|
|
874
511
|
throw new LLMUnavailableError(); return r; }));
|
|
875
512
|
state.steps[step.name] = { status: "ok", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
876
513
|
writeFileSync(join(stepLogDir, `${String(stepIndex).padStart(2, "0")}-${step.name}.md`), redactSecrets(result), "utf-8");
|
|
@@ -880,8 +517,24 @@ export async function runSleepCycle(opts) {
|
|
|
880
517
|
logWarn(TAG, `[SLEEP] 04b failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
881
518
|
state.steps[step.name] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
882
519
|
dreamySucceeded = false;
|
|
520
|
+
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)`);
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (state.steps[step.name]?.status !== "failed") {
|
|
528
|
+
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
|
+
});
|
|
883
537
|
}
|
|
884
|
-
writeStateFile(statePath, state);
|
|
885
538
|
continue;
|
|
886
539
|
}
|
|
887
540
|
// Standard prompt-driven step — JIT substitution
|
|
@@ -896,6 +549,8 @@ export async function runSleepCycle(opts) {
|
|
|
896
549
|
state.steps[step.name] = { status: "skipped" };
|
|
897
550
|
writeStateFile(statePath, state);
|
|
898
551
|
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" });
|
|
899
554
|
continue;
|
|
900
555
|
}
|
|
901
556
|
vars.NEW_EXTRACTIONS = newRows.map(r => `[id=${r.id}] (${r.memory_type}, trust=${r.trust}) ${r.content_en}`).join("\n");
|
|
@@ -925,6 +580,8 @@ export async function runSleepCycle(opts) {
|
|
|
925
580
|
logWarn(TAG, `[SLEEP] contradiction-and-graph var prep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
926
581
|
state.steps[step.name] = { status: "skipped" };
|
|
927
582
|
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" });
|
|
928
585
|
continue;
|
|
929
586
|
}
|
|
930
587
|
}
|
|
@@ -937,6 +594,8 @@ export async function runSleepCycle(opts) {
|
|
|
937
594
|
state.steps[step.name] = { status: "skipped" };
|
|
938
595
|
writeStateFile(statePath, state);
|
|
939
596
|
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" });
|
|
940
599
|
continue;
|
|
941
600
|
}
|
|
942
601
|
vars.REM_SAMPLE = sample.map(r => `[${r.memory_type}, ${new Date(r.created_at).toISOString().slice(0, 10)}] ${r.content_en}`).join("\n");
|
|
@@ -944,6 +603,8 @@ export async function runSleepCycle(opts) {
|
|
|
944
603
|
catch {
|
|
945
604
|
state.steps[step.name] = { status: "skipped" };
|
|
946
605
|
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" });
|
|
947
608
|
continue;
|
|
948
609
|
}
|
|
949
610
|
}
|
|
@@ -952,7 +613,22 @@ export async function runSleepCycle(opts) {
|
|
|
952
613
|
if (soulPrefix)
|
|
953
614
|
soulPrefix = ""; // only prepend to first step
|
|
954
615
|
const ctxBefore = -1;
|
|
955
|
-
|
|
616
|
+
let response;
|
|
617
|
+
try {
|
|
618
|
+
response = await sendWithRetry(runtime, fullPrompt, step.name, flags.verbose, budget, retryDelayMs, transportBackoffMs, transportRetryWindowMs, now);
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
if (err instanceof ModelUnavailableError) {
|
|
622
|
+
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 };
|
|
625
|
+
dreamySucceeded = false;
|
|
626
|
+
writeStateFile(statePath, state);
|
|
627
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "failed" });
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
throw err;
|
|
631
|
+
}
|
|
956
632
|
const ctxAfter = -1;
|
|
957
633
|
const duration = Date.now() - start;
|
|
958
634
|
if (response) {
|
|
@@ -1005,6 +681,14 @@ export async function runSleepCycle(opts) {
|
|
|
1005
681
|
dreamySucceeded = false;
|
|
1006
682
|
}
|
|
1007
683
|
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
|
+
});
|
|
1008
692
|
logInfo(TAG, `[SLEEP] ${response ? "✓" : "✗"} ${step.name} (${(duration / 1000).toFixed(1)}s, ${response?.length ?? 0} chars)`);
|
|
1009
693
|
// Backoff between steps: 10s → 30s → 60s on consecutive failures, reset on success
|
|
1010
694
|
if (response) {
|