@letta-ai/dreams 0.0.3 → 0.0.4

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/dist/sync.js CHANGED
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
2
  /**
3
- * Hook-safe `dreams sync` ([LET-10237] / [LET-10240]).
3
+ * Hook-safe `dreams sync` / `dreams wake` ([LET-10237] / [LET-10240]).
4
+ *
5
+ * - `dreams sync` (Stop / live turn): records a durable trigger (optional
6
+ * session_id from stdin) and may spawn a detached worker.
7
+ * - `dreams wake` (SessionStart): quiet housekeeping (managed CLI + skill
8
+ * repair) then force-kicks the worker for pending/backfill drain — no stdin
9
+ * session hint.
4
10
  *
5
- * Default invocation records a durable trigger and may spawn a detached worker.
6
11
  * The hook parent always exits 0. The worker performs live scan + live-first
7
12
  * upload, then — only when an active backfill policy already exists and upload
8
13
  * budget remains — one backfill continuation slice + remaining-budget upload.
@@ -51,6 +56,7 @@ exports.commandSyncPause = commandSyncPause;
51
56
  exports.commandSyncResume = commandSyncResume;
52
57
  exports.commandSyncStatus = commandSyncStatus;
53
58
  exports.commandSyncWorker = commandSyncWorker;
59
+ exports.commandWake = commandWake;
54
60
  exports.commandSync = commandSync;
55
61
  const node_child_process_1 = require("node:child_process");
56
62
  const path = __importStar(require("node:path"));
@@ -61,17 +67,15 @@ const leases_1 = require("./local/leases");
61
67
  const scan_1 = require("./local/scan");
62
68
  const state_lock_1 = require("./local/state-lock");
63
69
  const sync_control_1 = require("./local/sync-control");
70
+ const codex_1 = require("./local/codex");
64
71
  const source_adapters_1 = require("./local/source-adapters");
72
+ const managed_install_1 = require("./managed-install");
73
+ const skill_1 = require("./skill");
65
74
  const store_1 = require("./store");
66
75
  const STDIN_LIMIT_BYTES = 64 * 1024;
67
76
  /** Resolve `--source <type>` at the sync command boundary (stdin never selects source). */
68
77
  function resolveSyncAdapter(sourceType) {
69
- if (sourceType === undefined)
70
- return (0, source_adapters_1.defaultSourceAdapter)();
71
- const trimmed = sourceType.trim();
72
- if (!trimmed)
73
- throw new Error("unknown source type: (empty)");
74
- return (0, source_adapters_1.getSourceAdapterByType)(trimmed);
78
+ return (0, source_adapters_1.resolveSourceAdapter)(sourceType);
75
79
  }
