@joshuaswarren/openclaw-engram 9.0.107 → 9.0.108

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.
@@ -0,0 +1,15 @@
1
+ // openclaw-engram: Local-first memory plugin
2
+
3
+ // src/version-utils.ts
4
+ function compareVersions(a, b) {
5
+ for (let i = 0; i < 3; i += 1) {
6
+ if (a[i] > b[i]) return 1;
7
+ if (a[i] < b[i]) return -1;
8
+ }
9
+ return 0;
10
+ }
11
+
12
+ export {
13
+ compareVersions
14
+ };
15
+ //# sourceMappingURL=chunk-PKKCGNF2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/version-utils.ts"],"sourcesContent":["export type VersionTriple = readonly [number, number, number];\n\nexport function compareVersions(a: VersionTriple, b: VersionTriple): number {\n for (let i = 0; i < 3; i += 1) {\n if (a[i] > b[i]) return 1;\n if (a[i] < b[i]) return -1;\n }\n return 0;\n}\n"],"mappings":";;;AAEO,SAAS,gBAAgB,GAAkB,GAA0B;AAC1E,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;AAC7B,QAAI,EAAE,CAAC,IAAI,EAAE,CAAC,EAAG,QAAO;AACxB,QAAI,EAAE,CAAC,IAAI,EAAE,CAAC,EAAG,QAAO;AAAA,EAC1B;AACA,SAAO;AACT;","names":[]}
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // openclaw-engram: Local-first memory plugin
2
+ import {
3
+ compareVersions
4
+ } from "./chunk-PKKCGNF2.js";
2
5
  import {
3
6
  EngramAccessInputError,
4
7
  EngramAccessService,
@@ -127,6 +130,30 @@ import {
127
130
  log
128
131
  } from "./chunk-SSIIJJKA.js";
129
132
 
133
+ // src/index.ts
134
+ import { createRequire } from "module";
135
+
136
+ // src/sdk-compat.ts
137
+ function detectSdkCapabilities(api) {
138
+ const hasRegisterMemoryPromptSection = typeof api.registerMemoryPromptSection === "function";
139
+ const hasRuntimeNamespace = typeof api.runtime === "object" && api.runtime !== null;
140
+ const hasRegistrationMode = typeof api.registrationMode === "string";
141
+ const sdkVersion = (hasRuntimeNamespace && typeof api.runtime?.version === "string" ? api.runtime.version : null) ?? (typeof process?.env?.OPENCLAW_SERVICE_VERSION === "string" ? process.env.OPENCLAW_SERVICE_VERSION : null) ?? "legacy";
142
+ const isNewSdk = hasRegisterMemoryPromptSection || hasRuntimeNamespace || hasRegistrationMode;
143
+ const hasNewHookSystem = hasRegisterMemoryPromptSection || hasRegistrationMode;
144
+ return {
145
+ hasBeforePromptBuild: hasNewHookSystem,
146
+ hasRegisterMemoryPromptSection,
147
+ hasDefinePluginEntry: isNewSdk,
148
+ // entry point is less risky, keep broad detection
149
+ hasRuntimeNamespace,
150
+ hasRegistrationMode,
151
+ hasTypedHooks: hasNewHookSystem,
152
+ sdkVersion,
153
+ registrationMode: hasRegistrationMode ? api.registrationMode : void 0
154
+ };
155
+ }
156
+
130
157
  // src/tools.ts
131
158
  import { createHash } from "crypto";
132
159
  import { Type } from "@sinclair/typebox";
@@ -8314,18 +8341,8 @@ var EngramAccessHttpServer = class {
8314
8341
  import { access as access3, readFile as readFile16 } from "fs/promises";
8315
8342
  import path23 from "path";
8316
8343
  import { spawn as spawn2 } from "child_process";
8317
-
8318
- // src/version-utils.ts
8319
- function compareVersions(a, b) {
8320
- for (let i = 0; i < 3; i += 1) {
8321
- if (a[i] > b[i]) return 1;
8322
- if (a[i] < b[i]) return -1;
8323
- }
8324
- return 0;
8325
- }
8326
-
8327
- // src/compat/checks.ts
8328
- var REQUIRED_HOOKS = ["before_agent_start", "agent_end"];
8344
+ var REQUIRED_HOOKS_LEGACY = ["before_agent_start", "agent_end"];
8345
+ var REQUIRED_HOOKS_NEW = ["before_prompt_build", "agent_end"];
8329
8346
  function isSafeCommandToken(command) {
8330
8347
  return /^[a-zA-Z0-9._-]+$/.test(command);
8331
8348
  }
@@ -8627,7 +8644,12 @@ async function runCompatChecks(options) {
8627
8644
  const indexRaw = await readFile16(indexPath, "utf-8");
8628
8645
  const structuralSource = stripCommentsAndStrings(indexRaw);
8629
8646
  const hooks = parseHookRegistrations(indexRaw);
8630
- const missingHooks = REQUIRED_HOOKS.filter((hook) => !hooks.has(hook));
8647
+ const missingLegacy = REQUIRED_HOOKS_LEGACY.filter((hook) => !hooks.has(hook));
8648
+ const missingNew = REQUIRED_HOOKS_NEW.filter((hook) => !hooks.has(hook));
8649
+ const hasMemoryPromptSection = structuralSource.includes(".registerMemoryPromptSection(");
8650
+ const missingLegacyAdj = hasMemoryPromptSection ? missingLegacy.filter((h) => h !== "before_agent_start") : missingLegacy;
8651
+ const missingNewAdj = hasMemoryPromptSection ? missingNew.filter((h) => h !== "before_prompt_build") : missingNew;
8652
+ const missingHooks = missingNewAdj.length <= missingLegacyAdj.length ? missingNewAdj : missingLegacyAdj;
8631
8653
  const hasGatewayStartHook = hooks.has("gateway_start");
8632
8654
  const hasServiceStart = hasServiceStartRegistration(structuralSource);
8633
8655
  if (missingHooks.length === 0 && (hasGatewayStartHook || hasServiceStart)) {
@@ -8650,7 +8672,7 @@ async function runCompatChecks(options) {
8650
8672
  title: "Core hook registration",
8651
8673
  level: "error",
8652
8674
  message: `Missing expected registration(s): ${missingParts.join("; ")}`,
8653
- remediation: "Ensure src/index.ts registers before_agent_start and agent_end, plus either gateway_start or api.registerService({ start })."
8675
+ remediation: "Ensure src/index.ts registers before_prompt_build (or before_agent_start) and agent_end, plus either gateway_start or api.registerService({ start })."
8654
8676
  });
8655
8677
  }
8656
8678
  const cliWired = hasCliRegistration(structuralSource);
@@ -13117,34 +13139,6 @@ async function recordObjectiveStateSnapshotsFromAgentMessages(options) {
13117
13139
  return { snapshots, filePaths };
13118
13140
  }
13119
13141
 
13120
- // src/legacy-hook-compat.ts
13121
- var FIRST_PUBLISHED_RUNTIME_WITHOUT_AGENT_HEARTBEAT = [2026, 1, 29];
13122
- var OPENCLAW_VERSION_PREFIX = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
13123
- function parseOpenClawVersion(value) {
13124
- if (typeof value !== "string") return null;
13125
- const match = value.trim().match(OPENCLAW_VERSION_PREFIX);
13126
- if (!match) return null;
13127
- return {
13128
- triple: [
13129
- Number.parseInt(match[1], 10),
13130
- Number.parseInt(match[2], 10),
13131
- Number.parseInt(match[3], 10)
13132
- ],
13133
- prerelease: typeof match[4] === "string" && match[4].length > 0
13134
- };
13135
- }
13136
- function shouldRegisterTypedAgentHeartbeat(runtimeVersion) {
13137
- const parsed = parseOpenClawVersion(runtimeVersion);
13138
- if (!parsed) return false;
13139
- const comparison = compareVersions(
13140
- parsed.triple,
13141
- FIRST_PUBLISHED_RUNTIME_WITHOUT_AGENT_HEARTBEAT
13142
- );
13143
- if (comparison < 0) return true;
13144
- if (comparison > 0) return false;
13145
- return parsed.prerelease;
13146
- }
13147
-
13148
13142
  // src/index.ts
13149
13143
  import { readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
13150
13144
  import { readFileSync as readFileSync2 } from "fs";
@@ -13511,20 +13505,27 @@ var ENGRAM_ACCESS_SERVICE = "__openclawEngramAccessService";
13511
13505
  var ENGRAM_ACCESS_HTTP_SERVER = "__openclawEngramAccessHttpServer";
13512
13506
  var ENGRAM_SERVICE_STARTED = "__openclawEngramServiceStarted";
13513
13507
  var ENGRAM_INIT_PROMISE = "__openclawEngramInitPromise";
13514
- function loadPluginConfigFromFile() {
13508
+ function loadPluginEntryFromFile() {
13515
13509
  try {
13516
13510
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
13517
13511
  const homeDir = process.env.HOME ?? os3.homedir();
13518
13512
  const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path27.join(homeDir, ".openclaw", "openclaw.json");
13519
13513
  const content = readFileSync2(configPath, "utf-8");
13520
13514
  const config = JSON.parse(content);
13521
- const pluginEntry = config?.plugins?.entries?.["openclaw-engram"];
13522
- return pluginEntry?.config;
13515
+ return config?.plugins?.entries?.["openclaw-engram"];
13523
13516
  } catch (err) {
13524
13517
  log.warn(`Failed to load config from file: ${err}`);
13525
13518
  return void 0;
13526
13519
  }
13527
13520
  }
13521
+ function loadPluginConfigFromFile() {
13522
+ return loadPluginEntryFromFile()?.config;
13523
+ }
13524
+ function readPluginHooksPolicy(apiConfig) {
13525
+ const fromApi = apiConfig?.plugins?.entries?.["openclaw-engram"]?.hooks;
13526
+ if (fromApi && typeof fromApi === "object") return fromApi;
13527
+ return loadPluginEntryFromFile()?.hooks;
13528
+ }
13528
13529
  function wildcardToRegExp(pattern) {
13529
13530
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
13530
13531
  return new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
@@ -13543,20 +13544,31 @@ function shouldSkipRecallForSession(sessionKey, cfg) {
13543
13544
  }
13544
13545
  });
13545
13546
  }
13546
- function observeSessionHeartbeat(orchestrator, ctx) {
13547
- if (orchestrator.config.sessionObserverEnabled !== true) return;
13548
- const sessionKey = ctx?.sessionKey ?? "default";
13549
- void orchestrator.observeSessionHeartbeat(sessionKey).catch((err) => {
13550
- log.debug(`agent_heartbeat observer failed: ${err}`);
13551
- });
13547
+ function tryDefinePluginEntry(def) {
13548
+ try {
13549
+ const _require = createRequire(import.meta.url);
13550
+ const { definePluginEntry } = _require("openclaw/plugin-sdk/plugin-entry");
13551
+ return definePluginEntry(def);
13552
+ } catch {
13553
+ return def;
13554
+ }
13552
13555
  }
13553
- var index_default = {
13556
+ var sdkCaps;
13557
+ var pluginDefinition = {
13554
13558
  id: "openclaw-engram",
13555
13559
  name: "Engram (Local Memory)",
13556
13560
  description: "Local-first memory plugin. Uses GPT-5.2 for intelligent extraction and QMD for storage/retrieval.",
13557
13561
  kind: "memory",
13558
13562
  register(api) {
13559
13563
  initLogger(api.logger, false);
13564
+ sdkCaps = detectSdkCapabilities(api);
13565
+ log.info(
13566
+ `SDK detection: version=${sdkCaps.sdkVersion}, beforePromptBuild=${sdkCaps.hasBeforePromptBuild}, memoryPromptSection=${sdkCaps.hasRegisterMemoryPromptSection}, typedHooks=${sdkCaps.hasTypedHooks}`
13567
+ );
13568
+ if (sdkCaps.registrationMode === "setup-only") {
13569
+ log.info("registrationMode=setup-only \u2014 skipping full initialization");
13570
+ return;
13571
+ }
13560
13572
  const fileConfig = loadPluginConfigFromFile();
13561
13573
  const cfg = parseConfig({
13562
13574
  ...api.pluginConfig,
@@ -13599,65 +13611,112 @@ var index_default = {
13599
13611
  maxBodyBytes: cfg.agentAccessHttp.maxBodyBytes
13600
13612
  });
13601
13613
  globalThis[ENGRAM_ACCESS_HTTP_SERVER] = accessHttpServer;
13602
- api.on(
13603
- "before_agent_start",
13604
- async (event, ctx) => {
13605
- const prompt = event.prompt;
13606
- if (!prompt || prompt.length < 5) return;
13607
- const sessionKey = ctx?.sessionKey ?? "default";
13608
- log.debug(`before_agent_start: sessionKey=${sessionKey}, promptLen=${prompt.length}`);
13614
+ const hooksPolicy = readPluginHooksPolicy(api.config);
13615
+ const promptInjectionAllowed = hooksPolicy?.allowPromptInjection !== false;
13616
+ const useMemoryPromptSection = sdkCaps.hasRegisterMemoryPromptSection && typeof api.registerMemoryPromptSection === "function" && promptInjectionAllowed;
13617
+ async function recallHookHandler(hookLabel, event, ctx) {
13618
+ let prompt = event.prompt;
13619
+ if ((!prompt || prompt.length < 5) && Array.isArray(event.messages)) {
13620
+ const msgs = event.messages;
13621
+ for (let i = msgs.length - 1; i >= 0; i--) {
13622
+ if (msgs[i]?.role === "user") {
13623
+ const text = extractTextContent2(msgs[i]);
13624
+ if (text.length >= 5) {
13625
+ prompt = text;
13626
+ break;
13627
+ }
13628
+ }
13629
+ }
13630
+ }
13631
+ if (!prompt || prompt.length < 5) return;
13632
+ const sessionKey = ctx?.sessionKey ?? "default";
13633
+ log.debug(`${hookLabel}: sessionKey=${sessionKey}, promptLen=${prompt.length}`);
13634
+ log.debug(
13635
+ `${hookLabel}: cronRecallMode=${cfg.cronRecallMode}, allowlistCount=${cfg.cronRecallAllowlist.length}`
13636
+ );
13637
+ if (sessionKey.includes(":cron:") && cfg.cronRecallMode === "allowlist") {
13638
+ const matchedPattern = cfg.cronRecallAllowlist.find((pattern) => {
13639
+ const re = wildcardToRegExp(pattern);
13640
+ return re.test(sessionKey);
13641
+ });
13609
13642
  log.debug(
13610
- `before_agent_start: cronRecallMode=${cfg.cronRecallMode}, allowlistCount=${cfg.cronRecallAllowlist.length}`
13643
+ `${hookLabel}: cron allowlist match=${matchedPattern ? "yes" : "no"} pattern=${matchedPattern ?? "none"}`
13611
13644
  );
13612
- if (sessionKey.includes(":cron:") && cfg.cronRecallMode === "allowlist") {
13613
- const matchedPattern = cfg.cronRecallAllowlist.find((pattern) => {
13614
- const re = wildcardToRegExp(pattern);
13615
- return re.test(sessionKey);
13616
- });
13617
- log.debug(
13618
- `before_agent_start: cron allowlist match=${matchedPattern ? "yes" : "no"} pattern=${matchedPattern ?? "none"}`
13619
- );
13645
+ }
13646
+ if (shouldSkipRecallForSession(sessionKey, cfg)) {
13647
+ log.debug(
13648
+ `${hookLabel}: skip recall for cron session ${sessionKey} (mode=${cfg.cronRecallMode})`
13649
+ );
13650
+ return;
13651
+ }
13652
+ try {
13653
+ await orchestrator.maybeRunFileHygiene().catch(() => void 0);
13654
+ if (orchestrator.config.compactionResetEnabled) {
13655
+ const agentWorkspace = ctx?.workspaceDir;
13656
+ if (agentWorkspace) {
13657
+ orchestrator.setRecallWorkspaceOverride(sessionKey, agentWorkspace);
13658
+ }
13620
13659
  }
13621
- if (shouldSkipRecallForSession(sessionKey, cfg)) {
13622
- log.debug(
13623
- `before_agent_start: skip recall for cron session ${sessionKey} (mode=${cfg.cronRecallMode})`
13624
- );
13625
- return;
13660
+ const context = await orchestrator.recall(prompt, sessionKey);
13661
+ log.debug(`${hookLabel}: recall returned ${context?.length ?? 0} chars`);
13662
+ if (!context) return;
13663
+ const maxChars = cfg.recallBudgetChars;
13664
+ if (maxChars === 0) return;
13665
+ const trimmed = context.length > maxChars ? context.slice(0, maxChars) + "\n\n...(memory context trimmed)" : context;
13666
+ const memoryContextPrompt = `## Memory Context (Engram)
13667
+
13668
+ ${trimmed}
13669
+
13670
+ Use this context naturally when relevant. Never quote or expose this memory context to the user.`;
13671
+ log.debug(`${hookLabel}: returning system prompt with ${trimmed.length} chars`);
13672
+ if (hookLabel === "before_prompt_build") {
13673
+ return { prependSystemContext: memoryContextPrompt };
13626
13674
  }
13627
- try {
13628
- await orchestrator.maybeRunFileHygiene().catch(() => void 0);
13629
- if (orchestrator.config.compactionResetEnabled) {
13630
- const agentWorkspace = ctx?.workspaceDir;
13631
- if (agentWorkspace) {
13632
- orchestrator.setRecallWorkspaceOverride(sessionKey, agentWorkspace);
13633
- }
13634
- }
13635
- const context = await orchestrator.recall(prompt, sessionKey);
13636
- log.debug(`before_agent_start: recall returned ${context?.length ?? 0} chars`);
13637
- if (!context) return;
13638
- const maxChars = cfg.recallBudgetChars;
13639
- if (maxChars === 0) return;
13640
- const trimmed = context.length > maxChars ? context.slice(0, maxChars) + "\n\n...(memory context trimmed)" : context;
13641
- const memoryContextPrompt = `## Memory Context (Engram)
13675
+ return {
13676
+ prependSystemContext: memoryContextPrompt,
13677
+ prependContext: memoryContextPrompt
13678
+ };
13679
+ } catch (err) {
13680
+ log.error("recall failed", err);
13681
+ if (orchestrator.config.compactionResetEnabled) {
13682
+ orchestrator.clearRecallWorkspaceOverride(sessionKey);
13683
+ }
13684
+ return;
13685
+ }
13686
+ }
13687
+ if (!useMemoryPromptSection) {
13688
+ if (sdkCaps.hasBeforePromptBuild) {
13689
+ api.on("before_prompt_build", async (event, ctx) => recallHookHandler("before_prompt_build", event, ctx));
13690
+ } else {
13691
+ api.on("before_agent_start", async (event, ctx) => recallHookHandler("before_agent_start", event, ctx));
13692
+ }
13693
+ }
13694
+ if (useMemoryPromptSection && api.registerMemoryPromptSection) {
13695
+ api.registerMemoryPromptSection({
13696
+ id: "engram-memory",
13697
+ label: "Engram Memory Context",
13698
+ build: async ({ prompt, sessionKey }) => {
13699
+ if (!prompt || prompt.length < 5) return null;
13700
+ if (shouldSkipRecallForSession(sessionKey, cfg)) return null;
13701
+ try {
13702
+ await orchestrator.maybeRunFileHygiene().catch(() => void 0);
13703
+ const context = await orchestrator.recall(prompt, sessionKey);
13704
+ if (!context) return null;
13705
+ const maxChars = cfg.recallBudgetChars;
13706
+ if (maxChars === 0) return null;
13707
+ const trimmed = context.length > maxChars ? context.slice(0, maxChars) + "\n\n...(memory context trimmed)" : context;
13708
+ return `## Memory Context (Engram)
13642
13709
 
13643
13710
  ${trimmed}
13644
13711
 
13645
13712
  Use this context naturally when relevant. Never quote or expose this memory context to the user.`;
13646
- log.debug(`before_agent_start: returning system prompt with ${trimmed.length} chars`);
13647
- return {
13648
- prependSystemContext: memoryContextPrompt,
13649
- // Backward-compat path for gateway builds that consume prependContext.
13650
- prependContext: memoryContextPrompt
13651
- };
13652
- } catch (err) {
13653
- log.error("recall failed", err);
13654
- if (orchestrator.config.compactionResetEnabled) {
13655
- orchestrator.clearRecallWorkspaceOverride(sessionKey);
13713
+ } catch (err) {
13714
+ log.error("registerMemoryPromptSection build failed", err);
13715
+ return null;
13656
13716
  }
13657
- return;
13658
13717
  }
13659
- }
13660
- );
13718
+ });
13719
+ }
13661
13720
  api.on(
13662
13721
  "agent_end",
13663
13722
  async (event, ctx) => {
@@ -13772,30 +13831,6 @@ Use this context naturally when relevant. Never quote or expose this memory cont
13772
13831
  }
13773
13832
  }
13774
13833
  );
13775
- const runtimeApi = api;
13776
- runtimeApi.registerHook?.(
13777
- ["agent_heartbeat", "agent:heartbeat"],
13778
- (event) => {
13779
- observeSessionHeartbeat(orchestrator, event.context);
13780
- },
13781
- {
13782
- name: "engram_agent_heartbeat_legacy",
13783
- description: "Observe legacy OpenClaw heartbeat events when the runtime still emits them."
13784
- }
13785
- );
13786
- const runtimeVersion = runtimeApi.runtime?.version || process.env.OPENCLAW_SERVICE_VERSION || "unknown";
13787
- if (shouldRegisterTypedAgentHeartbeat(runtimeVersion)) {
13788
- api.on("agent_heartbeat", (_event, ctx) => {
13789
- observeSessionHeartbeat(orchestrator, ctx);
13790
- });
13791
- log.info(
13792
- `registered legacy typed agent_heartbeat hook for OpenClaw ${runtimeVersion}`
13793
- );
13794
- } else {
13795
- log.debug(
13796
- `skipping legacy typed agent_heartbeat hook for OpenClaw ${runtimeVersion}; published builds from 2026.1.29 onward do not expose it`
13797
- );
13798
- }
13799
13834
  const lcmTokensBefore = /* @__PURE__ */ new Map();
13800
13835
  api.on(
13801
13836
  "before_compaction",
@@ -13848,7 +13883,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
13848
13883
  api.on(
13849
13884
  "after_compaction",
13850
13885
  async (event, ctx) => {
13851
- const sessionKey = ctx?.sessionKey ?? "default";
13886
+ const sessionKey = ctx?.sessionKey ?? event?.sessionKey ?? "default";
13852
13887
  try {
13853
13888
  if (orchestrator.lcmEngine?.enabled) {
13854
13889
  try {
@@ -13919,6 +13954,98 @@ Use this context naturally when relevant. Never quote or expose this memory cont
13919
13954
  }
13920
13955
  }
13921
13956
  );
13957
+ if (sdkCaps.hasBeforePromptBuild) {
13958
+ api.on(
13959
+ "session_start",
13960
+ async (event, _ctx) => {
13961
+ const sessionKey = event.sessionKey ?? "default";
13962
+ log.debug(`session_start: ${sessionKey}`);
13963
+ try {
13964
+ await orchestrator.maybeRunFileHygiene().catch(() => void 0);
13965
+ } catch (err) {
13966
+ log.debug(`session_start file hygiene failed: ${err}`);
13967
+ }
13968
+ }
13969
+ );
13970
+ api.on(
13971
+ "session_end",
13972
+ async (event, _ctx) => {
13973
+ const sessionKey = event.sessionKey ?? "default";
13974
+ log.debug(`session_end: ${sessionKey}`);
13975
+ if (orchestrator.config.compactionResetEnabled) {
13976
+ orchestrator.clearRecallWorkspaceOverride(sessionKey);
13977
+ }
13978
+ }
13979
+ );
13980
+ api.on(
13981
+ "before_tool_call",
13982
+ async (event, _ctx) => {
13983
+ if (event.toolName) {
13984
+ log.debug(`before_tool_call: ${event.toolName}`);
13985
+ }
13986
+ }
13987
+ );
13988
+ api.on(
13989
+ "after_tool_call",
13990
+ async (event, _ctx) => {
13991
+ if (event.toolName) {
13992
+ log.debug(`after_tool_call: ${event.toolName} (${event.durationMs ?? "?"}ms)`);
13993
+ }
13994
+ }
13995
+ );
13996
+ api.on(
13997
+ "llm_output",
13998
+ async (event, ctx) => {
13999
+ const sessionKey = ctx?.sessionKey ?? "default";
14000
+ if (event.tokenUsage) {
14001
+ log.debug(
14002
+ `llm_output: model=${event.model ?? "?"}, tokens=${event.tokenUsage.input ?? 0}/${event.tokenUsage.output ?? 0}, ${event.durationMs ?? "?"}ms, session=${sessionKey}`
14003
+ );
14004
+ }
14005
+ }
14006
+ );
14007
+ api.on(
14008
+ "subagent_spawning",
14009
+ async (event, _ctx) => {
14010
+ log.debug(`subagent_spawning: ${event.subagentId ?? "?"} purpose=${event.purpose ?? "?"}`);
14011
+ }
14012
+ );
14013
+ api.on(
14014
+ "subagent_ended",
14015
+ async (event, _ctx) => {
14016
+ log.debug(
14017
+ `subagent_ended: ${event.subagentId ?? "?"} success=${event.success ?? "?"} ${event.durationMs ?? "?"}ms`
14018
+ );
14019
+ }
14020
+ );
14021
+ } else {
14022
+ const runtimeApi = api;
14023
+ runtimeApi.registerHook?.(
14024
+ ["agent_heartbeat", "agent:heartbeat"],
14025
+ (event) => {
14026
+ if (orchestrator.config.sessionObserverEnabled !== true) return;
14027
+ const sessionKey = event?.context?.sessionKey ?? "default";
14028
+ void orchestrator.observeSessionHeartbeat(sessionKey).catch((err) => {
14029
+ log.debug(`agent_heartbeat observer failed: ${err}`);
14030
+ });
14031
+ },
14032
+ { name: "engram_agent_heartbeat_legacy", description: "Observe legacy heartbeat events for session observation." }
14033
+ );
14034
+ const runtimeVersion = runtimeApi.runtime?.version || process.env.OPENCLAW_SERVICE_VERSION || "unknown";
14035
+ void import("./legacy-hook-compat-722BG5XG.js").then(({ shouldRegisterTypedAgentHeartbeat }) => {
14036
+ if (shouldRegisterTypedAgentHeartbeat(runtimeVersion)) {
14037
+ api.on("agent_heartbeat", (_event, ctx) => {
14038
+ if (orchestrator.config.sessionObserverEnabled !== true) return;
14039
+ const sessionKey = ctx?.sessionKey ?? "default";
14040
+ void orchestrator.observeSessionHeartbeat(sessionKey).catch((err) => {
14041
+ log.debug(`agent_heartbeat typed observer failed: ${err}`);
14042
+ });
14043
+ });
14044
+ log.info(`registered typed agent_heartbeat hook for OpenClaw ${runtimeVersion}`);
14045
+ }
14046
+ }).catch(() => {
14047
+ });
14048
+ }
13922
14049
  async function ensureHourlySummaryCron(api2) {
13923
14050
  const jobId = "engram-hourly-summary";
13924
14051
  const cronFilePath = path27.join(os3.homedir(), ".openclaw", "cron", "jobs.json");
@@ -14086,6 +14213,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
14086
14213
  });
14087
14214
  }
14088
14215
  };
14216
+ var index_default = tryDefinePluginEntry(pluginDefinition);
14089
14217
  function extractLastTurn(messages) {
14090
14218
  let lastUserIdx = -1;
14091
14219
  for (let i = messages.length - 1; i >= 0; i--) {