dexterm-cli 0.2.0 → 0.2.2
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/packages/agents/src/spectator.js +28 -2
- package/dist/packages/agents/src/tiers.js +3 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/autotrade/src/backtest.js +6 -2
- package/dist/packages/autotrade/src/chat/connector.js +5 -4
- package/dist/packages/autotrade/src/chat/control.js +3 -0
- package/dist/packages/autotrade/src/chat/hub.js +37 -2
- package/dist/packages/autotrade/src/chat/index.js +3 -2
- package/dist/packages/autotrade/src/chat/readonly.js +530 -0
- package/dist/packages/autotrade/src/chat/service.js +12 -1
- package/dist/packages/autotrade/src/engine.js +75 -29
- package/dist/packages/autotrade/src/fleet.js +2 -2
- package/dist/packages/autotrade/src/index.js +4 -0
- package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
- package/dist/packages/autotrade/src/rugproof.js +231 -0
- package/dist/packages/autotrade/src/runner.js +203 -30
- package/dist/packages/autotrade/src/service-install.js +6 -4
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/store.js +1 -1
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +29 -1
- package/dist/packages/autotrade/src/tiers.js +194 -0
- package/dist/packages/classifier/src/calibrate.js +138 -0
- package/dist/packages/classifier/src/classify.js +107 -0
- package/dist/packages/classifier/src/config.js +298 -0
- package/dist/packages/classifier/src/dossier.js +95 -0
- package/dist/packages/classifier/src/hardware.js +63 -0
- package/dist/packages/classifier/src/index.js +19 -0
- package/dist/packages/classifier/src/models.js +111 -0
- package/dist/packages/classifier/src/providers/heuristic.js +77 -0
- package/dist/packages/classifier/src/providers/http.js +63 -0
- package/dist/packages/classifier/src/providers/jev-open.js +86 -0
- package/dist/packages/classifier/src/providers/llm.js +89 -0
- package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
- package/dist/packages/classifier/src/questions.js +97 -0
- package/dist/packages/classifier/src/registry.js +42 -0
- package/dist/packages/classifier/src/snapshot.js +31 -0
- package/dist/packages/classifier/src/systemone.js +88 -0
- package/dist/packages/classifier/src/types.js +18 -0
- package/dist/packages/classifier/src/win-acl.js +71 -0
- package/dist/packages/cli/assets/dash/dash.css +139 -0
- package/dist/packages/cli/assets/dash/dash.js +200 -0
- package/dist/packages/cli/assets/dash/icon.svg +1 -0
- package/dist/packages/cli/assets/dash/index.html +36 -0
- package/dist/packages/cli/src/app-help-gen.js +13 -1
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +12 -3
- package/dist/packages/cli/src/autotrade-cmd/run.js +14 -2
- package/dist/packages/cli/src/autotrade-cmd/service.js +3 -1
- package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
- package/dist/packages/cli/src/autotrade-cmd/strategies.js +1 -1
- package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +2 -0
- package/dist/packages/cli/src/classifier-cmd.js +361 -0
- package/dist/packages/cli/src/classifier-live.js +81 -0
- package/dist/packages/cli/src/dash/data.js +217 -0
- package/dist/packages/cli/src/dash/server.js +92 -0
- package/dist/packages/cli/src/dash-cmd.js +113 -0
- package/dist/packages/cli/src/doctor.js +34 -0
- package/dist/packages/cli/src/live/screens/token-live.js +4 -0
- package/dist/packages/cli/src/live/signals.js +8 -0
- package/dist/packages/cli/src/main.js +98 -6
- package/dist/packages/cli/src/mcp-follow.js +21 -1
- package/dist/packages/cli/src/narratives-cmd.js +147 -0
- package/dist/packages/cli/src/narratives.js +114 -0
- package/dist/packages/cli/src/overlay/model.js +147 -0
- package/dist/packages/cli/src/overlay/page.js +146 -0
- package/dist/packages/cli/src/overlay/pnl.js +75 -0
- package/dist/packages/cli/src/overlay/server.js +170 -0
- package/dist/packages/cli/src/overlay-cmd.js +367 -0
- package/dist/packages/cli/src/record-cmd.js +82 -11
- package/dist/packages/cli/src/rules-cmd.js +2 -1
- package/dist/packages/cli/src/rules.js +24 -4
- package/dist/packages/cli/src/screens/narrative.js +154 -2
- package/dist/packages/cli/src/skill-eval-build.js +7 -2
- package/dist/packages/cli/src/state.js +1 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +5 -1
- package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-confidence-tiers-experimental-unproven-paper-only.md +56 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-read-only-data-check-top-and-signals-experimental.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
- package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
- package/dist/packages/mcp/src/server.js +106 -1
- package/dist/packages/mcp/src/spectator-sink.js +27 -0
- package/dist/packages/narratives/src/cluster.js +193 -0
- package/dist/packages/narratives/src/embed.js +184 -0
- package/dist/packages/narratives/src/export.js +60 -0
- package/dist/packages/narratives/src/index.js +10 -0
- package/dist/packages/narratives/src/label.js +48 -0
- package/dist/packages/narratives/src/normalize.js +179 -0
- package/dist/packages/narratives/src/recorder.js +93 -0
- package/dist/packages/narratives/src/tracker.js +328 -0
- package/dist/packages/packs/src/skill-care.js +10 -2
- package/dist/packages/recorder/src/exit-probes.js +215 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +11 -1
- package/dist/packages/recorder/src/retention.js +4 -0
- package/dist/packages/recorder/src/schema.js +20 -1
- package/dist/packages/tui/src/keymap.js +5 -0
- package/package.json +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
|
@@ -21,12 +21,13 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, write
|
|
|
21
21
|
import { uptime } from 'node:os';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
import { Ledger, writeJsonAtomic } from "./ledger.js";
|
|
24
|
-
import { EntryRules, SimBook, clampEntry, simBuy, simSell } from "./engine.js";
|
|
24
|
+
import { EntryRules, SimBook, clampEntry, simBuy, tierHardCapUsd, simSell } from "./engine.js";
|
|
25
25
|
import { monitorTick } from "./monitor.js";
|
|
26
26
|
import { buildDelta, renderDelta, renderEvent, urgent, view } from "./session.js";
|
|
27
27
|
import { SessionGate, renderDossierRecord, renderPositionDossier, tapeDossierRecord } from "./sessions.js";
|
|
28
28
|
import { DEFAULT_RUNNER_CONFIG, effectiveCaps } from "./runner-config.js";
|
|
29
29
|
import { DEFAULT_PRICE_BASIS, fillMcaps, levelText } from "./basis-text.js";
|
|
30
|
+
import { confidenceTierLine, tierRecord } from "./tiers.js";
|
|
30
31
|
export { Ledger } from "./ledger.js";
|
|
31
32
|
export function simExecutor(x) {
|
|
32
33
|
return { name: 'sim', buy: (k, usd) => simBuy(k, usd, x), sell: (k, tokens, reason) => simSell(k, tokens, x, reason) };
|
|
@@ -34,10 +35,16 @@ export function simExecutor(x) {
|
|
|
34
35
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
35
36
|
const REVIEW = ['time_review', 'time_tighten'];
|
|
36
37
|
const INFO_ONLY = ['mechanical_exit', 'stuck'];
|
|
38
|
+
/** Session ends that fail the same way every time: no new session for the rest of the run. */
|
|
39
|
+
const GUARD_REASONS = new Set(['account-mismatch', 'not-tool-less', 'account-unavailable']);
|
|
37
40
|
async function* fromArray(ticks) { for (const k of ticks)
|
|
38
41
|
yield k; }
|
|
39
42
|
export async function runAutotrade(o) {
|
|
40
43
|
const s = o.strategy;
|
|
44
|
+
// 0.2.2: EXPERIMENTAL confidence tiers run on paper only: never in a real-signal run, never in any mode but paper
|
|
45
|
+
// (validateStrategy refuses the block outside paper and the engine refuses it too; this is the runner's own gate).
|
|
46
|
+
if (s.sizing.tiers && !(!o.real && s.mode === 'paper'))
|
|
47
|
+
throw new Error(`strategy ${s.id}: confidence tiers (sizing.tiers) are EXPERIMENTAL and PAPER ONLY: refused ${o.real ? 'in a real-signal run' : `in mode "${s.mode}"`} (remove sizing.tiers to run it there)`);
|
|
41
48
|
// task-1571: autotrade runs on paper only. dexterm holds no key and places no live order; live trading happens in the
|
|
42
49
|
// Telegram trading bot of the user's choice.
|
|
43
50
|
// task-1645: mode "real" = the same paper engine, plus a LIVE TRADE FIRE signal per fill for the user's own executor;
|
|
@@ -76,6 +83,8 @@ export async function runAutotrade(o) {
|
|
|
76
83
|
const skippedUntil = new Map();
|
|
77
84
|
/** Coins the vet passed this run (a refused one goes to skippedUntil for good). */
|
|
78
85
|
const vetted = new Set();
|
|
86
|
+
/** 0.2.2 confidence tiers: coins whose T0 (skip) line was already written at the pre-check. */
|
|
87
|
+
const tierSkipSaid = new Set();
|
|
79
88
|
const orders = [];
|
|
80
89
|
const refusals = {};
|
|
81
90
|
const clampCount = {};
|
|
@@ -95,6 +104,24 @@ export async function runAutotrade(o) {
|
|
|
95
104
|
const pumps = new Set();
|
|
96
105
|
/** Proposals waiting for an answer, by mint (task-1527). */
|
|
97
106
|
const approvalsOpen = new Map();
|
|
107
|
+
/** task-2156: the first-tranche dollars of each open approval (an in-flight entry for the tier clamp). */
|
|
108
|
+
const approvalUsd = new Map();
|
|
109
|
+
/** task-2156: entries decided but not filled (pending open orders + open approvals), other than `except`'s own. */
|
|
110
|
+
const inFlight = (except) => {
|
|
111
|
+
let opens = 0;
|
|
112
|
+
let usd = 0;
|
|
113
|
+
for (const q of orders)
|
|
114
|
+
if (q.kind === 'open' && q.mint !== except) {
|
|
115
|
+
opens++;
|
|
116
|
+
usd += q.usd;
|
|
117
|
+
}
|
|
118
|
+
for (const m of approvalsOpen.keys())
|
|
119
|
+
if (m !== except) {
|
|
120
|
+
opens++;
|
|
121
|
+
usd += approvalUsd.get(m) ?? Infinity;
|
|
122
|
+
}
|
|
123
|
+
return { opens, usd };
|
|
124
|
+
};
|
|
98
125
|
ledger.add(t0, 'run_start', { strategy: s.id, version: s.version, strategyHash: s.hash, mode: s.mode, ...(o.real ? { tag: 'real-signal', accountId: o.real.accountId } : {}), ...(s.highRisk ? { highRisk: true } : {}), tape: o.tape?.source ?? o.source.name, ticks: tapeTicks?.length ?? null, decide: decideAuto ? 'auto' : s.entry.decide, ...(o.approvals ? { approvals: o.approvals.required(s) ? 'required' : 'when-asked' } : {}), pacing: pacing.mode, executor: exec.name, caps: capsView(gate), ...(o.account ? { account: o.account.id } : {}) });
|
|
99
126
|
// task-1645 r2: a real run takes over the real positions an earlier run of this strategy left open (held.json),
|
|
100
127
|
// so their exits keep firing. A profile the strategy no longer has falls back to its default profile.
|
|
@@ -121,27 +148,105 @@ export async function runAutotrade(o) {
|
|
|
121
148
|
const count = (rule) => { refusals[rule] = (refusals[rule] ?? 0) + 1; };
|
|
122
149
|
const refuse = (rule, data) => { count(rule); ledger.add(now, 'refused', { rule, ...data }); };
|
|
123
150
|
// A vet that throws or hangs is a check that could not run: the strategy's `unknown` decides (reject by default).
|
|
124
|
-
const vetCoin = async (k) => {
|
|
151
|
+
const vetCoin = async (k, ctx) => {
|
|
125
152
|
let r;
|
|
126
153
|
try {
|
|
127
|
-
r = await o.vet(k);
|
|
154
|
+
r = await o.vet(k, ctx);
|
|
128
155
|
}
|
|
129
156
|
catch (e) {
|
|
130
157
|
r = { ok: false, rule: 'vet-error', detail: e.message.slice(0, 160) };
|
|
131
158
|
}
|
|
132
159
|
if (r.ok) {
|
|
133
|
-
|
|
160
|
+
if (!r.recheck)
|
|
161
|
+
vetted.add(k.mint);
|
|
134
162
|
if (r.note)
|
|
135
|
-
ledger.add(now, 'vetted', { mint: k.mint, note: r.note.slice(0, 200) });
|
|
163
|
+
ledger.add(now, 'vetted', { mint: k.mint, note: r.note.slice(0, 200), ...(r.sizeCapPct !== undefined ? { sizeCapPct: r.sizeCapPct } : {}) });
|
|
136
164
|
}
|
|
137
165
|
return r;
|
|
138
166
|
};
|
|
167
|
+
const vetCtx = (k, sizeUsd, h) => ({ history: h, sizeUsd, exitFloorUsd: x.minExitLiquidityUsd, maxTop10Pct: s.entry.safety.maxTop10Pct, maxDevPct: s.entry.safety.maxDevPct, unknownHolders: s.entry.safety.unknownHolders });
|
|
168
|
+
const drainGuard = s.entry.safety.rugproof?.drainGuard ?? null;
|
|
169
|
+
const stuckSince = new Map();
|
|
170
|
+
/** The liquidity read each position was bought at (the drain guard's base). */
|
|
171
|
+
const entryLiq = new Map();
|
|
172
|
+
// Paper only: a real-signal bag is still in the user's wallet, so it keeps its exit signals and carries to the next
|
|
173
|
+
// real run (validateStrategy also refuses the option outside mode paper).
|
|
174
|
+
const writeOffMin = !o.real && s.mode === 'paper' ? x.stuckWriteOffMin ?? null : null;
|
|
175
|
+
/** Positions the pool probe saw drained (no feed-priced retry while it still says so). */
|
|
176
|
+
const drained = new Set();
|
|
177
|
+
/**
|
|
178
|
+
* task-1748, every step, for every open position:
|
|
179
|
+
* - the drain guard: an exit verdict from the pool poller (SOL vault / Jupiter, <= 10 s apart), or this coin's own
|
|
180
|
+
* feed liquidity down more than drainGuard.liqDropPct from the entry read, sells the whole position at once. The
|
|
181
|
+
* sale is priced on the liquidity the guard saw (0 when the route is gone), not on a lagging feed row: an
|
|
182
|
+
* emptied pool leaves nothing to sell into, the bag goes stuck and the write-off below closes it.
|
|
183
|
+
* - execution.stuckWriteOffMin (paper): a bag stuck that long is closed at its realizable value (sells already
|
|
184
|
+
* made kept, the rest 0), with a written_off event and a trade_closed line, so crash recovery never adopts it.
|
|
185
|
+
*/
|
|
186
|
+
const sweepOpen = async (tick) => {
|
|
187
|
+
for (const p of [...book.open.values()]) {
|
|
188
|
+
if (p.status !== 'open')
|
|
189
|
+
continue;
|
|
190
|
+
const k = latest.get(p.mint) ?? p.lastTick;
|
|
191
|
+
if (drainGuard && !p.stuck) {
|
|
192
|
+
const v = o.drain?.verdict(p.id) ?? null;
|
|
193
|
+
const base = entryLiq.has(p.id) ? entryLiq.get(p.id) ?? null : (p.liqWindow[0]?.liq || null);
|
|
194
|
+
const own = tick && tick.mint === p.mint && base && tick.liq !== null && (1 - tick.liq / base) * 100 > drainGuard.liqDropPct
|
|
195
|
+
? { reason: 'drain-liquidity', detail: `liquidity $${Math.round(base)} -> $${Math.round(tick.liq)} since entry (-${((1 - tick.liq / base) * 100).toFixed(1)}% > ${drainGuard.liqDropPct}%)`, liqUsd: tick.liq } : null;
|
|
196
|
+
const hit = v ?? own;
|
|
197
|
+
if (hit) {
|
|
198
|
+
ledger.add(now, 'drain_guard', { positionId: p.id, mint: p.mint, reason: hit.reason, detail: hit.detail, liqUsd: hit.liqUsd, source: v ? 'pool-probe' : 'feed' });
|
|
199
|
+
say(`DRAIN GUARD ${p.symbol}: ${hit.detail}`);
|
|
200
|
+
await sellPosition(p, { ...k, t: now, liq: hit.liqUsd ?? 0 }, p.tokens, hit.reason, hit.detail);
|
|
201
|
+
// The probe saw the pool emptied or the route gone: the lagging feed must not "sell" the bag at its old
|
|
202
|
+
// liquidity. While the probe still says so, the monitor's stuck retry is off for this position.
|
|
203
|
+
if (v && p.status === 'open' && p.stuck)
|
|
204
|
+
drained.add(p.id);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const since = stuckSince.get(p.id);
|
|
208
|
+
if (writeOffMin !== null && p.status === 'open' && p.stuck && since !== undefined && now - since >= writeOffMin * 60e3) {
|
|
209
|
+
const ct = book.close(p, now, 'written-off', true);
|
|
210
|
+
o.drain?.unwatch(p.id);
|
|
211
|
+
stuckSince.delete(p.id);
|
|
212
|
+
ledger.add(now, 'written_off', { positionId: p.id, mint: p.mint, stuckMin: Math.round((now - since) / 6e3) / 10, realizedUsd: ct.proceedsUsd, costUsd: ct.costUsd, why: `stuck ${writeOffMin} min under the exit floor: closed at realizable value (the rest counts 0)` });
|
|
213
|
+
ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null, writtenOff: true });
|
|
214
|
+
say(`WRITTEN OFF ${p.symbol}: stuck ${writeOffMin} min, ${ct.pnlPct}% realized`);
|
|
215
|
+
}
|
|
216
|
+
if (p.status === 'closed') {
|
|
217
|
+
const c = byPosition.get(p.id);
|
|
218
|
+
if (c) {
|
|
219
|
+
if (c.inflight)
|
|
220
|
+
await c.inflight;
|
|
221
|
+
await closeCandidate(c, 'position-closed');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
139
226
|
const liveCandidateSessions = () => [...cands.values()].filter((q) => q.session && !q.positionId).length;
|
|
140
227
|
const everyLive = () => [...new Set([...cands.values(), ...byPosition.values()])];
|
|
141
228
|
const newCand = (k, matched, session, kind, positionId) => ({
|
|
142
229
|
mint: k.mint, symbol: k.symbol, since: now, lastMatch: now, matched, session, sessionKind: kind, lastSent: view(k), lastSentT: now, seq: 0,
|
|
143
230
|
inflight: null, pendingText: [], notes: [], closing: false, positionId, awaitingReview: null, oneShot: kind === 'position', costSeen: 0, turns: 0, lastDecision: null,
|
|
144
231
|
});
|
|
232
|
+
/**
|
|
233
|
+
* Why the session's process ended on its own (null while alive). A session that proved the account or its tool list
|
|
234
|
+
* wrong (or had no account) blocks new sessions for the run; the guard is logged once, by the session that set it.
|
|
235
|
+
* Checked on every path that learns of a death: a null reply AND a close (a candidate's TTL can close a session that
|
|
236
|
+
* already died, before its null reply is read; task-1712).
|
|
237
|
+
*/
|
|
238
|
+
let guardLogged = false;
|
|
239
|
+
const sessionDied = (c) => {
|
|
240
|
+
const died = c.session?.endReason?.() ?? null;
|
|
241
|
+
if (c.session && died && GUARD_REASONS.has(died)) {
|
|
242
|
+
if (!guardLogged && (gate.blocked === null || gate.blockedBy === c.session.id)) {
|
|
243
|
+
guardLogged = true;
|
|
244
|
+
ledger.add(now, 'guard', { state: died, effect: 'no new sessions this run; mechanical exits continue', sessionId: c.session.id });
|
|
245
|
+
}
|
|
246
|
+
gate.block(died, c.session.id);
|
|
247
|
+
}
|
|
248
|
+
return died;
|
|
249
|
+
};
|
|
145
250
|
const closeCandidate = async (c, reason) => {
|
|
146
251
|
if (c.closing)
|
|
147
252
|
return;
|
|
@@ -163,8 +268,11 @@ export async function runAutotrade(o) {
|
|
|
163
268
|
c.costSeen = st.costUsd;
|
|
164
269
|
}
|
|
165
270
|
sessionCost += st.costUsd;
|
|
166
|
-
|
|
167
|
-
|
|
271
|
+
// A session that had already failed its proof ended for that reason, whatever closed its candidate.
|
|
272
|
+
const died = sessionDied(c);
|
|
273
|
+
const why = died && GUARD_REASONS.has(died) ? died : reason;
|
|
274
|
+
ledger.add(now, 'session_end', { sessionId: c.session.id, sessionKind: c.sessionKind, mint: c.mint, reason: why, turns: st.turns, costUsd: st.costUsd });
|
|
275
|
+
say(`session ${c.session.id} ended (${why}) · ${st.turns} turns · $${st.costUsd.toFixed(4)}`);
|
|
168
276
|
}
|
|
169
277
|
};
|
|
170
278
|
const sellPosition = async (p, k, tokens, reason, detail) => {
|
|
@@ -174,21 +282,37 @@ export async function runAutotrade(o) {
|
|
|
174
282
|
if ('refused' in f) {
|
|
175
283
|
if (!p.stuck) {
|
|
176
284
|
p.stuck = true;
|
|
285
|
+
stuckSince.set(p.id, now);
|
|
177
286
|
ledger.add(now, 'stuck', { positionId: p.id, mint: p.mint, why: f.refused, reason });
|
|
178
287
|
say(`STUCK ${p.symbol}: cannot sell (${f.refused})`);
|
|
179
288
|
}
|
|
180
289
|
return;
|
|
181
290
|
}
|
|
182
291
|
p.stuck = false;
|
|
292
|
+
stuckSince.delete(p.id);
|
|
183
293
|
ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, ...fillMcaps(fillView(f), k), detail, ...sigTag(f) });
|
|
184
294
|
say(`SELL ${p.symbol} ${reason}: $${f.usd.toFixed(2)} at ${lvl(f.price, k)} (impact ${f.impactPct}%) — ${detail}`);
|
|
185
295
|
const ct = book.sell(p, f);
|
|
296
|
+
if (ct)
|
|
297
|
+
o.drain?.unwatch(p.id);
|
|
186
298
|
if (ct) {
|
|
187
299
|
// task-1530: the price extremes the monitor saw while held (the trade ledger's MFE / MAE)
|
|
188
300
|
ledger.add(now, 'trade_closed', { trade: { ...ct, fills: undefined }, entryPrice: p.entryPrice, peakPrice: p.peakPrice, troughPrice: p.troughPrice ?? null, ...(o.real ? { tag: 'real-signal' } : {}) });
|
|
189
301
|
say(`CLOSED ${ct.symbol}: ${ct.pnlUsd >= 0 ? '+' : ''}$${ct.pnlUsd.toFixed(2)} (${ct.pnlPct}%) by ${ct.exitReason}`);
|
|
190
302
|
}
|
|
191
303
|
};
|
|
304
|
+
/** task-1748: a young or partly verified pool takes at most the vet's sizeCapPct of the strategy's pick. */
|
|
305
|
+
const capYoungPool = (r, c) => {
|
|
306
|
+
if (!r.ok || c.sizeCapPct === undefined)
|
|
307
|
+
return r;
|
|
308
|
+
const capUsd = Math.round(r.usd * c.sizeCapPct) / 100;
|
|
309
|
+
if (capUsd >= r.usd)
|
|
310
|
+
return r;
|
|
311
|
+
if (capUsd < s.sizing.minUsd)
|
|
312
|
+
return { ok: false, refused: 'below-min-after-rugproof-young-pool', clamps: [...r.clamps, { rule: 'rugproof-young-pool', fromUsd: r.usd, toUsd: capUsd }] };
|
|
313
|
+
const f = r.usd ? capUsd / r.usd : 1;
|
|
314
|
+
return { ...r, usd: capUsd, firstTrancheUsd: Math.round(r.firstTrancheUsd * f * 100) / 100, clamps: [...r.clamps, { rule: 'rugproof-young-pool', fromUsd: r.usd, toUsd: capUsd }] };
|
|
315
|
+
};
|
|
192
316
|
/** Apply a validated decision at the current virtual time. */
|
|
193
317
|
const apply = async (c, d) => {
|
|
194
318
|
const k = latest.get(c.mint);
|
|
@@ -213,8 +337,10 @@ export async function runAutotrade(o) {
|
|
|
213
337
|
return;
|
|
214
338
|
}
|
|
215
339
|
const req = { profile: d.profile, sizePct: d.sizePct, sizeUsd: d.sizeUsd, confidence: d.confidence };
|
|
216
|
-
const r = clampEntry(book, k, req, killed);
|
|
217
|
-
ledger.add(now, 'clamp', { mint: c.mint, sessionId: c.session?.id ?? null, request: req, result: r.ok ? { ok: true, usd: r.usd, firstTrancheUsd: r.firstTrancheUsd, profile: r.profile.id, basis: r.basis, clamps: r.clamps } : r });
|
|
340
|
+
const r = capYoungPool(clampEntry(book, k, req, killed, now, inFlight(c.mint)), c);
|
|
341
|
+
ledger.add(now, 'clamp', { mint: c.mint, sessionId: c.session?.id ?? null, request: req, result: r.ok ? { ok: true, usd: r.usd, firstTrancheUsd: r.firstTrancheUsd, profile: r.profile.id, basis: r.basis, clamps: r.clamps } : r.tier ? { ok: false, refused: r.refused, clamps: r.clamps } : r, ...(r.tier ? { tier: tierRecord(r.tier) } : {}) });
|
|
342
|
+
if (r.tier)
|
|
343
|
+
say(confidenceTierLine(r.tier, k, r.ok ? r.usd : undefined));
|
|
218
344
|
for (const cl of r.clamps)
|
|
219
345
|
clampCount[cl.rule] = (clampCount[cl.rule] ?? 0) + 1;
|
|
220
346
|
if (!r.ok) {
|
|
@@ -299,7 +425,7 @@ export async function runAutotrade(o) {
|
|
|
299
425
|
const t = latest.get(c.mint);
|
|
300
426
|
if (!t)
|
|
301
427
|
return { ok: false, refused: 'no-tick' };
|
|
302
|
-
const rc = clampEntry(book, t, { profile: prof.id, sizeUsd: usd }, killed);
|
|
428
|
+
const rc = clampEntry(book, t, { profile: prof.id, sizeUsd: usd }, killed, now, inFlight(c.mint));
|
|
303
429
|
return rc.ok ? { ok: true, usd: rc.usd, clamps: rc.clamps.map((q) => q.rule) } : { ok: false, refused: rc.refused };
|
|
304
430
|
},
|
|
305
431
|
blocked: entryBlocked,
|
|
@@ -320,19 +446,32 @@ export async function runAutotrade(o) {
|
|
|
320
446
|
await closeCandidate(c, 'refused:approval-dropped');
|
|
321
447
|
return;
|
|
322
448
|
}
|
|
323
|
-
const usd = Math.min(out.usd, r.usd);
|
|
324
|
-
const scale = r.usd > 0 ? usd / r.usd : 0;
|
|
325
449
|
const t = latest.get(c.mint) ?? k;
|
|
450
|
+
let usd = Math.min(out.usd, r.usd);
|
|
451
|
+
// task-2156: a tiered entry re-clamps at the approval (one open, concurrency, room, a fresh tick), since other
|
|
452
|
+
// entries may have been decided or filled while it waited.
|
|
453
|
+
if (s.sizing.tiers) {
|
|
454
|
+
const rc = clampEntry(book, t, { profile: prof.id, sizeUsd: usd }, killed, now, inFlight(c.mint));
|
|
455
|
+
if (!rc.ok) {
|
|
456
|
+
refuse('approval-dropped', { mint: c.mint, proposalId: out.proposalId, why: rc.refused });
|
|
457
|
+
if (!c.closing)
|
|
458
|
+
await closeCandidate(c, 'refused:approval-dropped');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
usd = Math.min(usd, rc.usd);
|
|
462
|
+
}
|
|
463
|
+
const scale = r.usd > 0 ? usd / r.usd : 0;
|
|
326
464
|
ledger.add(now, 'approval_granted', { mint: c.mint, proposalId: out.proposalId, via: out.via, usd, proofId: out.proofId ? `${out.proofId.slice(0, 6)}…` : null });
|
|
327
465
|
say(`APPROVED ${k.symbol}: $${round2(usd)} via ${out.via}`);
|
|
328
466
|
const ladder = (s.sizing.ladder ?? []).slice(1).map((tr) => ({ frac: tr.frac, atPct: tr.atPct, deadline: now + tr.withinMin * 60e3, usd: usd * tr.frac }));
|
|
329
467
|
const first = round2(r.firstTrancheUsd * scale);
|
|
330
468
|
orders.push({ mint: c.mint, fillAt: now + x.latencyMs, decidedPrice: t.price ?? 0, usd: first, firstUsd: first, profile: prof.id, sessionId: c.session?.id ?? null, trigger: c.matched, ladder, kind: 'open', ctx: { proofId: out.proofId, proposalId: out.proposalId, orderHash: out.orderHash } });
|
|
331
469
|
};
|
|
470
|
+
approvalUsd.set(c.mint, r.firstTrancheUsd);
|
|
332
471
|
const pending = o.approvals.request(prop, live)
|
|
333
472
|
.catch((e) => ({ approved: false, state: 'ERROR', why: e instanceof Error ? e.message.slice(0, 120) : String(e) }))
|
|
334
473
|
.then(settle)
|
|
335
|
-
.finally(() => { approvalsOpen.delete(c.mint); });
|
|
474
|
+
.finally(() => { approvalsOpen.delete(c.mint); approvalUsd.delete(c.mint); });
|
|
336
475
|
approvalsOpen.set(c.mint, pending);
|
|
337
476
|
// Lockstep (backtests, tests) waits for the answer; every other pacing lets it land when it lands.
|
|
338
477
|
if (pacing.mode === 'lockstep')
|
|
@@ -356,14 +495,9 @@ export async function runAutotrade(o) {
|
|
|
356
495
|
c.costSeen = Math.max(c.costSeen, r.costUsd);
|
|
357
496
|
}
|
|
358
497
|
if (!r || !r.decision.ok) {
|
|
359
|
-
const died = c
|
|
498
|
+
const died = sessionDied(c);
|
|
360
499
|
if (!r)
|
|
361
500
|
ledger.add(now, 'session_timeout', { sessionId: c.session.id, mint: c.mint, ended: died });
|
|
362
|
-
if (died === 'account-mismatch' || died === 'not-tool-less' || died === 'account-unavailable') {
|
|
363
|
-
if (!gate.blocked)
|
|
364
|
-
ledger.add(now, 'guard', { state: died, effect: 'no new sessions this run; mechanical exits continue', sessionId: c.session.id });
|
|
365
|
-
gate.block(died);
|
|
366
|
-
}
|
|
367
501
|
if (review)
|
|
368
502
|
await noAnswer(review, !r ? (died ?? 'timeout') : 'invalid-decision');
|
|
369
503
|
if (!r) {
|
|
@@ -437,11 +571,16 @@ export async function runAutotrade(o) {
|
|
|
437
571
|
// Re-check at the fill, like a swap's slippage tolerance: a pool that drained or a price that ran
|
|
438
572
|
// away between the decision and the fill refuses the order instead of buying into it.
|
|
439
573
|
const prof0 = s.exits.profiles[q.profile];
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
574
|
+
// task-2156: a tiered open re-checks one-open and concurrency at the fill (another entry may have filled first).
|
|
575
|
+
const tierBusy = s.sizing.tiers && q.kind === 'open'
|
|
576
|
+
? (s.sizing.tiers.oneOpen !== false && book.open.size >= 1 ? 'fill-tier-one-open' : book.open.size >= s.risk.maxConcurrent ? 'fill-max-concurrent' : null)
|
|
577
|
+
: null;
|
|
578
|
+
const why = tierBusy ? tierBusy
|
|
579
|
+
: (k.liq ?? 0) < prof0.fit.minLiquidityUsd ? 'fill-liquidity-gone'
|
|
580
|
+
: k.price && q.decidedPrice && k.price > q.decidedPrice * (1 + x.slippageBps / 1e4) ? 'fill-slippage'
|
|
581
|
+
// task-1538: on a shared paper account another strategy may have spent the cash since this order was decided;
|
|
582
|
+
// the cash is held before the (async) fill so two strategies can never spend the same dollars.
|
|
583
|
+
: book.account && !book.account.hold(q.usd) ? 'fill-account-cash' : null;
|
|
445
584
|
let f;
|
|
446
585
|
try {
|
|
447
586
|
f = why ? { refused: why } : await exec.buy(k, q.usd, q.ctx);
|
|
@@ -477,6 +616,8 @@ export async function runAutotrade(o) {
|
|
|
477
616
|
ledger.add(now, 'fill', { positionId: p.id, mint: p.mint, profile: prof.id, ...fillMcaps(fillView(f), k), bookUsd, ...(q.ctx?.proposalId ? { proposalId: q.ctx.proposalId } : {}), ...sigTag(f) });
|
|
478
617
|
say(`BUY ${k.symbol} [${prof.id}]: $${f.usd.toFixed(2)} at ${lvl(f.price, k)}${f.partial ? ' (PARTIAL: impact cap)' : ''} · liq $${Math.round(k.liq ?? 0)}`);
|
|
479
618
|
const c = cands.get(q.mint);
|
|
619
|
+
entryLiq.set(p.id, k.liq ?? null);
|
|
620
|
+
o.drain?.watch({ positionId: p.id, mint: p.mint, entryLiqUsd: k.liq ?? null, poolAddress: c?.pool?.address ?? null, quoteVault: c?.pool?.quoteVault ?? null, tokens: () => p.tokens });
|
|
480
621
|
if (c) {
|
|
481
622
|
c.positionId = p.id;
|
|
482
623
|
byPosition.set(p.id, c);
|
|
@@ -617,6 +758,7 @@ export async function runAutotrade(o) {
|
|
|
617
758
|
ledger.add(now, 'halt', { reason: book.state.halted });
|
|
618
759
|
o.approvals?.cancelAll(`halted:${book.state.halted}`);
|
|
619
760
|
}
|
|
761
|
+
await sweepOpen(k);
|
|
620
762
|
// Held coin → the monitor (mechanical first, then wakes).
|
|
621
763
|
const pos = book.open.get(k.mint);
|
|
622
764
|
const chatSell = o.approvals?.takeExit?.(k.mint) ?? null;
|
|
@@ -646,7 +788,11 @@ export async function runAutotrade(o) {
|
|
|
646
788
|
}
|
|
647
789
|
}
|
|
648
790
|
}
|
|
791
|
+
else if (pos && drained.has(pos.id) && o.drain?.verdict(pos.id)) {
|
|
792
|
+
// task-1748: still drained per the pool probe: nothing sells into an emptied pool (the write-off closes it).
|
|
793
|
+
}
|
|
649
794
|
else if (pos) {
|
|
795
|
+
drained.delete(pos.id);
|
|
650
796
|
const prof0 = s.exits.profiles[pos.profileId];
|
|
651
797
|
// task-1654: a real run's (or a paper scope's) maxHoldMinutes limit tightens the profile's holding cap (never loosens it).
|
|
652
798
|
const holdCap = o.real?.maxHoldMinutes?.() ?? o.maxHoldMinutes?.() ?? null;
|
|
@@ -662,7 +808,16 @@ export async function runAutotrade(o) {
|
|
|
662
808
|
else if (a.kind === 'add') {
|
|
663
809
|
if (killed || book.state.halted)
|
|
664
810
|
continue;
|
|
665
|
-
|
|
811
|
+
let room = Math.min(a.usd, book.state.cashUsd, (book.bookEquity() * s.risk.maxExposurePct) / 100 - book.exposureUsd());
|
|
812
|
+
// 0.2.2 confidence tiers: the whole position (every tranche) stays within the engine's tier ceiling (fails closed).
|
|
813
|
+
if (s.sizing.tiers) {
|
|
814
|
+
const left = tierHardCapUsd(book.bookEquity()) - pos.costUsd;
|
|
815
|
+
if (!(left >= s.sizing.minUsd)) {
|
|
816
|
+
refuse('ladder-add-tier-hard-cap', { positionId: pos.id });
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
room = Number.isFinite(room) ? Math.min(room, left) : 0;
|
|
820
|
+
}
|
|
666
821
|
if (room < s.sizing.minUsd) {
|
|
667
822
|
refuse('ladder-add-no-room', { positionId: pos.id });
|
|
668
823
|
continue;
|
|
@@ -706,19 +861,31 @@ export async function runAutotrade(o) {
|
|
|
706
861
|
const sr = rules.screen(k);
|
|
707
862
|
if (sr.pass) {
|
|
708
863
|
// Cheap pre-check so a full book does not spawn sessions it could never act on.
|
|
709
|
-
const pre = clampEntry(book, k, {}, killed);
|
|
710
|
-
const v = pre.ok && o.vet && !vetted.has(k.mint) ? await vetCoin(k) : null;
|
|
864
|
+
const pre = clampEntry(book, k, {}, killed, now, inFlight(k.mint));
|
|
865
|
+
const v = pre.ok && o.vet && !vetted.has(k.mint) ? await vetCoin(k, vetCtx(k, pre.usd, h)) : null;
|
|
711
866
|
if (!pre.ok) {
|
|
712
867
|
if (!['already-open'].includes(pre.refused))
|
|
713
868
|
count(`precheck:${pre.refused}`);
|
|
869
|
+
if (pre.tier?.tier === 'T0' && !tierSkipSaid.has(k.mint)) {
|
|
870
|
+
tierSkipSaid.add(k.mint);
|
|
871
|
+
say(confidenceTierLine(pre.tier, k));
|
|
872
|
+
}
|
|
714
873
|
}
|
|
715
874
|
else if (v && !v.ok) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
875
|
+
// task-1748: a time gate (pool age, the second liquidity read) looks again later; any other refusal is for good.
|
|
876
|
+
const retry = v.retryAfterMs !== undefined;
|
|
877
|
+
refuse(v.rule, { mint: k.mint, symbol: k.symbol, stage: 'vet', detail: v.detail, ...(retry ? { retryAfterMs: v.retryAfterMs } : {}) });
|
|
878
|
+
skippedUntil.set(k.mint, retry ? now + v.retryAfterMs : Infinity);
|
|
879
|
+
say(`SKIP ${k.symbol} (${k.mint.slice(0, 4)}…): ${v.rule} (${v.detail})${retry ? `; again in ${Math.round(v.retryAfterMs / 1000)} s` : ''}`);
|
|
719
880
|
}
|
|
720
881
|
else if (decideAuto) {
|
|
721
882
|
const cand = newCand(k, sr.matched, null, 'candidate', null);
|
|
883
|
+
if (v?.ok) {
|
|
884
|
+
if (v.sizeCapPct !== undefined)
|
|
885
|
+
cand.sizeCapPct = v.sizeCapPct;
|
|
886
|
+
if (v.pool)
|
|
887
|
+
cand.pool = v.pool;
|
|
888
|
+
}
|
|
722
889
|
cands.set(k.mint, cand);
|
|
723
890
|
ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
|
|
724
891
|
await apply(cand, { action: 'enter', reason: 'auto' });
|
|
@@ -734,6 +901,12 @@ export async function runAutotrade(o) {
|
|
|
734
901
|
const session = o.sessions(sid, s, { kind: 'candidate', mint: k.mint });
|
|
735
902
|
gate.opened('candidate');
|
|
736
903
|
const cand = newCand(k, sr.matched, session, 'candidate', null);
|
|
904
|
+
if (v?.ok) {
|
|
905
|
+
if (v.sizeCapPct !== undefined)
|
|
906
|
+
cand.sizeCapPct = v.sizeCapPct;
|
|
907
|
+
if (v.pool)
|
|
908
|
+
cand.pool = v.pool;
|
|
909
|
+
}
|
|
737
910
|
cands.set(k.mint, cand);
|
|
738
911
|
ledger.add(now, 'candidate', { mint: k.mint, symbol: k.symbol, matched: sr.matched, tick: view(k) });
|
|
739
912
|
ledger.add(now, 'session_start', { sessionId: sid, sessionKind: 'candidate', agent: session.kind, mint: k.mint, model: sessionModel(session, s), budgetUsd: gate.caps.budgetUsdPerSession });
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// of its path, so a test home never replaces (or removes) the real one. A non-default home is passed as `--home`.
|
|
20
20
|
// Only PATH is copied into the job's environment (the service needs to find `claude`); nothing else, no secrets.
|
|
21
21
|
import { createHash } from 'node:crypto';
|
|
22
|
-
import { join } from 'node:path';
|
|
22
|
+
import { join, posix } from 'node:path';
|
|
23
23
|
export const SERVICE_LABEL = 'xyz.dexterm.autotrade';
|
|
24
24
|
export const INSTALL_MANIFEST_SCHEMA = 'dexterm-autotrade-service-install/1';
|
|
25
25
|
/** The label for a DEXTERM_HOME: the default one, or one suffixed by a hash of the path. */
|
|
@@ -32,7 +32,9 @@ export function serviceLabel(dexHome, defaultHome) {
|
|
|
32
32
|
export const ensureArgs = (s) => [s.nodePath, s.binPath, 'autotrade', 'service', 'ensure', '--quiet', ...(s.dexHome ? ['--home', s.dexHome] : [])];
|
|
33
33
|
// ───────────────────────────── macOS ─────────────────────────────
|
|
34
34
|
const xml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
35
|
-
|
|
35
|
+
// LaunchAgent and systemd paths are macOS / Linux paths whatever host computes the plan (task-1712: a Windows host
|
|
36
|
+
// joined them with `\`); the Windows task and the manifest live under the local dexHome and use the host's join.
|
|
37
|
+
export const launchAgentPath = (s) => posix.join(s.userHome, 'Library', 'LaunchAgents', `${s.label}.plist`);
|
|
36
38
|
export function renderLaunchAgent(s) {
|
|
37
39
|
const args = ensureArgs(s).map((a) => ` <string>${xml(a)}</string>`).join('\n');
|
|
38
40
|
const env = s.path ? ` <key>EnvironmentVariables</key>\n <dict>\n <key>PATH</key>\n <string>${xml(s.path)}</string>\n </dict>\n` : '';
|
|
@@ -70,7 +72,7 @@ ${env} <key>StartInterval</key>
|
|
|
70
72
|
}
|
|
71
73
|
// ───────────────────────────── Linux (systemd user) ─────────────────────────────
|
|
72
74
|
export const systemdUnit = (s) => s.label.replace(/^xyz\./, '').replace(/\./g, '-');
|
|
73
|
-
export const systemdDir = (s) => join(s.userHome, '.config', 'systemd', 'user');
|
|
75
|
+
export const systemdDir = (s) => posix.join(s.userHome, '.config', 'systemd', 'user');
|
|
74
76
|
/** systemd quoting: double quotes, backslash and quote escaped, `%` doubled (specifier expansion). */
|
|
75
77
|
const sd = (a) => `"${a.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"`;
|
|
76
78
|
export function renderSystemdService(s) {
|
|
@@ -189,7 +191,7 @@ export function installPlan(s, dexHome) {
|
|
|
189
191
|
const dir = systemdDir(s);
|
|
190
192
|
return {
|
|
191
193
|
platform: s.platform, label: s.label, name: `${unit}.timer`,
|
|
192
|
-
files: [{ path: join(dir, `${unit}.service`), content: renderSystemdService(s) }, { path: join(dir, `${unit}.timer`), content: renderSystemdTimer(s) }],
|
|
194
|
+
files: [{ path: posix.join(dir, `${unit}.service`), content: renderSystemdService(s) }, { path: posix.join(dir, `${unit}.timer`), content: renderSystemdTimer(s) }],
|
|
193
195
|
activate: [['systemctl', '--user', 'daemon-reload'], ['systemctl', '--user', 'enable', '--now', `${unit}.timer`]],
|
|
194
196
|
deactivate: [['systemctl', '--user', 'disable', '--now', `${unit}.timer`]],
|
|
195
197
|
notes: ['A systemd user timer runs while you are logged in; `loginctl enable-linger` (your choice) keeps it running after logout and from boot.'],
|
|
@@ -131,12 +131,10 @@ class ClaudeCliSession {
|
|
|
131
131
|
tools: iso.tools, mcpServers: iso.mcpServers, plugins: iso.plugins, agents: iso.agents, skills: iso.skills, slashCommands: iso.slashCommands,
|
|
132
132
|
};
|
|
133
133
|
this.o.onInit?.(init);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (init.tools !== 0 || init.mcpServers !== 0) {
|
|
139
|
-
this.kill('not-tool-less');
|
|
134
|
+
const failed = !this.o.account.expectApiKeySource.includes(init.apiKeySource) ? 'account-mismatch' : init.tools !== 0 || init.mcpServers !== 0 ? 'not-tool-less' : null;
|
|
135
|
+
if (failed) {
|
|
136
|
+
this.o.onProofFailed?.(failed);
|
|
137
|
+
this.kill(failed);
|
|
140
138
|
return;
|
|
141
139
|
}
|
|
142
140
|
continue;
|
|
@@ -229,6 +227,8 @@ export class SessionGate {
|
|
|
229
227
|
pausedUntil = 0;
|
|
230
228
|
/** Set when a session proved the account or the tool list wrong: no new session until the runner restarts. */
|
|
231
229
|
blocked = null;
|
|
230
|
+
/** The session whose own failure set `blocked` (null when the block came from elsewhere, e.g. a sibling strategy). */
|
|
231
|
+
blockedBy = null;
|
|
232
232
|
constructor(caps, guard, opts = {}) {
|
|
233
233
|
this.caps = caps;
|
|
234
234
|
this.guard = guard;
|
|
@@ -312,7 +312,12 @@ export class SessionGate {
|
|
|
312
312
|
/** Are new sessions paused by the allowance guard right now (wall clock)? */
|
|
313
313
|
paused() { return this.clock() < this.pausedUntil; }
|
|
314
314
|
/** A misconfigured account (or a CLI that reports tools) fails the same way every time: stop spawning for this run. */
|
|
315
|
-
block(reason) {
|
|
315
|
+
block(reason, sessionId) {
|
|
316
|
+
if (this.blocked !== null)
|
|
317
|
+
return;
|
|
318
|
+
this.blocked = reason;
|
|
319
|
+
this.blockedBy = sessionId ?? null;
|
|
320
|
+
}
|
|
316
321
|
}
|
|
317
322
|
const isRec = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
318
323
|
/** The MCP-shaped sections a replay can honestly fill from the tape row; everything else is named as unavailable. */
|
|
@@ -455,6 +460,7 @@ export function claudeSessionFactory(f) {
|
|
|
455
460
|
catch (e) {
|
|
456
461
|
failed = 'account-unavailable';
|
|
457
462
|
f.log('session_account_error', { sessionId: id, error: e.message });
|
|
463
|
+
f.gate.block(failed, id);
|
|
458
464
|
return null;
|
|
459
465
|
}
|
|
460
466
|
if (closed)
|
|
@@ -467,6 +473,9 @@ export function claudeSessionFactory(f) {
|
|
|
467
473
|
onRateLimit: (r) => { f.gate.rateLimit(r); f.log('allowance', { sessionId: id, status: r.status, fiveHour: r.fiveHour, sevenDay: r.sevenDay, resetsAt: r.resetsAt }); },
|
|
468
474
|
onLate: (text) => f.log('late_reply_ignored', { sessionId: id, why: 'arrived after decideWithinSec', text: text.slice(0, 400) }),
|
|
469
475
|
onRaw: f.onRaw ? (line) => f.onRaw(id, line) : undefined,
|
|
476
|
+
// The gate closes at the failed proof itself, not when the runner later reads this session's (null) reply: a
|
|
477
|
+
// candidate that closes first (its TTL) must not leave the gate open for more sessions on the wrong account.
|
|
478
|
+
onProofFailed: (why) => f.gate.block(why, id),
|
|
470
479
|
});
|
|
471
480
|
return inner;
|
|
472
481
|
})());
|
|
@@ -81,7 +81,7 @@ export class StrategyStore {
|
|
|
81
81
|
return { id, source, ok: false, errors: r.errors, warnings: r.warnings };
|
|
82
82
|
const s = r.strategy;
|
|
83
83
|
return {
|
|
84
|
-
id, source, ok: true, name: s.name, version: s.version, hash: s.hash, mode: s.mode, ...(s.highRisk ? { highRisk: true } : {}), profiles: Object.keys(s.exits.profiles), warnings: r.warnings,
|
|
84
|
+
id, source, ok: true, name: s.name, version: s.version, hash: s.hash, mode: s.mode, ...(s.highRisk ? { highRisk: true } : {}), ...(s.sizing.tiers ? { experimentalTiers: true } : {}), profiles: Object.keys(s.exits.profiles), warnings: r.warnings,
|
|
85
85
|
...(last[s.hash] ? { lastBacktest: last[s.hash] } : {}),
|
|
86
86
|
};
|
|
87
87
|
};
|