abmind 0.2.5-alpha.0 → 0.2.5
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/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-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/sleep/audit.d.ts +27 -0
- package/dist/src/sleep/audit.d.ts.map +1 -0
- package/dist/src/sleep/audit.js +162 -0
- package/dist/src/sleep/audit.js.map +1 -0
- package/dist/src/sleep/locks.d.ts +15 -0
- package/dist/src/sleep/locks.d.ts.map +1 -0
- package/dist/src/sleep/locks.js +45 -0
- package/dist/src/sleep/locks.js.map +1 -0
- package/dist/src/sleep/orchestrator.d.ts +33 -0
- package/dist/src/sleep/orchestrator.d.ts.map +1 -1
- package/dist/src/sleep/orchestrator.js +186 -22
- 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 +47 -0
- package/dist/src/sleep/state.d.ts.map +1 -0
- package/dist/src/sleep/state.js +45 -0
- package/dist/src/sleep/state.js.map +1 -0
- package/package.json +2 -5
|
@@ -52,6 +52,16 @@ export class SleepInitError extends Error {
|
|
|
52
52
|
export class SleepTimeoutError extends Error {
|
|
53
53
|
constructor(message) { super(message); this.name = "SleepTimeoutError"; }
|
|
54
54
|
}
|
|
55
|
+
/** Best-effort onStep invoker — swallow handler errors so a display callback
|
|
56
|
+
* can never break the pipeline. */
|
|
57
|
+
function fireOnStep(handler, e) {
|
|
58
|
+
if (!handler)
|
|
59
|
+
return;
|
|
60
|
+
try {
|
|
61
|
+
handler(e);
|
|
62
|
+
}
|
|
63
|
+
catch { /* host display only — never fail the cycle */ }
|
|
64
|
+
}
|
|
55
65
|
export function parseArgs(argv) {
|
|
56
66
|
const args = argv.slice(2);
|
|
57
67
|
const parsed = { dryRun: false, verbose: false, force: false };
|
|
@@ -136,6 +146,17 @@ function formatWiredResults(r) {
|
|
|
136
146
|
// ── Transport ───────────────────────────────────────────────────────────────
|
|
137
147
|
// (SleepRuntime is imported at the top of the file.)
|
|
138
148
|
const MAX_RETRIES = 3;
|
|
149
|
+
const TRANSPORT_MAX_RETRIES = 6;
|
|
150
|
+
const DEFAULT_TRANSPORT_RETRY_WINDOW_MS = 8 * 60_000; // 8 minutes
|
|
151
|
+
/** Thrown by sendWithRetry when runtime.complete() throws on every attempt within the retry window.
|
|
152
|
+
* Extends LLMUnavailableError so buildDailySummary and extractFromDaily rethrow it naturally
|
|
153
|
+
* (those functions check instanceof LLMUnavailableError to propagate errors up). */
|
|
154
|
+
export class ModelUnavailableError extends LLMUnavailableError {
|
|
155
|
+
constructor(stepName) {
|
|
156
|
+
super(`Model unavailable for step "${stepName}" after retry window`);
|
|
157
|
+
this.name = "ModelUnavailableError";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
139
160
|
/** Budget tracker — shared across all sendWithRetry calls in a sleep cycle. */
|
|
140
161
|
class LlmBudget {
|
|
141
162
|
state;
|
|
@@ -157,17 +178,28 @@ class LlmBudget {
|
|
|
157
178
|
}
|
|
158
179
|
get calls() { return this.state.llmCalls ?? 0; }
|
|
159
180
|
}
|
|
160
|
-
async function sendWithRetry(runtime, prompt, stepName, _verbose, budget, delayMs = 6000) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
async function sendWithRetry(runtime, prompt, stepName, _verbose, budget, delayMs = 6000, transportBackoffMs = (n) => Math.min(30_000 * Math.pow(2, n - 1), 120_000), transportRetryWindowMs = DEFAULT_TRANSPORT_RETRY_WINDOW_MS, nowFn = Date.now) {
|
|
182
|
+
const deadline = nowFn() + transportRetryWindowMs;
|
|
183
|
+
let emptyAttempts = 0;
|
|
184
|
+
let transportAttempts = 0;
|
|
185
|
+
// Budget-exhaustion pre-check: if we are already over the cap, bail immediately.
|
|
186
|
+
// This mirrors the old pre-loop consume() guard without burning a call.
|
|
187
|
+
if (budget?.exhausted) {
|
|
188
|
+
logWarn(TAG, `[BUDGET] LLM call limit (${getAbmindEnv().sleepMaxLlmCalls}) reached at step ${stepName} — suspending`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
while (true) {
|
|
166
192
|
try {
|
|
167
193
|
const result = await runtime.complete(prompt);
|
|
194
|
+
// Real call reached the model — count it now (success OR empty, never a throw).
|
|
195
|
+
if (budget && !budget.consume()) {
|
|
196
|
+
logWarn(TAG, `[BUDGET] LLM call limit (${getAbmindEnv().sleepMaxLlmCalls}) reached at step ${stepName} — suspending`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
168
199
|
if (!result || !result.trim()) {
|
|
169
|
-
|
|
170
|
-
|
|
200
|
+
emptyAttempts++;
|
|
201
|
+
logWarn(TAG, `Step ${stepName} attempt ${emptyAttempts}/${MAX_RETRIES} returned empty response`);
|
|
202
|
+
if (emptyAttempts >= MAX_RETRIES) {
|
|
171
203
|
logError(TAG, `Step ${stepName} failed after ${MAX_RETRIES} attempts (empty), skipping`);
|
|
172
204
|
return null;
|
|
173
205
|
}
|
|
@@ -178,17 +210,21 @@ async function sendWithRetry(runtime, prompt, stepName, _verbose, budget, delayM
|
|
|
178
210
|
return result;
|
|
179
211
|
}
|
|
180
212
|
catch (err) {
|
|
213
|
+
// Transport failure — model unreachable. Do NOT consume budget (no tokens used).
|
|
214
|
+
transportAttempts++;
|
|
181
215
|
const msg = err instanceof Error ? err.message : String(err);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
const backoff = transportBackoffMs(transportAttempts);
|
|
217
|
+
const timeRemaining = deadline - nowFn();
|
|
218
|
+
logWarn(TAG, `Step ${stepName} transport fail (${transportAttempts}/${TRANSPORT_MAX_RETRIES}): ${msg}`);
|
|
219
|
+
if (transportAttempts >= TRANSPORT_MAX_RETRIES || timeRemaining <= backoff) {
|
|
220
|
+
logError(TAG, `Step ${stepName} — model unavailable after ${transportAttempts} attempt(s), ${Math.round(transportRetryWindowMs / 60_000)}min window exhausted`);
|
|
221
|
+
throw new ModelUnavailableError(stepName);
|
|
186
222
|
}
|
|
187
|
-
|
|
188
|
-
|
|
223
|
+
logInfo(TAG, `[SLEEP] Model unreachable — waiting ${Math.round(backoff / 1000)}s before retry (step ${stepName})`);
|
|
224
|
+
if (backoff > 0)
|
|
225
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
189
226
|
}
|
|
190
227
|
}
|
|
191
|
-
return null;
|
|
192
228
|
}
|
|
193
229
|
// ── Audit trail helpers ─────────────────────────────────────────────────────
|
|
194
230
|
function buildSnapshotSummary(snapshot) {
|
|
@@ -372,7 +408,7 @@ function failedEssentials(state) {
|
|
|
372
408
|
}
|
|
373
409
|
return failed;
|
|
374
410
|
}
|
|
375
|
-
async function runCatchUp(locks, sleepData, memoryConfig, steps, flags, runtime, budget, retryDelayMs = 6000) {
|
|
411
|
+
async function runCatchUp(locks, sleepData, memoryConfig, steps, flags, runtime, budget, retryDelayMs = 6000, onStep) {
|
|
376
412
|
for (const lock of locks) {
|
|
377
413
|
if (lock.ageDays > CATCHUP_MAX_AGE_DAYS) {
|
|
378
414
|
logError(TAG, `[CATCH-UP] Abandoning stale lock ${basename(lock.path)} — ${lock.ageDays} days old, data unrecoverable`);
|
|
@@ -407,10 +443,22 @@ async function runCatchUp(locks, sleepData, memoryConfig, steps, flags, runtime,
|
|
|
407
443
|
lock.state.steps["daily-summary"] = { status: "skipped" };
|
|
408
444
|
}
|
|
409
445
|
logInfo(TAG, `[CATCH-UP] ✓ 04a-daily-summary for ${lock.dateStr} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
446
|
+
// #895: emit terminal event for catch-up daily-summary
|
|
447
|
+
fireOnStep(onStep, {
|
|
448
|
+
name: "daily-summary", filename: "catch-up",
|
|
449
|
+
index: 0, total: 0,
|
|
450
|
+
phase: summary ? "done" : "skipped",
|
|
451
|
+
});
|
|
410
452
|
}
|
|
411
453
|
catch (err) {
|
|
412
454
|
logWarn(TAG, `[CATCH-UP] ✗ 04a-daily-summary for ${lock.dateStr}: ${err instanceof Error ? err.message : String(err)}`);
|
|
413
455
|
lock.state.steps["daily-summary"] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
456
|
+
// #895: emit failed for catch-up daily-summary
|
|
457
|
+
fireOnStep(onStep, {
|
|
458
|
+
name: "daily-summary", filename: "catch-up",
|
|
459
|
+
index: 0, total: 0,
|
|
460
|
+
phase: "failed",
|
|
461
|
+
});
|
|
414
462
|
}
|
|
415
463
|
writeStateFile(lock.path, lock.state);
|
|
416
464
|
}
|
|
@@ -420,6 +468,12 @@ async function runCatchUp(locks, sleepData, memoryConfig, steps, flags, runtime,
|
|
|
420
468
|
if (!existsSync(dailyPath)) {
|
|
421
469
|
logInfo(TAG, `[CATCH-UP] ⏭ 04b — no daily file for ${lock.dateStr}`);
|
|
422
470
|
lock.state.steps["extract-memories"] = { status: "skipped" };
|
|
471
|
+
// #895: emit skip when no daily file for catch-up
|
|
472
|
+
fireOnStep(onStep, {
|
|
473
|
+
name: "extract-memories", filename: "catch-up",
|
|
474
|
+
index: 0, total: 0,
|
|
475
|
+
phase: "skipped",
|
|
476
|
+
});
|
|
423
477
|
}
|
|
424
478
|
else {
|
|
425
479
|
const start = Date.now();
|
|
@@ -429,10 +483,22 @@ async function runCatchUp(locks, sleepData, memoryConfig, steps, flags, runtime,
|
|
|
429
483
|
throw new LLMUnavailableError(); return r; }));
|
|
430
484
|
lock.state.steps["extract-memories"] = { status: "ok", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
431
485
|
logInfo(TAG, `[CATCH-UP] ✓ 04b-extract-memories for ${lock.dateStr} (${((Date.now() - start) / 1000).toFixed(1)}s) — ${result.slice(0, 80)}`);
|
|
486
|
+
// #895: emit done for catch-up extract-memories
|
|
487
|
+
fireOnStep(onStep, {
|
|
488
|
+
name: "extract-memories", filename: "catch-up",
|
|
489
|
+
index: 0, total: 0,
|
|
490
|
+
phase: "done",
|
|
491
|
+
});
|
|
432
492
|
}
|
|
433
493
|
catch (err) {
|
|
434
494
|
logWarn(TAG, `[CATCH-UP] ✗ 04b for ${lock.dateStr}: ${err instanceof Error ? err.message : String(err)}`);
|
|
435
495
|
lock.state.steps["extract-memories"] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
496
|
+
// #895: emit failed for catch-up extract-memories
|
|
497
|
+
fireOnStep(onStep, {
|
|
498
|
+
name: "extract-memories", filename: "catch-up",
|
|
499
|
+
index: 0, total: 0,
|
|
500
|
+
phase: "failed",
|
|
501
|
+
});
|
|
436
502
|
}
|
|
437
503
|
}
|
|
438
504
|
writeStateFile(lock.path, lock.state);
|
|
@@ -451,10 +517,22 @@ async function runCatchUp(locks, sleepData, memoryConfig, steps, flags, runtime,
|
|
|
451
517
|
if (response) {
|
|
452
518
|
lock.state.steps[stepName] = { status: "ok", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
453
519
|
logInfo(TAG, `[CATCH-UP] ✓ ${stepName} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
520
|
+
// #895: emit done for catch-up retrospective
|
|
521
|
+
fireOnStep(onStep, {
|
|
522
|
+
name: stepName, filename: "catch-up",
|
|
523
|
+
index: 0, total: 0,
|
|
524
|
+
phase: "done",
|
|
525
|
+
});
|
|
454
526
|
}
|
|
455
527
|
else {
|
|
456
528
|
lock.state.steps[stepName] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
457
529
|
logWarn(TAG, `[CATCH-UP] ✗ ${stepName}`);
|
|
530
|
+
// #895: emit failed for catch-up retrospective
|
|
531
|
+
fireOnStep(onStep, {
|
|
532
|
+
name: stepName, filename: "catch-up",
|
|
533
|
+
index: 0, total: 0,
|
|
534
|
+
phase: "failed",
|
|
535
|
+
});
|
|
458
536
|
}
|
|
459
537
|
writeStateFile(lock.path, lock.state);
|
|
460
538
|
}
|
|
@@ -484,6 +562,8 @@ export async function runSleepCycle(opts) {
|
|
|
484
562
|
const timeoutMs = opts.timeoutMs ?? getAbmindEnv().sleepTimeoutMin * 60 * 1000;
|
|
485
563
|
const backoffMs = opts.backoffMs ?? ((n) => [10, 30, 60][Math.min(n, 2)] * 1000);
|
|
486
564
|
const retryDelayMs = opts.retryDelayMs ?? 6000;
|
|
565
|
+
const transportBackoffMs = opts.transportBackoffMs ?? ((n) => Math.min(30_000 * Math.pow(2, n - 1), 120_000));
|
|
566
|
+
const transportRetryWindowMs = opts.transportRetryWindowMs ?? DEFAULT_TRANSPORT_RETRY_WINDOW_MS;
|
|
487
567
|
const runtime = opts.runtime;
|
|
488
568
|
if (flags.verbose) {
|
|
489
569
|
logInfo(TAG, "Verbose mode enabled");
|
|
@@ -641,6 +721,13 @@ export async function runSleepCycle(opts) {
|
|
|
641
721
|
const snapshotVars = buildSleepVars(snapshot);
|
|
642
722
|
for (const [k, v] of Object.entries(snapshotVars))
|
|
643
723
|
vars[k] = vars[k] ?? v;
|
|
724
|
+
// #895: Fire onCycleStart once before the first step — hosts use this to
|
|
725
|
+
// create a stepped card or reset per-night state. fireOnStep() already
|
|
726
|
+
// swallows handler errors so a misbehaving host can never break the cycle.
|
|
727
|
+
try {
|
|
728
|
+
opts.onCycleStart?.({ totalSteps: steps.length });
|
|
729
|
+
}
|
|
730
|
+
catch { /* host display only */ }
|
|
644
731
|
// Progress protocol — emit PROGRESS:<pct>:<label> on stdout
|
|
645
732
|
const totalSteps = steps.length;
|
|
646
733
|
let stepIndex = 0;
|
|
@@ -754,7 +841,7 @@ export async function runSleepCycle(opts) {
|
|
|
754
841
|
const previousLocks = scanPreviousLocks(sleepDir, dateStr);
|
|
755
842
|
if (previousLocks.length > 0) {
|
|
756
843
|
logInfo(TAG, `[CATCH-UP] Found ${previousLocks.length} previous lock(s)`);
|
|
757
|
-
await runCatchUp(previousLocks, sleepData, memoryConfig, steps, flags, runtime, budget, retryDelayMs);
|
|
844
|
+
await runCatchUp(previousLocks, sleepData, memoryConfig, steps, flags, runtime, budget, retryDelayMs, opts.onStep);
|
|
758
845
|
}
|
|
759
846
|
// Housekeeping: move misplaced daily/consolidation_* to weekly/ (#640)
|
|
760
847
|
try {
|
|
@@ -799,13 +886,19 @@ export async function runSleepCycle(opts) {
|
|
|
799
886
|
}
|
|
800
887
|
emitProgress(step.name);
|
|
801
888
|
stepIndex++;
|
|
889
|
+
// #895: "start" fires once per step (before skip checks) so every step
|
|
890
|
+
// emits exactly one start + one terminal (done/skipped/failed). Display-only,
|
|
891
|
+
// best-effort — a throwing handler never breaks the cycle.
|
|
892
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "start" });
|
|
802
893
|
// Resume: skip already completed steps
|
|
803
894
|
if (isResume && existingState?.steps[step.name]?.status === "ok") {
|
|
804
895
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — already done (resume)`);
|
|
896
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
805
897
|
continue;
|
|
806
898
|
}
|
|
807
899
|
if (isResume && existingState?.steps[step.name]?.status === "skipped") {
|
|
808
900
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — skipped (resume)`);
|
|
901
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
809
902
|
continue;
|
|
810
903
|
}
|
|
811
904
|
// Skip logic (essential steps always run)
|
|
@@ -813,6 +906,7 @@ export async function runSleepCycle(opts) {
|
|
|
813
906
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — skipped`);
|
|
814
907
|
state.steps[step.name] = { status: "skipped" };
|
|
815
908
|
writeStateFile(statePath, state);
|
|
909
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
816
910
|
continue;
|
|
817
911
|
}
|
|
818
912
|
const start = Date.now();
|
|
@@ -829,7 +923,7 @@ export async function runSleepCycle(opts) {
|
|
|
829
923
|
const firstMsgTs = sleepData.getFirstMessageAfter(userId, watermarkTs);
|
|
830
924
|
const firstMsgDate = firstMsgTs ? new Date(firstMsgTs) : new Date(now());
|
|
831
925
|
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)
|
|
926
|
+
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
927
|
throw new LLMUnavailableError(); return r; }), {
|
|
834
928
|
ctxWindow, memoryDir: memoryConfig.memoryDir, userId, watermarkTs,
|
|
835
929
|
});
|
|
@@ -846,9 +940,30 @@ export async function runSleepCycle(opts) {
|
|
|
846
940
|
logWarn(TAG, `[SLEEP] 04a failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
847
941
|
state.steps[step.name] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
848
942
|
dreamySucceeded = false;
|
|
943
|
+
writeStateFile(statePath, state);
|
|
944
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "failed" });
|
|
945
|
+
if (err instanceof ModelUnavailableError) {
|
|
946
|
+
logWarn(TAG, `[SLEEP] ${step.name} — model unavailable, stopping cycle (not advancing to next phase)`);
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (state.steps[step.name]?.status !== "failed") {
|
|
951
|
+
// success/skip path: persist state + log + fire event
|
|
952
|
+
writeStateFile(statePath, state);
|
|
849
953
|
}
|
|
850
|
-
writeStateFile(statePath, state);
|
|
851
954
|
logInfo(TAG, `[SLEEP] ${state.steps[step.name]?.status === "ok" ? "✓" : "✗"} ${step.name} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
955
|
+
// #895: emit terminal event for code-driven daily-summary (failure already fired in catch)
|
|
956
|
+
if (state.steps[step.name]?.status !== "failed") {
|
|
957
|
+
fireOnStep(opts.onStep, {
|
|
958
|
+
name: step.name,
|
|
959
|
+
filename: step.filename,
|
|
960
|
+
index: stepIndex,
|
|
961
|
+
total: totalSteps,
|
|
962
|
+
phase: state.steps[step.name]?.status === "ok" ? "done"
|
|
963
|
+
: state.steps[step.name]?.status === "skipped" ? "skipped"
|
|
964
|
+
: "failed",
|
|
965
|
+
});
|
|
966
|
+
}
|
|
852
967
|
continue;
|
|
853
968
|
}
|
|
854
969
|
if (step.name === "extract-memories") {
|
|
@@ -866,11 +981,13 @@ export async function runSleepCycle(opts) {
|
|
|
866
981
|
state.steps[step.name] = { status: "skipped" };
|
|
867
982
|
writeStateFile(statePath, state);
|
|
868
983
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — no daily summary`);
|
|
984
|
+
// #895: emit skip when there's no daily summary to extract from
|
|
985
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
869
986
|
continue;
|
|
870
987
|
}
|
|
871
988
|
try {
|
|
872
989
|
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)
|
|
990
|
+
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
991
|
throw new LLMUnavailableError(); return r; }));
|
|
875
992
|
state.steps[step.name] = { status: "ok", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
876
993
|
writeFileSync(join(stepLogDir, `${String(stepIndex).padStart(2, "0")}-${step.name}.md`), redactSecrets(result), "utf-8");
|
|
@@ -880,8 +997,24 @@ export async function runSleepCycle(opts) {
|
|
|
880
997
|
logWarn(TAG, `[SLEEP] 04b failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
881
998
|
state.steps[step.name] = { status: "failed", duration: Math.round((Date.now() - start) / 100) / 10 };
|
|
882
999
|
dreamySucceeded = false;
|
|
1000
|
+
writeStateFile(statePath, state);
|
|
1001
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "failed" });
|
|
1002
|
+
if (err instanceof ModelUnavailableError) {
|
|
1003
|
+
logWarn(TAG, `[SLEEP] ${step.name} — model unavailable, stopping cycle (not advancing to next phase)`);
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (state.steps[step.name]?.status !== "failed") {
|
|
1008
|
+
writeStateFile(statePath, state);
|
|
1009
|
+
// #895: emit terminal event for code-driven extract-memories (failure already fired in catch)
|
|
1010
|
+
fireOnStep(opts.onStep, {
|
|
1011
|
+
name: step.name,
|
|
1012
|
+
filename: step.filename,
|
|
1013
|
+
index: stepIndex,
|
|
1014
|
+
total: totalSteps,
|
|
1015
|
+
phase: state.steps[step.name]?.status === "ok" ? "done" : "failed",
|
|
1016
|
+
});
|
|
883
1017
|
}
|
|
884
|
-
writeStateFile(statePath, state);
|
|
885
1018
|
continue;
|
|
886
1019
|
}
|
|
887
1020
|
// Standard prompt-driven step — JIT substitution
|
|
@@ -896,6 +1029,8 @@ export async function runSleepCycle(opts) {
|
|
|
896
1029
|
state.steps[step.name] = { status: "skipped" };
|
|
897
1030
|
writeStateFile(statePath, state);
|
|
898
1031
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — no new extractions today`);
|
|
1032
|
+
// #895: emit skip when there's no work for contradiction-and-graph
|
|
1033
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
899
1034
|
continue;
|
|
900
1035
|
}
|
|
901
1036
|
vars.NEW_EXTRACTIONS = newRows.map(r => `[id=${r.id}] (${r.memory_type}, trust=${r.trust}) ${r.content_en}`).join("\n");
|
|
@@ -925,6 +1060,8 @@ export async function runSleepCycle(opts) {
|
|
|
925
1060
|
logWarn(TAG, `[SLEEP] contradiction-and-graph var prep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
926
1061
|
state.steps[step.name] = { status: "skipped" };
|
|
927
1062
|
writeStateFile(statePath, state);
|
|
1063
|
+
// #895: emit skip on var-prep failure
|
|
1064
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
928
1065
|
continue;
|
|
929
1066
|
}
|
|
930
1067
|
}
|
|
@@ -937,6 +1074,8 @@ export async function runSleepCycle(opts) {
|
|
|
937
1074
|
state.steps[step.name] = { status: "skipped" };
|
|
938
1075
|
writeStateFile(statePath, state);
|
|
939
1076
|
logInfo(TAG, `[SLEEP] ⏭ ${step.name} — not enough memories for REM`);
|
|
1077
|
+
// #895: emit skip when not enough memories for REM
|
|
1078
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
940
1079
|
continue;
|
|
941
1080
|
}
|
|
942
1081
|
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 +1083,8 @@ export async function runSleepCycle(opts) {
|
|
|
944
1083
|
catch {
|
|
945
1084
|
state.steps[step.name] = { status: "skipped" };
|
|
946
1085
|
writeStateFile(statePath, state);
|
|
1086
|
+
// #895: emit skip on var-prep failure for rem-synthesis
|
|
1087
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "skipped" });
|
|
947
1088
|
continue;
|
|
948
1089
|
}
|
|
949
1090
|
}
|
|
@@ -952,7 +1093,22 @@ export async function runSleepCycle(opts) {
|
|
|
952
1093
|
if (soulPrefix)
|
|
953
1094
|
soulPrefix = ""; // only prepend to first step
|
|
954
1095
|
const ctxBefore = -1;
|
|
955
|
-
|
|
1096
|
+
let response;
|
|
1097
|
+
try {
|
|
1098
|
+
response = await sendWithRetry(runtime, fullPrompt, step.name, flags.verbose, budget, retryDelayMs, transportBackoffMs, transportRetryWindowMs, now);
|
|
1099
|
+
}
|
|
1100
|
+
catch (err) {
|
|
1101
|
+
if (err instanceof ModelUnavailableError) {
|
|
1102
|
+
const duration = Date.now() - start;
|
|
1103
|
+
logWarn(TAG, `[SLEEP] ${step.name} — model unavailable, stopping cycle (not advancing to next phase)`);
|
|
1104
|
+
state.steps[step.name] = { status: "failed", duration: Math.round(duration / 100) / 10, ctxBefore, ctxAfter: -1 };
|
|
1105
|
+
dreamySucceeded = false;
|
|
1106
|
+
writeStateFile(statePath, state);
|
|
1107
|
+
fireOnStep(opts.onStep, { name: step.name, filename: step.filename, index: stepIndex, total: totalSteps, phase: "failed" });
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
throw err;
|
|
1111
|
+
}
|
|
956
1112
|
const ctxAfter = -1;
|
|
957
1113
|
const duration = Date.now() - start;
|
|
958
1114
|
if (response) {
|
|
@@ -1005,6 +1161,14 @@ export async function runSleepCycle(opts) {
|
|
|
1005
1161
|
dreamySucceeded = false;
|
|
1006
1162
|
}
|
|
1007
1163
|
writeStateFile(statePath, state);
|
|
1164
|
+
// #895: emit terminal event for the standard prompt-driven step
|
|
1165
|
+
fireOnStep(opts.onStep, {
|
|
1166
|
+
name: step.name,
|
|
1167
|
+
filename: step.filename,
|
|
1168
|
+
index: stepIndex,
|
|
1169
|
+
total: totalSteps,
|
|
1170
|
+
phase: response ? "done" : "failed",
|
|
1171
|
+
});
|
|
1008
1172
|
logInfo(TAG, `[SLEEP] ${response ? "✓" : "✗"} ${step.name} (${(duration / 1000).toFixed(1)}s, ${response?.length ?? 0} chars)`);
|
|
1009
1173
|
// Backoff between steps: 10s → 30s → 60s on consecutive failures, reset on success
|
|
1010
1174
|
if (response) {
|