bereach-openclaw 1.6.13 → 1.6.14

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.
@@ -38,6 +38,14 @@ export const READ_TOOLS = new Set([
38
38
  "bereach_get_connections",
39
39
  "bereach_resolve_parameters",
40
40
  "bereach_inbox_unread_count",
41
+ // Chat agent tools (same LinkedIn actions, different naming convention)
42
+ "bereach_search_people",
43
+ "bereach_search_posts",
44
+ "bereach_search_companies",
45
+ "bereach_search_jobs",
46
+ "bereach_search_by_url",
47
+ "bereach_search_sales_nav_people",
48
+ "bereach_search_sales_nav_companies",
41
49
  ]);
42
50
 
43
51
  /** LinkedIn write operations (DM, connect, like, comment, etc.). Higher pacing. 1 credit each. */
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bereach-openclaw",
3
3
  "name": "BeReach",
4
- "version": "1.6.13",
4
+ "version": "1.6.14",
5
5
  "description": "LinkedIn outreach automation — 75+ tools, hook-based enforcement, dynamic context",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bereach-openclaw",
3
- "version": "1.6.13",
3
+ "version": "1.6.14",
4
4
  "description": "BeReach LinkedIn automation plugin for OpenClaw",
5
5
  "license": "AGPL-3.0",
