openclaw-observability 2026.4.1 → 2026.4.21

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.
Files changed (112) hide show
  1. package/README.md +4 -4
  2. package/dist/cloud/api-key-auth.d.ts.map +1 -1
  3. package/dist/cloud/api-key-auth.js +4 -9
  4. package/dist/cloud/api-key-auth.js.map +1 -1
  5. package/dist/cloud/types.d.ts +2 -3
  6. package/dist/cloud/types.d.ts.map +1 -1
  7. package/dist/config.d.ts +34 -5
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +35 -2
  10. package/dist/config.js.map +1 -1
  11. package/dist/gateway/register-observability-gateway.d.ts +6 -4
  12. package/dist/gateway/register-observability-gateway.d.ts.map +1 -1
  13. package/dist/gateway/register-observability-gateway.js +105 -2
  14. package/dist/gateway/register-observability-gateway.js.map +1 -1
  15. package/dist/hooks/messages.d.ts +4 -3
  16. package/dist/hooks/messages.d.ts.map +1 -1
  17. package/dist/hooks/messages.js +23 -1
  18. package/dist/hooks/messages.js.map +1 -1
  19. package/dist/hooks/session.d.ts +4 -3
  20. package/dist/hooks/session.d.ts.map +1 -1
  21. package/dist/hooks/session.js +9 -4
  22. package/dist/hooks/session.js.map +1 -1
  23. package/dist/hooks/subagent.d.ts +4 -3
  24. package/dist/hooks/subagent.d.ts.map +1 -1
  25. package/dist/hooks/subagent.js +4 -1
  26. package/dist/hooks/subagent.js.map +1 -1
  27. package/dist/hooks/tools.d.ts +3 -3
  28. package/dist/hooks/tools.d.ts.map +1 -1
  29. package/dist/hooks/tools.js +122 -4
  30. package/dist/hooks/tools.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +472 -118
  34. package/dist/index.js.map +1 -1
  35. package/dist/llm/replay-runtime.d.ts +16 -0
  36. package/dist/llm/replay-runtime.d.ts.map +1 -0
  37. package/dist/llm/replay-runtime.js +596 -0
  38. package/dist/llm/replay-runtime.js.map +1 -0
  39. package/dist/llm/replay.d.ts +3 -0
  40. package/dist/llm/replay.d.ts.map +1 -1
  41. package/dist/llm/replay.js.map +1 -1
  42. package/dist/redaction.d.ts +1 -1
  43. package/dist/redaction.js +1 -1
  44. package/dist/runtime/index.d.ts +1 -1
  45. package/dist/runtime/index.d.ts.map +1 -1
  46. package/dist/runtime/index.js +3 -1
  47. package/dist/runtime/index.js.map +1 -1
  48. package/dist/runtime/session-context.d.ts +4 -3
  49. package/dist/runtime/session-context.d.ts.map +1 -1
  50. package/dist/runtime/session-context.js +37 -17
  51. package/dist/runtime/session-context.js.map +1 -1
  52. package/dist/security/chain-detector.d.ts +4 -4
  53. package/dist/security/chain-detector.d.ts.map +1 -1
  54. package/dist/security/chain-detector.js.map +1 -1
  55. package/dist/security/rules.d.ts +2 -2
  56. package/dist/security/rules.d.ts.map +1 -1
  57. package/dist/security/rules.js +9 -2
  58. package/dist/security/rules.js.map +1 -1
  59. package/dist/security/scanner.d.ts +8 -3
  60. package/dist/security/scanner.d.ts.map +1 -1
  61. package/dist/security/scanner.js +85 -7
  62. package/dist/security/scanner.js.map +1 -1
  63. package/dist/security/types.d.ts +3 -0
  64. package/dist/security/types.d.ts.map +1 -1
  65. package/dist/storage/buffer.d.ts +7 -7
  66. package/dist/storage/buffer.d.ts.map +1 -1
  67. package/dist/storage/buffer.js +2 -2
  68. package/dist/storage/buffer.js.map +1 -1
  69. package/dist/storage/cloud-export-writer.d.ts +23 -0
  70. package/dist/storage/cloud-export-writer.d.ts.map +1 -0
  71. package/dist/storage/cloud-export-writer.js +202 -0
  72. package/dist/storage/cloud-export-writer.js.map +1 -0
  73. package/dist/storage/duckdb-local-writer.d.ts +19 -3
  74. package/dist/storage/duckdb-local-writer.d.ts.map +1 -1
  75. package/dist/storage/duckdb-local-writer.js +261 -81
  76. package/dist/storage/duckdb-local-writer.js.map +1 -1
  77. package/dist/storage/duckdb-observability-forwarder.d.ts +16 -0
  78. package/dist/storage/duckdb-observability-forwarder.d.ts.map +1 -0
  79. package/dist/storage/duckdb-observability-forwarder.js +289 -0
  80. package/dist/storage/duckdb-observability-forwarder.js.map +1 -0
  81. package/dist/storage/mysql-writer.d.ts +35 -6
  82. package/dist/storage/mysql-writer.d.ts.map +1 -1
  83. package/dist/storage/mysql-writer.js +251 -32
  84. package/dist/storage/mysql-writer.js.map +1 -1
  85. package/dist/storage/schema.d.ts +2 -2
  86. package/dist/storage/schema.d.ts.map +1 -1
  87. package/dist/storage/schema.js +181 -53
  88. package/dist/storage/schema.js.map +1 -1
  89. package/dist/storage/structured-model.d.ts +11 -2
  90. package/dist/storage/structured-model.d.ts.map +1 -1
  91. package/dist/storage/structured-model.js +183 -5
  92. package/dist/storage/structured-model.js.map +1 -1
  93. package/dist/storage/writer.d.ts +14 -2
  94. package/dist/storage/writer.d.ts.map +1 -1
  95. package/dist/types.d.ts +28 -4
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/types.js +3 -1
  98. package/dist/types.js.map +1 -1
  99. package/dist/web/api.d.ts +80 -2
  100. package/dist/web/api.d.ts.map +1 -1
  101. package/dist/web/api.js +917 -113
  102. package/dist/web/api.js.map +1 -1
  103. package/dist/web/routes.d.ts +22 -2
  104. package/dist/web/routes.d.ts.map +1 -1
  105. package/dist/web/routes.js +264 -21
  106. package/dist/web/routes.js.map +1 -1
  107. package/dist/web/ui.d.ts +3 -1
  108. package/dist/web/ui.d.ts.map +1 -1
  109. package/dist/web/ui.js +2678 -633
  110. package/dist/web/ui.js.map +1 -1
  111. package/openclaw.plugin.json +145 -4
  112. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -53,6 +53,7 @@ const types_1 = require("./types");
53
53
  const redaction_1 = require("./redaction");
54
54
  const mysql_writer_1 = require("./storage/mysql-writer");
