solana-traderclaw 1.0.50 → 1.0.56

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.
package/README.md CHANGED
@@ -32,24 +32,31 @@ The plugin gives OpenClaw tools to interact with the Solana trading orchestrator
32
32
 
33
33
  ## Quick Start
34
34
 
35
- ### 1. Install the plugin
35
+ ### 1. Install the plugin (OpenClaw)
36
36
 
37
37
  ```bash
38
- npm install -g solana-traderclaw
38
+ openclaw plugins install solana-traderclaw
39
39
  ```
40
40
 
41
- Or install directly into OpenClaw:
41
+ The `solana-traderclaw` tarball is the **plugin only** (no global `traderclaw` binary), so OpenClaw’s install scanner stays clean.
42
+
43
+ ### 2. Install the global CLI (optional)
44
+
45
+ For `traderclaw --version`, `traderclaw install --wizard`, `traderclaw setup`, etc., install the companion package (it depends on `solana-traderclaw` and wires paths correctly):
42
46
 
43
47
  ```bash
44
- openclaw plugins install solana-traderclaw
48
+ npm install -g traderclaw-cli
45
49
  ```
46
50
 
51
+ That publishes the command name **`traderclaw`** on your PATH. From a git checkout you can also run `node bin/traderclaw.cjs …` at the repo root.
52
+
47
53
  Install name and config id are intentionally different:
48
- - npm package / install command: `solana-traderclaw`
54
+ - npm plugin package: `solana-traderclaw`
55
+ - global CLI package: `traderclaw-cli` (command: `traderclaw`)
49
56
  - OpenClaw plugin id in `openclaw.json`: `solana-trader`
50
57
  - OpenClaw allowlist entry: `plugins.allow: ["solana-trader"]`
51
58
 
52
- ### 2. Run setup
59
+ ### 3. Run setup
53
60
 
54
61
  ```bash
55
62
  traderclaw setup
@@ -84,7 +91,7 @@ traderclaw precheck --allow-install --output linux-qa-install.log
84
91
 
85
92
  Use `--dry-run` for non-mutating validation and `--allow-install` for guided dependency installs.
86
93
 
87
- ### 3. Run the mandatory startup sequence
94
+ ### 4. Run the mandatory startup sequence
88
95
 
89
96
  Send this prompt to your bot after startup:
90
97
 
@@ -135,6 +135,17 @@
135
135
  delivery: { mode: "none" },
136
136
  message: "CRON_JOB: source_trust_refresh\n\nStep 1: Call solana_source_trust_refresh to recalculate trust scores for all tracked alpha sources based on recent trade outcomes.\n\nStep 2: Call solana_deployer_trust_refresh to recalculate deployer trust scores based on recent token performance.\n\nStep 3: Call solana_source_trust_get to read updated scores. Flag any sources that dropped below 30 trust score.\n\nStep 4: Call solana_deployer_trust_get to read deployer scores. Flag any deployers with 3+ failed tokens.\n\nStep 5: Log trust score updates via solana_memory_write with tag 'trust_refresh'. Include: sources updated, deployers updated, flagged sources/deployers.\n\nDo not execute trades. Do not ask questions.",
137
137
  enabled: true
138
+ },
139
+
140
+ // ── Memory Maintenance ──────────────────────────────────────────
141
+ {
142
+ id: "memory-trim",
143
+ schedule: "0 3 * * *",
144
+ agentId: "main",
145
+ sessionTarget: "isolated",
146
+ delivery: { mode: "none" },
147
+ message: "CRON_JOB: memory_trim\n\nStep 1: Call solana_memory_trim with dryRun true first to preview what will be trimmed.\n\nStep 2: Review the dry run summary. Verify no critical data is flagged for removal.\n\nStep 3: Call solana_memory_trim with retentionDays 2 to execute the trim.\n\nStep 4: Log the trim summary via solana_memory_write with tag 'memory_trim'. Include: daily logs deleted, state keys pruned, watchlist entries trimmed, decision entries trimmed, bulletin entries trimmed.\n\nDo not execute trades. Do not ask questions.",
148
+ enabled: true
138
149
  }
139
150
  ]
140
151
  },
@@ -0,0 +1,40 @@
1
+ // src/telegram-resolve.ts
2
+ function looksLikeTelegramChatId(raw) {
3
+ const s = String(raw || "").trim();
4
+ return s.length > 0 && /^-?\d+$/.test(s);
5
+ }
6
+ function normalizeUsernameHandle(raw) {
7
+ const s = String(raw || "").trim();
8
+ if (!s) return "";
9
+ return s.startsWith("@") ? s : `@${s}`;
10
+ }
11
+ async function resolveTelegramRecipientToChatId(opts) {
12
+ const trimmed = String(opts.raw || "").trim();
13
+ if (!trimmed) {
14
+ throw new Error("Telegram recipient is empty");
15
+ }
16
+ if (looksLikeTelegramChatId(trimmed)) {
17
+ return trimmed;
18
+ }
19
+ const token = String(opts.botToken || "").trim();
20
+ if (!token) {
21
+ throw new Error(
22
+ "Set TELEGRAM_BOT_TOKEN (or OPENCLAW_TELEGRAM_BOT_TOKEN) on the gateway to resolve @username to a chat id"
23
+ );
24
+ }
25
+ const chatParam = normalizeUsernameHandle(trimmed);
26
+ const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(chatParam)}`;
27
+ const res = await fetch(url);
28
+ const data = await res.json();
29
+ if (!data.ok || data.result?.id == null) {
30
+ throw new Error(
31
+ data.description || "Telegram getChat failed \u2014 for private chats the user must message your bot first; or use numeric chat id from @userinfobot"
32
+ );
33
+ }
34
+ return String(data.result.id);
35
+ }
36
+
37
+ export {
38
+ looksLikeTelegramChatId,
39
+ resolveTelegramRecipientToChatId
40
+ };
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import {
2
2
  SessionManager
3
3
  } from "./chunk-PZCY6BQK.js";