6
6
  "exports": {
@@ -6,7 +6,7 @@
6
6
  * Uses TTL cache so only the first turn hits the API.
7
7
  */
8
8
 
9
- import { getOrFetch, sessionStart, invalidateAndRefresh, type CacheStore, type ContextEntry } from "../cache";
9
+ import { getOrFetch, sessionStart, type CacheStore, type ContextEntry } from "../cache";
10
10
  import { SOUL_TEMPLATE } from "../../soul-template-content";
11
11
 
12
12
  import { type SessionState, detectTaskMode } from "../types";
@@ -70,7 +70,17 @@ export function resetProfileState(state: SessionState) {
70
70
  // Soul template fetcher
71
71
  // ---------------------------------------------------------------------------
72
72
 
73
+ // Module-level soul cache — the soul template changes only when the user
74
+ // updates ICP/tone/playbook, which is rare. Refetching every turn wastes
75
+ // ~1-3s of API latency. 5-minute TTL matches Anthropic prompt cache TTL.
76
+ let _soulCache: { text: string; fetchedAt: number; key: string } | null = null;
77
+ const SOUL_TTL_MS = 5 * 60 * 1000;
78
+
73
79
  async function fetchSoulTemplate(apiKey?: string): Promise<string> {
80
+ const key = apiKey ?? "";
81
+ if (_soulCache && _soulCache.key === key && Date.now() - _soulCache.fetchedAt < SOUL_TTL_MS) {
82
+ return _soulCache.text;
83
+ }
74
84
  try {
75
85
  const headers: Record<string, string> = {};
76
86
  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
@@ -78,14 +88,25 @@ async function fetchSoulTemplate(apiKey?: string): Promise<string> {
78
88
  headers,
79
89
  signal: AbortSignal.timeout(3000),
80
90
  });
81
- if (!res.ok) return SOUL_TEMPLATE;
91
+ if (!res.ok) {
92
+ _soulCache = { text: SOUL_TEMPLATE, fetchedAt: Date.now(), key };
93
+ return SOUL_TEMPLATE;
94
+ }
82
95
  const { soul } = await res.json();
83
- return typeof soul === "string" && soul.includes(MARKER_START) ? soul : SOUL_TEMPLATE;
96
+ const result = typeof soul === "string" && soul.includes(MARKER_START) ? soul : SOUL_TEMPLATE;
97
+ _soulCache = { text: result, fetchedAt: Date.now(), key };
98
+ return result;
84
99
  } catch {
100
+ _soulCache = { text: SOUL_TEMPLATE, fetchedAt: Date.now(), key };
85
101
  return SOUL_TEMPLATE;
86
102
  }
87
103
  }
88
104
 
105
+ /** Clear the soul template cache. Called by tracking hook when contexts change. */
106
+ export function clearSoulCache(): void {
107
+ _soulCache = null;
108
+ }
109
+
89
110
  // ---------------------------------------------------------------------------
90
111
  // Profile auto-init + daily refresh
91
112
  // ---------------------------------------------------------------------------
@@ -206,12 +227,38 @@ function buildInteractiveContext(
206
227
  // Hook registration
207
228
  // ---------------------------------------------------------------------------
208
229
 
230
+ // Module-level dedup: OpenClaw calls register() with two api instances, both
231
+ // fire before_prompt_build for the same turn. Without dedup, we make double
232
+ // HTTP calls (soul + snapshot) and double-build the context. The inflight
233
+ // promise lets the second hook reuse the first's result.
234
+ let _inflightBuild: { promise: Promise<any>; at: number } | null = null;
235
+
209
236
  export function registerContextHook(api: any, apiKey: string | undefined, state: SessionState) {
210
237
  if (typeof api?.on !== "function") return;
211
238
 
212
239
  api.on(
213
240
  "before_prompt_build",
214
241
  async (promptCtx: any) => {
242
+ try {
243
+ // Dedup: if another hook instance is already building context for this
244
+ // turn (within 200ms), reuse its result instead of duplicating work.
245
+ if (_inflightBuild && Date.now() - _inflightBuild.at < 200) {
246
+ log("context: SKIP duplicate hook (reusing inflight)");
247
+ return _inflightBuild.promise;
248
+ }
249
+ const buildPromise = _buildContext(promptCtx, api, apiKey, state);
250
+ _inflightBuild = { promise: buildPromise, at: Date.now() };
251
+ return buildPromise;
252
+ } catch (err) {
253
+ log(`error: ${errMsg(err)}`);
254
+ return { appendSystemContext: SOUL_TEMPLATE };
255
+ }
256
+ },
257
+ { priority: 5 },
258
+ );
259
+ }
260
+
261
+ async function _buildContext(promptCtx: any, api: any, apiKey: string | undefined, state: SessionState): Promise<any> {
215
262
  try {
216
263
  // Track active sessions so clearAll() at agent_end doesn't wipe cache for concurrent sessions.
217
264
  // Only call once per session — before_prompt_build fires every turn, but sessionEnd is once.
@@ -303,13 +350,11 @@ export function registerContextHook(api: any, apiKey: string | undefined, state:
303
350
  }
304
351
 
305
352
  // INTERACTIVE MODE
306
- // Force-refresh the live snapshot on every turn so the agent never
307
- // reports stale status ("10 active campaigns" while the UI shows all
308
- // paused). One extra /agent/snapshot call per turn is fine — chat turns
309
- // are infrequent and the cost of staleness is a hallucinated reply.
310
- // Task mode keeps the cache because tasks run in isolation and don't
311
- // need mid-run refresh.
312
- invalidateAndRefresh("credits");
353
+ // Cache serves soul + snapshot from memory when fresh (TTL-based).
354
+ // Tool-call tracking already invalidates specific keys on mutation
355
+ // (credits, contexts, pipeline, drafts). No need to force-refresh
356
+ // the entire snapshot every turn it adds 2-3 blocking HTTP calls
357
+ // (~3-4s latency) per message.
313
358
  const [soulTemplate, liveData] = await Promise.all([
314
359
  fetchSoulTemplate(key),
315
360
  getOrFetch(key),
@@ -380,9 +425,6 @@ export function registerContextHook(api: any, apiKey: string | undefined, state:
380
425
  log(`error: ${errMsg(err)}`);
381
426
  return { appendSystemContext: SOUL_TEMPLATE };
382
427
  }
383
- },
384
- { priority: 5 },
385
- );
386
428
  }
387
429
 
388
430
  // ---------------------------------------------------------------------------
@@ -106,7 +106,15 @@ export function registerLifecycleHook(
106
106
  state.onboardingDirectiveInjected = false;
107
107
  state.sessionStarted = false;
108
108
  }
109
- clearAllSafe();
109
+
110
+ // Interactive mode: preserve cache across turns. The same user continues the
111
+ // conversation, so wiping forces 2-3 redundant HTTP calls per turn (~3-4s
112
+ // latency). TTL handles staleness; tool-call tracking invalidates specific
113
+ // keys on mutation. Multi-user key mismatch handled by getOrFetch().
114
+ // Task mode: clear cache so the next task gets fresh data for its campaign.
115
+ if (isTaskMode) {
116
+ clearAllSafe();
117
+ }
110
118
 
111
119
  // Reset per-turn state (always safe to reset)
112
120
  state.toolCallCount = 0;
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { invalidate, invalidateAndRefresh, clearAllSafe } from "./cache";
8
- import { resetProfileState } from "./context";
8
+ import { resetProfileState, clearSoulCache } from "./context";
9
9
  import { FREE_TOOLS, PACED_FREE_TOOLS, type SessionState } from "./types";
10
10
  import {
11
11
  errMsg,
@@ -245,7 +245,7 @@ export function registerTrackingHook(api: any, state: SessionState) {
245
245
 
246
246
  if (CONTACT_MUTATION_TOOLS.has(toolName)) invalidateAndRefresh("pipeline");
247
247
  if (DRAFT_MUTATION_TOOLS.has(toolName)) invalidateAndRefresh("pendingDrafts");
248
- if (toolName === "bereach_context_set" || toolName === "bereach_context_delete") invalidateAndRefresh("contexts");
248
+ if (toolName === "bereach_context_set" || toolName === "bereach_context_delete") { invalidateAndRefresh("contexts"); clearSoulCache(); }
249
249
  if (toolName === "bereach_switch_account") { clearAllSafe(); resetProfileState(state); log("account switch: cleared caches + reset profile state"); }
250
250
  if (CAMPAIGN_MUTATION_TOOLS.has(toolName)) clearAllSafe();
251
251
  } catch (err) {