@cereworker/core 26.403.2 → 26.404.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -89,6 +89,7 @@ export class Orchestrator extends TypedEventEmitter {
89
89
  streamPhase = 'idle';
90
90
  activeToolCall = null;
91
91
  currentStreamTurn = null;
92
+ currentQuerySession = null;
92
93
  currentAttemptCompletionState = null;
93
94
  currentPartialContent = '';
94
95
  currentLastContentKind = 'empty';
@@ -377,6 +378,8 @@ export class Orchestrator extends TypedEventEmitter {
377
378
  conversationId: options?.conversationId,
378
379
  sessionKey: options?.sessionKey,
379
380
  scopeKey: options?.scopeKey,
381
+ turnId: options?.turnId,
382
+ attempt: options?.attempt,
380
383
  });
381
384
  }
382
385
  unregisterTool(name) {
@@ -613,6 +616,67 @@ export class Orchestrator extends TypedEventEmitter {
613
616
  return [];
614
617
  return this.conversations.getMessages(id);
615
618
  }
619
+ getQuerySession(conversationId, sessionId) {
620
+ return this.conversations.getQuerySession(conversationId, sessionId);
621
+ }
622
+ getSessionEvents(conversationId, sessionId) {
623
+ return this.conversations.getSessionEvents(conversationId, sessionId);
624
+ }
625
+ recordSessionEvent(conversationId, sessionId, type, summary, data) {
626
+ const session = this.conversations.getQuerySession(conversationId, sessionId);
627
+ if (!session)
628
+ return;
629
+ const updatedAt = Date.now();
630
+ const updatedSession = {
631
+ ...session,
632
+ updatedAt,
633
+ summary: this.truncateResumeText(summary, 500),
634
+ };
635
+ this.conversations.saveQuerySession(conversationId, updatedSession);
636
+ const event = {
637
+ sessionId,
638
+ conversationId,
639
+ turnId: session.turnId,
640
+ attempt: session.attempt,
641
+ timestamp: updatedAt,
642
+ type,
643
+ state: updatedSession.state,
644
+ summary: this.truncateResumeText(summary, 500),
645
+ instanceId: updatedSession.instanceId,
646
+ checkpointPath: updatedSession.checkpointPath ?? null,
647
+ data,
648
+ };
649
+ this.conversations.appendSessionEvent(conversationId, sessionId, event);
650
+ }
651
+ recordSessionMemoryUpdate(conversationId, sessionId, snapshot) {
652
+ const session = this.conversations.getQuerySession(conversationId, sessionId);
653
+ if (!session)
654
+ return;
655
+ const updated = {
656
+ ...session,
657
+ memory: snapshot,
658
+ updatedAt: Date.now(),
659
+ summary: snapshot.summary || session.summary,
660
+ };
661
+ this.conversations.saveQuerySession(conversationId, updated);
662
+ const event = {
663
+ sessionId,
664
+ conversationId,
665
+ turnId: session.turnId,
666
+ attempt: session.attempt,
667
+ timestamp: snapshot.updatedAt,
668
+ type: 'memory_updated',
669
+ state: updated.state,
670
+ summary: this.truncateResumeText(`Session memory updated: ${snapshot.summary}`, 500),
671
+ instanceId: updated.instanceId,
672
+ checkpointPath: updated.checkpointPath ?? null,
673
+ data: {
674
+ excerpt: snapshot.excerpt,
675
+ },
676
+ };
677
+ this.conversations.appendSessionEvent(conversationId, sessionId, event);
678
+ this.emit({ type: 'session:memory-updated', conversationId, sessionId, snapshot });
679
+ }
616
680
  startConversation() {
617
681
  const conversation = this.conversations.create();
618
682
  this.activeConversationId = conversation.id;
@@ -749,6 +813,105 @@ export class Orchestrator extends TypedEventEmitter {
749
813
  this.currentLastContentKind = 'empty';
750
814
  this.currentJournaledContentLength = 0;
751
815
  }
816
+ createQuerySession(conversationId, turnId, attempt, source, latestUserMessage, stallRetryCount, completionRetryCount, priorSession) {
817
+ const timestamp = Date.now();
818
+ const instance = this.instanceStore?.get();
819
+ return {
820
+ id: turnId,
821
+ conversationId,
822
+ turnId,
823
+ attempt,
824
+ source,
825
+ state: 'ready',
826
+ startedAt: timestamp,
827
+ updatedAt: timestamp,
828
+ summary: `Turn attempt ${attempt} started.`,
829
+ latestUserMessage: this.truncateResumeText(latestUserMessage, 1200),
830
+ stallRetryCount,
831
+ completionRetryCount,
832
+ instanceId: instance?.id,
833
+ checkpointPath: instance?.activeCheckpoint ?? null,
834
+ // Carry forward recovery state from prior attempt for crash recovery
835
+ ...(priorSession?.latestBoundary ? { latestBoundary: priorSession.latestBoundary } : {}),
836
+ ...(priorSession?.lastOutcome ? { lastOutcome: priorSession.lastOutcome } : {}),
837
+ ...(priorSession?.lastError ? { lastError: priorSession.lastError } : {}),
838
+ };
839
+ }
840
+ saveCurrentQuerySession() {
841
+ if (!this.currentStreamTurn || !this.currentQuerySession)
842
+ return;
843
+ this.conversations.saveQuerySession(this.currentStreamTurn.conversationId, this.currentQuerySession);
844
+ }
845
+ updateCurrentQuerySession(type, summary, data) {
846
+ if (!this.currentStreamTurn || !this.currentQuerySession)
847
+ return;
848
+ const eventState = this.getQuerySessionState(type, data);
849
+ const updatedAt = Date.now();
850
+ const next = {
851
+ ...this.currentQuerySession,
852
+ attempt: this.currentStreamTurn.attempt,
853
+ updatedAt,
854
+ summary: this.truncateResumeText(summary, 500),
855
+ state: this.resolveQuerySessionState(type, eventState, data),
856
+ checkpointPath: this.instanceStore?.get()?.activeCheckpoint ?? this.currentQuerySession.checkpointPath ?? null,
857
+ };
858
+ if (type === 'partial_text') {
859
+ const excerpt = typeof data?.excerpt === 'string' ? data.excerpt : summary;
860
+ next.latestAssistantMessage = this.truncateResumeText(excerpt, 1200);
861
+ }
862
+ if (type === 'tool_start') {
863
+ next.activeToolName = typeof data?.toolName === 'string' ? data.toolName : undefined;
864
+ next.activeToolCallId = typeof data?.callId === 'string' ? data.callId : undefined;
865
+ }
866
+ else if (type !== 'tool_end') {
867
+ next.activeToolName = undefined;
868
+ next.activeToolCallId = undefined;
869
+ }
870
+ if (type === 'tool_end') {
871
+ next.activeToolName = undefined;
872
+ next.activeToolCallId = undefined;
873
+ }
874
+ if (type === 'turn_finished') {
875
+ const finalContent = typeof data?.finalContent === 'string' ? data.finalContent.trim() : '';
876
+ if (finalContent) {
877
+ next.latestAssistantMessage = this.truncateResumeText(finalContent, 1200);
878
+ }
879
+ if (typeof data?.turnOutcome === 'string') {
880
+ next.lastOutcome = data.turnOutcome;
881
+ }
882
+ }
883
+ if (type === 'turn_error') {
884
+ next.lastError = typeof data?.error === 'string' ? data.error : summary;
885
+ if (data?.aborted === true) {
886
+ next.lastOutcome = 'aborted';
887
+ }
888
+ else {
889
+ next.lastOutcome = 'protocol_error';
890
+ }
891
+ }
892
+ if (type === 'completion_signal' && typeof data?.signal === 'string') {
893
+ next.summary = this.truncateResumeText(summary, 500);
894
+ }
895
+ this.currentQuerySession = next;
896
+ this.saveCurrentQuerySession();
897
+ }
898
+ resolveQuerySessionState(type, defaultState, data) {
899
+ if (type === 'turn_finished') {
900
+ const turnOutcome = data?.turnOutcome;
901
+ if (turnOutcome === 'completed' || turnOutcome === 'completed_no_text')
902
+ return 'completed';
903
+ if (turnOutcome === 'stalled')
904
+ return 'stalled';
905
+ if (turnOutcome === 'aborted')
906
+ return 'aborted';
907
+ if (turnOutcome === 'ended_on_tool_calls' || turnOutcome === 'completion_signal_missing') {
908
+ return 'waiting_followup';
909
+ }
910
+ if (turnOutcome === 'protocol_error')
911
+ return 'failed';
912
+ }
913
+ return defaultState;
914
+ }
752
915
  appendTurnJournalEntry(type, summary, data) {
753
916
  if (!this.currentStreamTurn)
754
917
  return;
@@ -761,6 +924,70 @@ export class Orchestrator extends TypedEventEmitter {
761
924
  ...(data ? { data } : {}),
762
925
  };
763
926
  this.conversations.appendTurnJournalEntry(this.currentStreamTurn.conversationId, this.currentStreamTurn.turnId, entry);
927
+ this.appendSessionEvent(type, entry.summary, data);
928
+ }
929
+ appendSessionEvent(type, summary, data) {
930
+ if (!this.currentStreamTurn)
931
+ return;
932
+ const instance = this.instanceStore?.get();
933
+ const event = {
934
+ sessionId: this.currentStreamTurn.sessionId,
935
+ conversationId: this.currentStreamTurn.conversationId,
936
+ turnId: this.currentStreamTurn.turnId,
937
+ attempt: this.currentStreamTurn.attempt,
938
+ timestamp: Date.now(),
939
+ type: this.mapJournalEntryToSessionEvent(type),
940
+ state: this.getQuerySessionState(type, data),
941
+ summary: this.truncateResumeText(summary, 500),
942
+ instanceId: instance?.id,
943
+ checkpointPath: instance?.activeCheckpoint ?? null,
944
+ ...(data ? { data } : {}),
945
+ };
946
+ this.conversations.appendSessionEvent(this.currentStreamTurn.conversationId, this.currentStreamTurn.sessionId, event);
947
+ this.updateCurrentQuerySession(type, summary, data);
948
+ }
949
+ mapJournalEntryToSessionEvent(type) {
950
+ switch (type) {
951
+ case 'tool_start':
952
+ return 'tool_started';
953
+ case 'tool_end':
954
+ return 'tool_finished';
955
+ case 'checkpoint':
956
+ return 'checkpoint_recorded';
957
+ case 'boundary':
958
+ return 'boundary_committed';
959
+ case 'completion_signal':
960
+ return 'completion_signal_recorded';
961
+ case 'recovery':
962
+ return 'recovery_assessed';
963
+ case 'turn_error':
964
+ return 'turn_failed';
965
+ default:
966
+ return type;
967
+ }
968
+ }
969
+ getQuerySessionState(type, data) {
970
+ switch (type) {
971
+ case 'turn_started':
972
+ return 'ready';
973
+ case 'partial_text':
974
+ return 'sampling';
975
+ case 'tool_start':
976
+ return 'tool_execution';
977
+ case 'tool_end':
978
+ case 'checkpoint':
979
+ case 'boundary':
980
+ case 'completion_signal':
981
+ return 'waiting_followup';
982
+ case 'recovery':
983
+ return data?.cause === 'stall' ? 'stalled' : 'waiting_followup';
984
+ case 'turn_finished':
985
+ return 'completed';
986
+ case 'turn_error':
987
+ return data?.aborted ? 'aborted' : 'failed';
988
+ default:
989
+ return 'waiting_followup';
990
+ }
764
991
  }
765
992
  pruneTurnJournals(conversationId) {
766
993
  try {
@@ -817,6 +1044,14 @@ export class Orchestrator extends TypedEventEmitter {
817
1044
  evidence: summary.evidence,
818
1045
  browserState: summary.browserState,
819
1046
  });
1047
+ if (this.currentQuerySession) {
1048
+ this.currentQuerySession = {
1049
+ ...this.currentQuerySession,
1050
+ latestBoundary: summary,
1051
+ updatedAt: Date.now(),
1052
+ };
1053
+ this.saveCurrentQuerySession();
1054
+ }
820
1055
  return summary;
821
1056
  }
822
1057
  deriveRepetitionSignals(continuity) {
@@ -1606,7 +1841,7 @@ export class Orchestrator extends TypedEventEmitter {
1606
1841
  }
1607
1842
  this.resetStreamState();
1608
1843
  }
1609
- async sendMessage(content, conversationId) {
1844
+ async sendMessage(content, conversationId, options) {
1610
1845
  if (!this.cerebrum)
1611
1846
  throw new Error('Cerebrum not connected');
1612
1847
  if (this.cerebellum && !this.cerebellum.isConnected()) {
@@ -1629,7 +1864,7 @@ export class Orchestrator extends TypedEventEmitter {
1629
1864
  let nextRetryContext = null;
1630
1865
  // Track message IDs from failed attempts so they can be excluded from retries and cleaned up.
1631
1866
  const failedAttemptMessageIds = [];
1632
- const turnId = nanoid(10);
1867
+ const turnId = options?.turnId ?? nanoid(10);
1633
1868
  const maxTotalAttempts = 1 + this.maxNudgeRetries + this.maxCompletionRetries;
1634
1869
  let loopTerminated = false;
1635
1870
  let nextRetryCause = null;
@@ -1647,11 +1882,27 @@ export class Orchestrator extends TypedEventEmitter {
1647
1882
  const isCurrentAttempt = () => this.abortController === abortController;
1648
1883
  this.abortController = abortController;
1649
1884
  this.currentAttemptCompletionState = completionState;
1885
+ const sessionSource = options?.source ?? 'local';
1650
1886
  this.currentStreamTurn = {
1651
1887
  turnId,
1652
1888
  attempt: attemptNumber,
1653
1889
  conversationId: convId,
1890
+ sessionId: turnId,
1891
+ source: sessionSource,
1654
1892
  };
1893
+ const priorSession = this.currentQuerySession;
1894
+ this.currentQuerySession = this.createQuerySession(convId, turnId, attemptNumber, sessionSource, latestUserMessage, this.streamNudgeCount, completionRetryCount, priorSession);
1895
+ this.saveCurrentQuerySession();
1896
+ if (attemptNumber === 1 && sessionSource === 'channel' && options?.ingress) {
1897
+ this.recordSessionEvent(convId, turnId, 'channel_ingress', `Received channel message from ${options.ingress.senderName || options.ingress.senderId || 'unknown sender'}.`, {
1898
+ channelId: options.ingress.channelId,
1899
+ senderId: options.ingress.senderId,
1900
+ senderName: options.ingress.senderName,
1901
+ threadId: options.ingress.threadId,
1902
+ replyToId: options.ingress.replyToId,
1903
+ timestamp: options.ingress.timestamp,
1904
+ });
1905
+ }
1655
1906
  this.currentPartialContent = '';
1656
1907
  this.currentLastContentKind = 'empty';
1657
1908
  this.currentJournaledContentLength = 0;
@@ -1670,7 +1921,13 @@ export class Orchestrator extends TypedEventEmitter {
1670
1921
  stallRetryCount: this.streamNudgeCount,
1671
1922
  completionRetryCount,
1672
1923
  });
1673
- this.emit({ type: 'message:cerebrum:start', conversationId: convId });
1924
+ this.emit({
1925
+ type: 'message:cerebrum:start',
1926
+ conversationId: convId,
1927
+ turnId,
1928
+ sessionId: turnId,
1929
+ source: sessionSource,
1930
+ });
1674
1931
  this.startStreamWatchdog(latestUserMessage);
1675
1932
  let messages = this.conversations.getMessages(convId);
1676
1933
  // On retry: exclude failed attempts' messages from history.
@@ -1795,8 +2052,10 @@ export class Orchestrator extends TypedEventEmitter {
1795
2052
  toolCall,
1796
2053
  tools: allTools,
1797
2054
  conversationId: convId,
1798
- sessionKey: 'agent:main',
2055
+ sessionKey: turnId,
1799
2056
  scopeKey: convId,
2057
+ turnId,
2058
+ attempt: attemptNumber,
1800
2059
  abortSignal: abortController.signal,
1801
2060
  });
1802
2061
  this.logStreamDebug('tool_callback_finished', this.buildToolDebugPayload(toolCall, result, toolName));
@@ -1839,6 +2098,16 @@ export class Orchestrator extends TypedEventEmitter {
1839
2098
  result.output += `\n[Cerebellum warning: ${failedChecks}]`;
1840
2099
  }
1841
2100
  if (verification) {
2101
+ result.metadata = {
2102
+ ...(result.metadata ?? {}),
2103
+ verification: {
2104
+ passed: verification.passed,
2105
+ modelVerdict: verification.modelVerdict,
2106
+ failedChecks: verification.checks
2107
+ .filter((check) => !check.passed)
2108
+ .map((check) => check.description),
2109
+ },
2110
+ };
1842
2111
  const vResult = {
1843
2112
  passed: verification.passed,
1844
2113
  checks: verification.checks,
@@ -1846,7 +2115,12 @@ export class Orchestrator extends TypedEventEmitter {
1846
2115
  toolCallId: toolCall.id,
1847
2116
  toolName,
1848
2117
  };
1849
- this.emit({ type: 'verification:end', result: vResult });
2118
+ this.emit({
2119
+ type: 'verification:end',
2120
+ result: vResult,
2121
+ conversationId: convId,
2122
+ sessionId: turnId,
2123
+ });
1850
2124
  }
1851
2125
  }
1852
2126
  catch {
@@ -1965,7 +2239,14 @@ export class Orchestrator extends TypedEventEmitter {
1965
2239
  }
1966
2240
  }
1967
2241
  const cerebrumMessage = this.conversations.appendMessage(convId, 'cerebrum', displayContent, visibleToolCalls?.length ? { toolCalls: visibleToolCalls } : undefined);
1968
- this.emit({ type: 'message:cerebrum:end', message: cerebrumMessage });
2242
+ this.emit({
2243
+ type: 'message:cerebrum:end',
2244
+ conversationId: convId,
2245
+ turnId,
2246
+ sessionId: turnId,
2247
+ source: sessionSource,
2248
+ message: cerebrumMessage,
2249
+ });
1969
2250
  log.info('stream_finished', {
1970
2251
  turnId,
1971
2252
  attempt: attemptNumber,
@@ -2049,6 +2330,12 @@ export class Orchestrator extends TypedEventEmitter {
2049
2330
  const diagnosticMessage = this.conversations.appendMessage(convId, 'system', assessment.operatorMessage);
2050
2331
  this.emit({ type: 'message:system', message: diagnosticMessage });
2051
2332
  this.emitCompletionTrace('retry_failed', assessment.diagnosis, completionSignal, 'error');
2333
+ this.appendTurnJournalEntry('turn_error', assessment.diagnosis || 'Recovery returned stop.', {
2334
+ retryCause: 'completion',
2335
+ completionRetryCount,
2336
+ stallRetryCount: this.streamNudgeCount,
2337
+ error: assessment.diagnosis || 'Recovery returned stop.',
2338
+ });
2052
2339
  this.emit({
2053
2340
  type: 'error',
2054
2341
  error: new Error(assessment.diagnosis ||
@@ -2076,6 +2363,12 @@ export class Orchestrator extends TypedEventEmitter {
2076
2363
  : '[System fallback] The turn ended repeatedly without a valid completion signal or final answer.');
2077
2364
  this.emit({ type: 'message:system', message: diagnosticMessage });
2078
2365
  this.emitCompletionTrace('retry_failed', `Completion retries exhausted after ${completionRetryCount}/${this.maxCompletionRetries}: ${assessment.diagnosis || completionFailure.message}`, completionSignal, 'error');
2366
+ this.appendTurnJournalEntry('turn_error', `Completion retries exhausted (${completionRetryCount}/${this.maxCompletionRetries}).`, {
2367
+ retryCause: 'completion',
2368
+ completionRetryCount,
2369
+ stallRetryCount: this.streamNudgeCount,
2370
+ error: assessment.diagnosis || 'Completion retries exhausted.',
2371
+ });
2079
2372
  this.emit({
2080
2373
  type: 'error',
2081
2374
  error: new Error(assessment.diagnosis ||
@@ -2118,6 +2411,12 @@ export class Orchestrator extends TypedEventEmitter {
2118
2411
  stallRecovery.assessment.action === 'stop') {
2119
2412
  const systemMessage = this.conversations.appendMessage(convId, 'system', stallRecovery.assessment.operatorMessage);
2120
2413
  this.emit({ type: 'message:system', message: systemMessage });
2414
+ this.appendTurnJournalEntry('turn_error', stallRecovery.assessment.diagnosis || 'Stall recovery returned stop.', {
2415
+ retryCause: 'stall',
2416
+ stallRetryCount: this.streamNudgeCount,
2417
+ completionRetryCount,
2418
+ error: stallRecovery.assessment.diagnosis || 'Stall recovery returned stop.',
2419
+ });
2121
2420
  this.emit({ type: 'error', error: new Error(stallRecovery.assessment.diagnosis) });
2122
2421
  if (failedAttemptMessageIds.length > 0) {
2123
2422
  this.conversations.deleteMessages(convId, failedAttemptMessageIds);
@@ -2129,6 +2428,13 @@ export class Orchestrator extends TypedEventEmitter {
2129
2428
  if (this.cerebellum && !this.cerebellum.isConnected() && abortController.signal.aborted) {
2130
2429
  const err = new Error('Cerebellum disconnected during active response. Restart it with: docker compose up -d cerebellum');
2131
2430
  log.error('Cerebellum disconnected mid-stream', { error: err.message });
2431
+ this.appendTurnJournalEntry('turn_error', err.message, {
2432
+ retryCause,
2433
+ stallRetryCount: this.streamNudgeCount,
2434
+ completionRetryCount,
2435
+ error: err.message,
2436
+ aborted: true,
2437
+ });
2132
2438
  this.emit({ type: 'error', error: err });
2133
2439
  if (failedAttemptMessageIds.length > 0) {
2134
2440
  this.conversations.deleteMessages(convId, failedAttemptMessageIds);
@@ -2191,6 +2497,7 @@ export class Orchestrator extends TypedEventEmitter {
2191
2497
  }
2192
2498
  }
2193
2499
  finally {
2500
+ this.currentQuerySession = null;
2194
2501
  this.currentStreamTurn = null;
2195
2502
  }
2196
2503
  }