simmer-automaton 0.6.8 → 0.7.1
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/dist/api.d.ts +28 -1
- package/dist/api.js +23 -2
- package/dist/bandit.d.ts +5 -0
- package/dist/bandit.js +10 -6
- package/dist/index.js +149 -11
- package/package.json +1 -1
- package/src/api.ts +40 -2
- package/src/bandit.ts +12 -5
- package/src/index.ts +165 -13
package/dist/api.d.ts
CHANGED
|
@@ -99,9 +99,16 @@ export interface BriefingResponse {
|
|
|
99
99
|
latest: string;
|
|
100
100
|
message: string;
|
|
101
101
|
} | null;
|
|
102
|
+
skill_updates: Array<{
|
|
103
|
+
slug: string;
|
|
104
|
+
current: string;
|
|
105
|
+
latest: string;
|
|
106
|
+
message: string;
|
|
107
|
+
}> | null;
|
|
102
108
|
}
|
|
103
109
|
export interface SkillOutcome {
|
|
104
110
|
skill_slug: string;
|
|
111
|
+
venue: string;
|
|
105
112
|
trades: number;
|
|
106
113
|
total_cost: number;
|
|
107
114
|
period_pnl: number;
|
|
@@ -155,7 +162,7 @@ export declare class SimmerApi {
|
|
|
155
162
|
outcomes: SkillOutcome[];
|
|
156
163
|
since: string;
|
|
157
164
|
}>;
|
|
158
|
-
getBriefing(): Promise<BriefingResponse>;
|
|
165
|
+
getBriefing(skillVersions?: Record<string, string>): Promise<BriefingResponse>;
|
|
159
166
|
getPositions(): Promise<{
|
|
160
167
|
positions: PositionsPosition[];
|
|
161
168
|
}>;
|
|
@@ -174,9 +181,29 @@ export declare class SimmerApi {
|
|
|
174
181
|
new: any;
|
|
175
182
|
reason: string;
|
|
176
183
|
}>;
|
|
184
|
+
epsilon?: number;
|
|
185
|
+
bandit_arms?: BanditArmState[];
|
|
177
186
|
}): Promise<{
|
|
178
187
|
ok: boolean;
|
|
179
188
|
active_skills: string[];
|
|
180
189
|
cycle_number: number;
|
|
181
190
|
}>;
|
|
191
|
+
getBanditState(): Promise<{
|
|
192
|
+
epsilon: number;
|
|
193
|
+
cycle_count: number;
|
|
194
|
+
arms: BanditArmState[];
|
|
195
|
+
} | null>;
|
|
196
|
+
resetBanditState(): Promise<void>;
|
|
197
|
+
}
|
|
198
|
+
/** Serialized form of a bandit arm written to / read from the API. */
|
|
199
|
+
export interface BanditArmState {
|
|
200
|
+
slug: string;
|
|
201
|
+
enabled: boolean;
|
|
202
|
+
timesSelected: number;
|
|
203
|
+
timesRewarded: number;
|
|
204
|
+
totalPnl: number;
|
|
205
|
+
consecutiveZeroSignals: number;
|
|
206
|
+
signalsFoundTotal: number;
|
|
207
|
+
tradesExecutedTotal: number;
|
|
208
|
+
ewmRoc: number;
|
|
182
209
|
}
|
package/dist/api.js
CHANGED
|
@@ -70,8 +70,13 @@ export class SimmerApi {
|
|
|
70
70
|
const params = new URLSearchParams({ since });
|
|
71
71
|
return this.request(`/api/sdk/automaton/outcomes?${params}`);
|
|
72
72
|
}
|
|
73
|
-
async getBriefing() {
|
|
74
|
-
|
|
73
|
+
async getBriefing(skillVersions) {
|
|
74
|
+
let url = "/api/sdk/briefing";
|
|
75
|
+
if (skillVersions && Object.keys(skillVersions).length > 0) {
|
|
76
|
+
const params = new URLSearchParams({ skill_versions: JSON.stringify(skillVersions) });
|
|
77
|
+
url += `?${params}`;
|
|
78
|
+
}
|
|
79
|
+
return this.request(url);
|
|
75
80
|
}
|
|
76
81
|
async getPositions() {
|
|
77
82
|
return this.request("/api/sdk/positions?status=active");
|
|
@@ -82,4 +87,20 @@ export class SimmerApi {
|
|
|
82
87
|
body: JSON.stringify(data),
|
|
83
88
|
});
|
|
84
89
|
}
|
|
90
|
+
async getBanditState() {
|
|
91
|
+
try {
|
|
92
|
+
return await this.request("/api/sdk/automaton/bandit-state");
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async resetBanditState() {
|
|
99
|
+
try {
|
|
100
|
+
await this.request("/api/sdk/automaton/bandit-state", { method: "DELETE" });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Best-effort — don't block re-init if this fails
|
|
104
|
+
}
|
|
105
|
+
}
|
|
85
106
|
}
|
package/dist/bandit.d.ts
CHANGED
|
@@ -11,10 +11,15 @@ export interface SkillState {
|
|
|
11
11
|
consecutiveZeroSignals: number;
|
|
12
12
|
signalsFoundTotal: number;
|
|
13
13
|
tradesExecutedTotal: number;
|
|
14
|
+
/** Exponentially weighted return-on-capital (period_pnl / total_cost per cycle).
|
|
15
|
+
* Alpha=0.1 gives ~7-cycle half-life. Primary bandit arm score. */
|
|
16
|
+
ewmRoc: number;
|
|
14
17
|
lastCycle?: {
|
|
15
18
|
skipCounts?: Record<string, number>;
|
|
16
19
|
};
|
|
17
20
|
}
|
|
21
|
+
/** EWM decay factor. Alpha=0.1 → ~7-cycle half-life. */
|
|
22
|
+
export declare const EWM_ALPHA = 0.1;
|
|
18
23
|
export interface SelectionMeta {
|
|
19
24
|
slug: string;
|
|
20
25
|
reason: string;
|
package/dist/bandit.js
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* Epsilon-greedy bandit for skill selection.
|
|
3
3
|
* Ported from automaton.py — select_skills, _avg_reward, tier_max_skills, tier_effective_epsilon.
|
|
4
4
|
*/
|
|
5
|
+
/** EWM decay factor. Alpha=0.1 → ~7-cycle half-life. */
|
|
6
|
+
export const EWM_ALPHA = 0.1;
|
|
5
7
|
function avgReward(skill) {
|
|
6
8
|
if (skill.timesSelected === 0)
|
|
7
9
|
return Infinity;
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// Primary: exponentially weighted return-on-capital. Normalizes for position
|
|
11
|
+
// size and decays stale regime data (~7-cycle half-life at alpha=0.1).
|
|
12
|
+
if (skill.ewmRoc !== 0)
|
|
13
|
+
return skill.ewmRoc;
|
|
14
|
+
// Bootstrap: a skill that executes trades at all beats one that finds nothing.
|
|
15
|
+
if (skill.tradesExecutedTotal > 0)
|
|
16
|
+
return 0.001;
|
|
17
|
+
return 0;
|
|
14
18
|
}
|
|
15
19
|
export function tierMaxSkills(tier, maxConcurrent) {
|
|
16
20
|
if (tier === "thriving" || tier === "normal")
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* API enforces hard spending limits on every trade.
|
|
8
8
|
*/
|
|
9
9
|
import { SimmerApi } from "./api.js";
|
|
10
|
-
import { selectSkills, tierMaxSkills } from "./bandit.js";
|
|
10
|
+
import { selectSkills, tierMaxSkills, EWM_ALPHA } from "./bandit.js";
|
|
11
11
|
import { computeTier } from "./tiers.js";
|
|
12
12
|
import { generateTuningHints, computeTuningChanges } from "./tuning.js";
|
|
13
13
|
// Plugin-local state (in-memory, refreshed from API each cycle)
|
|
@@ -18,6 +18,9 @@ let cachedSkills = [];
|
|
|
18
18
|
let banditState = [];
|
|
19
19
|
let currentTier = "normal";
|
|
20
20
|
let cycleCount = 0;
|
|
21
|
+
let lastUpdateCheckCycle = 0;
|
|
22
|
+
const UPDATE_CHECK_INTERVAL = 50; // cycles between version checks
|
|
23
|
+
const INSTALL_TIMEOUT_MS = 30_000; // 30s per skill install
|
|
21
24
|
let currentSelectedMeta = [];
|
|
22
25
|
let lastExecutionResults = [];
|
|
23
26
|
let cachedPortfolio = null;
|
|
@@ -34,7 +37,7 @@ let config = {
|
|
|
34
37
|
epsilonDecay: 0.995,
|
|
35
38
|
minEpsilon: 0.05,
|
|
36
39
|
maxConcurrent: 2,
|
|
37
|
-
venue: "
|
|
40
|
+
venue: "sim",
|
|
38
41
|
};
|
|
39
42
|
function loadConfig(pluginConfig) {
|
|
40
43
|
if (!pluginConfig)
|
|
@@ -75,9 +78,12 @@ async function refreshState(logger) {
|
|
|
75
78
|
if (lastKnownStartedAt !== null) {
|
|
76
79
|
logger.info(`[simmer] Detected re-init (started_at changed) — resetting cycle counter and bandit state`);
|
|
77
80
|
cycleCount = 0;
|
|
81
|
+
config.epsilon = 0.2;
|
|
78
82
|
banditState = [];
|
|
79
83
|
currentSelectedMeta = [];
|
|
80
84
|
lastCycleTimestamp = new Date().toISOString();
|
|
85
|
+
// Delete persisted bandit state so the next startup doesn't restore stale arms
|
|
86
|
+
api.resetBanditState().catch((e) => logger.warn(`[simmer] Failed to reset bandit state: ${e}`));
|
|
81
87
|
}
|
|
82
88
|
lastKnownStartedAt = cachedState.started_at;
|
|
83
89
|
}
|
|
@@ -102,6 +108,7 @@ async function refreshState(logger) {
|
|
|
102
108
|
timesSelected: 0,
|
|
103
109
|
timesRewarded: 0,
|
|
104
110
|
totalPnl: 0,
|
|
111
|
+
ewmRoc: 0,
|
|
105
112
|
consecutiveZeroSignals: 0,
|
|
106
113
|
signalsFoundTotal: 0,
|
|
107
114
|
tradesExecutedTotal: 0,
|
|
@@ -115,7 +122,7 @@ async function refreshState(logger) {
|
|
|
115
122
|
}
|
|
116
123
|
function fmtCurrency(amount) {
|
|
117
124
|
const val = amount.toFixed(2);
|
|
118
|
-
return config.venue === "simmer" ? `${val} $SIM` : `$${val}`;
|
|
125
|
+
return (config.venue === "sim" || config.venue === "simmer") ? `${val} $SIM` : `$${val}`;
|
|
119
126
|
}
|
|
120
127
|
function buildPromptContext() {
|
|
121
128
|
if (!cachedState || !cachedState.initialized) {
|
|
@@ -216,9 +223,73 @@ function formatStatus() {
|
|
|
216
223
|
`Cycles: ${cycleCount}`,
|
|
217
224
|
`Skills available: ${cachedSkills.length}`,
|
|
218
225
|
];
|
|
226
|
+
if (cachedPortfolio) {
|
|
227
|
+
const pnlStr = cachedPortfolio.totalPnl >= 0
|
|
228
|
+
? `+${fmtCurrency(cachedPortfolio.totalPnl)}`
|
|
229
|
+
: fmtCurrency(cachedPortfolio.totalPnl);
|
|
230
|
+
lines.push(`Positions: ${cachedPortfolio.positionCount} open | P&L: ${pnlStr} | Recent trades: ${cachedPortfolio.recentTradeCount}`);
|
|
231
|
+
const top = cachedPortfolio.positions.slice(0, 3);
|
|
232
|
+
for (const p of top) {
|
|
233
|
+
const pnl = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
|
|
234
|
+
lines.push(` · ${p.question.slice(0, 55)} | ${p.side} | ${pnl}`);
|
|
235
|
+
}
|
|
236
|
+
if (cachedPortfolio.positions.length > 3) {
|
|
237
|
+
lines.push(` · ...and ${cachedPortfolio.positions.length - 3} more (use /simmer portfolio for full list)`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
219
240
|
return lines.join("\n");
|
|
220
241
|
}
|
|
221
242
|
// =============================================================================
|
|
243
|
+
// Skill auto-update — keep installed skills at latest ClawHub version
|
|
244
|
+
// =============================================================================
|
|
245
|
+
/**
|
|
246
|
+
* Read installed skill versions from SKILL.md frontmatter.
|
|
247
|
+
* Returns { slug: version } for skills that have a parseable version.
|
|
248
|
+
*/
|
|
249
|
+
function getLocalSkillVersions(workspaceDir) {
|
|
250
|
+
const fs = require("fs");
|
|
251
|
+
const cwd = workspaceDir || process.cwd();
|
|
252
|
+
const versions = {};
|
|
253
|
+
for (const skill of cachedSkills) {
|
|
254
|
+
const slug = skill.id;
|
|
255
|
+
try {
|
|
256
|
+
const skillMd = fs.readFileSync(`${cwd}/skills/${slug}/SKILL.md`, "utf-8");
|
|
257
|
+
const versionMatch = skillMd.match(/^\s*version:\s*["']?([^"'\n]+)["']?/m);
|
|
258
|
+
if (versionMatch) {
|
|
259
|
+
versions[slug] = versionMatch[1].trim();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Skill not installed locally or no SKILL.md — skip
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return versions;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Check for outdated skills and auto-install updates.
|
|
270
|
+
* Uses the skill_updates from the most recent briefing response.
|
|
271
|
+
*/
|
|
272
|
+
async function checkAndUpdateSkills(skillUpdates, workspaceDir, logger) {
|
|
273
|
+
if (!runtime || skillUpdates.length === 0)
|
|
274
|
+
return;
|
|
275
|
+
const cwd = workspaceDir || process.cwd();
|
|
276
|
+
logger.info(`[simmer] ${skillUpdates.length} skill update(s) available: ${skillUpdates.map((u) => `${u.slug} ${u.current}→${u.latest}`).join(", ")}`);
|
|
277
|
+
for (const update of skillUpdates) {
|
|
278
|
+
try {
|
|
279
|
+
const result = await runtime.system.runCommandWithTimeout(["npx", "clawhub@latest", "install", update.slug, "--force"], { timeoutMs: INSTALL_TIMEOUT_MS, cwd });
|
|
280
|
+
if (result.code === 0) {
|
|
281
|
+
logger.info(`[simmer] Updated ${update.slug} to ${update.latest}`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
logger.warn(`[simmer] Failed to update ${update.slug}: exit ${result.code} — ${result.stderr.slice(0, 200)}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
logger.warn(`[simmer] Failed to update ${update.slug}: ${e}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// =============================================================================
|
|
222
293
|
// Skill execution — deterministic, no LLM in the loop
|
|
223
294
|
// =============================================================================
|
|
224
295
|
async function executeSkills(selectedSlugs, workspaceDir, logger) {
|
|
@@ -324,8 +395,49 @@ export default function register(pluginApi) {
|
|
|
324
395
|
start: async (ctx) => {
|
|
325
396
|
ctx.logger.info("[simmer] Automaton service starting");
|
|
326
397
|
serviceRunning = true;
|
|
327
|
-
// Initial state fetch
|
|
398
|
+
// Initial state fetch (seeds banditState with current skills)
|
|
328
399
|
await refreshState(ctx.logger);
|
|
400
|
+
// Restore persisted bandit arm statistics — overwrite in-memory state seeded above.
|
|
401
|
+
// This lets the bandit resume learning after a plugin restart without starting cold.
|
|
402
|
+
try {
|
|
403
|
+
const saved = await api.getBanditState();
|
|
404
|
+
if (saved && saved.arms.length > 0) {
|
|
405
|
+
const savedBySlug = new Map(saved.arms.map((a) => [a.slug, a]));
|
|
406
|
+
banditState = banditState.map((arm) => {
|
|
407
|
+
const s = savedBySlug.get(arm.slug);
|
|
408
|
+
if (s) {
|
|
409
|
+
// Restore learned stats; keep live enabled status from skills registry
|
|
410
|
+
return {
|
|
411
|
+
...s,
|
|
412
|
+
enabled: arm.enabled,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return arm; // new skill not in saved state — start fresh
|
|
416
|
+
});
|
|
417
|
+
config.epsilon = saved.epsilon;
|
|
418
|
+
cycleCount = saved.cycle_count;
|
|
419
|
+
ctx.logger.info(`[simmer] Restored bandit state: ${saved.arms.length} arms, ε=${saved.epsilon.toFixed(3)}, cycle=${saved.cycle_count}`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
ctx.logger.info("[simmer] No saved bandit state — starting fresh");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (e) {
|
|
426
|
+
ctx.logger.warn(`[simmer] Failed to restore bandit state: ${e}`);
|
|
427
|
+
}
|
|
428
|
+
// Check for skill updates on startup
|
|
429
|
+
try {
|
|
430
|
+
const skillVersions = getLocalSkillVersions(ctx.workspaceDir);
|
|
431
|
+
if (Object.keys(skillVersions).length > 0) {
|
|
432
|
+
const briefing = await api.getBriefing(skillVersions);
|
|
433
|
+
if (briefing.skill_updates && briefing.skill_updates.length > 0) {
|
|
434
|
+
await checkAndUpdateSkills(briefing.skill_updates, ctx.workspaceDir, ctx.logger);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
ctx.logger.warn(`[simmer] Startup skill update check failed: ${e}`);
|
|
440
|
+
}
|
|
329
441
|
// Periodic refresh
|
|
330
442
|
cycleTimer = setInterval(async () => {
|
|
331
443
|
if (!serviceRunning)
|
|
@@ -336,20 +448,26 @@ export default function register(pluginApi) {
|
|
|
336
448
|
lastCycleTimestamp = new Date().toISOString();
|
|
337
449
|
await refreshState(ctx.logger);
|
|
338
450
|
// Query outcomes since last cycle and update bandit reward data
|
|
451
|
+
// Filter to outcomes matching this plugin's configured venue
|
|
339
452
|
try {
|
|
340
453
|
const outcomeRes = await api.getOutcomes(cycleStarted);
|
|
341
|
-
|
|
454
|
+
const venueOutcomes = outcomeRes.outcomes.filter((o) => o.venue === config.venue);
|
|
455
|
+
for (const o of venueOutcomes) {
|
|
342
456
|
const skill = banditState.find((s) => s.slug === o.skill_slug);
|
|
343
457
|
if (skill) {
|
|
344
458
|
skill.tradesExecutedTotal += o.trades;
|
|
345
|
-
|
|
459
|
+
// Return-on-capital for this cycle (0 if no cost data)
|
|
460
|
+
const roc = o.total_cost > 0 ? o.period_pnl / o.total_cost : 0;
|
|
461
|
+
// Exponentially weighted RoC — decays stale data (~7-cycle half-life)
|
|
462
|
+
skill.ewmRoc = EWM_ALPHA * roc + (1 - EWM_ALPHA) * skill.ewmRoc;
|
|
463
|
+
skill.timesRewarded += roc > 0 ? 1 : 0;
|
|
346
464
|
skill.totalPnl += o.period_pnl;
|
|
347
465
|
skill.consecutiveZeroSignals = o.trades > 0 ? 0 : skill.consecutiveZeroSignals + 1;
|
|
348
466
|
}
|
|
349
467
|
}
|
|
350
468
|
// Skills that were selected last cycle but had zero trades won't appear in outcomes
|
|
351
469
|
// (GROUP BY only returns rows with trades). Increment consecutiveZeroSignals for them.
|
|
352
|
-
const outcomeSkillSlugs = new Set(
|
|
470
|
+
const outcomeSkillSlugs = new Set(venueOutcomes.map((o) => o.skill_slug));
|
|
353
471
|
for (const m of currentSelectedMeta) {
|
|
354
472
|
if (!outcomeSkillSlugs.has(m.slug)) {
|
|
355
473
|
const skill = banditState.find((s) => s.slug === m.slug);
|
|
@@ -358,8 +476,8 @@ export default function register(pluginApi) {
|
|
|
358
476
|
}
|
|
359
477
|
}
|
|
360
478
|
}
|
|
361
|
-
if (
|
|
362
|
-
ctx.logger.info(`[simmer] Outcomes: ${
|
|
479
|
+
if (venueOutcomes.length > 0) {
|
|
480
|
+
ctx.logger.info(`[simmer] Outcomes (${config.venue}): ${venueOutcomes.map((o) => `${o.skill_slug}:${o.trades}t/$${o.period_pnl.toFixed(2)}pnl`).join(", ")}`);
|
|
363
481
|
}
|
|
364
482
|
}
|
|
365
483
|
catch (e) {
|
|
@@ -403,6 +521,19 @@ export default function register(pluginApi) {
|
|
|
403
521
|
cycle_number: thisCycleNumber,
|
|
404
522
|
selection_meta: meta.map((m) => ({ slug: m.slug, reason: m.reason, score: m.score })),
|
|
405
523
|
config_changes: configChangesPayload,
|
|
524
|
+
// Bandit persistence — write current arm stats so restarts resume where we left off
|
|
525
|
+
epsilon: parseFloat(config.epsilon.toFixed(6)),
|
|
526
|
+
bandit_arms: banditState.map((s) => ({
|
|
527
|
+
slug: s.slug,
|
|
528
|
+
enabled: s.enabled,
|
|
529
|
+
timesSelected: s.timesSelected,
|
|
530
|
+
timesRewarded: s.timesRewarded,
|
|
531
|
+
totalPnl: s.totalPnl,
|
|
532
|
+
consecutiveZeroSignals: s.consecutiveZeroSignals,
|
|
533
|
+
signalsFoundTotal: s.signalsFoundTotal,
|
|
534
|
+
tradesExecutedTotal: s.tradesExecutedTotal,
|
|
535
|
+
ewmRoc: s.ewmRoc,
|
|
536
|
+
})),
|
|
406
537
|
});
|
|
407
538
|
}
|
|
408
539
|
catch (e) {
|
|
@@ -423,8 +554,9 @@ export default function register(pluginApi) {
|
|
|
423
554
|
}
|
|
424
555
|
// Fetch portfolio snapshot for prompt context
|
|
425
556
|
try {
|
|
426
|
-
const
|
|
427
|
-
const
|
|
557
|
+
const skillVersions = getLocalSkillVersions(ctx.workspaceDir);
|
|
558
|
+
const briefing = await api.getBriefing(skillVersions);
|
|
559
|
+
const venue = briefing.venues?.[config.venue];
|
|
428
560
|
const attentionPositions = venue?.positions_needing_attention || [];
|
|
429
561
|
cachedPortfolio = {
|
|
430
562
|
totalPnl: briefing.performance?.total_pnl ?? 0,
|
|
@@ -432,6 +564,12 @@ export default function register(pluginApi) {
|
|
|
432
564
|
positions: attentionPositions,
|
|
433
565
|
recentTradeCount: 0,
|
|
434
566
|
};
|
|
567
|
+
// Periodic skill auto-update
|
|
568
|
+
if (briefing.skill_updates && briefing.skill_updates.length > 0 &&
|
|
569
|
+
thisCycleNumber - lastUpdateCheckCycle >= UPDATE_CHECK_INTERVAL) {
|
|
570
|
+
lastUpdateCheckCycle = thisCycleNumber;
|
|
571
|
+
await checkAndUpdateSkills(briefing.skill_updates, ctx.workspaceDir, ctx.logger);
|
|
572
|
+
}
|
|
435
573
|
}
|
|
436
574
|
catch (e) {
|
|
437
575
|
ctx.logger.warn(`[simmer] Failed to fetch briefing: ${e}`);
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -99,10 +99,12 @@ export interface BriefingResponse {
|
|
|
99
99
|
};
|
|
100
100
|
checked_at: string;
|
|
101
101
|
sdk_update: { current: string; latest: string; message: string } | null;
|
|
102
|
+
skill_updates: Array<{ slug: string; current: string; latest: string; message: string }> | null;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export interface SkillOutcome {
|
|
105
106
|
skill_slug: string;
|
|
107
|
+
venue: string;
|
|
106
108
|
trades: number;
|
|
107
109
|
total_cost: number;
|
|
108
110
|
period_pnl: number;
|
|
@@ -201,8 +203,13 @@ export class SimmerApi {
|
|
|
201
203
|
return this.request(`/api/sdk/automaton/outcomes?${params}`);
|
|
202
204
|
}
|
|
203
205
|
|
|
204
|
-
async getBriefing(): Promise<BriefingResponse> {
|
|
205
|
-
|
|
206
|
+
async getBriefing(skillVersions?: Record<string, string>): Promise<BriefingResponse> {
|
|
207
|
+
let url = "/api/sdk/briefing";
|
|
208
|
+
if (skillVersions && Object.keys(skillVersions).length > 0) {
|
|
209
|
+
const params = new URLSearchParams({ skill_versions: JSON.stringify(skillVersions) });
|
|
210
|
+
url += `?${params}`;
|
|
211
|
+
}
|
|
212
|
+
return this.request(url);
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
async getPositions(): Promise<{ positions: PositionsPosition[] }> {
|
|
@@ -214,10 +221,41 @@ export class SimmerApi {
|
|
|
214
221
|
cycle_number: number;
|
|
215
222
|
selection_meta: Array<{ slug: string; reason: string; score: number | null }>;
|
|
216
223
|
config_changes: Array<{ slug: string; env: string; old: any; new: any; reason: string }>;
|
|
224
|
+
epsilon?: number;
|
|
225
|
+
bandit_arms?: BanditArmState[];
|
|
217
226
|
}): Promise<{ ok: boolean; active_skills: string[]; cycle_number: number }> {
|
|
218
227
|
return this.request("/api/sdk/automaton/cycle", {
|
|
219
228
|
method: "POST",
|
|
220
229
|
body: JSON.stringify(data),
|
|
221
230
|
});
|
|
222
231
|
}
|
|
232
|
+
|
|
233
|
+
async getBanditState(): Promise<{ epsilon: number; cycle_count: number; arms: BanditArmState[] } | null> {
|
|
234
|
+
try {
|
|
235
|
+
return await this.request("/api/sdk/automaton/bandit-state");
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async resetBanditState(): Promise<void> {
|
|
242
|
+
try {
|
|
243
|
+
await this.request("/api/sdk/automaton/bandit-state", { method: "DELETE" });
|
|
244
|
+
} catch {
|
|
245
|
+
// Best-effort — don't block re-init if this fails
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Serialized form of a bandit arm written to / read from the API. */
|
|
251
|
+
export interface BanditArmState {
|
|
252
|
+
slug: string;
|
|
253
|
+
enabled: boolean;
|
|
254
|
+
timesSelected: number;
|
|
255
|
+
timesRewarded: number;
|
|
256
|
+
totalPnl: number;
|
|
257
|
+
consecutiveZeroSignals: number;
|
|
258
|
+
signalsFoundTotal: number;
|
|
259
|
+
tradesExecutedTotal: number;
|
|
260
|
+
ewmRoc: number;
|
|
223
261
|
}
|
package/src/bandit.ts
CHANGED
|
@@ -12,11 +12,17 @@ export interface SkillState {
|
|
|
12
12
|
consecutiveZeroSignals: number;
|
|
13
13
|
signalsFoundTotal: number;
|
|
14
14
|
tradesExecutedTotal: number;
|
|
15
|
+
/** Exponentially weighted return-on-capital (period_pnl / total_cost per cycle).
|
|
16
|
+
* Alpha=0.1 gives ~7-cycle half-life. Primary bandit arm score. */
|
|
17
|
+
ewmRoc: number;
|
|
15
18
|
lastCycle?: {
|
|
16
19
|
skipCounts?: Record<string, number>;
|
|
17
20
|
};
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
/** EWM decay factor. Alpha=0.1 → ~7-cycle half-life. */
|
|
24
|
+
export const EWM_ALPHA = 0.1;
|
|
25
|
+
|
|
20
26
|
export interface SelectionMeta {
|
|
21
27
|
slug: string;
|
|
22
28
|
reason: string;
|
|
@@ -25,11 +31,12 @@ export interface SelectionMeta {
|
|
|
25
31
|
|
|
26
32
|
function avgReward(skill: SkillState): number {
|
|
27
33
|
if (skill.timesSelected === 0) return Infinity;
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
// Primary: exponentially weighted return-on-capital. Normalizes for position
|
|
35
|
+
// size and decays stale regime data (~7-cycle half-life at alpha=0.1).
|
|
36
|
+
if (skill.ewmRoc !== 0) return skill.ewmRoc;
|
|
37
|
+
// Bootstrap: a skill that executes trades at all beats one that finds nothing.
|
|
38
|
+
if (skill.tradesExecutedTotal > 0) return 0.001;
|
|
39
|
+
return 0;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
export function tierMaxSkills(tier: string, maxConcurrent: number): number {
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { SimmerApi } from "./api.js";
|
|
11
|
-
import type { AutomatonState, Skill, SkillOutcome, BriefingPosition, PositionsPosition } from "./api.js";
|
|
12
|
-
import { selectSkills, tierMaxSkills, type SkillState } from "./bandit.js";
|
|
11
|
+
import type { AutomatonState, Skill, SkillOutcome, BriefingPosition, PositionsPosition, BanditArmState } from "./api.js";
|
|
12
|
+
import { selectSkills, tierMaxSkills, EWM_ALPHA, type SkillState } from "./bandit.js";
|
|
13
13
|
import { computeTier, type Tier } from "./tiers.js";
|
|
14
14
|
import { generateTuningHints, computeTuningChanges, type ConfigChange } from "./tuning.js";
|
|
15
15
|
|
|
@@ -56,6 +56,9 @@ let cachedSkills: Skill[] = [];
|
|
|
56
56
|
let banditState: SkillState[] = [];
|
|
57
57
|
let currentTier: Tier = "normal";
|
|
58
58
|
let cycleCount = 0;
|
|
59
|
+
let lastUpdateCheckCycle = 0;
|
|
60
|
+
const UPDATE_CHECK_INTERVAL = 50; // cycles between version checks
|
|
61
|
+
const INSTALL_TIMEOUT_MS = 30_000; // 30s per skill install
|
|
59
62
|
let currentSelectedMeta: Array<{ slug: string; reason: string; score: number | null }> = [];
|
|
60
63
|
let lastExecutionResults: Array<{ slug: string; ok: boolean; detail: string }> = [];
|
|
61
64
|
let cachedPortfolio: { totalPnl: number; positionCount: number; positions: BriefingPosition[]; recentTradeCount: number } | null = null;
|
|
@@ -73,7 +76,7 @@ let config = {
|
|
|
73
76
|
epsilonDecay: 0.995,
|
|
74
77
|
minEpsilon: 0.05,
|
|
75
78
|
maxConcurrent: 2,
|
|
76
|
-
venue: "
|
|
79
|
+
venue: "sim",
|
|
77
80
|
};
|
|
78
81
|
|
|
79
82
|
function loadConfig(pluginConfig?: Record<string, unknown>) {
|
|
@@ -88,7 +91,7 @@ function loadConfig(pluginConfig?: Record<string, unknown>) {
|
|
|
88
91
|
if (pluginConfig.venue) config.venue = pluginConfig.venue as string;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
async function refreshState(logger: { info: (m: string) => void; error: (m: string) => void }) {
|
|
94
|
+
async function refreshState(logger: { info: (m: string) => void; warn: (m: string) => void; error: (m: string) => void }) {
|
|
92
95
|
try {
|
|
93
96
|
cachedState = await api.getAutomatonState();
|
|
94
97
|
// Use automaton skills endpoint (respects per-user enable/disable prefs)
|
|
@@ -107,9 +110,12 @@ async function refreshState(logger: { info: (m: string) => void; error: (m: stri
|
|
|
107
110
|
if (lastKnownStartedAt !== null) {
|
|
108
111
|
logger.info(`[simmer] Detected re-init (started_at changed) — resetting cycle counter and bandit state`);
|
|
109
112
|
cycleCount = 0;
|
|
113
|
+
config.epsilon = 0.2;
|
|
110
114
|
banditState = [];
|
|
111
115
|
currentSelectedMeta = [];
|
|
112
116
|
lastCycleTimestamp = new Date().toISOString();
|
|
117
|
+
// Delete persisted bandit state so the next startup doesn't restore stale arms
|
|
118
|
+
api.resetBanditState().catch((e) => logger.warn(`[simmer] Failed to reset bandit state: ${e}`));
|
|
113
119
|
}
|
|
114
120
|
lastKnownStartedAt = cachedState.started_at;
|
|
115
121
|
}
|
|
@@ -135,6 +141,7 @@ async function refreshState(logger: { info: (m: string) => void; error: (m: stri
|
|
|
135
141
|
timesSelected: 0,
|
|
136
142
|
timesRewarded: 0,
|
|
137
143
|
totalPnl: 0,
|
|
144
|
+
ewmRoc: 0,
|
|
138
145
|
consecutiveZeroSignals: 0,
|
|
139
146
|
signalsFoundTotal: 0,
|
|
140
147
|
tradesExecutedTotal: 0,
|
|
@@ -148,7 +155,7 @@ async function refreshState(logger: { info: (m: string) => void; error: (m: stri
|
|
|
148
155
|
|
|
149
156
|
function fmtCurrency(amount: number): string {
|
|
150
157
|
const val = amount.toFixed(2);
|
|
151
|
-
return config.venue === "simmer" ? `${val} $SIM` : `$${val}`;
|
|
158
|
+
return (config.venue === "sim" || config.venue === "simmer") ? `${val} $SIM` : `$${val}`;
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
function buildPromptContext(): string {
|
|
@@ -262,9 +269,83 @@ function formatStatus(): string {
|
|
|
262
269
|
`Cycles: ${cycleCount}`,
|
|
263
270
|
`Skills available: ${cachedSkills.length}`,
|
|
264
271
|
];
|
|
272
|
+
|
|
273
|
+
if (cachedPortfolio) {
|
|
274
|
+
const pnlStr = cachedPortfolio.totalPnl >= 0
|
|
275
|
+
? `+${fmtCurrency(cachedPortfolio.totalPnl)}`
|
|
276
|
+
: fmtCurrency(cachedPortfolio.totalPnl);
|
|
277
|
+
lines.push(`Positions: ${cachedPortfolio.positionCount} open | P&L: ${pnlStr} | Recent trades: ${cachedPortfolio.recentTradeCount}`);
|
|
278
|
+
const top = cachedPortfolio.positions.slice(0, 3);
|
|
279
|
+
for (const p of top) {
|
|
280
|
+
const pnl = p.pnl >= 0 ? `+${fmtCurrency(p.pnl)}` : fmtCurrency(p.pnl);
|
|
281
|
+
lines.push(` · ${p.question.slice(0, 55)} | ${p.side} | ${pnl}`);
|
|
282
|
+
}
|
|
283
|
+
if (cachedPortfolio.positions.length > 3) {
|
|
284
|
+
lines.push(` · ...and ${cachedPortfolio.positions.length - 3} more (use /simmer portfolio for full list)`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
265
288
|
return lines.join("\n");
|
|
266
289
|
}
|
|
267
290
|
|
|
291
|
+
// =============================================================================
|
|
292
|
+
// Skill auto-update — keep installed skills at latest ClawHub version
|
|
293
|
+
// =============================================================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Read installed skill versions from SKILL.md frontmatter.
|
|
297
|
+
* Returns { slug: version } for skills that have a parseable version.
|
|
298
|
+
*/
|
|
299
|
+
function getLocalSkillVersions(workspaceDir: string | undefined): Record<string, string> {
|
|
300
|
+
const fs = require("fs");
|
|
301
|
+
const cwd = workspaceDir || process.cwd();
|
|
302
|
+
const versions: Record<string, string> = {};
|
|
303
|
+
for (const skill of cachedSkills) {
|
|
304
|
+
const slug = skill.id;
|
|
305
|
+
try {
|
|
306
|
+
const skillMd = fs.readFileSync(`${cwd}/skills/${slug}/SKILL.md`, "utf-8");
|
|
307
|
+
const versionMatch = skillMd.match(/^\s*version:\s*["']?([^"'\n]+)["']?/m);
|
|
308
|
+
if (versionMatch) {
|
|
309
|
+
versions[slug] = versionMatch[1].trim();
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// Skill not installed locally or no SKILL.md — skip
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return versions;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check for outdated skills and auto-install updates.
|
|
320
|
+
* Uses the skill_updates from the most recent briefing response.
|
|
321
|
+
*/
|
|
322
|
+
async function checkAndUpdateSkills(
|
|
323
|
+
skillUpdates: Array<{ slug: string; current: string; latest: string; message: string }>,
|
|
324
|
+
workspaceDir: string | undefined,
|
|
325
|
+
logger: { info: (m: string) => void; warn: (m: string) => void; error: (m: string) => void },
|
|
326
|
+
): Promise<void> {
|
|
327
|
+
if (!runtime || skillUpdates.length === 0) return;
|
|
328
|
+
|
|
329
|
+
const cwd = workspaceDir || process.cwd();
|
|
330
|
+
logger.info(`[simmer] ${skillUpdates.length} skill update(s) available: ${skillUpdates.map((u) => `${u.slug} ${u.current}→${u.latest}`).join(", ")}`);
|
|
331
|
+
|
|
332
|
+
for (const update of skillUpdates) {
|
|
333
|
+
try {
|
|
334
|
+
const result = await runtime.system.runCommandWithTimeout(
|
|
335
|
+
["npx", "clawhub@latest", "install", update.slug, "--force"],
|
|
336
|
+
{ timeoutMs: INSTALL_TIMEOUT_MS, cwd },
|
|
337
|
+
);
|
|
338
|
+
if (result.code === 0) {
|
|
339
|
+
logger.info(`[simmer] Updated ${update.slug} to ${update.latest}`);
|
|
340
|
+
} else {
|
|
341
|
+
logger.warn(`[simmer] Failed to update ${update.slug}: exit ${result.code} — ${result.stderr.slice(0, 200)}`);
|
|
342
|
+
}
|
|
343
|
+
} catch (e) {
|
|
344
|
+
logger.warn(`[simmer] Failed to update ${update.slug}: ${e}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
268
349
|
// =============================================================================
|
|
269
350
|
// Skill execution — deterministic, no LLM in the loop
|
|
270
351
|
// =============================================================================
|
|
@@ -389,9 +470,51 @@ export default function register(pluginApi: PluginApi) {
|
|
|
389
470
|
ctx.logger.info("[simmer] Automaton service starting");
|
|
390
471
|
serviceRunning = true;
|
|
391
472
|
|
|
392
|
-
// Initial state fetch
|
|
473
|
+
// Initial state fetch (seeds banditState with current skills)
|
|
393
474
|
await refreshState(ctx.logger);
|
|
394
475
|
|
|
476
|
+
// Restore persisted bandit arm statistics — overwrite in-memory state seeded above.
|
|
477
|
+
// This lets the bandit resume learning after a plugin restart without starting cold.
|
|
478
|
+
try {
|
|
479
|
+
const saved = await api.getBanditState();
|
|
480
|
+
if (saved && saved.arms.length > 0) {
|
|
481
|
+
const savedBySlug = new Map(saved.arms.map((a: BanditArmState) => [a.slug, a]));
|
|
482
|
+
banditState = banditState.map((arm) => {
|
|
483
|
+
const s = savedBySlug.get(arm.slug);
|
|
484
|
+
if (s) {
|
|
485
|
+
// Restore learned stats; keep live enabled status from skills registry
|
|
486
|
+
return {
|
|
487
|
+
...s,
|
|
488
|
+
enabled: arm.enabled,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return arm; // new skill not in saved state — start fresh
|
|
492
|
+
});
|
|
493
|
+
config.epsilon = saved.epsilon;
|
|
494
|
+
cycleCount = saved.cycle_count;
|
|
495
|
+
ctx.logger.info(
|
|
496
|
+
`[simmer] Restored bandit state: ${saved.arms.length} arms, ε=${saved.epsilon.toFixed(3)}, cycle=${saved.cycle_count}`,
|
|
497
|
+
);
|
|
498
|
+
} else {
|
|
499
|
+
ctx.logger.info("[simmer] No saved bandit state — starting fresh");
|
|
500
|
+
}
|
|
501
|
+
} catch (e) {
|
|
502
|
+
ctx.logger.warn(`[simmer] Failed to restore bandit state: ${e}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Check for skill updates on startup
|
|
506
|
+
try {
|
|
507
|
+
const skillVersions = getLocalSkillVersions(ctx.workspaceDir);
|
|
508
|
+
if (Object.keys(skillVersions).length > 0) {
|
|
509
|
+
const briefing = await api.getBriefing(skillVersions);
|
|
510
|
+
if (briefing.skill_updates && briefing.skill_updates.length > 0) {
|
|
511
|
+
await checkAndUpdateSkills(briefing.skill_updates, ctx.workspaceDir, ctx.logger);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} catch (e) {
|
|
515
|
+
ctx.logger.warn(`[simmer] Startup skill update check failed: ${e}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
395
518
|
// Periodic refresh
|
|
396
519
|
cycleTimer = setInterval(async () => {
|
|
397
520
|
if (!serviceRunning) return;
|
|
@@ -403,20 +526,28 @@ export default function register(pluginApi: PluginApi) {
|
|
|
403
526
|
await refreshState(ctx.logger);
|
|
404
527
|
|
|
405
528
|
// Query outcomes since last cycle and update bandit reward data
|
|
529
|
+
// Filter to outcomes matching this plugin's configured venue
|
|
406
530
|
try {
|
|
407
531
|
const outcomeRes = await api.getOutcomes(cycleStarted);
|
|
408
|
-
|
|
532
|
+
const venueOutcomes = outcomeRes.outcomes.filter(
|
|
533
|
+
(o: SkillOutcome) => o.venue === config.venue
|
|
534
|
+
);
|
|
535
|
+
for (const o of venueOutcomes) {
|
|
409
536
|
const skill = banditState.find((s) => s.slug === o.skill_slug);
|
|
410
537
|
if (skill) {
|
|
411
538
|
skill.tradesExecutedTotal += o.trades;
|
|
412
|
-
|
|
539
|
+
// Return-on-capital for this cycle (0 if no cost data)
|
|
540
|
+
const roc = o.total_cost > 0 ? o.period_pnl / o.total_cost : 0;
|
|
541
|
+
// Exponentially weighted RoC — decays stale data (~7-cycle half-life)
|
|
542
|
+
skill.ewmRoc = EWM_ALPHA * roc + (1 - EWM_ALPHA) * skill.ewmRoc;
|
|
543
|
+
skill.timesRewarded += roc > 0 ? 1 : 0;
|
|
413
544
|
skill.totalPnl += o.period_pnl;
|
|
414
545
|
skill.consecutiveZeroSignals = o.trades > 0 ? 0 : skill.consecutiveZeroSignals + 1;
|
|
415
546
|
}
|
|
416
547
|
}
|
|
417
548
|
// Skills that were selected last cycle but had zero trades won't appear in outcomes
|
|
418
549
|
// (GROUP BY only returns rows with trades). Increment consecutiveZeroSignals for them.
|
|
419
|
-
const outcomeSkillSlugs = new Set(
|
|
550
|
+
const outcomeSkillSlugs = new Set(venueOutcomes.map((o: SkillOutcome) => o.skill_slug));
|
|
420
551
|
for (const m of currentSelectedMeta) {
|
|
421
552
|
if (!outcomeSkillSlugs.has(m.slug)) {
|
|
422
553
|
const skill = banditState.find((s) => s.slug === m.slug);
|
|
@@ -425,8 +556,8 @@ export default function register(pluginApi: PluginApi) {
|
|
|
425
556
|
}
|
|
426
557
|
}
|
|
427
558
|
}
|
|
428
|
-
if (
|
|
429
|
-
ctx.logger.info(`[simmer] Outcomes: ${
|
|
559
|
+
if (venueOutcomes.length > 0) {
|
|
560
|
+
ctx.logger.info(`[simmer] Outcomes (${config.venue}): ${venueOutcomes.map((o: SkillOutcome) => `${o.skill_slug}:${o.trades}t/$${o.period_pnl.toFixed(2)}pnl`).join(", ")}`);
|
|
430
561
|
}
|
|
431
562
|
} catch (e) {
|
|
432
563
|
ctx.logger.error(`[simmer] Failed to fetch outcomes: ${e}`);
|
|
@@ -476,6 +607,19 @@ export default function register(pluginApi: PluginApi) {
|
|
|
476
607
|
cycle_number: thisCycleNumber,
|
|
477
608
|
selection_meta: meta.map((m) => ({ slug: m.slug, reason: m.reason, score: m.score })),
|
|
478
609
|
config_changes: configChangesPayload,
|
|
610
|
+
// Bandit persistence — write current arm stats so restarts resume where we left off
|
|
611
|
+
epsilon: parseFloat(config.epsilon.toFixed(6)),
|
|
612
|
+
bandit_arms: banditState.map((s) => ({
|
|
613
|
+
slug: s.slug,
|
|
614
|
+
enabled: s.enabled,
|
|
615
|
+
timesSelected: s.timesSelected,
|
|
616
|
+
timesRewarded: s.timesRewarded,
|
|
617
|
+
totalPnl: s.totalPnl,
|
|
618
|
+
consecutiveZeroSignals: s.consecutiveZeroSignals,
|
|
619
|
+
signalsFoundTotal: s.signalsFoundTotal,
|
|
620
|
+
tradesExecutedTotal: s.tradesExecutedTotal,
|
|
621
|
+
ewmRoc: s.ewmRoc,
|
|
622
|
+
})),
|
|
479
623
|
});
|
|
480
624
|
} catch (e) {
|
|
481
625
|
ctx.logger.warn(`[simmer] Failed to post cycle (active_skills may be stale): ${e}`);
|
|
@@ -495,8 +639,9 @@ export default function register(pluginApi: PluginApi) {
|
|
|
495
639
|
|
|
496
640
|
// Fetch portfolio snapshot for prompt context
|
|
497
641
|
try {
|
|
498
|
-
const
|
|
499
|
-
const
|
|
642
|
+
const skillVersions = getLocalSkillVersions(ctx.workspaceDir);
|
|
643
|
+
const briefing = await api.getBriefing(skillVersions);
|
|
644
|
+
const venue = (briefing.venues as Record<string, any>)?.[config.venue];
|
|
500
645
|
const attentionPositions = venue?.positions_needing_attention || [];
|
|
501
646
|
cachedPortfolio = {
|
|
502
647
|
totalPnl: briefing.performance?.total_pnl ?? 0,
|
|
@@ -504,6 +649,13 @@ export default function register(pluginApi: PluginApi) {
|
|
|
504
649
|
positions: attentionPositions,
|
|
505
650
|
recentTradeCount: 0,
|
|
506
651
|
};
|
|
652
|
+
|
|
653
|
+
// Periodic skill auto-update
|
|
654
|
+
if (briefing.skill_updates && briefing.skill_updates.length > 0 &&
|
|
655
|
+
thisCycleNumber - lastUpdateCheckCycle >= UPDATE_CHECK_INTERVAL) {
|
|
656
|
+
lastUpdateCheckCycle = thisCycleNumber;
|
|
657
|
+
await checkAndUpdateSkills(briefing.skill_updates, ctx.workspaceDir, ctx.logger);
|
|
658
|
+
}
|
|
507
659
|
} catch (e) {
|
|
508
660
|
ctx.logger.warn(`[simmer] Failed to fetch briefing: ${e}`);
|
|
509
661
|
}
|