(opts)
| 250 | } |
| 251 | |
| 252 | async function _considerOrderSerialized(opts) { |
| 253 | const now = Date.now(); |
| 254 | let ledger = _readLedger(); |
| 255 | ledger = _rotateIfNewDay(ledger, now); |
| 256 | ledger = _pruneDedup(ledger, now); |
| 257 | |
| 258 | const hash = _questionHash(opts); |
| 259 | if (ledger.dedup[hash]) { |
| 260 | return { ok: false, skipped: true, reason: 'dedup_hit', hash: hash }; |
| 261 | } |
| 262 | |
| 263 | const dailyCap = _effectiveCap(_config.dailyCap, now); |
| 264 | const perOrderCap = _effectiveCap(_config.perOrderCap, now); |
| 265 | const remaining = Math.max(0, dailyCap - (ledger.spent || 0)); |
| 266 | if (remaining <= 0) { |
| 267 | console.warn('[ATP-AutoBuyer] Daily cap reached, skipping order (spent=' + ledger.spent + ', cap=' + dailyCap + ')'); |
| 268 | return { ok: false, skipped: true, reason: 'daily_cap_reached', spent: ledger.spent, cap: dailyCap }; |
| 269 | } |
| 270 | |
| 271 | const requested = Math.max(1, Math.floor(Number(opts.budget) || perOrderCap)); |
| 272 | const budget = Math.min(requested, perOrderCap, remaining); |
| 273 | if (budget <= 0) { |
| 274 | return { ok: false, skipped: true, reason: 'budget_clamped_to_zero' }; |
| 275 | } |
| 276 | |
| 277 | const orderOpts = { |
| 278 | capabilities: opts.capabilities, |
| 279 | budget: budget, |
| 280 | routingMode: opts.routingMode || 'fastest', |
| 281 | verifyMode: opts.verifyMode || 'auto', |
| 282 | question: opts.question, |
| 283 | signals: opts.signals, |
| 284 | minReputation: opts.minReputation, |
| 285 | }; |
| 286 | |
| 287 | const result = await _withTimeout(hubClient.placeOrder(orderOpts), _config.timeoutMs); |
| 288 | |
| 289 | if (result && result.ok) { |
| 290 | ledger.spent = (ledger.spent || 0) + budget; |
| 291 | ledger.dedup[hash] = { ts: now, failed: false }; |
| 292 | _writeLedger(ledger); |
| 293 | console.log('[ATP-AutoBuyer] Order placed: ' + (result.data && result.data.order_id) + ' budget=' + budget + ' remaining_today=' + Math.max(0, dailyCap - ledger.spent)); |
| 294 | return { ok: true, data: result.data, spent: budget }; |
| 295 | } |
| 296 | |
| 297 | // On failure record a SHORT-TTL dedup entry (5 min) so we don't hammer the |
| 298 | // hub for the same capability gap inside a tight retry loop, but the user |
| 299 | // can retry the same question once the transient error clears — far better |
| 300 | // than the previous 24h block for a single 503. |
| 301 | ledger.dedup[hash] = { ts: now, failed: true }; |
| 302 | _writeLedger(ledger); |
| 303 | return { ok: false, error: (result && result.error) || 'unknown_error' }; |
| 304 | } |
| 305 | |
| 306 | // Write the consent ack file. Used by `evolver atp enable|disable` and the |
| 307 | // first-run prompt. `enabled=true` persists opt-in; `enabled=false` persists |
no test coverage detected