55
55
  const duckdb_local_writer_1 = require("./storage/duckdb-local-writer");
56
+ const cloud_export_writer_1 = require("./storage/cloud-export-writer");
56
57
  const buffer_1 = require("./storage/buffer");
57
58
  const routes_1 = require("./web/routes");
58
59
  const scanner_1 = require("./security/scanner");
@@ -62,6 +63,7 @@ const os = __importStar(require("node:os"));
62
63
  const node_crypto_1 = require("node:crypto");
63
64
  const runtime_1 = require("./runtime");
64
65
  const register_observability_gateway_1 = require("./gateway/register-observability-gateway");
66
+ const replay_runtime_1 = require("./llm/replay-runtime");
65
67
  const hooks_1 = require("./hooks");
66
68
  /* ------------------------------------------------------------------ */
67
69
  /* Runtime state */
@@ -82,6 +84,100 @@ const pendingRuntimeEvents = new Map();
82
84
  const runtimeTextSeen = new Set();
83
85
  const replayedAssistantMessages = new Map();
84
86
  const sessionTranscriptPathCache = new Map();
87
+ const runParentLinks = new Map();
88
+ function normalizeOtlpBasePath(raw) {
89
+ const trimmed = String(raw || '').trim();
90
+ if (!trimmed)
91
+ return '/plugins/observability/api/otel';
92
+ const withLeading = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
93
+ return withLeading.replace(/\/+$/, '') || '/plugins/observability/api/otel';
94
+ }
95
+ function resolveOpenclawStateDir() {
96
+ const envDir = String(process.env.OPENCLAW_HOME || process.env.OPENCLAW_STATE_DIR || '').trim();
97
+ if (envDir)
98
+ return path.resolve(envDir);
99
+ // Runtime layout: <stateDir>/extensions/openclaw-observability/dist/index.js
100
+ const fromModule = path.resolve(__dirname, '..', '..', '..');
101
+ if (path.basename(fromModule).startsWith('.openclaw'))
102
+ return fromModule;
103
+ return path.join(os.homedir(), '.openclaw');
104
+ }
105
+ function ensureDiagnosticsOtelBootstrap(params) {
106
+ const { configPath, otlpPath, token } = params;
107
+ try {
108
+ if (!fs.existsSync(configPath))
109
+ return;
110
+ const raw = fs.readFileSync(configPath, 'utf8');
111
+ const parsed = JSON.parse(raw);
112
+ const gateway = (parsed.gateway && typeof parsed.gateway === 'object') ? parsed.gateway : {};
113
+ const gatewayPort = Number(gateway.port);
114
+ const port = Number.isFinite(gatewayPort) && gatewayPort > 0 ? Math.floor(gatewayPort) : 18789;
115
+ const endpoint = `http://127.0.0.1:${port}${normalizeOtlpBasePath(otlpPath)}`;
116
+ const diagnostics = (parsed.diagnostics && typeof parsed.diagnostics === 'object') ? parsed.diagnostics : {};
117
+ const currentOtel = (diagnostics.otel && typeof diagnostics.otel === 'object') ? diagnostics.otel : {};
118
+ const headers = (currentOtel.headers && typeof currentOtel.headers === 'object')
119
+ ? { ...currentOtel.headers }
120
+ : {};
121
+ if (token)
122
+ headers.Authorization = `Bearer ${token}`;
123
+ const nextOtel = {
124
+ ...currentOtel,
125
+ enabled: true,
126
+ endpoint,
127
+ protocol: 'http/protobuf',
128
+ headers,
129
+ serviceName: String(currentOtel.serviceName || 'openclaw'),
130
+ traces: false,
131
+ metrics: true,
132
+ logs: false,
133
+ flushIntervalMs: Number(currentOtel.flushIntervalMs) > 0 ? Number(currentOtel.flushIntervalMs) : 5000,
134
+ };
135
+ const next = {
136
+ ...parsed,
137
+ diagnostics: {
138
+ ...diagnostics,
139
+ enabled: true,
140
+ otel: nextOtel,
141
+ },
142
+ };
143
+ if (JSON.stringify(parsed) === JSON.stringify(next))
144
+ return;
145
+ fs.writeFileSync(configPath, JSON.stringify(next, null, 2), 'utf8');
146
+ console.log(`[openclaw-observability] diagnostics.otel bootstrap enabled -> ${endpoint}/v1/metrics`);
147
+ }
148
+ catch (e) {
149
+ console.warn('[openclaw-observability] Failed to bootstrap diagnostics.otel:', e.message);
150
+ }
151
+ }
152
+ function persistScopeTokenToOpenclawConfig(params) {
153
+ const { configPath, scopeId, scopeToken } = params;
154
+ try {
155
+ if (!configPath || !scopeToken)
156
+ return;
157
+ if (!fs.existsSync(configPath))
158
+ return;
159
+ const raw = fs.readFileSync(configPath, 'utf8');
160
+ const parsed = JSON.parse(raw);
161
+ const plugins = (parsed.plugins && typeof parsed.plugins === 'object') ? parsed.plugins : {};
162
+ const entries = (plugins.entries && typeof plugins.entries === 'object') ? plugins.entries : {};
163
+ const obs = (entries['openclaw-observability'] && typeof entries['openclaw-observability'] === 'object')
164
+ ? entries['openclaw-observability']
165
+ : { enabled: true, config: {} };
166
+ const cfg = (obs.config && typeof obs.config === 'object') ? obs.config : {};
167
+ if (String(cfg.scopeToken || '') === scopeToken && String(cfg.scopeId || '') === scopeId)
168
+ return;
169
+ cfg.scopeId = scopeId;
170
+ cfg.scopeToken = scopeToken;
171
+ obs.config = cfg;
172
+ entries['openclaw-observability'] = obs;
173
+ plugins.entries = entries;
174
+ parsed.plugins = plugins;
175
+ fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf8');
176
+ }
177
+ catch (e) {
178
+ console.warn('[openclaw-observability] Failed to persist scope token to openclaw.json:', e.message);
179
+ }
180
+ }
85
181
  /* ------------------------------------------------------------------ */
86
182
  /* Fetch interceptor — inject stream_options to capture token usage */
87
183
  /* ------------------------------------------------------------------ */
@@ -237,7 +333,7 @@ function resolveSessionTranscriptPath(sessionId) {
237
333
  return sessionTranscriptPathCache.get(sessionId) ?? null;
238
334
  }
