bereach-openclaw 1.6.8 → 1.6.9

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bereach-openclaw",
3
3
  "name": "BeReach",
4
- "version": "1.6.8",
4
+ "version": "1.6.9",
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.8",
3
+ "version": "1.6.9",
4
4
  "description": "BeReach LinkedIn automation plugin for OpenClaw",
5
5
  "license": "AGPL-3.0",
6
6
  "exports": {
@@ -387,210 +387,69 @@ export function formatLiveStatus(state: SessionState, data: CacheStore, apiKey?:
387
387
  return "";
388
388
  }
389
389
 
390
- const lines: string[] = ["", "## BeReach Live Status", ""];
391
-
392
390
  const onboardingBlock = formatOnboardingDirective(state, data, apiKey);
393
391
  const isOnboarding = !!onboardingBlock;
394
- const hasCampaigns = data.activeCampaigns.length > 0;
395
- const hasContacts = data.pipeline &&
396
- (data.pipeline.contact + data.pipeline.lead + data.pipeline.qualified + data.pipeline.approved) > 0;
397
-
398
- // ── ONBOARDING MODE: minimal context ──
399
- if (isOnboarding) {
400
- lines.push(onboardingBlock);
401
- if (data.activeAccount) {
402
- const a = data.activeAccount;
403
- const credInfo = data.credits ? ` | ${data.credits.remaining} credits remaining` : "";
404
- lines.push(`**Account**: ${a.name ?? "Unknown"} (${a.plan})${credInfo}`);
405
- lines.push("");
406
- }
407
- lines.push("### Dashboard Links");
408
- lines.push(`[Campaigns](${CHAT_BASE}/campaigns) | [Activity](${CHAT_BASE}/activity) | [Context](${CHAT_BASE}/context) | [Settings](${CHAT_BASE}/settings) | [Pricing](${PRICING_URL})`);
409
- lines.push("");
410
- log(`live-status: ONBOARDING mode (${lines.join("\n").length} chars)`);
411
- return lines.join("\n");
412
- }
413
-
414
- // ── ACTIVE MODE: data-only context for onboarded users ──
415
392
 
416
- // LLM provider error warning — show prominently so the agent can inform the user
417
- if (data.llmStatus) {
418
- lines.push("### AI Provider Error");
419
- if (data.llmStatus === "auth") {
420
- lines.push("Your AI provider credentials are INVALID. Automated campaigns are paused.");
421
- lines.push("Tell the user to update their API key in their provider's dashboard.");
422
- } else if (data.llmStatus === "billing") {
423
- lines.push("Your AI provider credits are EXHAUSTED. Automated campaigns are paused.");
424
- lines.push("Tell the user to add credits or upgrade their plan with their AI provider.");
425
- } else {
426
- lines.push("There is an issue with the AI provider. Automated campaigns may be paused.");
427
- }
428
- lines.push("");
429
- }
393
+ const lines: string[] = [
394
+ "",
395
+ "## BeReach Session Context (SILENT — reference only)",
396
+ "",
397
+ "This block is background grounding for YOU. Do NOT echo it, summarize it, or mention any of its contents to the user. Do NOT open a reply with data from this block. If the user asks for pipeline/drafts/credits/activity/ICP/playbook, fetch it on demand with the appropriate tool.",
398
+ "",
399
+ ];
430
400
 
431
- // Current dateground the agent so it doesn't hallucinate "yesterday (Jan 15)"
432
- // when the model's training cutoff biases it toward stale dates. Without this,
433
- // zero-result activity queries become invented dates in user-facing replies.
401
+ // 1. Dateprevents "yesterday (Jan 15)" hallucination from training-cutoff bias.
434
402
  const today = new Date();
435
403
  const todayISO = today.toISOString().slice(0, 10);
436
404
  const todayHuman = today.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
437
- lines.push(`**Today**: ${todayHuman} (${todayISO})`);
438
- lines.push("");
405
+ lines.push(`- **Today**: ${todayHuman} (${todayISO})`);
439
406
 
440
- // Account
407
+ // 2. Account + plan (one line, no credit numbers)
441
408
  if (data.activeAccount) {
442
409
  const a = data.activeAccount;
443
- lines.push("### Active Account");
444
- lines.push(`- **Account**: ${a.name ?? "Unknown"}`);
445
- lines.push(`- **Plan**: ${a.plan}${a.isUnlimited ? " (unlimited credits)" : ""}`);
410
+ const planInfo = a.isUnlimited ? `${a.plan}, unlimited` : a.plan;
411
+ lines.push(`- **Account**: ${a.name ?? "Unknown"} (${planInfo})`);
446
412
  if (data.accounts.length > 1) {
447
- lines.push(`- ${data.accounts.length} accounts. Switch: \`bereach_switch_account\``);
448
- for (const o of data.accounts.filter((acc) => acc.id !== a.id)) {
449
- lines.push(` - ${o.name ?? o.id} (${o.plan})`);
450
- }
451
- }
452
- lines.push("");
453
- }
454
-
455
- // Budget
456
- if (data.credits) {
457
- const c = data.credits;
458
- lines.push("### Budget");
459
- if (c.isUnlimited) {
460
- lines.push(`- **Credits**: ${c.current} used / unlimited`);
461
- } else {
462
- lines.push(`- **Credits**: ${c.current}/${c.limit} used (${c.remaining} remaining, ${c.percentage}%)`);
413
+ lines.push(`- **Other accounts**: ${data.accounts.length - 1} (switch with \`bereach_switch_account\`)`);
463
414
  }
464
- if (data.limits) {
465
- const used: string[] = [];
466
- let hasLimits = false;
467
- for (const [action, info] of Object.entries(data.limits.limits)) {
468
- const d = info.daily;
469
- if (d && d.limit > 0) {
470
- hasLimits = true;
471
- if (d.current > 0) used.push(`${action} ${d.current}/${d.limit}`);
472
- }
473
- }
474
- if (used.length > 0) lines.push(`- **LinkedIn Safety Limits** (daily): ${used.join(" | ")}`);
475
- else if (hasLimits) lines.push(`- **LinkedIn Safety Limits**: all within safe range`);
476
- if (data.limits.multiplier > 1) lines.push(`- **Account Multiplier**: ${data.limits.multiplier}x`);
477
- }
478
-
479
- // DM pacing interval (dynamic value, rule is in soul template)
480
- const dmPacingCtx = data.contexts.find((c) => c.type === "dm_pacing_minutes");
481
- const dmPacingMin = dmPacingCtx ? (parseInt(dmPacingCtx.content, 10) || 5) : 5;
482
- lines.push(`- **DM Pacing**: 1 direct DM every ${dmPacingMin} minutes`);
483
- lines.push("");
484
415
  }
485
416
 
486
- // Pipeline
487
- if (data.pipeline && hasContacts) {
488
- const p = data.pipeline;
489
- const parts = [`Contact ${p.contact}`, `Lead ${p.lead}`, `Qualified ${p.qualified}`, `Approved ${p.approved}`];
490
- if (p.rejected > 0) parts.push(`Rejected ${p.rejected}`);
491
- lines.push(`**Pipeline**: ${parts.join(" | ")}`);
492
- lines.push("");
493
- }
494
-
495
- // Pending state (reference only — NEVER preempt with this data)
496
- const pendingItems: string[] = [];
497
- if (data.pendingDrafts > 0)
498
- pendingItems.push(`${data.pendingDrafts} draft message${data.pendingDrafts > 1 ? "s" : ""} waiting`);
499
- if (data.failedDrafts > 0)
500
- pendingItems.push(`${data.failedDrafts} scheduled message(s) failed`);
501
- if (data.unreadDMs > 0)
502
- pendingItems.push(`${data.unreadDMs} unread LinkedIn message${data.unreadDMs > 1 ? "s" : ""}`);
503
- if (data.pendingSentInvitations > 0)
504
- pendingItems.push(
505
- `${data.pendingSentInvitations} pending sent invitation${data.pendingSentInvitations > 1 ? "s" : ""}`,
506
- );
507
- if (pendingItems.length > 0) {
508
- lines.push("### Pending State (reference only)");
509
- for (const item of pendingItems) lines.push(`- ${item}`);
510
- lines.push("");
511
- lines.push("_Do NOT mention these counts unless the user explicitly asks about drafts, failed sends, unread messages, or invitations. Do NOT say \"you have N drafts waiting\" as an opener or a P.S. to an unrelated question._");
512
- lines.push("");
417
+ // 3. DM pacing — small dynamic value the agent needs to answer pacing questions.
418
+ const dmPacingCtx = data.contexts.find((c) => c.type === "dm_pacing_minutes");
419
+ const dmPacingMin = dmPacingCtx ? (parseInt(dmPacingCtx.content, 10) || 5) : 5;
420
+ lines.push(`- **DM pacing**: 1 direct DM every ${dmPacingMin} minutes`);
421
+
422
+ // 4. Active campaign names ONLY — no ICP, no playbook, no funnel, no pipeline URL.
423
+ // Agent fetches campaign context via `bereach_context_get` when it actually needs it.
424
+ if (data.activeCampaigns.length > 0) {
425
+ const names = data.activeCampaigns
426
+ .slice(0, 10)
427
+ .map((c) => `"${c.name}" (${c.type}, id: ${c.id})`)
428
+ .join(", ");
429
+ const overflow = data.activeCampaigns.length > 10 ? ` + ${data.activeCampaigns.length - 10} more` : "";
430
+ lines.push(`- **Active campaigns (${data.activeCampaigns.length})**: ${names}${overflow}`);
431
+ lines.push(`- To read a campaign's ICP/playbook: \`bereach_context_list({ scope: "campaign:<id>" })\``);
513
432
  }
514
433
 
515
- // User context entries
516
- const MAX_INLINE_CONTEXTS = 10;
517
- const priorityTypes = ["icp", "tone-voice", "playbook", "user-profile"];
518
- const activeId = data.activeAccount?.id;
519
- const meaningful = data.contexts.filter((c) => {
520
- if (!c.content?.trim()) return false;
521
- if (c.scope?.startsWith("campaign:")) return false; // already shown in campaign dispatch
522
- if (c.scope === "user") return true;
523
- if (activeId && c.scope === `account:${activeId}`) return true;
524
- return false;
525
- });
526
-
527
- if (meaningful.length > 0) {
528
- const sorted = [...meaningful].sort((a, b) => {
529
- const aPri = priorityTypes.indexOf(a.type);
530
- const bPri = priorityTypes.indexOf(b.type);
531
- return (aPri === -1 ? 99 : aPri) - (bPri === -1 ? 99 : bPri);
532
- });
533
- const shown = sorted.slice(0, MAX_INLINE_CONTEXTS);
534
- const hidden = meaningful.length - shown.length;
535
-
536
- lines.push("### User Context");
537
- for (const ctx of shown) {
538
- const label = ctx.label ?? ctx.type;
539
- const scopeNote = ctx.scope !== "user" ? ` [${ctx.scope}]` : "";
540
- lines.push(`**${label}${scopeNote}:**`);
541
- lines.push(ctx.content.trim());
542
- lines.push("");
543
- }
544
- if (hidden > 0) {
545
- lines.push(`_${hidden} more context entries available via \`bereach_context_list\`_`);
546
- lines.push("");
547
- }
548
- }
434
+ lines.push("");
549
435
 
550
- // Session recovery
551
- const DONE_PHASES = new Set(["complete", "done", "finished", "completed"]);
552
- const lgState = data.leadGenState as Record<string, unknown> | null;
553
- const orState = data.outreachState as Record<string, unknown> | null;
554
- const lgPhase = String(lgState?.phase ?? lgState?.currentPhase ?? "").toLowerCase();
555
- const orPhase = String(orState?.phase ?? orState?.currentPhase ?? "").toLowerCase();
556
- const lgIncomplete = lgState && lgPhase && !DONE_PHASES.has(lgPhase);
557
- const orIncomplete = orState && orPhase && !DONE_PHASES.has(orPhase);
558
-
559
- if (lgIncomplete || orIncomplete) {
560
- lines.push("### Resume Task");
561
- if (lgIncomplete) {
562
- const found = lgState.totalFound ?? lgState.leadsFound ?? 0;
563
- const target = lgState.target ?? lgState.totalTarget;
564
- lines.push(`- Lead gen: phase=${lgPhase}, ${found}${target ? `/${target}` : ""} found. Load: \`bereach_state_get('lead-gen')\``);
565
- }
566
- if (orIncomplete) {
567
- lines.push(`- Outreach: phase=${orPhase}. Load: \`bereach_state_get('outreach')\``);
436
+ // 5. LLM provider error — critical, keep it (but tight).
437
+ if (data.llmStatus) {
438
+ if (data.llmStatus === "auth") {
439
+ lines.push("**AI provider error**: credentials INVALID, automated campaigns paused. Tell user to update API key.");
440
+ } else if (data.llmStatus === "billing") {
441
+ lines.push("**AI provider error**: credits EXHAUSTED, automated campaigns paused. Tell user to add credits or upgrade.");
442
+ } else {
443
+ lines.push("**AI provider error**: automated campaigns may be paused.");
568
444
  }
569
445
  lines.push("");
570
- } else if (lgState || orState) {
571
- const keys = [lgState && "'lead-gen'", orState && "'outreach'"].filter(Boolean).join(", ");
572
- lines.push(`_State available: ${keys}. Load with \`bereach_state_get(key)\` if needed._`);
573
- lines.push("");
574
446
  }
575
447
 
576
- // Campaign dispatch
577
- const dispatchBlock = formatCampaignDispatch(data.activeCampaigns, data.campaignChecks, data.contexts);
578
- if (dispatchBlock) lines.push(dispatchBlock);
579
-
580
- // Anti-cron directive: campaigns are automated by the task scheduler, not crons
581
- if (hasCampaigns) {
582
- lines.push("**Scheduling**: Campaigns are automated by the task scheduler. Never suggest crons or polling — guide users to campaigns instead.");
583
- lines.push("");
448
+ // 6. Onboarding — only fires when onboardingState.completed === false.
449
+ if (isOnboarding) {
450
+ lines.push(onboardingBlock);
584
451
  }
585
452
 
586
- // Upgrade signals
587
- const upgradeBlock = formatUpgradeSignals(data);
588
- if (upgradeBlock) lines.push(upgradeBlock);
589
-
590
- // Dashboard links
591
- lines.push(`**Links**: [Campaigns](${CHAT_BASE}/campaigns) | [Activity](${CHAT_BASE}/activity) | [Context](${CHAT_BASE}/context) | [Settings](${CHAT_BASE}/settings) | [Pricing](${PRICING_URL})`);
592
- lines.push("");
593
-
594
- log(`live-status: ACTIVE mode, campaigns=${hasCampaigns} contacts=${hasContacts} pending=${data.pendingDrafts > 0 || data.failedDrafts > 0 || data.unreadDMs > 0} (${lines.join("\n").length} chars)`);
453
+ log(`live-status: minimal (${lines.join("\n").length} chars, campaigns=${data.activeCampaigns.length}, onboarding=${isOnboarding})`);
595
454
  return lines.join("\n");
596
455
  }
@@ -23,7 +23,6 @@ import {
23
23
  formatLiveStatus,
24
24
  formatToneInferenceDirective,
25
25
  formatAnthropicKeyWarning,
26
- formatRecentActivity,
27
26
  checkBusinessHours,
28
27
  } from "./formatters";
29
28
  import { buildTaskContext } from "./task-context";
@@ -148,67 +147,52 @@ async function autoInitProfile(state: SessionState, data: CacheStore, apiKey: st
148
147
  // ---------------------------------------------------------------------------
149
148
 
150
149
  /**
151
- * Build interactive context, split into static (cacheable) and dynamic (per-turn) parts.
152
- *
153
- * The OpenClaw gateway treats `appendSystemContext` as provider-cacheable and
154
- * `prependContext` as per-turn. By separating the soul template (static, ~8KB)
155
- * from the live status (dynamic), the gateway can cache the soul template across
156
- * all turns in a session via Anthropic prompt caching — saving ~90% on those tokens.
150
+ * Build the interactive system prompt: soul template + minimal live status.
151
+ * Everything ships via `appendSystemContext` so the gateway caches it as a
152
+ * single system prompt. `prependContext` is not used in interactive mode.
157
153
  */
158
154
  function buildInteractiveContext(
159
155
  state: SessionState,
160
156
  soulTemplate: string,
161
157
  liveData: CacheStore,
162
158
  apiKey: string,
163
- ): { staticContext: string; dynamicContext: string } {
164
- // Live-status block: inject on the FIRST turn of the session only. After
165
- // that, the agent already has the context from turn 1 in the conversation
166
- // history and doesn't need it re-injected on every turn — re-injecting
167
- // every turn was causing the agent to preempt answers with dashboard
168
- // summaries ("you have 12 failed drafts, 24 pending…") instead of answering
169
- // the user's actual question. Same gate applies to recent activity: the
170
- // agent was opening replies with "another reply came in, drafts dropped to
171
- // 11" on every turn. Both blocks are background reference only if the
172
- // user wants an update, they'll ask.
173
- const isFirstTurn = !state.liveStatusInjected;
174
- const liveStatus = isFirstTurn ? formatLiveStatus(state, liveData, apiKey) : "";
175
- const activityBlock = isFirstTurn ? formatRecentActivity(liveData.recentEvents) : "";
176
- if (isFirstTurn) state.liveStatusInjected = true;
177
-
159
+ ): { staticContext: string } {
160
+ // EVERYTHING goes in staticContext appendSystemContext cached system prompt.
161
+ //
162
+ // Previously the live-status block was sent via `prependContext`, which the
163
+ // gateway renders per-turn. Haiku was echoing that block back as its visible
164
+ // reply ("Here's your status: ..."), polluting every conversation turn.
165
+ //
166
+ // The fix has two parts:
167
+ // 1. The live-status block is now MINIMAL (date + account + DM pacing +
168
+ // campaign names + onboarding). Full ICP / playbook / pipeline / drafts /
169
+ // activity / credit details are fetched on demand via tools.
170
+ // 2. It ships via `appendSystemContext` so the gateway treats it as a
171
+ // system instruction (invisible to chat UI, cached across turns) instead
172
+ // of content to echo.
173
+ const liveStatus = formatLiveStatus(state, liveData, apiKey);
178
174
  const toneDirective = formatToneInferenceDirective(state, liveData);
179
175
 
180
- // Static part: soul template with rules, identity, protocols — identical every turn.
181
- // Goes into appendSystemContext so the gateway can cache it.
182
- const staticContext = soulTemplate;
183
-
184
- // Dynamic part: live status (first turn only), activity, tone, warnings.
185
- // Goes into prependContext so it doesn't invalidate the cached soul template.
186
- let dynamicContext = liveStatus + activityBlock;
187
-
188
- if (toneDirective) dynamicContext += toneDirective;
176
+ let staticContext = soulTemplate + liveStatus;
177
+ if (toneDirective) staticContext += toneDirective;
189
178
 
190
179
  if (!state.anthropicKeyWarningInjected) {
191
180
  const anthropicWarning = formatAnthropicKeyWarning();
192
- if (anthropicWarning) dynamicContext += anthropicWarning;
181
+ if (anthropicWarning) staticContext += anthropicWarning;
193
182
  state.anthropicKeyWarningInjected = true;
194
183
  }
195
184
 
196
- const totalLength = staticContext.length + dynamicContext.length;
197
-
198
- // Size guard — log warning but NEVER truncate. User content (ICP, playbook, tone)
199
- // must always be injected in full. Truncating can silently drop critical instructions
200
- // that the agent needs for correct outreach and qualification. The LLM context window
201
- // is large enough to handle the full context in practice.
185
+ const totalLength = staticContext.length;
202
186
  if (totalLength > MAX_CONTEXT_CHARS) {
203
187
  log(`context size WARNING: ${totalLength} chars exceeds ${MAX_CONTEXT_CHARS} soft limit (NOT truncating)`);
204
188
  }
205
189
 
206
190
  const yn = (v: unknown) => (v ? "yes" : "no");
207
191
  const ob = liveData.onboardingState;
208
- log(`context: soul=${staticContext.length} live=${liveStatus.length} tone=${yn(toneDirective)} total=${totalLength} (static=${staticContext.length} dynamic=${dynamicContext.length})`);
209
- log(`sections: account=${yn(liveData.activeAccount)} credits=${yn(liveData.credits)} limits=${yn(liveData.limits)} pipeline=${yn(liveData.pipeline)} contexts=${liveData.contexts.length} campaigns=${liveData.activeCampaigns.length} drafts=${liveData.pendingDrafts} failed=${liveData.failedDrafts} unread=${liveData.unreadDMs} onboarding=${ob == null ? "null" : ob.completed ? "done" : "pending"} firstSession=${yn(!liveData.sessionMeta?.lastSessionAt)}`);
192
+ log(`context: soul=${soulTemplate.length} live=${liveStatus.length} tone=${yn(toneDirective)} total=${totalLength}`);
193
+ log(`sections: account=${yn(liveData.activeAccount)} campaigns=${liveData.activeCampaigns.length} onboarding=${ob == null ? "null" : ob.completed ? "done" : "pending"} firstSession=${yn(!liveData.sessionMeta?.lastSessionAt)}`);
210
194
 
211
- return { staticContext, dynamicContext };
195
+ return { staticContext };
212
196
  }
213
197
 
214
198
  // ---------------------------------------------------------------------------
@@ -345,7 +329,7 @@ export function registerContextHook(api: any, apiKey: string | undefined, state:
345
329
  providerMismatch = fastChanged || creativeChanged;
346
330
  }
347
331
 
348
- const { staticContext, dynamicContext } = buildInteractiveContext(state, soulTemplate, liveData, key);
332
+ const { staticContext } = buildInteractiveContext(state, soulTemplate, liveData, key);
349
333
 
350
334
  if (providerMismatch && !state.providerMismatchWarningInjected) {
351
335
  state.providerMismatchWarningInjected = true;
@@ -370,16 +354,15 @@ export function registerContextHook(api: any, apiKey: string | undefined, state:
370
354
  ].join("\n");
371
355
  log(`provider mismatch detected — injecting /new warning (was fast=${state.initialAiFastModel} now fast=${currentFast})`);
372
356
  return {
373
- appendSystemContext: staticContext,
374
- prependContext: warning + dynamicContext,
357
+ appendSystemContext: staticContext + warning,
375
358
  };
376
359
  }
377
360
 
378
- // appendSystemContext = cached by gateway (soul template, static across turns)
379
- // prependContext = per-turn dynamic data (live status, activity, tone)
361
+ // Everything ships via appendSystemContext (cached system prompt).
362
+ // `prependContext` is intentionally never used in interactive mode — it
363
+ // rendered per-turn and Haiku was echoing it back as its visible reply.
380
364
  return {
381
365
  appendSystemContext: staticContext,
382
- prependContext: dynamicContext,
383
366
  };
384
367
  } catch (err) {
385
368
  log(`error: ${errMsg(err)}`);