mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* fix(minions): honest attempt accounting + cooperative abort-honoring + per-handler timeouts (#1737) - Wall-clock and stall dead-letter paths now increment attempts_made (terminal, no retry — wall-clock fires at 2x cumulative timeout; retrying non-idempotent embed/subagent work would duplicate side effects). Surface stalled_counter in jobs get so 'started 3 / stalled 2 / attempts 0' reads true instead of looking like broken accounting. - Thread AbortSignal through embed-backfill/autopilot-cycle -> runPhaseEmbed -> runEmbedCore -> embedAll(Stale)/embedPage, checking it on BOTH --stale and --all paths and between embed batches. A timed-out embed phase now bails within a batch, so the cycle finally releases gbrain_cycle_locks instead of running the full 10-15 min after the job was killed (the daily cycle-wedge). New shared src/core/abort-check.ts (isAborted/throwIfAborted/anySignal). - Per-handler default wall-clock budget (handler-timeouts.ts) stamped at submit for long handlers without an explicit timeout_ms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(minions): queue-scoped DB supervisor singleton + canonical pidfile + doctor max-rss check (#1849) - Acquire a queue-scoped DB lock (tryAcquireDbLock, keyed on the raw DB identity + queue) on supervisor.start(): a second supervisor on the same (db, queue) fails fast with exit 2 regardless of $HOME/--pid-file. Refresh on a dedicated timer; on refresh failure past the threshold, fail SAFE (exit non-zero) before the TTL could lapse and let a second supervisor take over. Release on shutdown. - Canonical default pidfile keyed on brain id (currentBrainId, config-only, no DB connect) so two brains under one HOME no longer share supervisor.pid. - doctor: new supervisor_singleton check surfaces the effective --max-rss (from the started audit event) and warns when the lock holder's (host,pid) differs from the local pidfile — comparing host+pid, not bare pid. Registered in doctor-categories. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-voice): topic-aware persona context via server-resolved topicId (#1851) Summon Mars/Venus into a specific conversation topic so they boot already knowing the recent thread. A per-topic call link carries only topicId (+ optional display topicName); the server resolves the recent-conversation context from the brain (topics/<topicId>.md) — topic CONTENT is never accepted over the wire (that would be prompt injection + a leak into URLs/referrers/logs). topicId is a strict slug with a path-traversal guard. New '# Topic Context' prompt slot injected after the persona body so identity-first ordering still wins; persona identity unchanged. No topicId -> generic behavior. Contract doc + persona skill docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: file #1737 slot-reservation fair-scheduling follow-up TODO (F7) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.42.29.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: sync KEY_FILES.md for v0.42.29.0 minions + abort wave (#1737, #1849) Fold the #1737/#1849 behavior into the existing per-file entries and add the two new core files, keeping the doc at current-state truth: - queue.ts: honest attempt accounting on wall-clock + stall dead-letter paths; defaultTimeoutMsFor stamping at submit. - supervisor.ts: queue-scoped DB singleton lock (supervisorLockId, classifySupervisorSingleton, LOCK_LOST, refresh-fail-safe, brain-id pidfile, max_rss_mb audit). - worker-registry.ts: currentDbIdentity(). - New entries: src/core/abort-check.ts, src/core/minions/handler-timeouts.ts. - New doctor.ts extension: supervisor_singleton check. - cycle.ts / embed.ts extensions: AbortSignal threading note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
/**
|
|
* Tests for src/core/abort-check.ts (#1737 cooperative-abort helper).
|
|
*/
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { isAborted, throwIfAborted, anySignal, AbortError } from '../src/core/abort-check.ts';
|
|
|
|
describe('abort-check: isAborted', () => {
|
|
test('false for undefined / null / not-yet-fired signal', () => {
|
|
expect(isAborted(undefined)).toBe(false);
|
|
expect(isAborted(null)).toBe(false);
|
|
expect(isAborted(new AbortController().signal)).toBe(false);
|
|
});
|
|
|
|
test('true once the controller aborts', () => {
|
|
const ac = new AbortController();
|
|
ac.abort();
|
|
expect(isAborted(ac.signal)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('abort-check: throwIfAborted', () => {
|
|
test('no-op when signal absent or unfired', () => {
|
|
expect(() => throwIfAborted(undefined)).not.toThrow();
|
|
expect(() => throwIfAborted(new AbortController().signal)).not.toThrow();
|
|
});
|
|
|
|
test('throws AbortError carrying the signal reason', () => {
|
|
const ac = new AbortController();
|
|
ac.abort(new Error('wall-clock'));
|
|
try {
|
|
throwIfAborted(ac.signal, '[cycle]');
|
|
throw new Error('should have thrown');
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(AbortError);
|
|
expect((e as Error).name).toBe('AbortError');
|
|
expect((e as Error).message).toBe('[cycle]: wall-clock');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('abort-check: anySignal', () => {
|
|
test('returns the internal signal unchanged when no external signal', () => {
|
|
const internal = new AbortController().signal;
|
|
expect(anySignal(internal, undefined)).toBe(internal);
|
|
expect(anySignal(internal, null)).toBe(internal);
|
|
});
|
|
|
|
test('combined signal fires when the EXTERNAL signal fires', () => {
|
|
const internal = new AbortController();
|
|
const external = new AbortController();
|
|
const combined = anySignal(internal.signal, external.signal);
|
|
expect(combined.aborted).toBe(false);
|
|
external.abort();
|
|
expect(combined.aborted).toBe(true);
|
|
});
|
|
|
|
test('combined signal fires when the INTERNAL signal fires', () => {
|
|
const internal = new AbortController();
|
|
const external = new AbortController();
|
|
const combined = anySignal(internal.signal, external.signal);
|
|
internal.abort();
|
|
expect(combined.aborted).toBe(true);
|
|
});
|
|
|
|
test('combined is already aborted if external was pre-aborted', () => {
|
|
const internal = new AbortController();
|
|
const external = new AbortController();
|
|
external.abort();
|
|
const combined = anySignal(internal.signal, external.signal);
|
|
expect(combined.aborted).toBe(true);
|
|
});
|
|
});
|