239
335
  try {
240
- const agentsRoot = path.join(os.homedir(), '.openclaw', 'agents');
336
+ const agentsRoot = path.join(resolveOpenclawStateDir(), 'agents');
241
337
  const agentDirs = fs.readdirSync(agentsRoot, { withFileTypes: true });
242
338
  for (const dir of agentDirs) {
243
339
  if (!dir.isDirectory())
@@ -364,6 +460,147 @@ function makeToolTimingKey(sessionId, runId, toolName, toolCallId) {
364
460
  return `tc:id:${toolCallId}`;
365
461
  return `tc:fallback:${sessionId}:${runId ?? 'norun'}:${toolName}`;
366
462
  }
463
+ function toObject(value) {
464
+ if (value && typeof value === 'object' && !Array.isArray(value))
465
+ return value;
466
+ return {};
467
+ }
468
+ function pickFirstString(...values) {
469
+ for (const v of values) {
470
+ if (typeof v !== 'string')
471
+ continue;
472
+ const s = v.trim();
473
+ if (s)
474
+ return s;
475
+ }
476
+ return '';
477
+ }
478
+ function parseJsonObjectFromText(value) {
479
+ if (typeof value !== 'string')
480
+ return null;
481
+ const t = value.trim();
482
+ if (!t || (t[0] !== '{' && t[0] !== '['))
483
+ return null;
484
+ try {
485
+ const parsed = JSON.parse(t);
486
+ return parsed && typeof parsed === 'object' ? parsed : null;
487
+ }
488
+ catch {
489
+ return null;
490
+ }
491
+ }
492
+ function getActionRunIdFromPayload(action) {
493
+ return pickFirstString(action.runId, toObject(action.inputParams).runId, toObject(action.outputResult).runId, toObject(action.inputParams).run_id, toObject(action.outputResult).run_id);
494
+ }
495
+ function parseAnnounceChildRunId(runId) {
496
+ if (!runId.startsWith('announce:v1:'))
497
+ return '';
498
+ const pos = runId.lastIndexOf(':');
499
+ if (pos < 0 || pos >= runId.length - 1)
500
+ return '';
501
+ const tail = runId.slice(pos + 1).trim();
502
+ return /^[0-9a-fA-F-]{16,}$/.test(tail) ? tail : '';
503
+ }
504
+ function extractSpawnChildRunId(action) {
505
+ if (action.actionName !== 'tool_call:sessions_spawn')
506
+ return '';
507
+ const out = toObject(action.outputResult);
508
+ const details = toObject(out.details);
509
+ let childRunId = pickFirstString(details.runId, details.run_id);
510
+ if (childRunId)
511
+ return childRunId;
512
+ const content = out.content;
513
+ if (!Array.isArray(content))
514
+ return '';
515
+ for (const item of content) {
516
+ if (!item || typeof item !== 'object')
517
+ continue;
518
+ const block = item;
519
+ if (block.type !== 'text')
520
+ continue;
521
+ const parsed = parseJsonObjectFromText(block.text);
522
+ if (!parsed)
523
+ continue;
524
+ childRunId = pickFirstString(parsed.runId, parsed.run_id);
525
+ if (childRunId)
526
+ return childRunId;
527
+ }
528
+ return '';
529
+ }
530
+ function enrichRunLineage(action) {
531
+ const runId = getActionRunIdFromPayload(action);
532
+ if (runId && !action.runId)
533
+ action.runId = runId;
534
+ const effectiveRunId = action.runId || '';
535
+ const inputObj = toObject(action.inputParams);
536
+ const outputObj = toObject(action.outputResult);
537
+ const now = Date.now();
538
+ if (effectiveRunId) {
539
+ const link = runParentLinks.get(effectiveRunId);
540
+ if (link)
541
+ link.seenAt = now;
542
+ }
543
+ if (action.actionName === 'tool_call:sessions_spawn') {
544
+ const childRunId = extractSpawnChildRunId(action);
545
+ const toolCallId = pickFirstString(inputObj.toolCallId, outputObj.toolCallId, inputObj.callId, outputObj.callId, inputObj.tool_call_id, outputObj.tool_call_id);
546
+ const parentObservationId = effectiveRunId && toolCallId ? `tool:${effectiveRunId}:${toolCallId}` : '';
547
+ if (effectiveRunId && childRunId && childRunId !== effectiveRunId) {
548
+ runParentLinks.set(childRunId, {
549
+ parentRunId: effectiveRunId,
550
+ parentObservationId: parentObservationId || undefined,
551
+ seenAt: now,
552
+ });
553
+ }
554
+ }
555
+ if (!action.parentRunId && effectiveRunId) {
556
+ const announceChildRunId = parseAnnounceChildRunId(effectiveRunId);
557
+ if (announceChildRunId) {
558
+ const link = runParentLinks.get(announceChildRunId);
559
+ if (link?.parentRunId && link.parentRunId !== effectiveRunId) {
560
+ action.parentRunId = link.parentRunId;
561
+ if (!inputObj.parentRunId && !inputObj.parent_run_id)
562
+ inputObj.parentRunId = link.parentRunId;
563
+ if (!outputObj.parentRunId && !outputObj.parent_run_id)
564
+ outputObj.parentRunId = link.parentRunId;
565
+ if (!action.parentObservationId && link.parentObservationId) {
566
+ action.parentObservationId = link.parentObservationId;
567
+ }
568
+ if (link.parentObservationId) {
569
+ if (!inputObj.parentObservationId && !inputObj.parent_observation_id)
570
+ inputObj.parentObservationId = link.parentObservationId;
571
+ if (!outputObj.parentObservationId && !outputObj.parent_observation_id)
572
+ outputObj.parentObservationId = link.parentObservationId;
573
+ }
574
+ link.seenAt = now;
575
+ }
576
+ }
577
+ else {
578
+ const direct = runParentLinks.get(effectiveRunId);
579
+ if (direct?.parentRunId && direct.parentRunId !== effectiveRunId) {
580
+ action.parentRunId = direct.parentRunId;
581
+ if (!inputObj.parentRunId && !inputObj.parent_run_id)
582
+ inputObj.parentRunId = direct.parentRunId;
583
+ if (!outputObj.parentRunId && !outputObj.parent_run_id)
584
+ outputObj.parentRunId = direct.parentRunId;
585
+ if (!action.parentObservationId && direct.parentObservationId) {
586
+ action.parentObservationId = direct.parentObservationId;
587
+ }
588
+ if (direct.parentObservationId) {
589
+ if (!inputObj.parentObservationId && !inputObj.parent_observation_id)
590
+ inputObj.parentObservationId = direct.parentObservationId;
591
+ if (!outputObj.parentObservationId && !outputObj.parent_observation_id)
592
+ outputObj.parentObservationId = direct.parentObservationId;
593
+ }
594
+ direct.seenAt = now;
595
+ }
596
+ }
597
+ }
598
+ if (Object.keys(inputObj).length > 0)
599
+ action.inputParams = inputObj;
600
+ if (Object.keys(outputObj).length > 0)
601
+ action.outputResult = outputObj;
602
+ return action;
603
+ }
367
604
  /**
368
605
  * Periodically clean up stale Map entries to prevent memory leaks when llm_input fires but llm_output does not
369
606
  */
@@ -418,6 +655,17 @@ function cleanupStaleMaps() {
418
655
  replayedAssistantMessages.delete(key);
419
656
  }
420
657
  }
658
+ for (const [childRunId, link] of runParentLinks) {
659
+ if (!link || now - link.seenAt > MAP_ENTRY_TTL_MS) {
660
+ runParentLinks.delete(childRunId);
661
+ }
662
+ }
663
+ if (runParentLinks.size > MAX_MAP_ENTRIES) {
664
+ const entries = Array.from(runParentLinks.entries()).sort((a, b) => a[1].seenAt - b[1].seenAt);
665
+ const drop = entries.length - MAX_MAP_ENTRIES;
666
+ for (let i = 0; i < drop; i++)
667
+ runParentLinks.delete(entries[i][0]);
668
+ }
421
669
  }