76
80
  function defaultWorkerEntry() {
77
81
  const candidate = process.argv[1] ? path.resolve(process.argv[1]) : "";
@@ -215,7 +219,7 @@ async function commandSyncSchedule(options) {
215
219
  const adapter = options.adapter;
216
220
  const sourceId = adapter.sourceId;
217
221
  try {
218
- const sessionHint = await readSessionHintFromStdin(adapter);
222
+ const sessionHint = options.skipStdin ? null : await readSessionHintFromStdin(adapter);
219
223
  const { db } = openInstallationDb();
220
224
  try {
221
225
  const leaseRow = db.prepare("SELECT owner, expires_at FROM leases WHERE name = ?").get(leases_1.SYNC_WORKER_LEASE);
@@ -333,28 +337,78 @@ async function commandSyncResume(json) {
333
337
  (0, db_1.closeDb)();
334
338
  }
335
339
  }
336
- function commandSyncStatus(json) {
340
+ function commandSyncStatus(json, sourceType) {
341
+ let adapter;
342
+ try {
343
+ adapter = resolveSyncAdapter(sourceType);
344
+ }
345
+ catch (error) {
346
+ const message = error instanceof Error ? error.message : String(error);
347
+ if (json) {
348
+ console.log(JSON.stringify({ status: "error", schema_version: 1, code: "invalid_argument", message }));
349
+ }
350
+ else {
351
+ process.stderr.write(`${message}\n`);
352
+ }
353
+ return 1;
354
+ }
355
+ const harnessPresent = adapter.detect().present;
337
356
  const { db } = openInstallationDb();
338
357
  try {
358
+ const configured = (0, sync_control_1.sourceExists)(db, adapter.sourceId);
339
359
  const leaseRow = db.prepare("SELECT expires_at FROM leases WHERE name = ?").get(leases_1.SYNC_WORKER_LEASE);
340
- const workerActive = Boolean(leaseRow && leaseRow.expires_at > Date.now());
341
- (0, sync_control_1.normalizeStaleRunning)(db, workerActive);
342
- const runtime = (0, sync_control_1.readRuntime)(db);
343
- const trigger = (0, sync_control_1.observeTrigger)(db);
344
- const pending = trigger.requested_gen > trigger.processed_gen || trigger.forced;
345
- const effective = (0, sync_control_1.effectiveSyncState)({
346
- runtime,
347
- pending,
348
- forced: trigger.forced,
349
- workerActive,
350
- });
360
+ const workerLeaseHeld = Boolean(leaseRow && leaseRow.expires_at > Date.now());
361
+ (0, sync_control_1.normalizeStaleRunning)(db, workerLeaseHeld);
362
+ // Peek only status must not mint source/trigger rows for unconfigured adapters.
363
+ const runtime = (0, sync_control_1.peekRuntime)(db, adapter.sourceId);
364
+ const trigger = (0, sync_control_1.observeTrigger)(db, adapter.sourceId);
365
+ const pending = configured && (trigger.requested_gen > trigger.processed_gen || trigger.forced);
366
+ const workerActiveForSource = workerLeaseHeld && configured && (0, sync_control_1.readActiveSourceId)(db) === adapter.sourceId;
367
+ const effective = configured
368
+ ? (0, sync_control_1.effectiveSyncState)({
369
+ runtime,
370
+ pending,
371
+ forced: trigger.forced,
372
+ workerActive: workerActiveForSource,
373
+ })
374
+ : runtime.paused
375
+ ? "paused"
376
+ : "idle";
377
+ const discovery = adapter.sourceType === codex_1.CODEX_SOURCE_TYPE
378
+ ? (() => {
379
+ const report = (0, codex_1.inspectCodexDiscovery)();
380
+ return {
381
+ status: report.status === "warning" ? "warning" : report.status === "ok" ? "ok" : report.status,
382
+ harness_present: report.harness_present,
383
+ sessions_dir_present: report.sessions_dir_present,
384
+ candidates: report.candidates,
385
+ supported: report.supported,
386
+ unsupported: report.unsupported,
387
+ excluded_subagent: report.excluded_subagent,
388
+ by_reason: report.by_reason,
389
+ examples: report.examples,
390
+ message: report.message,
391
+ };
392
+ })()
393
+ : {
394
+ status: harnessPresent ? "ok" : "absent",
395
+ harness_present: harnessPresent,
396
+ message: harnessPresent ? null : `${adapter.displayName} was not detected`,
397
+ };
351
398
  const payload = {
352
399
  schema_version: 1,
353
400
  status: "ok",
401
+ source: adapter.sourceType,
402
+ source_id: adapter.sourceId,
403
+ source_configured: configured,
404
+ source_present: harnessPresent,
354
405
  sync: {
355
406
  ...runtime,
356
407
  effective_state: effective,
357
- worker_active: workerActive,
408
+ worker_active: workerActiveForSource,
409
+ worker_lease_held: workerLeaseHeld,
410
+ // Stale ownership must not outlive the lease (belt-and-suspenders with completeSyncPass).
411
+ active_source_id: workerLeaseHeld ? (0, sync_control_1.readActiveSourceId)(db) : null,
358
412
  pending,
359
413
  requested_gen: trigger.requested_gen,
360
414
  processed_gen: trigger.processed_gen,
@@ -362,19 +416,24 @@ function commandSyncStatus(json) {
362
416
  session_hints: trigger.session_hints,
363
417
  cadence_ms: sync_control_1.CADENCE_MS,
364
418
  },
419
+ discovery,
365
420
  };
366
421
  emit(json, payload, () => [
367
422
  "Dreams sync status",
368
423
  "",
424
+ ` Source: ${adapter.sourceType} (${adapter.sourceId})${configured ? "" : " — not configured"}`,
369
425
  ` Effective: ${effective}`,
370
426
  ` Runtime: ${runtime.state}`,
371
427
  ` Paused: ${runtime.paused ? "yes" : "no"}`,
372
428
  ` Pending: ${pending ? "yes" : "no"}`,
373
- ` Worker: ${workerActive ? "active" : "idle"}`,
429
+ ` Worker: ${workerActiveForSource ? "active for this source" : workerLeaseHeld ? "active for another source" : "idle"}`,
374
430
  ` Next due: ${runtime.next_due_at ?? "(immediately)"}`,
375
431
  ` Last success:${runtime.last_success_at ? ` ${runtime.last_success_at}` : " (none)"}`,
376
432
  ` Last error: ${runtime.last_error_code ? `${runtime.last_error_code}: ${runtime.last_error_message}` : " (none)"}`,
377
- ].join("\n"));
433
+ discovery.message ? ` Discovery: ${discovery.message}` : "",
434
+ ]
435
+ .filter((line) => line !== "")
436
+ .join("\n"));
378
437
  return 0;
379
438
  }
380
439
  finally {
@@ -594,7 +653,7 @@ async function commandSyncWorker(options) {
594
653
  console.log(JSON.stringify({ status: "paused", schema_version: 1, source: adapter.sourceType }));
595
654
  return 0;
596
655
  }
597
- (0, sync_control_1.markRuntimeRunning)(db);
656
+ (0, sync_control_1.markRuntimeRunning)(db, sourceId);
598
657
  while (passes < sync_control_1.MAX_TRAILING_PASSES) {
599
658
  if ((0, sync_control_1.isPaused)(db))
600
659
  break;
@@ -704,8 +763,16 @@ async function commandSyncWorker(options) {
704
763
  catch {
705
764
  // test hooks must not break cleanup
706
765
  }
707
- if (workerOwner)
766
+ if (workerOwner) {
708
767
  (0, leases_1.releaseLease)(db, leases_1.SYNC_WORKER_LEASE, workerOwner);
768
+ try {
769
+ // Ownership spans trailing passes; clear only when this worker exits.
770
+ (0, sync_control_1.clearActiveSourceId)(db);
771
+ }
772
+ catch {
773
+ // best effort
774
+ }
775
+ }
709
776
  (0, db_1.closeDb)();
710
777
  // Release the local-state lock before the final pending check so a hook that
711
778
  // spawned during shutdown cannot observe busy-on-lock and exit while we also
@@ -734,6 +801,103 @@ async function commandSyncWorker(options) {
734
801
  spawnDetachedWorker(true, successorType);
735
802
  }
736
803
  }
804
+ /**
805
+ * SessionStart entrypoint: repair local CLI/skill quietly, then force-schedule
806
+ * the sync worker for pending live/backfill drain (no session stdin hint).
807
+ * Always exits 0 so harness hooks stay non-blocking.
808
+ */
809
+ async function commandWake(options) {
810
+ let adapter;
811
+ try {
812
+ adapter = resolveSyncAdapter(options.sourceType);
813
+ }
814
+ catch (error) {
815
+ // Still exit 0 for hook safety; surface the error in JSON/human when possible.
816
+ const message = error instanceof Error ? error.message : String(error);
817
+ try {
818
+ emit(options.json, { status: "coalesced", schema_version: 1, command: "wake", worker: "schedule_error", message }, () => `Wake could not resolve source: ${message}`);
819
+ }
820
+ catch {
821
+ // ignore
822
+ }
823
+ return 0;
824
+ }
825
+ const housekeeping = { managed_install: null, skill: null, error: null };
826
+ try {
827
+ const managed = (0, managed_install_1.ensureManagedInstall)();
828
+ housekeeping.managed_install = managed.status;
829
+ housekeeping.skill = (0, skill_1.installSkill)();
830
+ }
831
+ catch (error) {
832
+ housekeeping.error = error instanceof Error ? error.message : String(error);
833
+ }
834
+ // Reuse the sync scheduler with force + no stdin. Emit a wake-shaped envelope
835
+ // by wrapping schedule output when JSON is requested.
836
+ if (!options.json) {
837
+ return commandSyncSchedule({ json: false, force: true, adapter, skipStdin: true });
838
+ }
839
+ // Capture schedule JSON by temporarily patching console.log is brittle; call
840
+ // the schedule path with json:true — it already prints. Prepend housekeeping
841
+ // by running schedule logic inline via a thin re-emit is hard. Instead: run
842
+ // schedule with json false internals... Simplest: run schedule with json true
843
+ // after printing nothing, and document housekeeping in a second object — agents
844
+ // prefer one object. So duplicate the force-schedule emit with housekeeping.
845
+ try {
846
+ const { db } = openInstallationDb();
847
+ try {
848
+ const leaseRow = db.prepare("SELECT owner, expires_at FROM leases WHERE name = ?").get(leases_1.SYNC_WORKER_LEASE);
849
+ const workerActive = Boolean(leaseRow && leaseRow.expires_at > Date.now());
850
+ (0, sync_control_1.normalizeStaleRunning)(db, workerActive);
851
+ const recorded = (0, sync_control_1.recordSyncRequest)(db, { forced: true, sessionHint: null, sourceId: adapter.sourceId });
852
+ if (recorded.paused) {
853
+ emit(true, { status: "paused", schema_version: 1, command: "wake", source: adapter.sourceType, housekeeping }, () => "Wake: sync paused.");
854
+ return 0;
855
+ }
856
+ if (workerActive) {
857
+ emit(true, {
858
+ status: "coalesced",
859
+ schema_version: 1,
860
+ command: "wake",
861
+ source: adapter.sourceType,
862
+ trigger_generation: recorded.generation,
863
+ forced: true,
864
+ housekeeping,
865
+ }, () => "Wake coalesced into running worker.");
866
+ return 0;
867
+ }
868
+ const spawned = spawnDetachedWorker(true, adapter.sourceType);
869
+ if (!spawned) {
870
+ (0, sync_control_1.markRuntimeDegraded)(db, "spawn_failed", "worker could not be detached; trigger left pending for next wake", null, adapter.sourceId);
871
+ }
872
+ emit(true, {
873
+ status: spawned ? "scheduled" : "coalesced",
874
+ schema_version: 1,
875
+ command: "wake",
876
+ source: adapter.sourceType,
877
+ trigger_generation: recorded.generation,
878
+ forced: true,
879
+ worker: spawned ? "spawned" : "spawn_failed",
880
+ housekeeping,
881
+ }, () => (spawned ? "Wake scheduled sync worker." : "Wake recorded; worker spawn failed."));
882
+ return 0;
883
+ }
884
+ finally {
885
+ (0, db_1.closeDb)();
886
+ }
887
+ }
888
+ catch (error) {
889
+ emit(true, {
890
+ status: "coalesced",
891
+ schema_version: 1,
892
+ command: "wake",
893
+ source: adapter.sourceType,
894
+ worker: "schedule_error",
895
+ message: error instanceof Error ? error.message : String(error),
896
+ housekeeping,
897
+ }, () => "Wake could not fully schedule; will retry later.");
898
+ return 0;
899
+ }
900
+ }
737
901
  async function commandSync(subcommand, options) {
738
902
  let adapter;
739
903
  try {
@@ -778,11 +942,7 @@ async function commandSync(subcommand, options) {
778
942
  process.stderr.write(`--force is not valid with sync status\n`);
779
943
  return 1;
780
944
  }
781
- if (options.sourceType) {
782
- process.stderr.write(`--source is not valid with sync status\n`);
783
- return 1;
784
- }
785
- return commandSyncStatus(options.json);
945
+ return commandSyncStatus(options.json, options.sourceType);
786
946
  default:
787
947
  process.stderr.write(`Unknown sync subcommand: ${subcommand} — expected pause, resume, or status\n`);
788
948
  return 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/dreams",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "private": false,
5
5
  "description": "Dreams CLI - shared organizational memory from coding-agent transcripts",
6
6
  "license": "Apache-2.0",