4
+ import {
5
+ looksLikeTelegramChatId,
6
+ resolveTelegramRecipientToChatId
7
+ } from "./chunk-5RCGTPR3.js";
4
8
  import {
5
9
  normalizeToolError,
6
10
  normalizeToolSuccess,
@@ -775,6 +779,7 @@ function parseConfig(raw) {
775
779
  const agentId = typeof obj.agentId === "string" ? obj.agentId : void 0;
776
780
  const gatewayBaseUrl = typeof obj.gatewayBaseUrl === "string" ? obj.gatewayBaseUrl : void 0;
777
781
  const gatewayToken = typeof obj.gatewayToken === "string" ? obj.gatewayToken : void 0;
782
+ const forwardTelegramRecipient = typeof obj.forwardTelegramRecipient === "string" ? obj.forwardTelegramRecipient : typeof obj.forwardTelegramChatId === "string" ? obj.forwardTelegramChatId : void 0;
778
783
  const dataDir = typeof obj.dataDir === "string" ? obj.dataDir : void 0;
779
784
  const workspaceDir = typeof obj.workspaceDir === "string" ? obj.workspaceDir : void 0;
780
785
  const bootstrapDecisionCount = typeof obj.bootstrapDecisionCount === "number" ? obj.bootstrapDecisionCount : 10;
@@ -797,6 +802,7 @@ function parseConfig(raw) {
797
802
  agentId,
798
803
  gatewayBaseUrl,
799
804
  gatewayToken,
805
+ forwardTelegramRecipient,
800
806
  dataDir,
801
807
  workspaceDir,
802
808
  bootstrapDecisionCount,
@@ -1029,6 +1035,21 @@ var solanaTraderPlugin = {
1029
1035
  onUnauthorized
1030
1036
  });
1031
1037
  };
1038
+ const getTelegramBotTokenFromEnv = () => String(process.env.TELEGRAM_BOT_TOKEN || process.env.OPENCLAW_TELEGRAM_BOT_TOKEN || "").trim();
1039
+ const resolveForwardTelegramDestination = async () => {
1040
+ const raw = String(config.forwardTelegramRecipient || "").trim() || String(process.env.TRADERCLAW_FORWARD_TELEGRAM_RECIPIENT || "").trim() || String(process.env.TRADERCLAW_FORWARD_TELEGRAM_CHAT_ID || "").trim() || String(process.env.OPENCLAW_TELEGRAM_CHAT_ID || "").trim();
1041
+ if (!raw) return "";
1042
+ if (looksLikeTelegramChatId(raw)) return raw;
1043
+ const botToken = getTelegramBotTokenFromEnv();
1044
+ try {
1045
+ return await resolveTelegramRecipientToChatId({ botToken, raw });
1046
+ } catch (e) {
1047
+ api.logger.warn(
1048
+ `[solana-trader] Telegram recipient "${raw}" could not be resolved: ${e instanceof Error ? e.message : String(e)}`
1049
+ );
1050
+ return "";
1051
+ }
1052
+ };
1032
1053
  const json = (data) => ({
1033
1054
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1034
1055
  });
@@ -2059,15 +2080,20 @@ var solanaTraderPlugin = {
2059
2080
  });
