Hmbown/CodeWhale · error · Error

Too many active Engine pet spans.

Error message

Too many active Engine pet spans.

What it means

observe() tracks currently open activity spans in this.active (max 256). start() throws 'Too many active Engine pet spans.' when 256 spans are already open and a new distinct key arrives, bounding memory and catching leaked/unbalanced start events.

Solutions

  1. Ensure every start event has a matching finish (same key) in the observed stream
  2. Replay the log in order and verify start/finish pairing; fix key computation so finish keys match start keys
  3. Truncate or drop lowest-priority open spans before the 256 limit is hit
  4. Reset the observer (active.clear()) when starting a new replay

Example fix

// before
for (const ev of events) engine.observe(ev, ev.at);
// after
for (const ev of events) {
  if (engine.active.size >= 256 && ev.event === 'tool_call') ev = { ...ev, event: 'error', failed: true };
  engine.observe(ev, ev.at);
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (engine.active.size >= 256 && !engine.active.has(spanKey)) {
  // close or drop stale spans before starting a new one
}

Try / catch

try {
  engine.observe(ev, at);
} catch (err) {
  if (err.message === 'Too many active Engine pet spans.') {
    console.warn('span leak: force-closing oldest spans');
    engine.active.clear();
    engine.observe(ev, at);
  } else throw err;
}

Prevention

When it happens

Trigger: 256+ concurrent open spans — e.g. tool_call start events whose matching completion events were never observed, or a massively parallel agent session opening more than 256 spans at once.

Common situations: Dropped/lost tool_call_end events in truncated logs so starts leak, an agent spawning more than 256 parallel tools, or keys computed inconsistently so finishes never match their starts.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/e94d33693a71a439. Report an issue: GitHub.

Appendix: source

Thrown at pet/ios/Resources/pet-native.js:1749

            throw new Error('Invalid Engine pet metadata.');
        const e = value;
        const allowed = ['event', 'index', 'channel', 'tool_call_id', 'tool_name', 'id', 'worker_status', 'failed'];
        if (Object.keys(e).some(k => !allowed.includes(k)) || typeof e.event !== 'string'
            || Object.values(e).some(v => typeof v === 'string' && v.length > 4096)
            || e.channel !== undefined && !['text', 'reasoning'].includes(e.channel)
            || ['tool_call_id', 'tool_name', 'id', 'worker_status'].some(k => e[k] !== undefined && typeof e[k] !== 'string')
            || e.failed !== undefined && typeof e.failed !== 'boolean'
            || e.index !== undefined && (!Number.isSafeInteger(e.index) || e.index < 0))
            throw new Error('Invalid Engine pet metadata fields.');
        this.lastTime = at;
        this.events = this.events.filter(span => span.endTime >= at - 12_800);
        const id = (field) => { const s = e[field]; if (typeof s !== 'string' || !s)
            throw new Error(`Missing Engine ${field}.`); return s; };
        const index = () => { if (!Number.isSafeInteger(e.index))
            throw new Error('Missing Engine index.'); return String(e.index); };
        const start = (key, name, category, agentId) => {
            if (this.active.size >= 256 && !this.active.has(key))
                throw new Error('Too many active Engine pet spans.');
            this.active.set(key, this.add(name, category, at, agentId));
        };
        const finish = (key) => { this.pulse(key, at); this.active.delete(key); };
        switch (e.event) {
            case 'turn_started':
                this.active.clear();
                this.waiting = undefined;
                break;
            case 'message_started':
                start(`message:${index()}`, 'assistant_message', 'communication');
                this.waiting = undefined;
                break;
            case 'thinking_started':
                start(`thinking:${index()}`, 'thinking', 'reasoning');
                this.waiting = undefined;
                break;
            case 'response_delta': {
                const reasoning = e.channel === 'reasoning';

View on GitHub (pinned to 433685b202)