@yeaft/webchat-agent 1.0.59 → 1.0.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.59",
3
+ "version": "1.0.62",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/yeaft/cli.js CHANGED
@@ -116,7 +116,7 @@ function parseArgs(argv) {
116
116
 
117
117
  // ─── Trace query handler ───────────────────────────────────────
118
118
 
119
- function handleTraceQuery(args, config) {
119
+ async function handleTraceQuery(args, config) {
120
120
  const traceDir = config.dir;
121
121
  let trace;
122
122
  try {
@@ -128,7 +128,7 @@ function handleTraceQuery(args, config) {
128
128
  try {
129
129
  switch (args.trace) {
130
130
  case 'stats': {
131
- const s = trace.stats();
131
+ const s = await trace.stats();
132
132
  console.log('Debug Trace Statistics:');
133
133
  console.log(` Turns: ${s.turnCount}`);
134
134
  console.log(` Tools: ${s.toolCount}`);
@@ -137,7 +137,7 @@ function handleTraceQuery(args, config) {
137
137
  break;
138
138
  }
139
139
  case 'recent': {
140
- const turns = trace.queryRecent(20);
140
+ const turns = await trace.queryRecent(20);
141
141
  if (turns.length === 0) {
142
142
  console.log('No recent turns.');
143
143
  } else {
@@ -154,7 +154,7 @@ function handleTraceQuery(args, config) {
154
154
  if (!args.traceArg) {
155
155
  throw new Error('Usage: --trace search <keyword>');
156
156
  }
157
- const results = trace.search(args.traceArg);
157
+ const results = await trace.search(args.traceArg);
158
158
  console.log(`Found ${results.length} turns matching "${args.traceArg}":`);
159
159
  for (const t of results) {
160
160
  const time = new Date(t.started_at).toLocaleString();
@@ -164,7 +164,7 @@ function handleTraceQuery(args, config) {
164
164
  break;
165
165
  }
166
166
  case 'tools': {
167
- const tools = trace.queryTools({ name: args.traceArg });
167
+ const tools = await trace.queryTools({ name: args.traceArg });
168
168
  console.log(`Found ${tools.length} tool calls${args.traceArg ? ` for "${args.traceArg}"` : ''}:`);
169
169
  for (const t of tools.slice(0, 20)) {
170
170
  const time = new Date(t.created_at).toLocaleString();
@@ -174,10 +174,10 @@ function handleTraceQuery(args, config) {
174
174
  break;
175
175
  }
176
176
  case 'compact': {
177
- const s = trace.stats();
177
+ const s = await trace.stats();
178
178
  console.log(`Compacting debug trace files (${(s.dbSizeBytes / 1048576).toFixed(1)} MB, ${s.turnCount} turns)...`);
179
179
  console.log('This prunes old request folders and may take a moment. Do not interrupt.');
180
- const { before, after } = trace.compact();
180
+ const { before, after } = await trace.compact();
181
181
  const saved = Math.max(0, before - after);
182
182
  console.log(`Done. ${(before / 1048576).toFixed(1)} MB → ${(after / 1048576).toFixed(1)} MB (reclaimed ${(saved / 1048576).toFixed(1)} MB).`);
183
183
  break;
@@ -186,7 +186,7 @@ function handleTraceQuery(args, config) {
186
186
  throw new Error(`Unknown trace command: ${args.trace}. Available: stats, recent, search <keyword>, tools [name], compact`);
187
187
  }
188
188
  } finally {
189
- trace.close();
189
+ await trace.close();
190
190
  }
191
191
  }
192
192
 
@@ -383,7 +383,7 @@ async function runREPL(config, args) {
383
383
  case 'trace': {
384
384
  const subcmd = cmdArgs[0] || 'stats';
385
385
  try {
386
- handleTraceQuery({ trace: subcmd, traceArg: cmdArgs[1] }, session.config);
386
+ await handleTraceQuery({ trace: subcmd, traceArg: cmdArgs[1] }, session.config);
387
387
  } catch (e) {
388
388
  console.error(`Trace error: ${e.message}`);
389
389
  }
@@ -454,7 +454,7 @@ async function runREPL(config, args) {
454
454
  break;
455
455
 
456
456
  case 'stats': {
457
- const s = trace.stats();
457
+ const s = await trace.stats();
458
458
  console.log(`Session stats:`);
459
459
  console.log(` Debug: ${session.config.debug}`);
460
460
  console.log(` Turns: ${s.turnCount}`);
@@ -799,7 +799,7 @@ async function main() {
799
799
 
800
800
  // Handle --trace queries (no LLM needed, no session needed)
801
801
  if (args.trace) {
802
- handleTraceQuery(args, config);
802
+ await handleTraceQuery(args, config);
803
803
  return;
804
804
  }
805
805
 
@@ -11,16 +11,7 @@
11
11
  * and dropped, never allowed to stop the agent.
12
12
  */
13
13
 
14
- import {
15
- existsSync,
16
- mkdirSync,
17
- readFileSync,
18
- readdirSync,
19
- renameSync,
20
- rmSync,
21
- statSync,
22
- writeFileSync,
23
- } from 'fs';
14
+ import { promises as fsp } from 'fs';
24
15
  import { basename, dirname, extname, join } from 'path';
25
16
  import { randomUUID } from 'crypto';
26
17
 
@@ -56,20 +47,20 @@ function fileTraceRoot(inputPath) {
56
47
  return extname(basename(raw)) ? `${raw}.files` : raw;
57
48
  }
58
49
 
59
- function ensureDir(dir) {
60
- mkdirSync(dir, { recursive: true });
50
+ async function ensureDir(dir) {
51
+ await fsp.mkdir(dir, { recursive: true });
61
52
  }
62
53
 
63
- function atomicWriteJson(filePath, value) {
64
- ensureDir(dirname(filePath));
65
- const tmp = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
66
- writeFileSync(tmp, JSON.stringify(value), 'utf8');
67
- renameSync(tmp, filePath);
54
+ async function atomicWriteText(filePath, text) {
55
+ await ensureDir(dirname(filePath));
56
+ const tmp = `${filePath}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
57
+ await fsp.writeFile(tmp, text, 'utf8');
58
+ await fsp.rename(tmp, filePath);
68
59
  }
69
60
 
70
- function readJson(filePath) {
61
+ async function readJson(filePath) {
71
62
  try {
72
- return JSON.parse(readFileSync(filePath, 'utf8'));
63
+ return JSON.parse(await fsp.readFile(filePath, 'utf8'));
73
64
  } catch {
74
65
  return null;
75
66
  }
@@ -533,38 +524,40 @@ function traceToolToLegacy(trace, tool) {
533
524
  };
534
525
  }
535
526
 
536
- function collectTraceFiles(rootDir, sessionId = null) {
527
+ async function readdirSafe(dir) {
528
+ try { return await fsp.readdir(dir, { withFileTypes: true }); }
529
+ catch { return []; }
530
+ }
531
+
532
+ async function collectTraceFiles(rootDir, sessionId = null) {
537
533
  const files = [];
538
- const addFromRequestsDir = (requestsDir) => {
539
- let entries = [];
540
- try { entries = readdirSync(requestsDir, { withFileTypes: true }); }
541
- catch { return; }
542
- for (const entry of entries) {
534
+ const addFromRequestsDir = async (requestsDir) => {
535
+ for (const entry of await readdirSafe(requestsDir)) {
543
536
  if (!entry.isDirectory()) continue;
544
537
  const file = requestFilePath(join(requestsDir, entry.name));
545
- if (existsSync(file)) files.push(file);
538
+ try {
539
+ await fsp.access(file);
540
+ files.push(file);
541
+ } catch { /* trace.json not yet written for this request dir */ }
546
542
  }
547
543
  };
548
544
  if (sessionId) {
549
- addFromRequestsDir(sessionRequestsDir(rootDir, sessionId));
545
+ await addFromRequestsDir(sessionRequestsDir(rootDir, sessionId));
550
546
  return files;
551
547
  }
552
- addFromRequestsDir(sessionRequestsDir(rootDir, null));
548
+ await addFromRequestsDir(sessionRequestsDir(rootDir, null));
553
549
  const sessionsRoot = join(rootDir, 'sessions');
554
- let sessionEntries = [];
555
- try { sessionEntries = readdirSync(sessionsRoot, { withFileTypes: true }); }
556
- catch { return files; }
557
- for (const entry of sessionEntries) {
550
+ for (const entry of await readdirSafe(sessionsRoot)) {
558
551
  if (!entry.isDirectory()) continue;
559
- addFromRequestsDir(join(sessionsRoot, entry.name, 'debug', 'requests'));
552
+ await addFromRequestsDir(join(sessionsRoot, entry.name, 'debug', 'requests'));
560
553
  }
561
554
  return files;
562
555
  }
563
556
 
564
- function readTraceSummaries(rootDir, sessionId = null) {
557
+ async function readTraceSummaries(rootDir, sessionId = null) {
565
558
  const traces = [];
566
- for (const file of collectTraceFiles(rootDir, sessionId)) {
567
- const trace = readJson(file);
559
+ for (const file of await collectTraceFiles(rootDir, sessionId)) {
560
+ const trace = await readJson(file);
568
561
  if (!trace || !trace.requestId) continue;
569
562
  traces.push({ trace, file, openedAt: Number(trace.openedAt || 0) });
570
563
  }
@@ -572,23 +565,20 @@ function readTraceSummaries(rootDir, sessionId = null) {
572
565
  return traces;
573
566
  }
574
567
 
575
- function countDirFiles(rootDir) {
568
+ async function countDirFiles(rootDir) {
576
569
  let files = 0;
577
570
  let bytes = 0;
578
- const walk = (dir) => {
579
- let entries = [];
580
- try { entries = readdirSync(dir, { withFileTypes: true }); }
581
- catch { return; }
582
- for (const entry of entries) {
571
+ const walk = async (dir) => {
572
+ for (const entry of await readdirSafe(dir)) {
583
573
  const p = join(dir, entry.name);
584
- if (entry.isDirectory()) walk(p);
574
+ if (entry.isDirectory()) await walk(p);
585
575
  else {
586
576
  files += 1;
587
- try { bytes += statSync(p).size; } catch { /* ignore */ }
577
+ try { bytes += (await fsp.stat(p)).size; } catch { /* ignore */ }
588
578
  }
589
579
  }
590
580
  };
591
- walk(rootDir);
581
+ await walk(rootDir);
592
582
  return { files, bytes };
593
583
  }
594
584
 
@@ -605,6 +595,38 @@ export class DebugTrace {
605
595
  #flushTimer = null;
606
596
  /** @type {number} */
607
597
  #sequence = 0;
598
+ /**
599
+ * In-memory dream/event ring (authoritative copy; persisted async).
600
+ * @type {Array<object>}
601
+ */
602
+ #events = [];
603
+ /** @type {boolean} */
604
+ #eventsDirty = false;
605
+ /**
606
+ * Whether the on-disk events.json has been folded into #events yet. The
607
+ * events ring must merge prior-run history exactly once before any flush
608
+ * overwrites the file, or a live append landing before first hydrate would
609
+ * silently destroy cross-restart dream history.
610
+ * @type {boolean}
611
+ */
612
+ #eventsHydrated = false;
613
+ /** @type {NodeJS.Timeout|null} */
614
+ #eventFlushTimer = null;
615
+ /**
616
+ * One-time hydrate guard. Reads/maintenance load every on-disk trace into
617
+ * #requestCache exactly once; afterwards every query is served from memory
618
+ * with zero disk reads. The write path never hydrates — it only appends.
619
+ * @type {boolean}
620
+ */
621
+ #hydrated = false;
622
+ /** @type {Promise<void>|null} */
623
+ #hydratePromise = null;
624
+ /**
625
+ * Serializes async flushes so two overlapping flushes never interleave
626
+ * writes to the same trace file. close()/purge() await this to drain.
627
+ * @type {Promise<void>}
628
+ */
629
+ #flushChain = Promise.resolve();
608
630
 
609
631
  /**
610
632
  * @param {string} tracePath — Back-compatible path. If it looks like a DB
@@ -614,7 +636,9 @@ export class DebugTrace {
614
636
  const rootDir = fileTraceRoot(tracePath);
615
637
  if (!rootDir) throw new Error('DebugTrace requires a storage path');
616
638
  this.#rootDir = rootDir;
617
- ensureDir(rootDir);
639
+ // Best-effort, fire-and-forget: atomicWriteText re-ensures the request
640
+ // subdir before every write, so a missed mkdir here is harmless.
641
+ ensureDir(rootDir).catch(() => {});
618
642
  }
619
643
 
620
644
  startTurn({ traceId, messageId = null, mode = null, turnNumber = null, sessionId = null, vpId = null, threadId = null, userPrompt = null } = {}) {
@@ -713,19 +737,20 @@ export class DebugTrace {
713
737
 
714
738
  logEvent({ traceId, eventType, eventData = null } = {}) {
715
739
  const id = randomUUID();
716
- const file = join(this.#rootDir, 'events.json');
717
- const existingEvents = readJson(file);
718
- const events = Array.isArray(existingEvents) ? existingEvents : [];
719
- events.push({
740
+ // In-memory authoritative ring; persisted async (fire-and-forget). The
741
+ // dream/event sink runs on the engine hot path, so it must not block on a
742
+ // synchronous read-modify-write of events.json.
743
+ this.#events.push({
720
744
  id,
721
745
  traceId: traceId || String(eventType || 'event'),
722
746
  eventType: eventType || 'event',
723
747
  eventData: safeJsonValue(eventData),
724
748
  createdAt: Date.now(),
725
749
  });
726
- const trimmed = events.slice(-MAX_DREAM_EVENTS);
727
- try { atomicWriteJson(file, trimmed); }
728
- catch (err) { console.warn('[Yeaft] debug trace event write failed:', err?.message || err); }
750
+ if (this.#events.length > MAX_DREAM_EVENTS) {
751
+ this.#events = this.#events.slice(-MAX_DREAM_EVENTS);
752
+ }
753
+ this.#scheduleEventFlush();
729
754
  return id;
730
755
  }
731
756
 
@@ -736,24 +761,27 @@ export class DebugTrace {
736
761
  return this.logEvent({ traceId, eventType, eventData });
737
762
  }
738
763
 
739
- queryByMessage(messageId) {
740
- this.#flushPendingSync();
764
+ async queryByMessage(messageId) {
765
+ await this.#ensureHydrated();
766
+ await this.#drainWrites();
741
767
  const traces = this.#traceSummaries()
742
768
  .filter(({ trace }) => trace.messageId === messageId)
743
769
  .map(({ trace }) => trace);
744
770
  return this.#expandLegacy(traces);
745
771
  }
746
772
 
747
- queryByTrace(traceId) {
748
- this.#flushPendingSync();
773
+ async queryByTrace(traceId) {
774
+ await this.#ensureHydrated();
775
+ await this.#drainWrites();
749
776
  const traces = this.#traceSummaries()
750
777
  .filter(({ trace }) => trace.traceId === traceId || trace.requestId === traceId)
751
778
  .map(({ trace }) => trace);
752
779
  return this.#expandLegacy(traces);
753
780
  }
754
781
 
755
- queryRecent(limit = 20) {
756
- this.#flushPendingSync();
782
+ async queryRecent(limit = 20) {
783
+ await this.#ensureHydrated();
784
+ await this.#drainWrites();
757
785
  const lim = Math.max(1, Math.min(MAX_HISTORY_LIMIT, Number(limit) || MAX_HISTORY_LIMIT));
758
786
  return this.#traceSummaries()
759
787
  .slice(-lim)
@@ -761,8 +789,9 @@ export class DebugTrace {
761
789
  .flatMap(({ trace }) => traceToLegacyRows(trace));
762
790
  }
763
791
 
764
- fetchRecentDebugHistory({ limit = MAX_HISTORY_LIMIT, dreamLimit = 5, sessionId = null, threadId = null, indexOnly = false, detailTurnId = null, search = '' } = {}) {
765
- this.#flushPendingSync();
792
+ async fetchRecentDebugHistory({ limit = MAX_HISTORY_LIMIT, dreamLimit = 5, sessionId = null, threadId = null, indexOnly = false, detailTurnId = null, search = '' } = {}) {
793
+ await this.#ensureHydrated();
794
+ await this.#drainWrites();
766
795
  const lim = Math.max(1, Math.min(MAX_HISTORY_LIMIT, Number(limit) || MAX_HISTORY_LIMIT));
767
796
  const requestedDetailTurnId = typeof detailTurnId === 'string' && detailTurnId ? detailTurnId : null;
768
797
  const searchRegex = requestedDetailTurnId ? null : compileTraceSearchRegex(search);
@@ -797,8 +826,9 @@ export class DebugTrace {
797
826
  return { ...expanded, dreamEvents, hasMore: traces.length > selected.length, limit: lim, indexOnly: false };
798
827
  }
799
828
 
800
- queryTools({ name = null, since = null } = {}) {
801
- this.#flushPendingSync();
829
+ async queryTools({ name = null, since = null } = {}) {
830
+ await this.#ensureHydrated();
831
+ await this.#drainWrites();
802
832
  const tools = [];
803
833
  for (const { trace } of this.#traceSummaries()) {
804
834
  for (const tool of Array.isArray(trace.tools) ? trace.tools : []) {
@@ -812,8 +842,9 @@ export class DebugTrace {
812
842
  return tools.slice(0, 100);
813
843
  }
814
844
 
815
- search(keyword) {
816
- this.#flushPendingSync();
845
+ async search(keyword) {
846
+ await this.#ensureHydrated();
847
+ await this.#drainWrites();
817
848
  const needle = String(keyword || '').toLowerCase();
818
849
  if (!needle) return [];
819
850
  return this.#traceSummaries()
@@ -823,62 +854,79 @@ export class DebugTrace {
823
854
  .flatMap(({ trace }) => traceToLegacyRows(trace));
824
855
  }
825
856
 
826
- stats() {
827
- this.#flushPendingSync();
857
+ async stats() {
858
+ await this.#ensureHydrated();
859
+ await this.#drainWrites();
828
860
  const traces = this.#traceSummaries().map(({ trace }) => trace);
829
861
  const turnCount = traces.reduce((n, trace) => n + (Array.isArray(trace.loops) ? trace.loops.length : 0), 0);
830
862
  const toolCount = traces.reduce((n, trace) => n + (Array.isArray(trace.tools) ? trace.tools.length : 0), 0);
831
- const events = readJson(join(this.#rootDir, 'events.json'));
832
- const eventCount = Array.isArray(events) ? events.length : 0;
833
- const { bytes } = countDirFiles(this.#rootDir);
863
+ const eventCount = this.#events.length;
864
+ const { bytes } = await countDirFiles(this.#rootDir);
834
865
  return { turnCount, toolCount, eventCount, dbSizeBytes: bytes, fileSizeBytes: bytes, requestCount: traces.length };
835
866
  }
836
867
 
837
- cleanup(retention = REQUEST_RETENTION) {
838
- this.#flushPendingSync();
868
+ async cleanup(retention = REQUEST_RETENTION) {
869
+ await this.#ensureHydrated();
870
+ await this.#drainWrites();
839
871
  const keep = Math.max(1, Math.min(REQUEST_RETENTION, Number(retention) || REQUEST_RETENTION));
840
- const before = readTraceSummaries(this.#rootDir).length;
841
- this.#pruneAll(keep);
842
- const after = readTraceSummaries(this.#rootDir).length;
872
+ const before = this.#traceSummaries().length;
873
+ await this.#pruneAll(keep);
874
+ const after = this.#traceSummaries().length;
843
875
  return { deletedTurns: Math.max(0, before - after), deletedTools: 0, deletedEvents: 0, deletedRequests: Math.max(0, before - after) };
844
876
  }
845
877
 
846
- compact() {
847
- this.#flushPendingSync();
848
- const before = countDirFiles(this.#rootDir).bytes;
849
- this.cleanup(REQUEST_RETENTION);
850
- const after = countDirFiles(this.#rootDir).bytes;
878
+ async compact() {
879
+ await this.#ensureHydrated();
880
+ const before = (await countDirFiles(this.#rootDir)).bytes;
881
+ await this.cleanup(REQUEST_RETENTION);
882
+ const after = (await countDirFiles(this.#rootDir)).bytes;
851
883
  return { before, after };
852
884
  }
853
885
 
854
- purge() {
855
- this.#flushPendingSync();
856
- try { rmSync(this.#rootDir, { recursive: true, force: true }); }
886
+ async purge() {
887
+ await this.#drainWrites();
888
+ try { await fsp.rm(this.#rootDir, { recursive: true, force: true }); }
857
889
  catch { /* ignore */ }
858
- ensureDir(this.#rootDir);
890
+ await ensureDir(this.#rootDir);
859
891
  this.#turnIndex.clear();
860
892
  this.#requestCache.clear();
893
+ this.#events = [];
894
+ this.#hydrated = false;
895
+ this.#hydratePromise = null;
896
+ this.#eventsHydrated = false;
861
897
  }
862
898
 
863
- close() { this.#flushPendingSync(); }
899
+ async close() {
900
+ // #drainWrites() flushes pending writes itself; no separate #flushPending().
901
+ await this.#drainWrites();
902
+ }
903
+
904
+ /**
905
+ * Force all buffered trace + event writes to disk and await them. Unlike
906
+ * close() this does not tear down state, so the instance keeps accepting
907
+ * writes. Useful when a caller needs durability at a checkpoint.
908
+ */
909
+ async flush() {
910
+ await this.#drainWrites();
911
+ }
864
912
 
865
913
  #getOrCreateRequest({ traceId, turnNumber, messageId, mode, sessionId, vpId, threadId, userPrompt, now, turnRowId }) {
866
914
  const normalizedSessionId = sessionId || null;
867
- let all = null;
868
915
  const isUsableExisting = (t) => (
869
916
  t?.sessionId === normalizedSessionId
870
917
  && t?.traceId === traceId
871
918
  && !(turnNumber === 1 && (t.loops || []).some(l => l.loopNumber === 1))
872
919
  );
873
- const newestTrace = (items) => items
920
+ // Cache-only: the write path must NEVER touch disk (that was the O(N^2)
921
+ // event-loop stall). Every trace created in this process lives in
922
+ // #requestCache; same-traceId reuse and the legacy-duplicate-Loop-1 split
923
+ // both only concern traces opened in THIS run.
924
+ const sameSession = Array.from(this.#requestCache.values())
925
+ .filter(t => (t?.sessionId || null) === normalizedSessionId);
926
+ const existing = sameSession
874
927
  .filter(isUsableExisting)
875
928
  .sort((a, b) => ((a.openedAt || 0) - (b.openedAt || 0)) || String(a.requestKey || '').localeCompare(String(b.requestKey || '')))
876
929
  .at(-1) || null;
877
- let existing = newestTrace(Array.from(this.#requestCache.values()));
878
- if (!existing) {
879
- all = this.#traceSummaries(normalizedSessionId).map(({ trace }) => trace);
880
- existing = newestTrace(all);
881
- }
882
930
  if (existing) {
883
931
  existing.updatedAt = now;
884
932
  this.#requestCache.set(existing.requestKey, existing);
@@ -886,7 +934,7 @@ export class DebugTrace {
886
934
  }
887
935
  const seq = (this.#sequence = (this.#sequence + 1) % 1_000_000);
888
936
  const requestKey = `${String(now).padStart(13, '0')}-${String(seq).padStart(6, '0')}-${safeDirComponent(traceId || turnRowId, 'request')}-${turnRowId.slice(0, 8)}`;
889
- const requestId = turnNumber === 1 && all.some(t => t.traceId === traceId) ? turnRowId : traceId;
937
+ const requestId = turnNumber === 1 && sameSession.some(t => t.traceId === traceId) ? turnRowId : traceId;
890
938
  const trace = {
891
939
  version: TRACE_VERSION,
892
940
  requestKey,
@@ -911,27 +959,80 @@ export class DebugTrace {
911
959
  }
912
960
 
913
961
  #loadRequest(sessionId, requestKey) {
914
- const cached = this.#requestCache.get(requestKey);
915
- if (cached) return cached;
916
- const file = tracePathFor(this.#rootDir, sessionId, requestKey);
917
- const trace = readJson(file);
918
- if (trace) this.#requestCache.set(requestKey, trace);
919
- return trace;
962
+ // Cache-only by design: endTurn/logTool always run in the same turn that
963
+ // startTurn minted the trace, so it is already resident. Never read disk
964
+ // on the write path — that is the stall we are eliminating.
965
+ return this.#requestCache.get(requestKey) || null;
920
966
  }
921
967
 
922
968
  #traceSummaries(sessionId = null) {
923
- const byKey = new Map(readTraceSummaries(this.#rootDir, sessionId).map(item => [item.trace.requestKey, item]));
969
+ // Cache-only: #ensureHydrated() has already merged every on-disk trace
970
+ // into #requestCache exactly once, so we never touch disk here. This is
971
+ // what turns the old O(N^2) per-query rescan into an in-memory filter.
972
+ const out = [];
924
973
  for (const trace of this.#requestCache.values()) {
925
974
  if (sessionId && trace.sessionId !== sessionId) continue;
926
975
  if (!trace?.requestId || !trace?.requestKey) continue;
927
- byKey.set(trace.requestKey, {
976
+ out.push({
928
977
  trace,
929
978
  file: this.#traceFile(trace),
930
979
  openedAt: Number(trace.openedAt || 0),
931
980
  });
932
981
  }
933
- return Array.from(byKey.values())
934
- .sort((a, b) => ((a.openedAt || 0) - (b.openedAt || 0)) || String(a.trace?.requestKey || a.file).localeCompare(String(b.trace?.requestKey || b.file)));
982
+ return out.sort((a, b) => ((a.openedAt || 0) - (b.openedAt || 0)) || String(a.trace?.requestKey || a.file).localeCompare(String(b.trace?.requestKey || b.file)));
983
+ }
984
+
985
+ /**
986
+ * Load every on-disk trace into #requestCache exactly once. Live entries
987
+ * (created by this process) always win over their disk copy — a flush may
988
+ * be mid-flight, and the in-memory trace is the newer truth. Idempotent and
989
+ * concurrency-safe: overlapping callers await the same promise.
990
+ *
991
+ * Footprint trade-off (deliberate): this pins every session's full trace
992
+ * payload (systemPrompt + cumulative messages + rawRequest, each already
993
+ * byte-bounded) in #requestCache for the instance's lifetime — O(retention ×
994
+ * total sessions) memory instead of the old O(N²) per-query CPU rescan that
995
+ * stalled the event loop. Retention (10/session) and the byte caps keep it
996
+ * bounded, and the web path uses a single module-level instance. Follow-up if
997
+ * footprint ever bites: hydrate per queried sessionId, or keep only a
998
+ * lightweight summary resident and lazy-load full payloads on detailTurnId.
999
+ */
1000
+ async #ensureHydrated() {
1001
+ if (this.#hydrated) return;
1002
+ if (this.#hydratePromise) return this.#hydratePromise;
1003
+ this.#hydratePromise = (async () => {
1004
+ const [summaries, storedEvents] = await Promise.all([
1005
+ readTraceSummaries(this.#rootDir),
1006
+ readJson(join(this.#rootDir, 'events.json')),
1007
+ ]);
1008
+ for (const { trace } of summaries) {
1009
+ if (!trace?.requestKey) continue;
1010
+ // Never clobber a live in-memory trace with its older disk snapshot.
1011
+ if (!this.#requestCache.has(trace.requestKey)) {
1012
+ this.#requestCache.set(trace.requestKey, trace);
1013
+ }
1014
+ }
1015
+ if (Array.isArray(storedEvents)) this.#mergeStoredEvents(storedEvents);
1016
+ this.#hydrated = true;
1017
+ this.#hydratePromise = null;
1018
+ })();
1019
+ return this.#hydratePromise;
1020
+ }
1021
+
1022
+ /**
1023
+ * Fold on-disk events into the in-memory ring by id, preserving live order
1024
+ * and bounding to MAX_DREAM_EVENTS. Unseen disk records are prepended (they
1025
+ * are older); a live append therefore never erases prior-run history. Sets
1026
+ * #eventsHydrated so this runs at most once.
1027
+ */
1028
+ #mergeStoredEvents(stored) {
1029
+ if (this.#eventsHydrated) return;
1030
+ this.#eventsHydrated = true;
1031
+ if (!Array.isArray(stored) || stored.length === 0) return;
1032
+ const seen = new Set(this.#events.map(e => e?.id).filter(Boolean));
1033
+ const older = stored.filter(e => e && (!e.id || !seen.has(e.id)));
1034
+ if (older.length === 0) return;
1035
+ this.#events = [...older, ...this.#events].slice(-MAX_DREAM_EVENTS);
935
1036
  }
936
1037
 
937
1038
  #traceWriteKey(trace) {
@@ -960,7 +1061,7 @@ export class DebugTrace {
960
1061
  this.#requestCache.set(trace.requestKey, trace);
961
1062
 
962
1063
  if (force || item.dirtyLoops >= TRACE_FLUSH_DIRTY_LOOPS) {
963
- this.#flushPendingSync();
1064
+ this.#flushPending();
964
1065
  return;
965
1066
  }
966
1067
  this.#scheduleFlushTimer(now);
@@ -972,27 +1073,108 @@ export class DebugTrace {
972
1073
  const dueIn = Math.max(0, TRACE_FLUSH_INTERVAL_MS - (now - oldestDirtyAt));
973
1074
  this.#flushTimer = setTimeout(() => {
974
1075
  this.#flushTimer = null;
975
- this.#flushPendingSync();
1076
+ this.#flushPending();
976
1077
  }, dueIn);
977
1078
  if (typeof this.#flushTimer.unref === 'function') this.#flushTimer.unref();
978
1079
  }
979
1080
 
980
- #flushPendingSync() {
1081
+ #scheduleEventFlush() {
1082
+ this.#eventsDirty = true;
1083
+ if (this.#eventFlushTimer) return;
1084
+ this.#eventFlushTimer = setTimeout(() => {
1085
+ this.#eventFlushTimer = null;
1086
+ this.#flushEvents();
1087
+ }, TRACE_FLUSH_INTERVAL_MS);
1088
+ if (typeof this.#eventFlushTimer.unref === 'function') this.#eventFlushTimer.unref();
1089
+ }
1090
+
1091
+ /**
1092
+ * Drain pending trace writes synchronously-from-the-caller's-view: snapshot
1093
+ * each dirty trace to a JSON STRING now (so later in-place mutation of the
1094
+ * live `loops` array can't tear the payload), then chain the async writes
1095
+ * onto #flushChain. Returns nothing; callers that need durability await
1096
+ * #drainWrites().
1097
+ */
1098
+ #flushPending() {
981
1099
  if (this.#flushTimer) {
982
1100
  clearTimeout(this.#flushTimer);
983
1101
  this.#flushTimer = null;
984
1102
  }
985
1103
  const entries = Array.from(this.#pendingWrites.values());
986
- if (entries.length === 0) return;
1104
+ if (entries.length === 0) {
1105
+ this.#flushEvents();
1106
+ return;
1107
+ }
987
1108
  this.#pendingWrites.clear();
988
- for (const { trace } of entries) {
989
- try {
990
- atomicWriteJson(this.#traceFile(trace), this.#serializableTrace(trace));
991
- } catch (err) {
992
- console.warn('[Yeaft] debug trace write failed:', err?.message || err);
1109
+ // Capture point-in-time payloads synchronously (so later in-place mutation
1110
+ // of the live `loops` array can't tear a payload), but remember each
1111
+ // request key: a prune chained ahead of this write may evict the request
1112
+ // before the write runs, and we must NOT resurrect a pruned trace on disk.
1113
+ const jobs = entries.map(({ trace }) => ({
1114
+ requestKey: trace.requestKey,
1115
+ file: this.#traceFile(trace),
1116
+ text: JSON.stringify(this.#serializableTrace(trace)),
1117
+ }));
1118
+ this.#chain(async () => {
1119
+ for (const { requestKey, file, text } of jobs) {
1120
+ // Skip if an earlier chained prune already evicted this request.
1121
+ if (!this.#requestCache.has(requestKey)) continue;
1122
+ try { await atomicWriteText(file, text); }
1123
+ catch (err) { console.warn('[Yeaft] debug trace write failed:', err?.message || err); }
993
1124
  }
1125
+ try { await this.#pruneAll(REQUEST_RETENTION); }
1126
+ catch (err) { console.warn('[Yeaft] debug trace prune failed:', err?.message || err); }
1127
+ });
1128
+ this.#flushEvents();
1129
+ }
1130
+
1131
+ /**
1132
+ * Append `task` to the single-writer flush mutex. Each link is isolated:
1133
+ * a rejected predecessor can NEVER skip a later task (the classic
1134
+ * `chain = chain.then(task)` footgun, where one rejection poisons every
1135
+ * subsequent `.then(onFulfilled)` and silently stops all future writes).
1136
+ * Here we always `await prev` inside a swallowing try/catch before running
1137
+ * `task`, so the chain stays alive for the instance's lifetime.
1138
+ */
1139
+ #chain(task) {
1140
+ const prev = this.#flushChain;
1141
+ this.#flushChain = (async () => {
1142
+ try { await prev; } catch { /* predecessor errors already logged */ }
1143
+ await task();
1144
+ })();
1145
+ return this.#flushChain;
1146
+ }
1147
+
1148
+ #flushEvents() {
1149
+ if (!this.#eventsDirty) return;
1150
+ this.#eventsDirty = false;
1151
+ if (this.#eventFlushTimer) {
1152
+ clearTimeout(this.#eventFlushTimer);
1153
+ this.#eventFlushTimer = null;
1154
+ }
1155
+ const file = join(this.#rootDir, 'events.json');
1156
+ this.#chain(async () => {
1157
+ // Fold in any prior-run events we haven't loaded yet BEFORE overwriting,
1158
+ // so a write that beats #ensureHydrated can't drop cross-restart history.
1159
+ if (!this.#eventsHydrated) {
1160
+ this.#mergeStoredEvents(await readJson(file));
1161
+ }
1162
+ const text = JSON.stringify(this.#events.slice(-MAX_DREAM_EVENTS));
1163
+ try { await atomicWriteText(file, text); }
1164
+ catch (err) { console.warn('[Yeaft] debug trace event write failed:', err?.message || err); }
1165
+ });
1166
+ }
1167
+
1168
+ /** Await every scheduled write (and prune) to settle. Used by close()/queries. */
1169
+ async #drainWrites() {
1170
+ this.#flushPending();
1171
+ let prev = null;
1172
+ // The chain can grow while we await (a query's flush appends more); loop
1173
+ // until it stabilises so close() is a true barrier.
1174
+ while (this.#flushChain !== prev) {
1175
+ prev = this.#flushChain;
1176
+ try { await prev; } catch { /* per-write errors already logged */ }
994
1177
  }
995
- this.#pruneAll(REQUEST_RETENTION);
996
1178
  }
997
1179
 
998
1180
  #reconstructLastSnapshot(trace) {
@@ -1006,8 +1188,8 @@ export class DebugTrace {
1006
1188
  #readDreamEvents({ sessionId = null, dreamLimit = 5 } = {}) {
1007
1189
  const limit = Number.isFinite(Number(dreamLimit)) ? Math.max(0, Math.min(50, Number(dreamLimit))) : 5;
1008
1190
  if (limit <= 0) return [];
1009
- const storedEvents = readJson(join(this.#rootDir, 'events.json'));
1010
- const events = Array.isArray(storedEvents) ? storedEvents : [];
1191
+ // In-memory ring (hydrated once); no disk read on the query path.
1192
+ const events = this.#events;
1011
1193
  const out = [];
1012
1194
  for (const event of events.slice().reverse()) {
1013
1195
  const data = isPlainObject(event.eventData) ? event.eventData : {};
@@ -1029,22 +1211,25 @@ export class DebugTrace {
1029
1211
  return out.reverse();
1030
1212
  }
1031
1213
 
1032
- #pruneAll(keep) {
1214
+ async #pruneAll(keep) {
1215
+ // Cache-only enumeration; rm the pruned request dirs async. No disk scan.
1033
1216
  const sessions = new Set([null]);
1034
- for (const { trace } of this.#traceSummaries()) sessions.add(trace.sessionId || null);
1035
- for (const sid of sessions) this.#pruneSession(sid, keep);
1217
+ for (const trace of this.#requestCache.values()) sessions.add(trace.sessionId || null);
1218
+ for (const sid of sessions) await this.#pruneSession(sid, keep);
1036
1219
  }
1037
1220
 
1038
- #pruneSession(sessionId, keep = REQUEST_RETENTION) {
1221
+ async #pruneSession(sessionId, keep = REQUEST_RETENTION) {
1039
1222
  const traces = this.#traceSummaries(sessionId);
1040
1223
  const activeCutoff = Date.now() - 6 * 60 * 60 * 1000;
1041
1224
  const protectedItems = traces.filter(item => item.trace?.active && Number(item.trace?.updatedAt || 0) >= activeCutoff);
1042
1225
  const pruneCandidates = traces.filter(item => !protectedItems.includes(item));
1043
1226
  const stale = pruneCandidates.slice(0, Math.max(0, traces.length - protectedItems.length - keep));
1044
1227
  for (const item of stale) {
1045
- try { rmSync(dirname(item.file), { recursive: true, force: true }); }
1046
- catch { /* ignore */ }
1228
+ // Drop from cache first so subsequent queries never resurface it, even
1229
+ // if the async rm is still in flight or fails.
1047
1230
  this.#requestCache.delete(item.trace.requestKey);
1231
+ try { await fsp.rm(dirname(item.file), { recursive: true, force: true }); }
1232
+ catch { /* ignore */ }
1048
1233
  }
1049
1234
  }
1050
1235
 
@@ -1066,17 +1251,18 @@ export class NullTrace {
1066
1251
  logTool() { return 'null'; }
1067
1252
  logEvent() { return 'null'; }
1068
1253
  event() { return 'null'; }
1069
- queryByMessage() { return { turns: [], tools: [], events: [] }; }
1070
- queryByTrace() { return { turns: [], tools: [], events: [] }; }
1071
- queryRecent() { return []; }
1072
- queryTools() { return []; }
1073
- search() { return []; }
1074
- stats() { return { turnCount: 0, toolCount: 0, eventCount: 0, dbSizeBytes: 0, fileSizeBytes: 0, requestCount: 0 }; }
1075
- cleanup() { return { deletedTurns: 0, deletedTools: 0, deletedEvents: 0, deletedRequests: 0 }; }
1076
- compact() { return { before: 0, after: 0 }; }
1077
- purge() {}
1078
- close() {}
1079
- fetchRecentDebugHistory() { return { loops: [], turns: [], dreamEvents: [] }; }
1254
+ async queryByMessage() { return { turns: [], tools: [], events: [] }; }
1255
+ async queryByTrace() { return { turns: [], tools: [], events: [] }; }
1256
+ async queryRecent() { return []; }
1257
+ async queryTools() { return []; }
1258
+ async search() { return []; }
1259
+ async stats() { return { turnCount: 0, toolCount: 0, eventCount: 0, dbSizeBytes: 0, fileSizeBytes: 0, requestCount: 0 }; }
1260
+ async cleanup() { return { deletedTurns: 0, deletedTools: 0, deletedEvents: 0, deletedRequests: 0 }; }
1261
+ async compact() { return { before: 0, after: 0 }; }
1262
+ async purge() {}
1263
+ async close() {}
1264
+ async flush() {}
1265
+ async fetchRecentDebugHistory() { return { loops: [], turns: [], dreamEvents: [] }; }
1080
1266
  }
1081
1267
 
1082
1268
  export function createTrace({ enabled, dbPath, dirPath }) {
package/yeaft/engine.js CHANGED
@@ -96,7 +96,10 @@ const RETRY_DEFAULTS = Object.freeze({
96
96
  jitterRatio: 0.25,
97
97
  });
98
98
 
99
- const EXPLICIT_SKILL_COMMAND_RE = /^\/skill:([^\s]+)(\s+|$)/;
99
+ // Accept the Yeaft-native alias and the Claude Code plugin-style command.
100
+ // Autocomplete prefers /yeaft-skills:<name>; /skill:<name> remains supported
101
+ // for older clients and typed history.
102
+ const EXPLICIT_SKILL_COMMAND_RE = /^\/(?:skill|yeaft-skills):([^\s]+)(\s+|$)/;
100
103
 
101
104
  function parseExplicitSkillCommand(prompt) {
102
105
  if (typeof prompt !== 'string') {
package/yeaft/session.js CHANGED
@@ -229,10 +229,12 @@ export async function loadSession(options = {}) {
229
229
  dirPath: yeaftDir,
230
230
  });
231
231
  // Hard cap debug history to the most recent 10 requests per Session. Trace
232
- // failures are best-effort and must never stop the agent loop.
233
- try { trace.cleanup?.(10); } catch (err) {
232
+ // failures are best-effort and must never stop the agent loop. cleanup() is
233
+ // now async (it hydrates the in-memory index, then prunes); run it
234
+ // fire-and-forget so session load never blocks on the startup disk scan.
235
+ Promise.resolve(trace.cleanup?.(10)).catch((err) => {
234
236
  console.warn('[Yeaft] trace.cleanup failed:', err?.message || err);
235
- }
237
+ });
236
238
 
237
239
  // ─── 4. Create LLM adapter ────────────────────────────
238
240
  const adapter = await createLLMAdapter(config);
@@ -562,7 +564,7 @@ export async function loadSession(options = {}) {
562
564
  // Best-effort cleanup
563
565
  }
564
566
  try {
565
- trace.close();
567
+ await trace.close();
566
568
  } catch {
567
569
  // Trace might not have close() (NullTrace)
568
570
  }
@@ -266,15 +266,18 @@ export function buildRelevantScopes({ sessionId, chatId, vpId, extra } = {}) {
266
266
  scopes.push(`chat/${chatId}`);
267
267
  if (vpId) scopes.push(`chat/${chatId}/vp/${vpId}`);
268
268
  } else if (sessionId) {
269
- // Read both legacy `group/<id>` and new `session/<id>` scopes so recall
270
- // works regardless of which path the writers used for a given session.
271
- // (Writers still emit `group/<id>` for back-compat; migration may have
272
- // rewritten on-disk data to `session/<id>`.)
269
+ // Read every persisted Session spelling that can exist on disk. Current
270
+ // Dream writers use `sessions/<id>`; older readers/writers used singular
271
+ // `session/<id>` or legacy `group/<id>`. SQLite FTS is scope-filtered, so
272
+ // omitting any spelling makes valid indexed memory invisible.
273
+ scopes.push(`sessions/${sessionId}`);
274
+ scopes.push(`sessions/${sessionId}/user`);
273
275
  scopes.push(`session/${sessionId}`);
274
276
  scopes.push(`session/${sessionId}/user`);
275
277
  scopes.push(`group/${sessionId}`);
276
278
  scopes.push(`group/${sessionId}/user`);
277
279
  if (vpId) {
280
+ scopes.push(`sessions/${sessionId}/vp/${vpId}`);
278
281
  scopes.push(`session/${sessionId}/vp/${vpId}`);
279
282
  scopes.push(`group/${sessionId}/vp/${vpId}`);
280
283
  }
@@ -76,7 +76,8 @@ import { getAgentRegistry, agentBelongsToScope } from './tools/agent.js';
76
76
  import { isPromptableAgentStatus } from './sub-agent/status.js';
77
77
  import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
78
78
 
79
- const SKILL_COMMAND_PREFIX = 'skill:';
79
+ const LEGACY_SKILL_COMMAND_PREFIX = 'skill:';
80
+ const YEAFT_SKILL_COMMAND_PREFIX = 'yeaft-skills:';
80
81
 
81
82
  /** @type {import('./session.js').Session | null} */
82
83
  let session = null;
@@ -1854,9 +1855,15 @@ export function buildSkillSlashCommands(skillManager) {
1854
1855
  const descriptions = {};
1855
1856
  for (const skill of skillManager.list()) {
1856
1857
  if (!skill?.name || typeof skill.name !== 'string') continue;
1857
- const commandName = `${SKILL_COMMAND_PREFIX}${skill.name}`;
1858
+ const commandName = `${YEAFT_SKILL_COMMAND_PREFIX}${skill.name}`;
1858
1859
  commands.push(commandName);
1859
1860
  descriptions[commandName] = skill.description || skill.trigger || 'Load Yeaft skill';
1861
+
1862
+ // Legacy alias for older typed commands and persisted drafts. Do not add it
1863
+ // to the visible command list; autocomplete should show the same plugin-style
1864
+ // names users see in Claude Code, e.g. /yeaft-skills:code-review.
1865
+ const legacyCommandName = `${LEGACY_SKILL_COMMAND_PREFIX}${skill.name}`;
1866
+ descriptions[legacyCommandName] = descriptions[commandName];
1860
1867
  }
1861
1868
  commands.sort((a, b) => a.localeCompare(b));
1862
1869
  return { commands, descriptions };
@@ -1876,12 +1883,13 @@ export function buildMergedSkillSlashCommands(skillManagers = []) {
1876
1883
  function broadcastSkillSlashCommands(sessionLike, extraSkillManagers = []) {
1877
1884
  const managers = [sessionLike?.skillManager, ...extraSkillManagers].filter(Boolean);
1878
1885
  const { commands, descriptions } = buildMergedSkillSlashCommands(managers);
1879
- const nonSkillCommands = (ctx.slashCommands || [])
1880
- .filter(cmd => !(typeof cmd === 'string' && cmd.startsWith(SKILL_COMMAND_PREFIX)));
1886
+ const isYeaftSkillCommand = (cmd) => typeof cmd === 'string'
1887
+ && (cmd.startsWith(LEGACY_SKILL_COMMAND_PREFIX) || cmd.startsWith(YEAFT_SKILL_COMMAND_PREFIX));
1888
+ const nonSkillCommands = (ctx.slashCommands || []).filter(cmd => !isYeaftSkillCommand(cmd));
1881
1889
  const slashCommands = [...new Set([...nonSkillCommands, ...commands])];
1882
1890
  const slashCommandDescriptions = Object.fromEntries(
1883
1891
  Object.entries(ctx.slashCommandDescriptions || {})
1884
- .filter(([cmd]) => !(typeof cmd === 'string' && cmd.startsWith(SKILL_COMMAND_PREFIX)))
1892
+ .filter(([cmd]) => !isYeaftSkillCommand(cmd))
1885
1893
  );
1886
1894
  Object.assign(slashCommandDescriptions, descriptions);
1887
1895
  ctx.slashCommands = slashCommands;
@@ -4813,6 +4821,12 @@ export function __testAppendTurnToSessionHistory(...args) {
4813
4821
  * Backwards-compat: when neither field is set, defaults to `vpId='default'`
4814
4822
  * which matches the pre-v0.1.754 behavior.
4815
4823
  */
4824
+ function resolveDreamTriggerSessionId(msg = {}) {
4825
+ return typeof msg.sessionId === 'string' && msg.sessionId
4826
+ ? msg.sessionId
4827
+ : (typeof msg.groupId === 'string' && msg.groupId ? msg.groupId : null);
4828
+ }
4829
+
4816
4830
  export function normalizeDreamResult(result) {
4817
4831
  const sessions = Array.isArray(result?.sessions) ? result.sessions : [];
4818
4832
  const targets = Array.isArray(result?.targets) ? result.targets : [];
@@ -4861,7 +4875,7 @@ export async function handleYeaftDreamTrigger(msg = {}) {
4861
4875
  // route the error event back to the right row and the per-group
4862
4876
  // "Run dream now" button would stay stuck on "Running…" forever
4863
4877
  // (review feedback from PR #757).
4864
- const sessionId = typeof msg.sessionId === 'string' && msg.sessionId ? msg.sessionId : null;
4878
+ const sessionId = resolveDreamTriggerSessionId(msg);
4865
4879
  const vpId = !sessionId ? (msg.vpId || 'default') : null;
4866
4880
  const tag = sessionId ? { sessionId } : { vpId };
4867
4881
 
@@ -5069,7 +5083,7 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
5069
5083
  let hasMore = false;
5070
5084
  try {
5071
5085
  if (session?.trace && typeof session.trace.fetchRecentDebugHistory === 'function') {
5072
- const out = session.trace.fetchRecentDebugHistory({ limit, dreamLimit, sessionId, threadId, indexOnly, detailTurnId, search });
5086
+ const out = await session.trace.fetchRecentDebugHistory({ limit, dreamLimit, sessionId, threadId, indexOnly, detailTurnId, search });
5073
5087
  loops = Array.isArray(out?.loops) ? out.loops : [];
5074
5088
  turns = Array.isArray(out?.turns) ? out.turns : [];
5075
5089
  dreamEvents = Array.isArray(out?.dreamEvents) ? out.dreamEvents : [];
@@ -5916,6 +5930,7 @@ export const __testHooks = {
5916
5930
  return getVpStatusBroker().transition(status);
5917
5931
  },
5918
5932
  decorateSessionsWithRuntimeState,
5933
+ resolveDreamTriggerSessionId,
5919
5934
  async loadProjectRuntime(workDir) {
5920
5935
  return loadProjectRuntime(workDir);
5921
5936
  },