2060
2081
  api.registerTool({
2061
2082
  name: "solana_gateway_credentials_set",
2062
- description: "Register or update your OpenClaw Gateway credentials with the orchestrator. This enables event-to-agent forwarding \u2014 when subscriptions include agentId, the orchestrator delivers each stream event to your Gateway via /v1/responses. Call this once during initial setup (Step 0). The gatewayBaseUrl is your self-hosted OpenClaw Gateway's public URL. The gatewayToken is the Bearer token for authenticating forwarded events. Optional forwardTelegramChatId stores your Telegram chat id on this credential row so agent replies are announced to that chat (same row as api_key + agentId). Omit forwardTelegramChatId to leave the stored value unchanged; pass null to clear.",
2083
+ description: "Register or update your OpenClaw Gateway credentials with the orchestrator. This enables event-to-agent forwarding \u2014 when subscriptions include agentId, the orchestrator delivers each stream event to your Gateway via /v1/responses. Call this once during initial setup (Step 0). The gatewayBaseUrl is your self-hosted OpenClaw Gateway's public URL. The gatewayToken is the Bearer token for authenticating forwarded events. Use forwardTelegramRecipient with your @username or numeric chat id \u2014 the plugin resolves usernames via Telegram getChat when TELEGRAM_BOT_TOKEN is set on the gateway. Alternatively pass forwardTelegramChatId (numeric) or null to clear.",
2063
2084
  parameters: Type.Object({
2064
2085
  gatewayBaseUrl: Type.String({ description: "Your OpenClaw Gateway's public HTTPS URL (e.g., 'https://gateway.example.com')" }),
2065
2086
  gatewayToken: Type.String({ description: "Bearer token for authenticating forwarded events to your Gateway" }),
2066
2087
  agentId: Type.Optional(Type.String({ description: "Agent ID to associate credentials with (default: 'main'). Omit to store as the default fallback." })),
2067
2088
  active: Type.Optional(Type.Boolean({ description: "Whether forwarding is active (default: true)" })),
2089
+ forwardTelegramRecipient: Type.Optional(
2090
+ Type.String({
2091
+ description: "Telegram @username or numeric chat id. Resolved to chat id via Bot API when not numeric (requires TELEGRAM_BOT_TOKEN on gateway)."
2092
+ })
2093
+ ),
2068
2094
  forwardTelegramChatId: Type.Optional(
2069
2095
  Type.Union([
2070
- Type.String({ description: "Telegram chat id (digits, optional leading -) for routing agent responses" }),
2096
+ Type.String({ description: "Numeric Telegram chat id (digits, optional leading -)" }),
2071
2097
  Type.Null({ description: "Clear stored Telegram chat id" })
2072
2098
  ])
2073
2099
  )
@@ -2079,7 +2105,16 @@ var solanaTraderPlugin = {
2079
2105
  };
2080
2106
  if (params.agentId) body.agentId = params.agentId;
2081
2107
  if (params.active !== void 0) body.active = params.active;
2082
- if (params.forwardTelegramChatId !== void 0) body.forwardTelegramChatId = params.forwardTelegramChatId;
2108
+ const recipientRaw = params.forwardTelegramRecipient;
2109
+ if (recipientRaw !== void 0 && recipientRaw !== null && String(recipientRaw).trim()) {
2110
+ const id = await resolveTelegramRecipientToChatId({
2111
+ botToken: getTelegramBotTokenFromEnv(),
2112
+ raw: String(recipientRaw)
2113
+ });
2114
+ body.forwardTelegramChatId = id;
2115
+ } else if (params.forwardTelegramChatId !== void 0) {
2116
+ body.forwardTelegramChatId = params.forwardTelegramChatId;
2117
+ }
2083
2118
  return put("/api/agents/gateway-credentials", body);
2084
2119
  })
2085
2120
  });
@@ -2160,26 +2195,45 @@ var solanaTraderPlugin = {
2160
2195
  }
2161
2196
  let gatewayStepOk = false;