422
670
  /**
423
671
  * Extract image/media metadata from historyMessages
@@ -461,8 +709,8 @@ function extractMediaMeta(historyMessages) {
461
709
  const sessionStatsMap = new Map();
462
710
  /** Max session stats entries */
463
711
  const MAX_SESSION_STATS = 200;
464
- /** Update session statistics and return AuditSession */
465
- function updateSessionStats(sessionId, modelName, userId, tokens) {
712
+ /** Update session statistics and return ObservabilitySession */
713
+ function updateSessionStats(sessionId, modelName, userId, parentSessionId, tokens, organizationId, scopeId, environment) {
466
714
  const sctx = (0, runtime_1.getSessionCtx)(sessionId);
467
715
  let stats = sessionStatsMap.get(sessionId);
468
716
  if (!stats) {
@@ -471,7 +719,11 @@ function updateSessionStats(sessionId, modelName, userId, tokens) {
471
719
  lastSeen: new Date(),
472
720
  modelName,
473
721
  userId,
722
+ parentSessionId,
474
723
  channelId: sctx.channelId,
724
+ organizationId,
725
+ scopeId,
726
+ environment,
475
727
  totalActions: 0,
476
728
  totalTokens: 0,
477
729
  };
@@ -484,22 +736,41 @@ function updateSessionStats(sessionId, modelName, userId, tokens) {
484
736
  stats.modelName = modelName;
485
737
  if (userId)
486
738
  stats.userId = userId;
739
+ if (parentSessionId)
740
+ stats.parentSessionId = parentSessionId;
741
+ if (organizationId)
742
+ stats.organizationId = organizationId;
743
+ if (scopeId)
744
+ stats.scopeId = scopeId;
745
+ if (environment)
746
+ stats.environment = environment;
487
747
  if (sctx.channelId)
488
748
  stats.channelId = sctx.channelId;
749
+ if (sctx.parentSessionId)
750
+ stats.parentSessionId = sctx.parentSessionId;
489
751
  return {
490
752
  sessionId,
491
753
  userId: stats.userId,
754
+ parentSessionId: stats.parentSessionId,
492
755
  modelName: stats.modelName,
493
756
  channelId: stats.channelId,
757
+ organizationId: stats.organizationId,
758
+ scopeId: stats.scopeId,
759
+ environment: stats.environment,
494
760
  startTime: stats.firstSeen,
495
761
  endTime: stats.lastSeen,
496
762
  totalActions: stats.totalActions,
497
763
  totalTokens: stats.totalTokens,
498
764
  };
499
765
  }
500
- /** Helper to build an AuditAction (auto-fills defaults) */
766
+ /** Helper to build an ObservabilityAction (auto-fills defaults) */
501
767
  function makeAction(sessionId, overrides) {
502
768
  const sctx = (0, runtime_1.getSessionCtx)(sessionId);
769
+ const inputObj = toObject(overrides.inputParams);
770
+ const outputObj = toObject(overrides.outputResult);
771
+ const runId = pickFirstString(overrides.runId, inputObj.runId, outputObj.runId, inputObj.run_id, outputObj.run_id);
772
+ const parentRunId = pickFirstString(overrides.parentRunId, inputObj.parentRunId, outputObj.parentRunId, inputObj.parent_run_id, outputObj.parent_run_id);
773
+ const parentObservationId = pickFirstString(overrides.parentObservationId, inputObj.parentObservationId, outputObj.parentObservationId, inputObj.parent_observation_id, outputObj.parent_observation_id);
503
774
  return {
504
775
  sessionId,
505
776
  actionType: overrides.actionType,
@@ -511,7 +782,14 @@ function makeAction(sessionId, overrides) {
511
782
  completionTokens: overrides.completionTokens ?? null,
512
783
  durationMs: overrides.durationMs ?? null,
513
784
  userId: overrides.userId ?? sctx.userId,
785
+ parentSessionId: overrides.parentSessionId ?? sctx.parentSessionId,
786
+ runId: runId || undefined,
787
+ parentRunId: parentRunId || undefined,
788
+ parentObservationId: parentObservationId || undefined,
514
789
  channelId: overrides.channelId ?? sctx.channelId,
790
+ organizationId: overrides.organizationId ?? _defaultTenantScope.organizationId,
791
+ scopeId: overrides.scopeId ?? _defaultTenantScope.scopeId,
792
+ environment: overrides.environment ?? _defaultTenantScope.environment,
515
793
  createdAt: overrides.createdAt ?? new Date(),
516
794
  };
517
795
  }
@@ -534,9 +812,23 @@ let _mapCleanupTimer = null;
534
812
  let _checkpointTimer = null;
535
813
  let _metricsCleanupTimer = null;
536
814
  let _unsubscribeAgentEvents = null;
815
+ let _defaultTenantScope = {
816
+ organizationId: 'local',
817
+ scopeId: 'local',
818
+ environment: 'prod',
819
+ };
537
820
  function activate(api) {
538
821
  const rawConfig = (api.pluginConfig || api.config || {});
539
822
  const config = (0, config_1.resolveConfig)(rawConfig);
823
+ const stateDir = resolveOpenclawStateDir();
824
+ const openclawConfigPath = path.join(stateDir, 'openclaw.json');
825
+ const effectiveScopeId = String(process.env.OPENCLAW_OBSERVABILITY_SCOPE_ID || config.scopeId || 'local');
826
+ const effectiveScopeToken = String(process.env.OPENCLAW_OBSERVABILITY_SCOPE_TOKEN || config.scopeToken || '');
827
+ _defaultTenantScope = {
828
+ organizationId: String(process.env.OPENCLAW_OBSERVABILITY_ORG_ID || config.organizationId || 'local'),
829
+ scopeId: effectiveScopeId,
830
+ environment: String(process.env.OPENCLAW_OBSERVABILITY_ENV || config.environment || 'prod'),
831
+ };
540
832
  const logInfo = (message) => {
541
833
  if (typeof api.logger?.info === 'function') {
542
834
  api.logger.info(message);
@@ -548,10 +840,26 @@ function activate(api) {
548
840
  const isFirstActivation = !_writer;
549
841
  if (!_writer) {
550
842
  if (config.mode === 'remote') {
551
- _writer = new mysql_writer_1.MySQLWriter(config.mysql);
843
+ _writer = new mysql_writer_1.MySQLWriter(config.mysql, {
844
+ scopeId: effectiveScopeId,
845
+ scopeToken: effectiveScopeToken,
846
+ dataRetentionDays: config.dataRetentionDays,
847
+ onTokenIssued: (token) => {
848
+ persistScopeTokenToOpenclawConfig({
849
+ configPath: openclawConfigPath,
850
+ scopeId: effectiveScopeId,
851
+ scopeToken: token,
852
+ });
853
+ },
854
+ });
855
+ }
856
+ else if (config.mode === 'cloud') {
857
+ _writer = new cloud_export_writer_1.CloudExportWriter(config.cloud);
552
858
  }
553
859
  else {
554
- _writer = new duckdb_local_writer_1.DuckDBLocalWriter(config.duckdb);
860
+ _writer = new duckdb_local_writer_1.DuckDBLocalWriter(config.duckdb, {
861
+ dataRetentionDays: config.dataRetentionDays,
862
+ });
555
863
  }
556
864
  }
557
865
  if (!_buffer) {
@@ -570,7 +878,6 @@ function activate(api) {
570
878
  (0, runtime_1.installFetchInterceptor)();
571
879
  }
572
880
  const redactor = new redaction_1.Redactor(config.redaction);
573
- const stateDir = path.join(os.homedir(), '.openclaw');
574
881
  const metricsRetentionDaysRaw = Number(config.metrics.retentionDays);
575
882
  const metricsRetentionDays = Number.isFinite(metricsRetentionDaysRaw)
576
883
  ? Math.max(0, Math.floor(metricsRetentionDaysRaw))
@@ -657,6 +964,14 @@ function activate(api) {
657
964
  metricsCleanupRunning = false;
658
965
  }
659
966
  }
967
+ async function runStartupMaintenance() {
968
+ if (typeof writer.runStartupMaintenance !== 'function')
969
+ return;
970
+ const ready = await writer.ensureReady();
971
+ if (!ready)
972
+ return;
973
+ await writer.runStartupMaintenance();
974
+ }
660
975
  function startServices() {
661
976
  if (_servicesStarted)
662
977
  return;
@@ -700,12 +1015,29 @@ function activate(api) {
700
1015
  }
701
1016
  // Lazy initialization: database file will be created on first write or first API query
702
1017
  // This avoids creating empty database files when plugin is installed but never used
703
- const dbConfigured = config.mode === 'local'
704
- || (config.mysql.host !== 'localhost' || config.mysql.password !== '');
1018
+ const dbConfigured = config.mode === 'cloud'
1019
+ ? Boolean(config.cloud.endpoint && config.cloud.apiKey)
1020
+ : (config.mode === 'local'
1021
+ || (config.mysql.host !== 'localhost' || config.mysql.password !== ''));
705
1022
  const dbStatus = dbConfigured ? 'ready' : 'not-configured';
1023
+ // Pre-warm database initialization as early as possible to reduce
1024
+ // first-request latency after gateway/plugin startup.
1025
+ if (dbConfigured) {
1026
+ writer.ensureReady()
1027
+ .then((ok) => {
1028
+ if (ok) {
1029
+ void runStartupMaintenance().catch((err) => {
1030
+ console.warn('[openclaw-observability] Startup maintenance failed:', err.message);
1031
+ });
1032
+ startServices();
1033
+ }
1034
+ })
1035
+ .catch((err) => {
1036
+ console.warn('[openclaw-observability] Early DB warmup failed (will retry lazily):', err.message);
1037
+ });
1038
+ }
706
1039
  // UI shell stays directly open; legacy HTTP APIs still accept gateway token
707
1040
  // for backward compatibility. WS data access is authenticated at gateway connect.
708
- const openclawConfigPath = path.join(stateDir, 'openclaw.json');
709
1041
  let observabilityToken;
710
1042
  try {
711
1043
  if (fs.existsSync(openclawConfigPath)) {
@@ -726,12 +1058,24 @@ function activate(api) {
726
1058
  catch (e) {
727
1059
  console.warn('[openclaw-observability] Failed to read gateway token from config:', e.message);
728
1060
  }
1061
+ ensureDiagnosticsOtelBootstrap({
1062
+ configPath: openclawConfigPath,
1063
+ otlpPath: config.metrics.otlpPath,
1064
+ token: observabilityToken,
1065
+ });
729
1066
  const tokenCompatEnabled = Boolean(observabilityToken);
730
1067
  // Always register HTTP routes on the current api (the latest api serves HTTP)
731
1068
  const registerHttpRoute = api.registerHttpRoute;
732
1069
  const httpRouteEnabled = typeof registerHttpRoute === 'function';
733
1070
  if (httpRouteEnabled) {
734
- (0, routes_1.registerAuditRoutes)(registerHttpRoute.bind(api), writer, observabilityToken, {
1071
+ const runReplay = async (params) => {
1072
+ return (0, replay_runtime_1.runReplayRequestViaRuntime)({
1073
+ configPath: openclawConfigPath,
1074
+ runtime: api.runtime,
1075
+ ...params,
1076
+ });
1077
+ };
1078
+ (0, routes_1.registerObservabilityRoutes)(registerHttpRoute.bind(api), writer, observabilityToken, {
735
1079
  getConfig: () => securityScanner.getConfig(),
736
1080
  updateConfig: (patch) => {
737
1081
  const next = securityScanner.updateConfig(patch);
@@ -742,7 +1086,18 @@ function activate(api) {
742
1086
  `customRegexRules=${next.customRegexRules.length}`);
743
1087
  return next;
744
1088
  },
745
- }, config.metrics);
1089
+ }, config.metrics, {
1090
+ showTenantScope: config.mode !== 'local',
1091
+ }, {
1092
+ runReplay,
1093
+ }, {
1094
+ enabled: config.mode === 'cloud' || config.cloud.enabled,
1095
+ secretSalt: process.env.OPENCLAW_OBSERVABILITY_API_KEY_SALT || '',
1096
+ }, {
1097
+ openclawRootDir: stateDir,
1098
+ openclawConfigPath,
1099
+ workspaceMediaRoot: path.join(stateDir, 'workspace', 'media'),
1100
+ });
746
1101
  }
747
1102
  else {
748
1103
  console.warn('[openclaw-observability] registerHttpRoute not available — observability UI disabled');
@@ -756,6 +1111,8 @@ function activate(api) {
756
1111
  config,
757
1112
  securityScanner,
758
1113
  persistSecurityConfig,
1114
+ openclawConfigPath,
1115
+ runtime: api.runtime,
759
1116
  });
760
1117
  }
761
1118
  else {
@@ -764,12 +1121,13 @@ function activate(api) {
764
1121
  // ====================== Helper functions ======================
765
1122
  /** Record action, update session stats, and run security scan */
766
1123
  function recordAction(action, tokens = 0) {
767
- void buffer.addAction(action);
768
- const session = updateSessionStats(action.sessionId, action.modelName, action.userId, tokens);
1124
+ const enriched = enrichRunLineage(action);
1125
+ void buffer.addAction(enriched);
1126
+ const session = updateSessionStats(enriched.sessionId, enriched.modelName, enriched.userId, enriched.parentSessionId || '', tokens, enriched.organizationId || _defaultTenantScope.organizationId, enriched.scopeId || _defaultTenantScope.scopeId, enriched.environment || _defaultTenantScope.environment);
769
1127
  void buffer.addSession(session);
770
1128
  // --- Security scan ---
771
1129
  try {
772
- const alerts = securityScanner.scan(action);
1130
+ const alerts = securityScanner.scan(enriched);
773
1131
  if (alerts.length > 0) {
774
1132
  // Prevent _alertBuffer from growing unbounded
775
1133
  if (_alertBuffer.length + alerts.length > MAX_ALERT_BUFFER) {
@@ -953,52 +1311,8 @@ function activate(api) {
953
1311
  // =====================================================================
954
1312
  // 1. Agent lifecycle
955
1313
  // =====================================================================
956
- // before_model_resolve: before model selection
957
- api.on('before_model_resolve', (event, ctx) => {
958
- try {
959
- const e = event;
960
- const c = ctx;
961
- const sid = (0, runtime_1.resolveSessionId)(undefined, c?.sessionId);
962
- const sctx = (0, runtime_1.getSessionCtx)(sid);
963
- if (c?.agentId)
964
- sctx.userId = c.agentId;
965
- recordAction(makeAction(sid, {
966
- actionType: types_1.ActionType.ModelResolve,
967
- actionName: 'before_model_resolve',
968
- userId: c?.agentId,
969
- inputParams: redactor.redact({ prompt: e.prompt }),
970
- }));
971
- console.log(`[openclaw-observability] before_model_resolve: session=${sid}`);
972
- }
973
- catch (err) {
974
- console.error('[openclaw-observability] Error in before_model_resolve:', err);
975
- }
976
- });
977
- // before_prompt_build: before prompt construction
978
- api.on('before_prompt_build', (event, ctx) => {
979
- try {
980
- const e = event;
981
- const c = ctx;
982
- const sid = (0, runtime_1.resolveSessionId)(undefined, c?.sessionId);
983
- const summary = summarizePromptMessages(e.messages);
984
- recordAction(makeAction(sid, {
985
- actionType: types_1.ActionType.PromptBuild,
986
- actionName: 'before_prompt_build',
987
- userId: c?.agentId,
988
- inputParams: redactor.redact({
989
- prompt: e.prompt,
990
- messageCount: summary.messageCount,
991
- messageRoleCounts: summary.roleCounts,
992
- firstMessageRoles: summary.firstRoles,
993
- messages: (0, runtime_1.sanitizePayloadForStorage)(e.messages),
994
- }),
995
- }));
996
- console.log(`[openclaw-observability] before_prompt_build: session=${sid} msgs=${Array.isArray(e.messages) ? e.messages.length : '?'}`);
997
- }
998
- catch (err) {
999
- console.error('[openclaw-observability] Error in before_prompt_build:', err);
1000
- }
1001
- });
1314
+ // before_model_resolve / before_prompt_build intentionally not collected:
1315
+ // these hooks are high-frequency and overlap with llm_input/llm_output data.
1002
1316
  // before_agent_start: skipped — legacy hook that fires twice
1003
1317
  // and fully overlaps with before_model_resolve + before_prompt_build
1004
1318
  // agent_end: Agent run finished
@@ -1006,17 +1320,25 @@ function activate(api) {
1006
1320
  try {
1007
1321
  const e = event;
1008
1322
  const c = ctx;
1009
- const sid = (0, runtime_1.resolveSessionId)(undefined, c?.sessionId);
1323
+ const sid = (0, runtime_1.resolveSessionIdWithRun)({
1324
+ ctxSessionId: c?.sessionId,
1325
+ runId: e.runId || c?.runId,
1326
+ allowGlobalFallback: false,
1327
+ runSessionIds,
1328
+ });
1329
+ if (sid === 'unknown')
1330
+ return;
1010
1331
  recordAction(makeAction(sid, {
1011
1332
  actionType: types_1.ActionType.AgentEnd,
1012
1333
  actionName: 'agent_end',
1013
- userId: c?.agentId,
1014
1334
  outputResult: redactor.redact({
1015
1335
  success: e.success,
1016
1336
  error: e.error,
1017
1337
  messageCount: Array.isArray(e.messages) ? e.messages.length : 0,
1338
+ runId: e.runId || c?.runId,
1018
1339
  }),
1019
1340
  durationMs: e.durationMs ?? null,
1341
+ runId: e.runId || c?.runId,
1020
1342
  }));
1021
1343
  console.log(`[openclaw-observability] agent_end: session=${sid} success=${e.success} duration=${e.durationMs}ms`);
1022
1344
  }
@@ -1033,6 +1355,8 @@ function activate(api) {
1033
1355
  const e = event;
1034
1356
  const c = ctx;
1035
1357
  const sid = (0, runtime_1.resolveSessionId)(e.sessionId, c?.sessionId);
1358
+ if (sid === 'unknown')
1359
+ return;
1036
1360
  const sctx = (0, runtime_1.getSessionCtx)(sid);
1037
1361
  // Parse image/media metadata from historyMessages
1038
1362
  const media = extractMediaMeta(e.historyMessages ?? []);
@@ -1065,7 +1389,6 @@ function activate(api) {
1065
1389
  inputParams: redactor.redact(replayMeta),
1066
1390
  outputResult: redactor.redact({ text: assistantText, length: assistantText.length }),
1067
1391
  createdAt: new Date(replayAt.getTime() + 1),
1068
- userId: c?.agentId,
1069
1392
  }));
1070
1393
  }
1071
1394
  }
@@ -1076,6 +1399,15 @@ function activate(api) {
1076
1399
  prompt: e.prompt,
1077
1400
  });
1078
1401
  const sanitizedHistory = (0, runtime_1.sanitizePayloadForStorage)(e.historyMessages);
1402
+ const isReplayRun = ((typeof e.runId === 'string' && e.runId.startsWith('replay:'))
1403
+ || sid.startsWith('replay-'));
1404
+ const replayMeta = isReplayRun
1405
+ ? {
1406
+ isReplay: true,
1407
+ replaySessionId: sid,
1408
+ replayRunId: e.runId,
1409
+ }
1410
+ : undefined;
1079
1411
  llmRunStartTimes.set(e.runId, Date.now());
1080
1412
  runSessionIds.set(e.runId, sid);
1081
1413
  runInputs.set(e.runId, {
@@ -1085,9 +1417,10 @@ function activate(api) {
1085
1417
  imagesCount: e.imagesCount,
1086
1418
  media: media.length > 0 ? media : undefined,
1087
1419
  historyMessages: Array.isArray(sanitizedHistory) ? sanitizedHistory : undefined,
1420
+ replayMeta,
1088
1421
  });
1089
1422
  }
1090
- if (c?.agentId)
1423
+ if (c?.agentId && !sctx.userId)
1091
1424
  sctx.userId = c.agentId;
1092
1425
  // Identify channel from multiple sources
1093
1426
  const identifiedChannel = (0, runtime_1.identifyChannel)(c);
@@ -1121,6 +1454,8 @@ function activate(api) {
1121
1454
  runInputs.delete(e.runId);
1122
1455
  // Token usage: multiple extraction strategies
1123
1456
  const sid = (0, runtime_1.resolveSessionId)(e.sessionId, c?.sessionId);
1457
+ if (sid === 'unknown')
1458
+ return;
1124
1459
  let promptTokens = e.usage?.input ?? null;
1125
1460
  let completionTokens = e.usage?.output ?? null;
1126
1461
  let cacheRead = e.usage?.cacheRead ?? null;
@@ -1188,13 +1523,17 @@ function activate(api) {
1188
1523
  inputData.historyMessages = cachedInput.historyMessages;
1189
1524
  inputData.historyMessageCount = cachedInput.historyMessages.length;
1190
1525
  }
1526
+ if (cachedInput?.replayMeta?.isReplay) {
1527
+ inputData.replay = cachedInput.replayMeta;
1528
+ }
1191
1529
  const sctx = (0, runtime_1.getSessionCtx)(sid);
1192
- if (c?.agentId)
1530
+ if (c?.agentId && !sctx.userId)
1193
1531
  sctx.userId = c.agentId;
1194
1532
  sctx.modelName = modelName;
1533
+ const isReplayCall = cachedInput?.replayMeta?.isReplay === true;
1195
1534
  recordAction(makeAction(sid, {
1196
- actionType: types_1.ActionType.Message,
1197
- actionName: `llm_call:${modelName}`,
1535
+ actionType: isReplayCall ? types_1.ActionType.Replay : types_1.ActionType.Message,
1536
+ actionName: isReplayCall ? `replay_call:${modelName}` : `llm_call:${modelName}`,
1198
1537
  modelName,
1199
1538
  inputParams: redactor.redact(inputData),
1200
1539
  outputResult: redactor.redact({
@@ -1212,53 +1551,61 @@ function activate(api) {
1212
1551
  promptTokens,
1213
1552
  completionTokens,
1214
1553
  durationMs,
1215
- userId: c?.agentId,
1216
1554
  }), totalTokens);
1217
1555
  const endedAt = Date.now();
1218
1556
  const syntheticTs = endedAt;
1219
1557
  if (e.runId) {
1220
- const transcriptToolUseMessages = extractToolUseAssistantMessagesFromTranscript({
1221
- sessionId: sid,
1222
- runStartedAt,
1223
- runEndedAt: endedAt,
1224
- });
1225
- for (const msg of transcriptToolUseMessages) {
1226
- const replayKey = `${sid}:${e.runId}:${msg.messageId}`;
1227
- if (replayedAssistantMessages.has(replayKey))
1228
- continue;
1229
- replayedAssistantMessages.set(replayKey, Date.now());
1230
- const replayMeta = {
1231
- runId: e.runId,
1232
- source: 'transcript_message',
1233
- synthetic: true,
1234
- stopReason: msg.stopReason ?? 'toolUse',
1235
- messageId: msg.messageId,
1236
- toolCallIds: msg.toolCallIds,
1237
- };
1238
- const replayAt = new Date(msg.timestampMs);
1239
- if (msg.thinking.length > 0) {
1240
- const thinkingText = msg.thinking.join('\n\n');
1241
- recordAction(makeAction(sid, {
1242
- actionType: types_1.ActionType.Thinking,
1243
- actionName: 'thinking',
1244
- modelName,
1245
- inputParams: redactor.redact(replayMeta),
1246
- outputResult: redactor.redact({ text: thinkingText, length: thinkingText.length }),
1247
- createdAt: replayAt,
1248
- userId: c?.agentId,
1249
- }));
1250
- }
1251
- if (msg.texts.length > 0) {
1252
- const assistantText = msg.texts.join('\n\n');
1253
- recordAction(makeAction(sid, {
1254
- actionType: types_1.ActionType.AssistantStream,
1255
- actionName: 'assistant_stream',
1256
- modelName,
1257
- inputParams: redactor.redact(replayMeta),
1258
- outputResult: redactor.redact({ text: assistantText, length: assistantText.length }),
1259
- createdAt: new Date(msg.timestampMs + 1),
1260
- userId: c?.agentId,
1261
- }));
1558
+ // Transcript replay is a fallback-only path for providers/runtimes that
1559
+ // fail to expose assistant stream/thinking in llm_output payloads.
1560
+ // If we already have direct content for this run, replay can pull in a
1561
+ // neighboring toolUse message and mis-attach it to the current run.
1562
+ const shouldReplayFromTranscript = assistantTexts.length === 0 &&
1563
+ fallbackArtifacts.thinking.length === 0;
1564
+ if (!shouldReplayFromTranscript) {
1565
+ // skip transcript replay for this run
1566
+ }
1567
+ else {
1568
+ const transcriptToolUseMessages = extractToolUseAssistantMessagesFromTranscript({
1569
+ sessionId: sid,
1570
+ runStartedAt,
1571
+ runEndedAt: endedAt,
1572
+ });
1573
+ for (const msg of transcriptToolUseMessages) {
1574
+ const replayKey = `${sid}:${e.runId}:${msg.messageId}`;
1575
+ if (replayedAssistantMessages.has(replayKey))
1576
+ continue;
1577
+ replayedAssistantMessages.set(replayKey, Date.now());
1578
+ const replayMeta = {
1579
+ runId: e.runId,
1580
+ source: 'transcript_message',
1581
+ synthetic: true,
1582
+ stopReason: msg.stopReason ?? 'toolUse',
1583
+ messageId: msg.messageId,
1584
+ toolCallIds: msg.toolCallIds,
1585
+ };
1586
+ const replayAt = new Date(msg.timestampMs);
1587
+ if (msg.thinking.length > 0) {
1588
+ const thinkingText = msg.thinking.join('\n\n');
1589
+ recordAction(makeAction(sid, {
1590
+ actionType: types_1.ActionType.Thinking,
1591
+ actionName: 'thinking',
1592
+ modelName,
1593
+ inputParams: redactor.redact(replayMeta),
1594
+ outputResult: redactor.redact({ text: thinkingText, length: thinkingText.length }),
1595
+ createdAt: replayAt,
1596
+ }));
1597
+ }
1598
+ if (msg.texts.length > 0) {
1599
+ const assistantText = msg.texts.join('\n\n');
1600
+ recordAction(makeAction(sid, {
1601
+ actionType: types_1.ActionType.AssistantStream,
1602
+ actionName: 'assistant_stream',
1603
+ modelName,
1604
+ inputParams: redactor.redact(replayMeta),
1605
+ outputResult: redactor.redact({ text: assistantText, length: assistantText.length }),
1606
+ createdAt: new Date(msg.timestampMs + 1),
1607
+ }));
1608
+ }
1262
1609
  }
1263
1610
  }
1264
1611
  if (assistantTexts.length > 0 &&
@@ -1271,7 +1618,6 @@ function activate(api) {
1271
1618
  inputParams: redactor.redact({ runId: e.runId, stream: 'assistant', synthetic: true }),
1272
1619
  outputResult: redactor.redact({ text: assistantText, length: assistantText.length }),
1273
1620
  createdAt: new Date(syntheticTs - 1),
1274
- userId: c?.agentId,
1275
1621
  }));
1276
1622
  }
1277
1623
  if (fallbackArtifacts.thinking.length > 0 &&
@@ -1304,7 +1650,6 @@ function activate(api) {
1304
1650
  outputResult: redactor.redact({ text: thinkingText, length: thinkingText.length }),
1305
1651
  createdAt: new Date(thinkingEndAt),
1306
1652
  durationMs: inferredThinkingDuration,
1307
- userId: c?.agentId,
1308
1653
  }));
1309
1654
  }
1310
1655
  }
@@ -1333,10 +1678,11 @@ function activate(api) {
1333
1678
  const e = event;
1334
1679
  const c = ctx;
1335
1680
  const sid = (0, runtime_1.resolveSessionId)(undefined, c?.sessionId);
1681
+ if (sid === 'unknown')
1682
+ return;
1336
1683
  recordAction(makeAction(sid, {
1337
1684
  actionType: types_1.ActionType.Compaction,
1338
1685
  actionName: 'before_compaction',
1339
- userId: c?.agentId,
1340
1686
  inputParams: redactor.redact({
1341
1687
  messageCount: e.messageCount,
1342
1688
  compactingCount: e.compactingCount,
@@ -1357,10 +1703,11 @@ function activate(api) {
1357
1703
  const e = event;
1358
1704
  const c = ctx;
1359
1705
  const sid = (0, runtime_1.resolveSessionId)(undefined, c?.sessionId);
1706
+ if (sid === 'unknown')
1707
+ return;
1360
1708
  recordAction(makeAction(sid, {
1361
1709
  actionType: types_1.ActionType.Compaction,
1362
1710
  actionName: 'after_compaction',
1363
- userId: c?.agentId,
1364
1711
  outputResult: redactor.redact({
1365
1712
  messageCount: e.messageCount,
1366
1713
  compactedCount: e.compactedCount,
@@ -1380,10 +1727,11 @@ function activate(api) {
1380
1727
  const e = event;
1381
1728
  const c = ctx;
1382
1729
  const sid = (0, runtime_1.resolveSessionId)(undefined, c?.sessionId);
1730
+ if (sid === 'unknown')
1731
+ return;
1383
1732
  recordAction(makeAction(sid, {
1384
1733
  actionType: types_1.ActionType.Reset,
1385
1734
  actionName: 'before_reset',
1386
- userId: c?.agentId,
1387
1735
  inputParams: redactor.redact({
1388
1736
  reason: e.reason,
1389
1737
  messageCount: Array.isArray(e.messages) ? e.messages.length : 0,
@@ -1407,6 +1755,7 @@ function activate(api) {
1407
1755
  recordAction,
1408
1756
  identifyChannel: runtime_1.identifyChannel,
1409
1757
  saveChannelToContext: runtime_1.saveChannelToContext,
1758
+ saveUserToContext: runtime_1.saveUserToContext,
1410
1759
  resolveSessionIdWithRun: (args) => (0, runtime_1.resolveSessionIdWithRun)({ ...args, runSessionIds }),
1411
1760
  });
1412
1761
  // =====================================================================
@@ -1448,6 +1797,7 @@ function activate(api) {
1448
1797
  redactor,
1449
1798
  makeAction,
1450
1799
  recordAction,
1800
+ bindParentSession: runtime_1.bindParentSession,
1451
1801
  resolveSessionIdWithRun: (args) => (0, runtime_1.resolveSessionIdWithRun)({ ...args, runSessionIds }),
1452
1802
  });
1453
1803
  // =====================================================================
@@ -1460,7 +1810,9 @@ function activate(api) {
1460
1810
  // Pre-initialize DuckDB during startup (not lazily on first request).
1461
1811
  // This ensures WAL recovery completes before any user requests arrive,
1462
1812
  // preventing 503 errors on the first request after restart.
1463
- writer.ensureReady().catch((err) => {
1813
+ writer.ensureReady()
1814
+ .then(() => runStartupMaintenance())
1815
+ .catch((err) => {
1464
1816
  console.error('[openclaw-observability] Pre-init DuckDB failed (will retry on first write):', err);
1465
1817
  });
1466
1818
  recordAction(makeAction('system', {
@@ -1497,7 +1849,9 @@ function activate(api) {
1497
1849
  });
1498
1850
  const dbTarget = config.mode === 'remote'
1499
1851
  ? `${config.mysql.host}/${config.mysql.database}`
1500
- : config.duckdb.path;
1852
+ : config.mode === 'cloud'
1853
+ ? config.cloud.endpoint
1854
+ : config.duckdb.path;
1501
1855
  logInfo(`[Observability] Plugin activated (mode: ${config.mode}, db: ${dbStatus}:${dbTarget}, ui: ${httpRouteEnabled ? '/plugins/observability/' : 'disabled'}, metrics: ${config.metrics.enabled ? `${config.metrics.otlpPath}/v1/metrics` : 'disabled'}, metricsRetentionDays: ${metricsRetentionDays > 0 ? metricsRetentionDays : 'off'}, rpc: ${gatewayRpcEnabled ? 'enabled' : 'disabled'}, runtimeEvents: ${runtimeEventsStatus}, tokenCompat: ${tokenCompatEnabled ? 'on' : 'off'})`);
1502
1856
  // =====================================================================
1503
1857
  // Return deactivate function for cleanup