bereach-openclaw 1.5.9 → 1.5.10

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.
@@ -170,7 +170,7 @@ export const PACING = {
170
170
  export const DEFAULTS = {
171
171
  maxVisitsPerSession: 200,
172
172
  engagementThreshold: 5,
173
- contextCacheTtlMs: 5 * 60 * 1000, // 5 minutes
173
+ contextCacheTtlMs: 15 * 60 * 1000, // 15 minutes
174
174
  maxResultItems: 15,
175
175
  maxPostTextLength: 500,
176
176
  } as const;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bereach-openclaw",
3
3
  "name": "BeReach",
4
- "version": "1.5.9",
4
+ "version": "1.5.10",
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.5.9",
3
+ "version": "1.5.10",
4
4
  "description": "BeReach LinkedIn automation plugin for OpenClaw",
5
5
  "license": "AGPL-3.0",
6
6
  "exports": {
@@ -42,7 +42,7 @@ export function enrichResultWithTokens(
42
42
  export async function executeViaWebhook(
43
43
  config: ConnectorConfig,
44
44
  task: NonNullable<PullResponse["task"]>,
45
- ): Promise<{ result: TaskResult | null; error: string | null }> {
45
+ ): Promise<{ result: TaskResult | null; error: string | null; deferred?: boolean }> {
46
46
  const message = task.message || `Execute ${task.type} task`;
47
47
  const maxCredits = (task.payload as Record<string, unknown>)?.maxCredits ?? 100;
48
48
  const timeoutMs = (task.timeoutSeconds || 300) * 1000;
@@ -99,6 +99,17 @@ export async function executeViaWebhook(
99
99
  return { result: null, error: String(output.error).slice(0, 500) };
100
100
  }
101
101
 
102
+ // HTTP 202 Accepted = fire-and-forget. The gateway accepted the task but the
103
+ // agent hasn't run yet. The lifecycle hook (agent_end) inside the gateway will
104
+ // POST the real result (with token usage, contacts processed, etc.) when done.
105
+ // Do NOT submit a premature empty result here - it would win the race against
106
+ // the lifecycle hook and store { success: true } with no data.
107
+ if (res.status === 202) {
108
+ const runId = output.runId ?? "unknown";
109
+ console.log(`[connector] Webhook accepted (202) task ${task.id}, runId=${runId} - deferring result to lifecycle hook`);
110
+ return { result: null, error: null, deferred: true };
111
+ }
112
+
102
113
  const webhookResult = (output.result as TaskResult) ?? { success: true };
103
114
  enrichResultWithTokens(webhookResult, output, task.model);
104
115
  return { result: webhookResult, error: null };
@@ -112,11 +123,13 @@ export async function executeViaWebhook(
112
123
  /**
113
124
  * Execute a task via webhook.
114
125
  * Returns webhookDead=true if the hooks endpoint returned 404 (gateway restart needed).
126
+ * Returns deferred=true if the gateway accepted the task (202) and the lifecycle hook
127
+ * will submit the result — the connector should NOT submit a result in this case.
115
128
  */
116
129
  export async function executeOnOpenClaw(
117
130
  config: ConnectorConfig,
118
131
  task: NonNullable<PullResponse["task"]>,
119
- ): Promise<{ result: TaskResult | null; error: string | null; webhookDead?: boolean }> {
132
+ ): Promise<{ result: TaskResult | null; error: string | null; webhookDead?: boolean; deferred?: boolean }> {
120
133
  const res = await executeViaWebhook(config, task);
121
134
  if (res.error?.startsWith("Webhook HTTP 404")) {
122
135
  console.warn(`[connector] Webhook returned 404 for task ${task.id} — hooks endpoint lost`);
@@ -171,7 +171,7 @@ export async function runConnectorLoop(
171
171
 
172
172
  const execResult = await Promise.race([
173
173
  executeOnOpenClaw(config, task),
174
- new Promise<{ result: null; error: string; webhookDead?: boolean }>((resolve) => {
174
+ new Promise<{ result: null; error: string; webhookDead?: boolean; deferred?: boolean }>((resolve) => {
175
175
  const check = setTimeout(() => {
176
176
  if (watchdogFired) {
177
177
  resolve({ result: null, error: `Watchdog: task execution exceeded ${Math.round(watchdogMs / 1000)}s timeout` });
@@ -198,33 +198,41 @@ export async function runConnectorLoop(
198
198
  }
199
199
  clearTimeout(watchdogTimer);
200
200
 
201
- const taskStatus = error ? "failed" : (result?.success !== false ? "succeeded" : "failed");
202
- const execDuration = Date.now() - (task as any)._startedAt;
203
- console.log(`[connector] Task ${task.id} ${taskStatus} (${Math.round(execDuration / 1000)}s)${error ? `: ${error.slice(0, 100)}` : ""}${result?.reason ? ` reason=${result.reason}` : ""}`);
201
+ // When deferred=true, the gateway accepted the task (HTTP 202) and the
202
+ // lifecycle hook inside the gateway will submit the real result with full
203
+ // data (token usage, contacts processed, etc.). The connector must NOT
204
+ // submit a premature empty result that would win the race.
205
+ if (execResult.deferred) {
206
+ const execDuration = Date.now() - (task as any)._startedAt;
207
+ console.log(`[connector] Task ${task.id} dispatched to gateway (${Math.round(execDuration / 1000)}s) — result deferred to lifecycle hook`);
208
+ } else {
209
+ const taskStatus = error ? "failed" : (result?.success !== false ? "succeeded" : "failed");
210
+ const execDuration = Date.now() - (task as any)._startedAt;
211
+ console.log(`[connector] Task ${task.id} ${taskStatus} (${Math.round(execDuration / 1000)}s)${error ? `: ${error.slice(0, 100)}` : ""}${result?.reason ? ` reason=${result.reason}` : ""}`);
212
+
213
+ if (!error && result?.success === false && (result as any)?.toolCallCount === 0 && execDuration < 10_000) {
214
+ console.warn(`[connector] DIAGNOSTIC: Task ${task.id} failed in <10s with 0 tool calls`);
215
+ }
204
216
 
205
- if (!error && result?.success === false && (result as any)?.toolCallCount === 0 && execDuration < 10_000) {
206
- console.warn(`[connector] DIAGNOSTIC: Task ${task.id} failed in <10s with 0 tool calls`);
207
- }
217
+ // Scan for LLM provider errors
218
+ const allText = [error, result?.error, (result as any)?.reason].filter(Boolean).join(" ");
219
+ const llmError = detectLlmError(allText);
220
+ if (llmError && result && result.success !== false) {
221
+ console.warn(`[connector] LLM provider error detected in task ${task.id}: ${llmError}`);
222
+ result.success = false;
223
+ if (!result.error) result.error = `LLM provider error: ${llmError}`;
224
+ }
208
225
 
209
- // Scan for LLM provider errors
210
- const allText = [error, result?.error, (result as any)?.reason].filter(Boolean).join(" ");
211
- const llmError = detectLlmError(allText);
212
- if (llmError && result && result.success !== false) {
213
- console.warn(`[connector] LLM provider error detected in task ${task.id}: ${llmError}`);
214
- result.success = false;
215
- if (!result.error) result.error = `LLM provider error: ${llmError}`;
226
+ // Submit result only when NOT deferred. When the gateway returns a
227
+ // synchronous result (non-202), the connector acts as a safety net.
228
+ const derivedError = error ?? (
229
+ result?.success === false
230
+ ? (result.error ?? result.reason ?? "Task failed (no details from agent)")
231
+ : undefined
232
+ );
233
+ await submitResult(config, task.id, result, derivedError ?? null);
216
234
  }
217
235
 
218
- // Safety-net submission — lifecycle hook usually reports first,
219
- // but if it fails silently the result endpoint's optimistic lock
220
- // ensures double-submissions are harmless.
221
- const derivedError = error ?? (
222
- result?.success === false
223
- ? (result.error ?? result.reason ?? "Task failed (no details from agent)")
224
- : undefined
225
- );
226
- await submitResult(config, task.id, result, derivedError ?? null);
227
-
228
236
  totalTasksExecuted++;
229
237
  pollInterval = 5_000;
230
238
  } finally {
@@ -6,6 +6,7 @@
6
6
  import { cacheSet, type CacheStore, type ContextEntry, type OnboardingState, type RecentEvent } from "../cache";
7
7
  import { type DbCampaign, type SessionState } from "../types";
8
8
  import { errMsg, createLogger, CHAT_BASE, PRICING_URL, apiFetch } from "../utils";
9
+ import { readEnv } from "../../env";
9
10
 
10
11
  const log = createLogger("context");
11
12
 
@@ -286,10 +287,13 @@ export function formatUpgradeSignals(data: CacheStore): string {
286
287
  // ---------------------------------------------------------------------------
287
288
 
288
289
  export function formatAnthropicKeyWarning(): string {
290
+ const key = readEnv("ANTHROPIC_API_KEY");
291
+ // No key or already subscription → nothing to warn about
292
+ if (!key || key.startsWith("sk-ant-oat")) return "";
289
293
  return (
290
- "\n### Anthropic API Key Check\n\n" +
291
- "Silently check the Anthropic API key prefix: `sk-ant-oat` = subscription (OK, say nothing). " +
292
- "Other prefix = pay-per-use → warn user to switch to subscription key. Skip if key not found.\n"
294
+ "\n### Anthropic API Key Warning\n\n" +
295
+ "The Anthropic API key uses pay-per-use billing (not subscription). " +
296
+ "Recommend the user switch to a subscription key (`sk-ant-oat-*`) to reduce costs.\n"
293
297
  );
294
298
  }
295
299
 
@@ -563,7 +567,7 @@ export function formatLiveStatus(state: SessionState, data: CacheStore, apiKey?:
563
567
 
564
568
  // Anti-cron directive: campaigns are automated by the task scheduler, not crons
565
569
  if (hasCampaigns) {
566
- lines.push("**Scheduling**: Campaign automation is handled by the task scheduler and connector. Do NOT suggest cron-based scheduling, polling setups, or recurring manual actions. If the user asks for crons or polling, guide them to create or adjust campaigns instead.");
570
+ lines.push("**Scheduling**: Campaigns are automated by the task scheduler. Never suggest crons or polling guide users to campaigns instead.");
567
571
  lines.push("");
568
572
  }
569
573
 
@@ -24,17 +24,78 @@ export { parseStructuredResult as _parseStructuredResult };
24
24
  // Token usage enrichment (uses shared cost-estimation package)
25
25
  // ---------------------------------------------------------------------------
26
26
 
27
+ /**
28
+ * Extract cumulative token usage from assistant messages in the agent_end context.
29
+ *
30
+ * OpenClaw v2026.2.x does NOT pass meta.agentMeta to the agent_end hook,
31
+ * but assistant messages in the messages array carry per-turn `.usage` objects
32
+ * with { input_tokens, output_tokens, cache_read_input_tokens, ... }.
33
+ * We sum across all assistant messages to get the session total.
34
+ */
35
+ function extractUsageFromMessages(
36
+ messages: Array<{ role?: string; usage?: Record<string, unknown>; model?: string; provider?: string }>,
37
+ ): { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; model?: string } | null {
38
+ let inputTokens = 0;
39
+ let outputTokens = 0;
40
+ let cacheReadTokens = 0;
41
+ let cacheWriteTokens = 0;
42
+ let model: string | undefined;
43
+
44
+ for (const msg of messages) {
45
+ if (msg.role !== "assistant" || !msg.usage) continue;
46
+ const u = msg.usage as Record<string, number>;
47
+
48
+ // OpenClaw normalizes usage with multiple key conventions
49
+ const inp = u.input_tokens ?? u.inputTokens ?? u.input ?? 0;
50
+ const out = u.output_tokens ?? u.outputTokens ?? u.output ?? 0;
51
+ const cr = u.cache_read_input_tokens ?? u.cached_input_tokens ?? u.cacheRead ?? 0;
52
+ const cw = u.cache_write_input_tokens ?? u.cache_creation_input_tokens ?? u.cacheWrite ?? 0;
53
+
54
+ inputTokens += inp;
55
+ outputTokens += out;
56
+ cacheReadTokens += cr;
57
+ cacheWriteTokens += cw;
58
+
59
+ // Take model from the last assistant message that has one
60
+ if (msg.model) model = msg.model;
61
+ }
62
+
63
+ if (inputTokens === 0 && outputTokens === 0) return null;
64
+ return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, model };
65
+ }
66
+
27
67
  function enrichWithTokenUsage(
28
68
  taskResult: Record<string, unknown>,
29
69
  endCtx: unknown,
30
70
  taskMode: TaskModeInfo,
31
71
  ): void {
32
72
  try {
73
+ // Strategy 1: Try endCtx.meta (works on OpenClaw 2026.4+ if/when they expose agentMeta)
33
74
  const meta = (endCtx as any)?.meta ?? (endCtx as any)?.result?.meta ?? {};
34
- const extracted = extractTokenUsage(meta);
75
+ let extracted = extractTokenUsage(meta);
76
+
77
+ // Strategy 2: Extract from assistant messages' .usage fields
78
+ // OpenClaw v2026.2.x stores per-turn token usage on each assistant message
79
+ // but does not include agentMeta in the agent_end hook context.
80
+ if (!extracted) {
81
+ const allMessages: Array<{ role?: string; usage?: Record<string, unknown>; model?: string }> = (endCtx as any)?.messages ?? [];
82
+ const fromMessages = extractUsageFromMessages(allMessages);
83
+ if (fromMessages) {
84
+ extracted = {
85
+ usage: {
86
+ inputTokens: fromMessages.inputTokens,
87
+ outputTokens: fromMessages.outputTokens,
88
+ ...(fromMessages.cacheReadTokens > 0 ? { cacheReadTokens: fromMessages.cacheReadTokens } : {}),
89
+ ...(fromMessages.cacheWriteTokens > 0 ? { cacheWriteTokens: fromMessages.cacheWriteTokens } : {}),
90
+ },
91
+ model: fromMessages.model,
92
+ };
93
+ log(`token usage: extracted from ${allMessages.filter(m => m.role === "assistant").length} assistant messages for ${taskMode.taskId}`);
94
+ }
95
+ }
35
96
 
36
97
  if (!extracted) {
37
- log(`token usage: no data in endCtx.meta for ${taskMode.taskId} (keys: ${Object.keys(meta).join(",")})`);
98
+ log(`token usage: no data in endCtx.meta or messages for ${taskMode.taskId} (meta keys: ${Object.keys(meta).join(",")})`);
38
99
  return;
39
100
  }
40
101