2162
2197
  try {
2198
+ const gbu = String(config.gatewayBaseUrl || "").trim();
2199
+ const gt = String(config.gatewayToken || "").trim();
2200
+ const forwardChatId = await resolveForwardTelegramDestination();
2163
2201
  const creds = await get("/api/agents/gateway-credentials");
2164
2202
  let activeCredential = getActiveCredential(creds);
2165
2203
  if (!activeCredential && autoFixGateway) {
2166
- const gbu = String(config.gatewayBaseUrl || "").trim();
2167
- const gt = String(config.gatewayToken || "").trim();
2168
2204
  if (gbu && gt) {
2169
2205
  const body = { gatewayBaseUrl: gbu, gatewayToken: gt, active: true };
2170
2206
  if (config.agentId) body.agentId = config.agentId;
2207
+ if (forwardChatId) body.forwardTelegramChatId = forwardChatId;
2171
2208
  await put("/api/agents/gateway-credentials", body);
2172
2209
  }
2173
2210
  }
2174
- const refreshed = await get("/api/agents/gateway-credentials");
2211
+ let refreshed = await get("/api/agents/gateway-credentials");
2175
2212
  activeCredential = getActiveCredential(refreshed);
2213
+ if (activeCredential && autoFixGateway && gbu && gt && forwardChatId && !activeCredential.forwardTelegramChatId) {
2214
+ const syncBody = {
2215
+ gatewayBaseUrl: gbu,
2216
+ gatewayToken: gt,
2217
+ active: true,
2218
+ forwardTelegramChatId: forwardChatId
2219
+ };
2220
+ if (config.agentId) syncBody.agentId = config.agentId;
2221
+ await put("/api/agents/gateway-credentials", syncBody);
2222
+ refreshed = await get("/api/agents/gateway-credentials");
2223
+ activeCredential = getActiveCredential(refreshed);
2224
+ }
2176
2225
  gatewayStepOk = Boolean(activeCredential);
2177
2226
  if (!gatewayStepOk) throw new Error("Gateway credentials are missing or inactive");
2178
2227
  pushStep({
2179
2228
  step: "solana_gateway_credentials_get",
2180
2229
  ok: true,
2181
2230
  ts: Date.now(),
2182
- details: { active: true, agentId: String(activeCredential?.agentId || config.agentId || "main"), gatewayBaseUrl: String(activeCredential?.gatewayBaseUrl || "") }
2231
+ details: {
2232
+ active: true,
2233
+ agentId: String(activeCredential?.agentId || config.agentId || "main"),
2234
+ gatewayBaseUrl: String(activeCredential?.gatewayBaseUrl || ""),
2235
+ forwardTelegramChatId: activeCredential?.forwardTelegramChatId != null ? String(activeCredential.forwardTelegramChatId) : ""
2236
+ }
2183
2237
  });
2184
2238
  } catch (err) {
2185
2239
  pushStep({
@@ -3020,6 +3074,306 @@ ${String(params.summary)}
3020
3074
  return { ok: true, date: now.toISOString().slice(0, 10), time: timeStr, agent: agentId };
3021
3075
  })
3022
3076
  });
