Files
gbrain/recipes/agent-voice/code/lib/personas/context-builder.contract.md
T
613da94093 v0.42.29.0 fix(minions): long-job abort-honoring + attempt accounting + supervisor singleton; topic-aware voice (#1737, #1849, #1851) (#1943)
* 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>
2026-06-07 08:12:50 -07:00

6.1 KiB

Context Builder Contract

The voice personas (mars, venus) ship with static prompts. The operator's live brain context (recent activity, calendar, emotional themes, relationships) is injected at session start by a function the operator implements.

This file documents the API contract that implementation must satisfy.

A working example lives at ../context-builder.example.mjs — copy it, adapt it to your brain's actual layout, and import the real one from server.mjs at session-start time. The example reads a documented brain layout ($BRAIN_ROOT/memory/YYYY-MM-DD.md); operators with different layouts replace the file body but keep the function signatures.

Function signatures

/**
 * Build emotionally-salient context for Mars (solo mode).
 *
 * Returns a string ≤ 2500 chars summarizing what's going on in the
 * operator's inner life right now. Mars uses this as background — does NOT
 * recite it back. The format is informational, not a directive.
 *
 * Required: PII scrubbed (phone numbers, emails redacted via REDACT_RE).
 * Required: ≤ 2500 chars (longer is truncated at the boundary).
 *
 * @param {object} opts
 * @param {string} opts.brainRoot — absolute path to brain repo
 * @param {string} [opts.timezone] — IANA tz, e.g. "US/Pacific"
 * @returns {Promise<string>}
 */
export async function buildMarsContext(opts);

/**
 * Build logistics-salient context for Venus.
 *
 * Returns a terse summary of today's commitments, recent emails/messages
 * the operator hasn't seen, and one-liner notable items. Venus reads from
 * this to answer "what's on my calendar" / "any messages" / "what's the
 * status of X" instantly.
 *
 * Required: PII scrubbed.
 * Required: ≤ 2500 chars (longer is truncated).
 *
 * @param {object} opts
 * @param {string} opts.brainRoot
 * @param {string} [opts.timezone]
 * @returns {Promise<string>}
 */
export async function buildVenusContext(opts);

/**
 * #1851 — Build TOPIC context: the recent conversation in the topic the agent
 * was summoned into (persona-agnostic; the same block is used for Mars or
 * Venus). Lets a caller drop a persona into whatever thread they were already
 * discussing without re-explaining.
 *
 * The server resolves this from `topicId` at connect time. `topicId` is the
 * ONLY topic field accepted over the wire (a call link carries it). NEVER
 * accept topic CONTENT as a parameter — that's prompt injection + a leak into
 * URLs, browser history, referrers, and access logs.
 *
 * `topicId` MUST be a strict slug (^[a-z0-9][a-z0-9-]*$, ≤128 chars); the
 * shipped example reads `$BRAIN_ROOT/topics/<topicId>.md` and confines the
 * resolved path under `topics/` (defense-in-depth against traversal).
 *
 * Required: PII scrubbed. Required: ≤ 2500 chars. Returns '' when there is no
 * topic, the id is unsafe, or the file is missing → the persona falls back to
 * its generic live context (current behavior).
 *
 * @param {object} opts
 * @param {string} opts.brainRoot
 * @param {string} opts.topicId — strict slug; indexes topics/<topicId>.md
 * @returns {Promise<string>}
 */
export async function buildTopicContext(opts);

Brain layout expected by the shipped example

The shipped context-builder.example.mjs assumes:

$BRAIN_ROOT/
├── memory/
│   ├── YYYY-MM-DD.md              # daily memory file (markdown)
│   └── heartbeat-state.json       # optional: {currentLocation: {timezone: "US/Pacific"}}
├── people/
│   └── <slug>.md
├── companies/
│   └── <slug>.md
├── tasks/
│   └── open.md                    # active tasks list
└── calendar/
    └── today.md                   # today's events (optional)

If your brain uses a different layout (e.g. daily/YYYY-MM-DD.md instead of memory/), edit context-builder.example.mjs and adjust the path constants at the top. The function signatures must remain stable.

Signal-extraction policy

For Mars (emotional):

  • Pull recent emotionally-loaded lines from the most recent ≤ 2 memory files. Use a generic emotion-word filter (feel, heart, lonely, joy, grief, anger, fear, hope, ache, miss, alive, numb, etc.) — NOT hardcoded family names.
  • Pull "core context" from the operator's stable file (SOUL.md or equivalent) — the high-level emotional landscape the operator documented once. Cap at 600 chars.
  • Pull recent themes the operator has been chewing on (recurring concepts across the last 3 memory files).

For Venus (logistical):

  • Active task count + top 3 highest-priority titles.
  • Calendar events for today (if calendar/today.md exists).
  • Unread message count (if a messages/inbox.md or similar exists).
  • One-liner from the most recent meeting transcript (if any).

PII scrub requirements

Before returning, run the context string through a PII scrubber:

const REDACT_RE = {
  email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
  phone: /(?:\+?\d{1,3}[\s.-]?)?(?:\(\d{3}\)|\d{3})[\s.-]?\d{3}[\s.-]?\d{4}/g,
};
ctx = ctx.replace(REDACT_RE.email, '[email]')
         .replace(REDACT_RE.phone, '[phone]');

Mars and Venus are configured to never read PII aloud anyway, but scrubbing at the context-builder layer is defense-in-depth.

Failure modes

  • If the brain layout doesn't match the example, return an empty string. The personas degrade gracefully (Mars asks open questions; Venus says "I can't see your calendar from here — what do you need?").
  • Try/catch every file read. Missing files are normal, not errors.
  • The context-builder runs in the host process at session start; latency matters. Keep total wall-time under ~200ms.

Testing

mars-prompt-shape.test.mjs and venus-prompt-shape.test.mjs verify the SHIPPED prompts. They do NOT exercise the operator-implemented buildXContext functions — those are out of scope for the shipped tests.

If you want to test your local context-builder, write your own context-builder.local.test.mjs against the contract above. Suggested assertions:

  • Returns a string ≤ 2500 chars.
  • Contains no email-shaped or phone-shaped substrings.
  • Doesn't throw on missing files.
  • Doesn't throw on malformed memory files.
  • Returns within ~200ms for a brain with ~10k pages.