3077
+ api.registerTool({
3078
+ name: "solana_memory_trim",
3079
+ description: "Smart memory compaction \u2014 trims local memory footprint while preserving critical data. Reports bytesFreed/bytesFreedMB. Cleans: daily logs older than retention window, stale state keys, old decision log entries (preserving trade entries/exits for 7 days), old bulletin entries, and stale context snapshots (keeps only newest). NEVER touches: identity/config state keys (tier, walletId, mode, strategyVersion, featureWeights, permanentLearnings, namedPatterns, discoveryFilters, watchlist, consecutiveLosses, totalTrades, winCount, lossCount, peakCapital), skill files, or server-side memory. Designed to run as a daily cron job.",
3080
+ parameters: Type.Object({
3081
+ retentionDays: Type.Optional(Type.Number({ description: "Number of days of data to retain. Default 2." })),
3082
+ dryRun: Type.Optional(Type.Boolean({ description: "If true, report what would be trimmed without actually deleting. Default false." }))
3083
+ }),
3084
+ execute: wrapExecute("solana_memory_trim", async (_id, params) => {
3085
+ const rawDays = typeof params.retentionDays === "number" ? params.retentionDays : 2;
3086
+ const retentionDays = Math.max(1, Math.floor(rawDays));
3087
+ const dryRun = Boolean(params.dryRun);
3088
+ const now = /* @__PURE__ */ new Date();
3089
+ const cutoffMs = now.getTime() - retentionDays * 24 * 60 * 60 * 1e3;
3090
+ const tradeRetentionMs = now.getTime() - 7 * 24 * 60 * 60 * 1e3;
3091
+ const summary = { retentionDays, dryRun, trimmedAt: now.toISOString() };
3092
+ let bytesFreed = 0;
3093
+ let dailyLogsDeleted = 0;
3094
+ try {
3095
+ if (fs.existsSync(memoryDir)) {
3096
+ const files = fs.readdirSync(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
3097
+ for (const file of files) {
3098
+ const dateStr = file.replace(".md", "");
3099
+ if (new Date(dateStr).getTime() < cutoffMs) {
3100
+ const filePath = path.join(memoryDir, file);
3101
+ try {
3102
+ bytesFreed += fs.statSync(filePath).size;
3103
+ } catch {
3104
+ }
3105
+ if (!dryRun) {
3106
+ try {
3107
+ fs.unlinkSync(filePath);
3108
+ } catch {
3109
+ }
3110
+ }
3111
+ dailyLogsDeleted++;
3112
+ }
3113
+ }
3114
+ }
3115
+ } catch {
3116
+ }
3117
+ summary.dailyLogsDeleted = dailyLogsDeleted;
3118
+ const protectedStateKeys = /* @__PURE__ */ new Set([
3119
+ "tier",
3120
+ "walletId",
3121
+ "mode",
3122
+ "strategyVersion",
3123
+ "regime",
3124
+ "maxPositions",
3125
+ "maxPositionSizeSol",
3126
+ "defenseMode",
3127
+ "killSwitchActive",
3128
+ "permanentLearnings",
3129
+ "regimeCanary",
3130
+ "featureWeights",
3131
+ "killSwitchReason",
3132
+ "namedPatterns",
3133
+ "discoveryFilters",
3134
+ "watchlist",
3135
+ "consecutiveLosses",
3136
+ "totalTrades",
3137
+ "winCount",
3138
+ "lossCount",
3139
+ "peakCapital"
3140
+ ]);
3141
+ let stateKeysPruned = 0;
3142
+ let watchlistTrimmed = 0;
3143
+ try {
3144
+ const stateFiles = fs.existsSync(stateDir) ? fs.readdirSync(stateDir).filter((f) => f.endsWith(".json") && f !== "context-snapshot.json" && f !== "patterns.json") : [];
3145
+ for (const sf of stateFiles) {
3146
+ const statePath = path.join(stateDir, sf);
3147
+ const raw = readJsonFile(statePath);
3148
+ if (!raw || !raw.state || typeof raw.state !== "object") continue;
3149
+ const state = raw.state;
3150
+ const keysToRemove = [];
3151
+ for (const key of Object.keys(state)) {
3152
+ if (protectedStateKeys.has(key)) continue;
3153
+ const val = state[key];
3154
+ if (val === null || val === void 0) {
3155
+ keysToRemove.push(key);
3156
+ continue;
3157
+ }
3158
+ if (typeof val === "object" && !Array.isArray(val) && Object.keys(val).length === 0) {
3159
+ keysToRemove.push(key);
3160
+ continue;
3161
+ }
3162
+ if (Array.isArray(val) && val.length === 0) {
3163
+ keysToRemove.push(key);
3164
+ continue;
3165
+ }
3166
+ if (typeof val === "object" && val !== null) {
3167
+ const obj = val;
3168
+ const tsFields = ["ts", "timestamp", "updatedAt", "detectedAt", "lastUpdated", "lastRun"];
3169
+ let foundStaleTs = false;
3170
+ for (const tf of tsFields) {
3171
+ if (typeof obj[tf] === "string") {
3172
+ try {
3173
+ if (new Date(obj[tf]).getTime() < cutoffMs) {
3174
+ foundStaleTs = true;
3175
+ }
3176
+ } catch {
3177
+ }
3178
+ break;
3179
+ }
3180
+ }
3181
+ if (foundStaleTs) {
3182
+ keysToRemove.push(key);
3183
+ continue;
3184
+ }
3185
+ }
3186
+ if (typeof val === "string") {
3187
+ try {
3188
+ const parsed = new Date(val);
3189
+ if (!isNaN(parsed.getTime()) && /^\d{4}-\d{2}-\d{2}/.test(val) && parsed.getTime() < cutoffMs) {
3190
+ keysToRemove.push(key);
3191
+ continue;
3192
+ }
3193
+ } catch {
3194
+ }
3195
+ }
3196
+ }
3197
+ let fileWatchlistTrimmed = 0;
3198
+ if (state.watchlist && Array.isArray(state.watchlist) && state.watchlist.length > 0) {
3199
+ const wl = state.watchlist;
3200
+ const filtered = wl.filter((item) => {
3201
+ if (typeof item === "object" && item !== null) {
3202
+ const obj = item;
3203
+ const ts = obj.addedAt || obj.ts || obj.timestamp;
3204
+ if (typeof ts === "string") {
3205
+ try {
3206
+ return new Date(ts).getTime() >= cutoffMs;
3207
+ } catch {
3208
+ }
3209
+ }
3210
+ }
3211
+ return true;
3212
+ });
3213
+ const capped = filtered.length > 10 ? filtered.slice(-10) : filtered;
3214
+ if (capped.length < wl.length) {
3215
+ fileWatchlistTrimmed = wl.length - capped.length;
3216
+ if (!dryRun) state.watchlist = capped;
3217
+ }
3218
+ }
3219
+ watchlistTrimmed += fileWatchlistTrimmed;
3220
+ if (keysToRemove.length > 0 || fileWatchlistTrimmed > 0) {
3221
+ stateKeysPruned += keysToRemove.length;
3222
+ const sizeBefore = (() => {
3223
+ try {
3224
+ return fs.statSync(statePath).size;
3225
+ } catch {
3226
+ return 0;
3227
+ }
3228
+ })();
3229
+ if (!dryRun) {
3230
+ for (const k of keysToRemove) delete state[k];
3231
+ raw.state = state;
3232
+ raw.updatedAt = now.toISOString();
3233
+ writeJsonFile(statePath, raw);
3234
+ const aid = sf.replace(".json", "");
3235
+ writeMemoryMd(aid, state);
3236
+ const sizeAfter = (() => {
3237
+ try {
3238
+ return fs.statSync(statePath).size;
3239
+ } catch {
3240
+ return 0;
3241
+ }
3242
+ })();
3243
+ bytesFreed += Math.max(0, sizeBefore - sizeAfter);
3244
+ } else {
3245
+ bytesFreed += sizeBefore;
3246
+ }
3247
+ }
3248
+ }
3249
+ } catch {
3250
+ }
3251
+ summary.stateKeysPruned = stateKeysPruned;
3252
+ summary.watchlistTrimmed = watchlistTrimmed;
3253
+ const protectedDecisionTypes = /* @__PURE__ */ new Set(["trade_entry", "trade_exit", "position_update", "killswitch"]);
3254
+ let decisionEntriesTrimmed = 0;
3255
+ try {
3256
+ if (fs.existsSync(logsDir)) {
3257
+ const agentDirs = fs.readdirSync(logsDir).filter((d) => {
3258
+ try {
3259
+ return fs.statSync(path.join(logsDir, d)).isDirectory() && d !== "shared";
3260
+ } catch {
3261
+ return false;
3262
+ }
3263
+ });
3264
+ for (const ad of agentDirs) {
3265
+ const decLogPath = path.join(logsDir, ad, "decisions.jsonl");
3266
+ if (!fs.existsSync(decLogPath)) continue;
3267
+ const entries = readJsonlFile(decLogPath);
3268
+ const kept = entries.filter((e) => {
3269
+ const entryTs = new Date(e.ts).getTime();
3270
+ if (protectedDecisionTypes.has(e.type || "")) return entryTs >= tradeRetentionMs;
3271
+ return entryTs >= cutoffMs;
3272
+ });
3273
+ const removed = entries.length - kept.length;
3274
+ if (removed > 0) {
3275
+ decisionEntriesTrimmed += removed;
3276
+ const sizeBefore = (() => {
3277
+ try {
3278
+ return fs.statSync(decLogPath).size;
3279
+ } catch {
3280
+ return 0;
3281
+ }
3282
+ })();
3283
+ if (!dryRun) {
3284
+ fs.writeFileSync(decLogPath, kept.map((e) => JSON.stringify(e)).join("\n") + (kept.length > 0 ? "\n" : ""), "utf-8");
3285
+ const sizeAfter = (() => {
3286
+ try {
3287
+ return fs.statSync(decLogPath).size;
3288
+ } catch {
3289
+ return 0;
3290
+ }
3291
+ })();
3292
+ bytesFreed += Math.max(0, sizeBefore - sizeAfter);
3293
+ } else {
3294
+ bytesFreed += sizeBefore;
3295
+ }
3296
+ }
3297
+ }
3298
+ }
3299
+ } catch {
3300
+ }
3301
+ summary.decisionEntriesTrimmed = decisionEntriesTrimmed;
3302
+ let bulletinEntriesTrimmed = 0;
3303
+ try {
3304
+ const bulletinPath = path.join(sharedLogsDir, "team-bulletin.jsonl");
3305
+ if (fs.existsSync(bulletinPath)) {
3306
+ const entries = readJsonlFile(bulletinPath);
3307
+ let kept = entries.filter((e) => new Date(e.ts).getTime() >= cutoffMs);
3308
+ if (kept.length < 20 && entries.length >= 20) kept = entries.slice(-20);
3309
+ else if (kept.length < 20) kept = entries;
3310
+ const removed = entries.length - kept.length;
3311
+ if (removed > 0) {
3312
+ bulletinEntriesTrimmed = removed;
3313
+ const sizeBefore = (() => {
3314
+ try {
3315
+ return fs.statSync(bulletinPath).size;
3316
+ } catch {
3317
+ return 0;
3318
+ }
3319
+ })();
3320
+ if (!dryRun) {
3321
+ fs.writeFileSync(bulletinPath, kept.map((e) => JSON.stringify(e)).join("\n") + (kept.length > 0 ? "\n" : ""), "utf-8");
3322
+ const sizeAfter = (() => {
3323
+ try {
3324
+ return fs.statSync(bulletinPath).size;
3325
+ } catch {
3326
+ return 0;
3327
+ }
3328
+ })();
3329
+ bytesFreed += Math.max(0, sizeBefore - sizeAfter);
3330
+ } else {
3331
+ bytesFreed += sizeBefore;
3332
+ }
3333
+ }
3334
+ }
3335
+ } catch {
3336
+ }
3337
+ summary.bulletinEntriesTrimmed = bulletinEntriesTrimmed;
3338
+ let snapshotsDeleted = 0;
3339
+ try {
3340
+ if (fs.existsSync(stateDir)) {
3341
+ const snapshotFiles = fs.readdirSync(stateDir).filter((f) => f.startsWith("context-snapshot") && f.endsWith(".json"));
3342
+ if (snapshotFiles.length > 1) {
3343
+ const sorted = snapshotFiles.map((f) => ({
3344
+ name: f,
3345
+ mtime: (() => {
3346
+ try {
3347
+ return fs.statSync(path.join(stateDir, f)).mtimeMs;
3348
+ } catch {
3349
+ return 0;
3350
+ }
3351
+ })()
3352
+ })).sort((a, b) => b.mtime - a.mtime);
3353
+ for (let i = 1; i < sorted.length; i++) {
3354
+ const fp = path.join(stateDir, sorted[i].name);
3355
+ try {
3356
+ bytesFreed += fs.statSync(fp).size;
3357
+ } catch {
3358
+ }
3359
+ if (!dryRun) {
3360
+ try {
3361
+ fs.unlinkSync(fp);
3362
+ } catch {
3363
+ }
3364
+ }
3365
+ snapshotsDeleted++;
3366
+ }
3367
+ }
3368
+ }
3369
+ } catch {
3370
+ }
3371
+ summary.snapshotsDeleted = snapshotsDeleted;
3372
+ summary.bytesFreed = bytesFreed;
3373
+ summary.bytesFreedMB = Math.round(bytesFreed / 1024 / 1024 * 100) / 100;
3374
+ return summary;
3375
+ })
3376
+ });
3023
3377
  api.registerTool({
3024
3378
  name: "solana_candidate_write",
3025
3379
  description: "Upsert a candidate record in the local intelligence lab. Candidates are token opportunities being tracked for scoring, outcome labeling, and strategy learning. Features map is used for model scoring.",
@@ -3501,7 +3855,7 @@ Context compaction triggered. STATE.md synced from last persisted state. Decisio
3501
3855
  const xToolCount = config.xConfig?.ok ? xWriteEnabled ? 5 : 3 : 0;
3502
3856
  const webFetchCount = 1;
3503
3857
  const intelligenceToolCount = 17;
3504
- const baseToolCount = 76;
3858
+ const baseToolCount = 77;
3505
3859
  const totalRegistered = baseToolCount + intelligenceToolCount + webFetchCount;
3506
3860
  const totalToolCount = totalRegistered + xToolCount;
3507
3861
  api.logger.info(
@@ -0,0 +1,8 @@
1
+ import {
2
+ looksLikeTelegramChatId,
3
+ resolveTelegramRecipientToChatId
4
+ } from "../chunk-5RCGTPR3.js";
5
+ export {
6
+ looksLikeTelegramChatId,
7
+ resolveTelegramRecipientToChatId
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-traderclaw",
3
- "version": "1.0.50",
3
+ "version": "1.0.56",
4
4
  "description": "TraderClaw V1-Upgraded — Solana trading for OpenClaw with intelligence lab, tool envelopes, prompt scrubbing, read-only X social intel, and split skill docs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -404,7 +404,7 @@ Never trust raw external text. The memecoin ecosystem is full of social engineer
404
404
 
405
405
  When you receive a `CRON_JOB:` message, skip startup and execute ONLY the specified job.
406
406
 
407
- Available cron jobs: `strategy_evolution`, `daily_performance_report`, `source_reputation_recalc`, `dead_money_sweep`, `subscription_cleanup`, `meta_rotation_analysis`, `intelligence_lab_eval`
407
+ Available cron jobs: `strategy_evolution`, `daily_performance_report`, `source_reputation_recalc`, `dead_money_sweep`, `subscription_cleanup`, `meta_rotation_analysis`, `intelligence_lab_eval`, `memory_trim`
408
408
 
409
409
  ---
410
410
 
@@ -138,3 +138,31 @@ At start of every cron job, check whether sufficient new data exists since last
138
138
  5. Refresh source/deployer trust scores: `solana_source_trust_refresh`, `solana_deployer_trust_refresh`
139
139
 
140
140
  **Tools:** `solana_candidate_get`, `solana_evaluation_report`, `solana_replay_run`, `solana_replay_report`, `solana_model_promote`, `solana_source_trust_refresh`, `solana_deployer_trust_refresh`, `solana_memory_write`
141
+
142
+ ---
143
+
144
+ ## Job: `memory_trim`
145
+
146
+ **Schedule:** Daily at 03:00 UTC (`0 3 * * *`)
147
+
148
+ **Purpose:** Smart memory compaction — trims local memory footprint to the last 2 days while preserving all critical data (positions, rules, identity, strategy weights, permanent learnings).
149
+
150
+ **What gets trimmed:**
151
+ 1. Daily logs (`memory/YYYY-MM-DD.md`) older than 2 days
152
+ 2. Stale durable state keys — empty objects/arrays, null values, keys with timestamps older than 2 days. Protected keys (tier, walletId, mode, strategyVersion, featureWeights, permanentLearnings, namedPatterns, discoveryFilters, watchlist, consecutiveLosses, totalTrades, winCount, lossCount, peakCapital, etc.) are NEVER removed
153
+ 3. Watchlist entries — stale timestamped entries removed, then capped to most recent 10
154
+ 4. Decision log entries older than 2 days (trade_entry/trade_exit/position_update/killswitch entries retained for 7 days)
155
+ 5. Team bulletin entries older than 2 days (minimum 20 entries always kept)
156
+ 6. Stale context snapshots — only the newest snapshot file is kept, older ones are deleted
157
+
158
+ **Reports:** Summary includes `bytesFreed` and `bytesFreedMB` for all operations
159
+
160
+ **What is NEVER touched:** Identity state keys, skill files, server-side memory, permanentLearnings array, active position data
161
+
162
+ **Workflow:**
163
+ 1. Call `solana_memory_trim` with `dryRun: true` to preview
164
+ 2. Review summary — verify no critical data flagged
165
+ 3. Call `solana_memory_trim` with `retentionDays: 2` to execute
166
+ 4. Log results via `solana_memory_write` with tag `memory_trim`
167
+
168
+ **Tools:** `solana_memory_trim`, `solana_memory_write`
@@ -108,7 +108,7 @@ You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it
108
108
  - Never run a silent cycle. Crypto is 24/7. Every cycle reports.
109
109
 
110
110
  ### Memory maintenance:
111
- `solana_state_save` handles STATE.md updates automatically at session end. Daily logs are written via `solana_daily_log`. Deep memory curation happens via cron jobs periodically.
111
+ `solana_state_save` handles STATE.md updates automatically at session end. Daily logs are written via `solana_daily_log`. Deep memory curation happens via cron jobs periodically. `solana_memory_trim` runs daily at 03:00 UTC via `memory_trim` cron — compacts state, prunes old daily logs, trims stale decision/bulletin entries while preserving all critical data (positions, rules, identity, strategy weights, permanent learnings).
112
112
 
113
113
  ## Isolated Sessions
114
114
 
@@ -13,7 +13,7 @@ Things like:
13
13
  - Rate limit observations
14
14
  - Anything environment-specific
15
15
 
16
- ## Tool Inventory (97 tools — 94 Solana + 3 X read-only)
16
+ ## Tool Inventory (98 tools — 95 Solana + 3 X read-only)
17
17
 
18
18
  Every tool has a mandatory trigger — when the trigger condition is met, you MUST call the tool. "Cron-only" tools are called during cron jobs, not the heartbeat fast loop.
19
19
 
@@ -114,12 +114,13 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
114
114
  | `solana_bitquery_subscriptions` | List active subscriptions | Step 1: check for buffered events; `subscription_cleanup` cron |
115
115
  | `solana_bitquery_subscription_reopen` | Renew expiring subscription | `subscription_cleanup` cron for subscriptions nearing 24h expiry |
116
116
 
117
- ### Memory & State (12)
117
+ ### Memory & State (13)
118
118
  | Tool | Purpose | When to Call |
119
119
  |---|---|---|
120
120
  | `solana_state_save` | Persist durable state + MEMORY.md | Step 8 every heartbeat if state changed |
121
121
  | `solana_state_read` | Read durable state | When MEMORY.md is missing or stale |
122
122
  | `solana_daily_log` | Append to daily log | Step 8 every heartbeat — mandatory |
123
+ | `solana_memory_trim` | Smart memory compaction (daily logs, stale state, old decisions/bulletin, stale snapshots). Reports bytesFreed | `memory_trim` cron daily at 03:00 UTC |
123
124
  | `solana_decision_log` | Log trade decisions | Step 5 before every trade; Step 8 for significant non-trade decisions |
124
125
  | `solana_team_bulletin_post` | Post to team bulletin | Step 8 every heartbeat with tag `position_update` — mandatory |
125
126
  | `solana_team_bulletin_read` | Read team bulletin | Memory context load; when checking multi-agent coordination |