Compare commits

..
Author SHA1 Message Date
Garry TanandClaude Fable 5 bcdb435d73 fix(dream): deterministic last-writer attribution for colliding slugs
collectChildPutPageSlugs paired each slug to a jobId first-seen over an
unordered result set. When two children collide on a final slug, the pages
row holds the LAST put_page write, so the provenance stamp
(transcript_id/transcript_source/date) could attribute an arbitrary other
transcript. ORDER BY id + last-writer-wins aligns the stamp with the
surviving content; pinned by a collision test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:14:19 -07:00
Garry TanandClaude Fable 5 093d693502 test(progress): assert net-zero live-reporter delta, not absolute zero
The 'only one process-level signal handler' test asserted
__liveReporterCountForTest() === 0, which encodes 'no other test file in
this bun process left a live reporter' — a shard-composition property,
not this test's invariant. PR #3096's new test files reshuffled the LPT
shard packing so a shardmate's live reporter now lands before
progress.test.ts in CI shard 5, failing the test deterministically
(both run attempts). Snapshot the count before the 50 lifecycles and
assert the delta is zero, mirroring the installedBefore baseline the
test already uses for the signal handler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 11:22:03 -07:00
9268552d70 feat(dream): orchestrator-owned transcript metadata + transcriptSource discovery (#2285)
Takeover of #2286, rebased onto master so #1586's cycle-source scoping is
preserved (refs keep the cycleSourceId threading; the original branch's
rewritten collectChildPutPageSlugs hardcoded source_id 'default').

- DiscoveredTranscript carries transcriptSource, derived from the
  <corpus>/<source>/<date>/<id>.md layout; null for ad-hoc inputs.
- Transcript date inference now prefers the '| First message |' row in the
  transcript's '## Metadata' table (stable across mtime-restamping
  re-syncs), with the filename-regex date as fallback — implementing the
  cascade #2286 promised but didn't ship.
- The synthesize orchestrator stamps deterministic frontmatter
  (transcript_id, transcript_hash, transcript_source, chunk, date) through
  the existing #2569 stampDreamProvenance jsonb funnel — no second putPage
  write; reverseWriteRefs re-reads the row so DB and disk stay lockstep.
  Subagents retain authority over type/title/tags/body.

Co-authored-by: brettdavies <brettdavies@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:25:51 -07:00
19 changed files with 384 additions and 192 deletions
-4
View File
@@ -908,10 +908,6 @@ export const KNOWN_CONFIG_KEYS: readonly string[] = [
'facts.extraction_model',
// #2113: output-token cap for the per-turn facts extractor (default 4000).
'facts.extraction_max_tokens',
// Owner opt-in: let the local stdio MCP pipe read this owner's private facts
// (find_trajectory / recall). Default off; HTTP transport ignores it. See
// src/core/facts/reader-trust.ts.
'facts.trust_local_reads',
// Dream cycle config
'dream.synthesize.session_corpus_dir',
'dream.synthesize.meeting_transcripts_dir',
+101 -20
View File
@@ -428,8 +428,13 @@ export async function runPhaseSynthesize(
const queue = new MinionQueue(engine);
const childIds: number[] = [];
/** Map child job_id → chunk metadata for D6 orchestrator-side slug rewrite. */
const chunkInfo = new Map<number, { idx: number; hash6: string }>();
/**
* Map child job_id → transcript metadata. Drives D6 orchestrator-side
* slug rewrite for chunked transcripts AND the deterministic frontmatter
* stampDreamProvenance merges into each written page. Populated for
* every child (single-chunk children carry chunkTotal=1).
*/
const childMeta = new Map<number, ChildMeta>();
/** Skip reasons for the cycle report (D5 cap hits, D8 legacy-key skips). */
const skipReports: Array<{ filePath: string; reason: string }> = [];
@@ -513,9 +518,14 @@ export async function runPhaseSynthesize(
{ allowProtectedSubmit: true },
);
childIds.push(child.id);
if (isChunked) {
chunkInfo.set(child.id, { idx: i, hash6 });
}
childMeta.set(child.id, {
idx: i,
hash6,
chunkTotal: chunks.length,
transcriptSource: t.transcriptSource,
transcriptId: stripContentVersionSuffix(t.basename),
inferredDate: t.inferredDate,
});
}
}
@@ -544,14 +554,14 @@ export async function runPhaseSynthesize(
// Collect slugs from put_page tool executions across the children
// (codex finding #2: deterministic provenance, NOT pages.updated_at).
// D6 orchestrator slug rewrite: chunkInfo drives post-hoc rewrite of
// D6 orchestrator slug rewrite: childMeta drives post-hoc rewrite of
// bare-hash slugs to `<hash6>-c<idx>` so chunked siblings can't collide
// even if Sonnet drops the chunk suffix.
// v0.32.8: refs carry source_id so reverseWriteRefs picks the correct
// (source, slug) row. #1586: refs are stamped with the cycle's resolved
// source (children write there via SubagentHandlerData.source_id).
const cycleSourceId = opts.sourceId ?? 'default';
const writtenRefs = await collectChildPutPageSlugs(engine, childIds, chunkInfo, cycleSourceId);
const writtenRefs = await collectChildPutPageSlugs(engine, childIds, childMeta, cycleSourceId);
const summaryDate = opts.date ?? today();
@@ -559,7 +569,12 @@ export async function runPhaseSynthesize(
// of every child-written page BEFORE reverse-rendering, so generated pages
// are queryable (`frontmatter->>'dream_generated'`) and a later put_page
// write-through (which re-renders from the DB row) can't erase the stamp.
await stampDreamProvenance(engine, writtenRefs, summaryDate);
// #2285: the stamp also carries the orchestrator-owned deterministic
// frontmatter (transcript_id, transcript_source, transcript_hash, date,
// chunk) derived from childMeta — subagent drift on those fields can't
// leak, and reverseWriteRefs below re-reads the row so the same fields
// land in the on-disk markdown.
await stampDreamProvenance(engine, writtenRefs, summaryDate, childMeta);
// Dual-write: reverse-render each DB row → markdown file.
const reverseWriteCount = await reverseWriteRefs(engine, opts.brainDir, writtenRefs, cycleSourceId);
@@ -1095,15 +1110,17 @@ function sanitizeForSlug(s: string): string {
* fake"): we no longer need detection because the rewrite enforces
* uniqueness at slug-write time.
*
* `chunkInfo` maps child job_id → { chunk_index, hash6 }. Single-chunk
* children are absent from the map and pass through unchanged.
* `childMeta` maps child job_id → per-child transcript metadata. Chunked
* children (chunkTotal > 1) get the slug rewrite; single-chunk children
* pass through unchanged. Each returned ref carries the job_id that wrote
* it so stampDreamProvenance can pair the slug back to its childMeta entry.
*/
async function collectChildPutPageSlugs(
engine: BrainEngine,
childIds: number[],
chunkInfo: Map<number, { idx: number; hash6: string }>,
childMeta: Map<number, ChildMeta>,
sourceId = 'default',
): Promise<Array<{ slug: string; source_id: string }>> {
): Promise<Array<{ slug: string; source_id: string; jobId: number }>> {
if (childIds.length === 0) return [];
// Raw fetch — NO SELECT DISTINCT. Preserves per-child slug duplicates so
// the orchestrator sees what each child wrote. COALESCE handles both
@@ -1122,16 +1139,73 @@ async function collectChildPutPageSlugs(
FROM subagent_tool_executions
WHERE job_id = ANY($1::int[])
AND tool_name = 'brain_put_page'
AND status = 'complete'`,
AND status = 'complete'
ORDER BY id`,
[childIds],
);
const rewritten = new Set<string>();
const rewritten = new Map<string, number>();
for (const r of rows) {
if (typeof r.slug !== 'string' || r.slug.length === 0) continue;
const ci = chunkInfo.get(r.job_id);
rewritten.add(ci ? rewriteChunkedSlug(r.slug, ci.hash6, ci.idx) : r.slug);
const meta = childMeta.get(r.job_id);
const finalSlug = meta && meta.chunkTotal > 1
? rewriteChunkedSlug(r.slug, meta.hash6, meta.idx)
: r.slug;
// Last writer wins, in execution-row order (ORDER BY id): if two children
// collide on a final slug, the pages row holds the LAST put_page write, so
// the stamp must attribute that child's transcript — not an arbitrary one.
rewritten.set(finalSlug, r.job_id);
}
return Array.from(rewritten).sort().map(slug => ({ slug, source_id: sourceId }));
return [...rewritten.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([slug, jobId]) => ({ slug, source_id: sourceId, jobId }));
}
/**
* Per-child orchestrator state. Drives D6 chunked-slug rewrite (idx + hash6)
* AND the deterministic frontmatter stampDreamProvenance merges into each
* written page. Populated for every child, not just chunked ones.
*/
interface ChildMeta {
idx: number;
hash6: string;
chunkTotal: number;
transcriptSource: string | null;
transcriptId: string;
inferredDate: string | null;
}
/**
* Strip the content-version suffix that claude-code-archive appends when a
* conversation is edited (`<uuid>--<contentHash>.md`). The session UUID is
* the stable transcript identifier; the suffix changes with content. Used to
* populate `transcript_id` so edits of the same session collapse to one id.
*/
function stripContentVersionSuffix(basename: string): string {
return basename.replace(/--[a-f0-9]+$/i, '');
}
/**
* Deterministic frontmatter for one synthesized page (#2285). Every field
* here is owned by the orchestrator — the subagent's value for any of these
* is overwritten. The subagent retains authority over type / title / tags /
* body. `date` feeds the effective-date precedence chain
* (src/core/effective-date.ts) so re-imports keep the conversation date even
* when sync tools re-stamp file mtimes.
*/
function buildDeterministicFrontmatter(
meta: ChildMeta,
cycleDate: string,
): Record<string, unknown> {
const overrides: Record<string, unknown> = {
dream_generated: true,
dream_cycle_date: cycleDate,
transcript_id: meta.transcriptId,
transcript_hash: meta.hash6,
};
if (meta.transcriptSource) overrides.transcript_source = meta.transcriptSource;
if (meta.chunkTotal > 1) overrides.chunk = `${meta.idx + 1}/${meta.chunkTotal}`;
if (meta.inferredDate) overrides.date = meta.inferredDate;
return overrides;
}
/**
@@ -1177,12 +1251,19 @@ async function hasLegacySingleChunkCompletion(
*/
async function stampDreamProvenance(
engine: BrainEngine,
refs: Array<{ slug: string; source_id: string }>,
refs: Array<{ slug: string; source_id: string; jobId?: number }>,
cycleDate: string,
childMeta?: Map<number, ChildMeta>,
): Promise<void> {
if (refs.length === 0) return;
const { executeRawJsonb } = await import('../sql-query.ts');
for (const { slug, source_id } of refs) {
for (const { slug, source_id, jobId } of refs) {
// #2285: when the ref pairs back to a child, the stamp also carries the
// orchestrator-owned deterministic frontmatter for that transcript.
const meta = jobId !== undefined ? childMeta?.get(jobId) : undefined;
const stamp = meta
? buildDeterministicFrontmatter(meta, cycleDate)
: { dream_generated: true, dream_cycle_date: cycleDate };
try {
await executeRawJsonb(
engine,
@@ -1190,7 +1271,7 @@ async function stampDreamProvenance(
SET frontmatter = COALESCE(frontmatter, '{}'::jsonb) || $3::jsonb
WHERE slug = $1 AND source_id = $2`,
[slug, source_id],
[{ dream_generated: true, dream_cycle_date: cycleDate }],
[stamp],
);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
+58 -5
View File
@@ -10,7 +10,7 @@
*/
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, basename } from 'node:path';
import { join, basename, dirname } from 'node:path';
import { createHash } from 'node:crypto';
import { pruneDir } from '../sync.ts';
@@ -23,8 +23,22 @@ export interface DiscoveredTranscript {
content: string;
/** Filename basename without extension; used as a topic-slug seed. */
basename: string;
/** Inferred date if the basename matches `YYYY-MM-DD...` (or null). */
/**
* Inferred conversation date (YYYY-MM-DD) or null. Precedence: the
* `| First message | <ISO> |` row in the transcript's `## Metadata`
* table (stable across mtime-restamping re-syncs) wins; a leading
* `YYYY-MM-DD` in the basename is the fallback.
*/
inferredDate: string | null;
/**
* Transcript source archive name, derived from the path's grandparent
* directory (the immediate parent of the date directory). For the
* canonical layout `<corpus>/<source>/<date>/<id>.md` this yields the
* source-name segment — e.g. `claude-code` for the claude-code-archive
* output, `meetings` for meeting recordings. Null when the file does
* not live under a `<source>/<date>/` pair (ad-hoc inputs).
*/
transcriptSource: string | null;
}
export interface DiscoverOpts {
@@ -161,6 +175,36 @@ function matchesAnyExclude(text: string, patterns: RegExp[]): boolean {
return false;
}
/**
* Content-based conversation date: the `| First message | <ISO timestamp> |`
* row claude-code-archive writes into the transcript's `## Metadata` table.
* Stable across rsync/Dropbox/Syncthing/B2 re-syncs that re-stamp mtime,
* unlike anything derived from file metadata. Returns YYYY-MM-DD or null.
*/
const FIRST_MESSAGE_RE = /^\|\s*First message\s*\|\s*(\d{4}-\d{2}-\d{2})/im;
export function inferContentDate(content: string): string | null {
const m = FIRST_MESSAGE_RE.exec(content);
return m ? m[1] : null;
}
/**
* Derive the archive source name from a transcript path. Returns the basename
* of the directory two levels above the file when the immediate parent is a
* date directory and the grandparent looks like a source-name slug (lowercase
* alphanumeric segments separated by hyphens); otherwise null. This pins the
* canonical claude-code-archive layout `<corpus>/<source>/<date>/<id>.md`
* without claiming a source for ad-hoc inputs that don't match.
*/
export function deriveTranscriptSource(filePath: string): string | null {
const parentName = basename(dirname(filePath));
if (!/^\d{4}-\d{2}-\d{2}/.test(parentName)) return null;
const grandparentName = basename(dirname(dirname(filePath)));
if (!grandparentName) return null;
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(grandparentName)) return null;
return grandparentName;
}
function listTextFiles(dir: string): string[] {
// Recursive walk with descent-time pruning (closes codex C12/C13 spec gap).
// Accepts BOTH .txt and .md per transcript-discovery's domain rules — does
@@ -225,8 +269,11 @@ export function discoverTranscripts(opts: DiscoverOpts): DiscoveredTranscript[]
const ext = filePath.endsWith('.md') ? '.md' : '.txt';
const baseName = basename(filePath, ext);
const dateMatch = DATE_RE.exec(baseName);
const inferredDate = dateMatch ? dateMatch[1] : null;
if (!isInDateRange(inferredDate, opts)) continue;
const filenameDate = dateMatch ? dateMatch[1] : null;
// Fast path: date-named files outside the window skip before the read.
// ponytail: a date-named file whose content date differs is filtered on
// its filename date — acceptable; archive layouts use UUID basenames.
if (filenameDate && !isInDateRange(filenameDate, opts)) continue;
let content: string;
try {
@@ -241,12 +288,17 @@ export function discoverTranscripts(opts: DiscoverOpts): DiscoveredTranscript[]
}
if (matchesAnyExclude(content, excludeRes)) continue;
// Content-metadata date wins (survives mtime restamps); filename next.
const inferredDate = inferContentDate(content) ?? filenameDate;
if (!isInDateRange(inferredDate, opts)) continue;
results.push({
filePath,
contentHash: hashContent(content),
content,
basename: baseName,
inferredDate,
transcriptSource: deriveTranscriptSource(filePath),
});
}
}
@@ -290,6 +342,7 @@ export function readSingleTranscript(
contentHash: hashContent(content),
content,
basename: baseName,
inferredDate: dateMatch ? dateMatch[1] : null,
inferredDate: inferContentDate(content) ?? (dateMatch ? dateMatch[1] : null),
transcriptSource: deriveTranscriptSource(filePath),
};
}
-2
View File
@@ -591,8 +591,6 @@ export interface TrajectoryOpts {
sourceIds?: string[];
/** When true, filters to visibility='world' only. Set by MCP layer from ctx.remote. */
remote?: boolean;
/** Owner opt-in: read private facts despite `remote`. See facts/reader-trust.ts. */
trustedFactReads?: boolean;
/** Metric filter. When set, only facts with this canonical metric label participate. */
metric?: string;
/**
+5 -8
View File
@@ -17,7 +17,6 @@
import type { OperationContext } from './../operations.ts';
import type { FactRow } from './../engine.ts';
import { effectiveConfidence } from './decay.ts';
import { readableFactVisibilities } from './reader-trust.ts';
const DEFAULT_TTL_MS = 30_000;
const DEFAULT_TOP_K = 10;
@@ -51,13 +50,7 @@ export async function getBrainHotMemoryMeta(
const sessionId = (ctx as { source_session?: string }).source_session
?? null;
const allowListHash = hashAllowList(ctx.takesHoldersAllowList);
// Visibility tier: untrusted remote → world-only; trusted local +
// owner-trusted reads → all rows. Folded into the cache key (the header's
// "cache entries don't bleed across tiers" invariant): trustedFactReads is
// re-read from config per call, so a mid-session opt-out must not keep
// serving a private-inclusive cached payload for the TTL window.
const visibility = readableFactVisibilities(ctx);
const cacheKey = `${sourceId}::${sessionId ?? '_'}::${allowListHash}::${visibility ? 'world' : 'all'}`;
const cacheKey = `${sourceId}::${sessionId ?? '_'}::${allowListHash}`;
const ttl = Math.max(1000, opts.ttlMs ?? DEFAULT_TTL_MS);
const topK = Math.max(1, Math.min(opts.topK ?? DEFAULT_TOP_K, 25));
@@ -68,6 +61,10 @@ export async function getBrainHotMemoryMeta(
return cached.payload;
}
// Build a fresh payload. Visibility tier: remote → world-only;
// local → all rows.
const visibility = ctx.remote === false ? undefined : ['world'] as ('world' | 'private')[];
let rows: FactRow[] = [];
if (sessionId) {
rows = await ctx.engine.listFactsBySession(sourceId, sessionId, {
-48
View File
@@ -1,48 +0,0 @@
/**
* Fact-read visibility trust.
*
* Fact rows are tagged `private` | `world`. Remote/untrusted callers
* (`remote === true`) see only `world` rows — the posture that keeps a
* published or HTTP-served brain from leaking private claims to strangers.
*
* But the stdio MCP server is an unauthenticated LOCAL pipe: on a single-owner
* machine the caller IS the owner, yet it still defaults `remote: true` for
* safety, so the owner's own agent is denied the owner's own private facts
* (e.g. `find_trajectory` returns empty over MCP even though the facts exist).
*
* `trustedFactReads` is a narrow, READ-ONLY trust elevation, deliberately
* DECOUPLED from `remote` so every other remote protection — file_upload
* confinement, source isolation, fence stripping, takes-holder scoping — stays
* fully in force. The stdio MCP server sets it ONLY when the brain owner opts
* in via the `facts.trust_local_reads` config (default off). The HTTP/published
* transport never sets it, so a served brain stays world-only regardless.
*/
export interface FactReaderTrust {
/**
* Mirrors OperationContext.remote. FAIL-CLOSED: anything not strictly
* `false` is treated as remote/untrusted (CLAUDE.md trust invariant).
*/
remote?: boolean;
/** Owner opt-in: this remote caller may read private facts. */
trustedFactReads?: boolean;
}
/**
* True when the reader is restricted to `visibility = 'world'` rows.
* Fail-closed: an unset/undefined `remote` is untrusted — only an explicit
* `remote: false` (trusted local CLI) or an explicit owner opt-in
* (`trustedFactReads: true`) reads private rows.
*/
export function factsWorldOnly(t: FactReaderTrust): boolean {
return t.remote !== false && t.trustedFactReads !== true;
}
/**
* Visibility filter for list-style fact reads: `['world']` when the reader is
* world-only, `undefined` (no filter — all rows) when it is trusted.
*/
export function readableFactVisibilities(
t: FactReaderTrust,
): ('private' | 'world')[] | undefined {
return factsWorldOnly(t) ? ['world'] : undefined;
}
+7 -14
View File
@@ -18,7 +18,6 @@ import { captureEvalCandidate, isEvalCaptureEnabled, isEvalScrubEnabled } from '
import type { HybridSearchMeta } from './types.ts';
import { extractPageLinks, isAutoLinkEnabled, isAutoTimelineEnabled, isGlobalBasenameEnabled, parseTimelineEntries, makeResolver, type UnresolvedFrontmatterRef } from './link-extraction.ts';
import { isFactsBackstopEligible } from './facts/eligibility.ts';
import { readableFactVisibilities } from './facts/reader-trust.ts';
import { stripTakesFence } from './takes-fence.ts';
import { stripFactsFence } from './facts-fence.ts';
import { getContentFlag } from './quarantine.ts';
@@ -333,15 +332,6 @@ export interface OperationContext {
* remote/untrusted (defense in depth in case the type is bypassed via cast).
*/
remote: boolean;
/**
* Owner opt-in (`facts.trust_local_reads`): allow this remote caller to read
* `private` facts. A NARROW, read-only trust elevation decoupled from
* `remote` — every other remote protection (file confinement, source
* isolation, fence stripping, takes scoping) stays in force. Set ONLY by the
* stdio MCP server when the config is on; the HTTP transport never sets it.
* Consulted via `src/core/facts/reader-trust.ts`.
*/
trustedFactReads?: boolean;
/**
* Subagent runtime context (v0.16+). Set by the subagent tool dispatcher when
* dispatching an op as a tool call from an LLM loop. Used to enforce per-op
@@ -3639,7 +3629,6 @@ const find_trajectory: Operation = {
entitySlug: p.entity_slug,
...scope,
remote: ctx.remote === true,
trustedFactReads: ctx.trustedFactReads === true,
metric,
kind,
since,
@@ -4000,9 +3989,13 @@ const recall: Operation = {
const includeExpired = p.include_expired === true;
const grep = typeof p.grep === 'string' ? p.grep.toLowerCase() : null;
// Visibility filter: world-only for untrusted remote callers; all rows for
// trusted local CLI and owner-trusted reads (facts.trust_local_reads).
const visibility = readableFactVisibilities(ctx);
// Visibility filter: remote callers see world-only unless their token
// grants elevated visibility (future-proofing; v0.31 ships world-only
// for remote, all for local CLI).
const visibility =
ctx.remote === false
? undefined
: ['world'] as ('private' | 'world')[];
let rows: Awaited<ReturnType<typeof ctx.engine.listFactsByEntity>> = [];
+1 -5
View File
@@ -18,7 +18,6 @@ import type {
} from './engine.ts';
import { MAX_SEARCH_LIMIT, clampSearchLimit } from './engine.ts';
import { withRetry, BULK_RETRY_OPTS, resolveBulkRetryOpts, computeNextDelay, type BatchAuditSite } from './retry.ts';
import { factsWorldOnly } from './facts/reader-trust.ts';
import { logBatchRetry as auditLogBatchRetry, logBatchExhausted as auditLogBatchExhausted } from './audit/batch-retry-audit.ts';
import { runMigrations } from './migrate.ts';
import { PGLITE_SCHEMA_SQL, getPGLiteSchema } from './pglite-schema.ts';
@@ -4319,10 +4318,7 @@ export class PGLiteEngine implements BrainEngine {
const useArray = Array.isArray(opts.sourceIds) && opts.sourceIds.length > 0;
const sourceIds = useArray ? opts.sourceIds! : null;
const sourceId = opts.sourceId ?? 'default';
// Direct-engine contract: unset `remote` here means a trusted in-process
// caller (CLI/tests) — the fail-closed default lives in the op layer, which
// always passes explicit booleans. Normalize before the fail-closed helper.
const remoteFilter = factsWorldOnly({ remote: opts.remote === true, trustedFactReads: opts.trustedFactReads === true });
const remoteFilter = opts.remote === true;
// Build SQL dynamically. PGLite uses $N positional params; we
// assemble the WHERE clauses + params array in tandem to keep them
+1 -5
View File
@@ -14,7 +14,6 @@ import type {
SourceRow,
} from './engine.ts';
import { withRetry, BULK_RETRY_OPTS, resolveBulkRetryOpts, computeNextDelay, type BatchAuditSite } from './retry.ts';
import { factsWorldOnly } from './facts/reader-trust.ts';
import { logBatchRetry as auditLogBatchRetry, logBatchExhausted as auditLogBatchExhausted } from './audit/batch-retry-audit.ts';
import type {
DomainBankSampleOpts, CorpusSampleOpts, DomainBankRow,
@@ -4533,10 +4532,7 @@ export class PostgresEngine implements BrainEngine {
const useArray = Array.isArray(opts.sourceIds) && opts.sourceIds.length > 0;
const sourceIds = useArray ? opts.sourceIds! : null;
const sourceId = opts.sourceId ?? 'default';
// Direct-engine contract: unset `remote` here means a trusted in-process
// caller (CLI/tests) — the fail-closed default lives in the op layer, which
// always passes explicit booleans. Normalize before the fail-closed helper.
const remoteFilter = factsWorldOnly({ remote: opts.remote === true, trustedFactReads: opts.trustedFactReads === true });
const remoteFilter = opts.remote === true;
// Source-scope predicate: array path (federated) wins over scalar.
// Engine.ts contract: returns chronological points; regressions +
-8
View File
@@ -30,13 +30,6 @@ export interface ToolResult {
export interface DispatchOpts {
/** Defaults to true (remote/untrusted). Local CLI callers (`gbrain call`) pass false. */
remote?: boolean;
/**
* Owner opt-in (`facts.trust_local_reads`): let this remote caller read
* `private` facts. Set ONLY by the stdio MCP server; the HTTP transport
* leaves it unset so a served brain stays world-only. See
* `src/core/facts/reader-trust.ts`.
*/
trustedFactReads?: boolean;
/** Override the default stderr logger (e.g. CLI uses console.* directly). */
logger?: OperationContext['logger'];
/**
@@ -210,7 +203,6 @@ export function buildOperationContext(
logger: opts.logger || stderrLogger,
dryRun: !!params.dry_run,
remote: opts.remote ?? true,
trustedFactReads: opts.trustedFactReads === true,
takesHoldersAllowList: opts.takesHoldersAllowList,
// v0.34 D4: sourceId is REQUIRED at the type level. Auto-fill 'default'
// for single-source brains and any caller who didn't resolve a sourceId.
-11
View File
@@ -35,16 +35,6 @@ export async function startMcpServer(engine: BrainEngine) {
// shape and cast through `any` (the SDK accepts it via the ServerResult union).
server.setRequestHandler(CallToolRequestSchema, async (request: any): Promise<any> => {
const { name, arguments: params } = request.params;
// Owner opt-in: the stdio pipe is local + unauthenticated, so on a
// single-owner machine its caller is the owner. When facts.trust_local_reads
// is on, let fact reads (find_trajectory / recall) see this owner's own
// private facts. Narrow + read-only — every other remote protection stays
// on (remote stays true). HTTP transport never sets this. Best-effort: a
// config read blip falls back to the safe world-only default.
let trustedFactReads = false;
try {
trustedFactReads = (await engine.getConfig('facts.trust_local_reads')) === 'true';
} catch { /* keep world-only default */ }
// v0.28: stdio MCP has no per-token auth (local pipe). Default the
// takes-holder allow-list to ['world'] so agent-facing callers don't
// see private hunches via takes_list / takes_search / query. Operators
@@ -52,7 +42,6 @@ export async function startMcpServer(engine: BrainEngine) {
// `gbrain call <op>` (sets remote=false in src/cli.ts).
return dispatchToolCall(engine, name, params, {
remote: true,
trustedFactReads,
takesHoldersAllowList: ['world'],
// v0.31: source defaults to 'default' for stdio (no per-token scope).
// Operators who want a different source on stdio MCP should set
+1
View File
@@ -26,6 +26,7 @@ const transcript: DiscoveredTranscript = {
content: 'User: hello world',
contentHash: 'abcdef0123456789',
inferredDate: '2026-07-17',
transcriptSource: null,
} as DiscoveredTranscript;
describe('#2415: buildSynthesisPrompt output root', () => {
@@ -152,3 +152,94 @@ describe('#2569: stampDreamProvenance persists the marker into DB frontmatter',
await stampDreamProvenance(engine as any, refs, '2026-07-17'); // idempotent
});
});
describe('#2285: orchestrator-owned deterministic transcript frontmatter', () => {
const meta = {
idx: 1,
hash6: 'abc123',
chunkTotal: 3,
transcriptSource: 'claude-code',
transcriptId: 'session-uuid',
inferredDate: '2026-05-15',
};
test('collectChildPutPageSlugs pairs each ref back to the writing job', async () => {
const refs = await collectChildPutPageSlugs(
engine as any, [1001], new Map([[1001, { ...meta, chunkTotal: 1 }]]), 'mybrain',
);
expect(refs.length).toBeGreaterThan(0);
for (const r of refs) {
expect(r.jobId).toBe(1001);
expect(r.source_id).toBe('mybrain'); // #1586: cycle source, never hardcoded 'default'
}
});
test('slug collision across children attributes the LAST writer (matches surviving putPage)', async () => {
const db = (engine as any).db;
// Jobs 1001 then 1002 write the same slug; the pages row would hold
// 1002's content (last put_page wins), so the ref must carry jobId 1002.
await db.query(
`INSERT INTO subagent_tool_executions (job_id, message_idx, tool_use_id, tool_name, status, input)
VALUES (1001, 9, 'tool_dup_a', 'brain_put_page', 'complete', $1::jsonb)`,
[JSON.stringify({ slug: 'wiki/agents/test/collision', body: 'first' })],
);
await db.query(
`INSERT INTO subagent_tool_executions (job_id, message_idx, tool_use_id, tool_name, status, input)
VALUES (1002, 9, 'tool_dup_b', 'brain_put_page', 'complete', $1::jsonb)`,
[JSON.stringify({ slug: 'wiki/agents/test/collision', body: 'second' })],
);
const refs = await collectChildPutPageSlugs(engine as any, [1001, 1002], new Map());
const hit = refs.find((r: { slug: string }) => r.slug === 'wiki/agents/test/collision');
expect(hit?.jobId).toBe(1002);
});
test('stampDreamProvenance merges the transcript metadata into DB frontmatter', async () => {
const slug = 'wiki/originals/ideas/2026-07-17-transcript-meta-abc123';
await engine.putPage(slug, {
type: 'note',
title: 'Meta stamp',
compiled_truth: 'body',
timeline: '',
frontmatter: { keep_me: 'yes', transcript_id: 'subagent-drift' },
});
await stampDreamProvenance(
engine as any,
[{ slug, source_id: 'default', jobId: 42 }],
'2026-07-17',
new Map([[42, meta]]),
);
const rows = await engine.executeRaw<{ fm: Record<string, unknown> }>(
`SELECT frontmatter AS fm FROM pages WHERE slug = $1`, [slug],
);
const fm = rows[0].fm as Record<string, unknown>;
expect(fm.dream_generated).toBe(true);
expect(fm.dream_cycle_date).toBe('2026-07-17');
expect(fm.transcript_id).toBe('session-uuid'); // orchestrator wins over subagent drift
expect(fm.transcript_hash).toBe('abc123');
expect(fm.transcript_source).toBe('claude-code');
expect(fm.chunk).toBe('2/3');
expect(fm.date).toBe('2026-05-15');
expect(fm.keep_me).toBe('yes'); // subagent-owned keys survive
});
test('single-chunk children with no inferredDate stamp only the applicable fields', async () => {
const slug = 'wiki/originals/ideas/2026-07-17-minimal-meta-abc123';
await engine.putPage(slug, {
type: 'note', title: 'Minimal', compiled_truth: 'b', timeline: '', frontmatter: {},
});
await stampDreamProvenance(
engine as any,
[{ slug, source_id: 'default', jobId: 43 }],
'2026-07-17',
new Map([[43, { ...meta, chunkTotal: 1, transcriptSource: null, inferredDate: null }]]),
);
const rows = await engine.executeRaw<{ fm: Record<string, unknown> }>(
`SELECT frontmatter AS fm FROM pages WHERE slug = $1`, [slug],
);
const fm = rows[0].fm as Record<string, unknown>;
expect(fm.transcript_id).toBe('session-uuid');
expect(fm.chunk).toBeUndefined();
expect(fm.transcript_source).toBeUndefined();
expect(fm.date).toBeUndefined();
});
});
+2
View File
@@ -302,6 +302,7 @@ describe('judgeSignificance', () => {
content: 'A short conversation about something interesting.',
basename: 'x',
inferredDate: null,
transcriptSource: null,
};
}
@@ -415,6 +416,7 @@ describe('judgeSignificance — UTF-16 safety (v0.41.13)', () => {
content,
basename: 'long',
inferredDate: null,
transcriptSource: null,
};
}
@@ -45,6 +45,7 @@ const FIXTURE_TRANSCRIPT: DiscoveredTranscript = {
content: 'Synthetic transcript content for gateway-adapter parity tests.',
contentHash: 'sha-fixture-1',
inferredDate: '2026-05-24',
transcriptSource: null,
};
describe('makeJudgeClient — construction-time provider probe', () => {
@@ -0,0 +1,109 @@
/**
* #2285 — transcript metadata discovery.
*
* Pins the two discovery-side additions:
* 1. `transcriptSource` — derived from the `<source>/<date>/<file>` path
* layout; null for ad-hoc inputs that don't match.
* 2. Content-based date inference — the `| First message | <ISO> |` row in
* the transcript's `## Metadata` table wins over the filename-regex
* date (stable across mtime-restamping re-syncs); filename is the
* fallback.
*
* Pure filesystem; no engine, no LLM.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import {
discoverTranscripts,
readSingleTranscript,
deriveTranscriptSource,
inferContentDate,
} from '../../src/core/cycle/transcript-discovery.ts';
let tmpDir: string;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'gbrain-transcript-meta-'));
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
function write(relPath: string, body: string): string {
const full = join(tmpDir, relPath);
mkdirSync(dirname(full), { recursive: true });
writeFileSync(full, body);
return full;
}
const FILLER = 'User: hello world. '.repeat(200);
const METADATA_BLOCK =
'## Metadata\n\n| Key | Value |\n| --- | --- |\n| First message | 2026-05-15T03:51:11.584Z |\n\n';
describe('deriveTranscriptSource', () => {
test('extracts the source slug from <source>/<date>/<file> layout', () => {
expect(deriveTranscriptSource('/corpus/claude-code/2026-06-12/abc.md')).toBe('claude-code');
expect(deriveTranscriptSource('/corpus/voice-notes/2026-06-12/xyz.md')).toBe('voice-notes');
});
test('null when the parent dir is not a date dir or grandparent is not a slug', () => {
expect(deriveTranscriptSource('/corpus/flat-file.md')).toBeNull();
expect(deriveTranscriptSource('/corpus/claude-code/not-a-date/abc.md')).toBeNull();
expect(deriveTranscriptSource('/corpus/Not A Slug/2026-06-12/abc.md')).toBeNull();
});
});
describe('inferContentDate', () => {
test('parses the | First message | row', () => {
expect(inferContentDate(METADATA_BLOCK)).toBe('2026-05-15');
});
test('null when absent', () => {
expect(inferContentDate(FILLER)).toBeNull();
});
});
describe('discoverTranscripts — transcriptSource + date cascade', () => {
test('populates transcriptSource per file; null for flat files', () => {
write('claude-code/2026-06-12/aaaa.md', FILLER);
write('2026-06-12-flat.md', FILLER);
const out = discoverTranscripts({ corpusDir: tmpDir, minChars: 100 });
const byBase = new Map(out.map(t => [t.basename, t.transcriptSource]));
expect(byBase.get('aaaa')).toBe('claude-code');
expect(byBase.get('2026-06-12-flat')).toBeNull();
});
test('content First-message date wins over the filename date', () => {
write('2026-01-01-named.md', METADATA_BLOCK + FILLER);
const out = discoverTranscripts({ corpusDir: tmpDir, minChars: 100 });
expect(out).toHaveLength(1);
expect(out[0].inferredDate).toBe('2026-05-15');
});
test('filename date remains the fallback when content has no metadata row', () => {
write('2026-01-01-named.md', FILLER);
const out = discoverTranscripts({ corpusDir: tmpDir, minChars: 100 });
expect(out[0].inferredDate).toBe('2026-01-01');
});
test('date filter matches on the content date for UUID-named transcripts', () => {
write('claude-code/2026-05-15/uuid-basename.md', METADATA_BLOCK + FILLER);
const hit = discoverTranscripts({ corpusDir: tmpDir, minChars: 100, date: '2026-05-15' });
expect(hit).toHaveLength(1);
const miss = discoverTranscripts({ corpusDir: tmpDir, minChars: 100, date: '2026-05-16' });
expect(miss).toHaveLength(0);
});
});
describe('readSingleTranscript — same metadata surface', () => {
test('carries transcriptSource and prefers the content date', () => {
const p = write('claude-code/2026-05-15/2026-01-01-single.md', METADATA_BLOCK + FILLER);
const t = readSingleTranscript(p, { minChars: 100 });
expect(t).not.toBeNull();
expect(t!.transcriptSource).toBe('claude-code');
expect(t!.inferredDate).toBe('2026-05-15');
});
});
-16
View File
@@ -165,22 +165,6 @@ describe('findTrajectory — visibility filter (D-CDX-1 / R6)', () => {
const all = await engine.findTrajectory({ entitySlug: 'traj-vis-default' });
expect(all.length).toBe(2);
});
test('remote=true + trustedFactReads bypasses world-only (owner-trusted reads)', async () => {
await insertTyped({ entity_slug: 'traj-vis-trusted', metric: 'mrr', value: 50000, visibility: 'private', valid_from: new Date('2026-01-15') });
await insertTyped({ entity_slug: 'traj-vis-trusted', metric: 'mrr', value: 99999, visibility: 'world', valid_from: new Date('2026-04-12') });
// Untrusted remote: world only.
const untrusted = await engine.findTrajectory({ entitySlug: 'traj-vis-trusted', remote: true });
expect(untrusted.length).toBe(1);
expect(untrusted[0].value).toBe(99999);
// Owner-trusted remote: sees the private point too. remote stays true — only
// fact-read visibility is elevated.
const trusted = await engine.findTrajectory({ entitySlug: 'traj-vis-trusted', remote: true, trustedFactReads: true });
expect(trusted.length).toBe(2);
expect(trusted.map(p => p.value).sort((a, b) => (a! - b!))).toEqual([50000, 99999]);
});
});
describe('findTrajectory — metric + since + until filters', () => {
-43
View File
@@ -1,43 +0,0 @@
import { describe, test, expect } from 'bun:test';
import {
factsWorldOnly,
readableFactVisibilities,
} from '../src/core/facts/reader-trust.ts';
describe('factsWorldOnly', () => {
test('FAIL-CLOSED: unset remote is untrusted (world-only)', () => {
expect(factsWorldOnly({})).toBe(true);
expect(factsWorldOnly({ remote: undefined })).toBe(true);
expect(factsWorldOnly({ trustedFactReads: false })).toBe(true);
});
test('explicit remote=false (trusted local CLI) sees all', () => {
expect(factsWorldOnly({ remote: false })).toBe(false);
expect(factsWorldOnly({ remote: false, trustedFactReads: false })).toBe(false);
});
test('untrusted remote callers are world-only', () => {
expect(factsWorldOnly({ remote: true })).toBe(true);
expect(factsWorldOnly({ remote: true, trustedFactReads: false })).toBe(true);
});
test('owner-trusted remote reads bypass the world-only filter', () => {
expect(factsWorldOnly({ remote: true, trustedFactReads: true })).toBe(false);
});
test('trustedFactReads is a no-op for an already-trusted local caller', () => {
expect(factsWorldOnly({ remote: false, trustedFactReads: true })).toBe(false);
});
});
describe('readableFactVisibilities', () => {
test("world-only readers get the ['world'] filter (incl. unset remote)", () => {
expect(readableFactVisibilities({ remote: true })).toEqual(['world']);
expect(readableFactVisibilities({})).toEqual(['world']);
});
test('trusted readers get undefined (no filter — all rows)', () => {
expect(readableFactVisibilities({ remote: false })).toBeUndefined();
expect(readableFactVisibilities({ remote: true, trustedFactReads: true })).toBeUndefined();
});
});
+7 -3
View File
@@ -216,17 +216,21 @@ describe('progress reporter', () => {
});
test('only one process-level signal handler installed across many reporters', () => {
// Baseline: one handler already installed by prior tests in this file.
// Baseline: one handler already installed by prior tests in this file, and
// possibly live reporters from OTHER test files sharing this bun process
// (shard composition is not this test's invariant — assert the delta, not
// an absolute zero, or shard reshuffles make this fail spuriously).
const installedBefore = __signalHandlerInstalledForTest();
const liveBefore = __liveReporterCountForTest();
const { stream } = sink(false);
for (let i = 0; i < 50; i++) {
const p = createProgress({ mode: 'json', stream, minIntervalMs: 0, minItems: 1 });
p.start(`phase_${i}`, 1);
p.finish();
}
// After 50 reporter lifecycles, still exactly one handler and zero leaked live entries.
// After 50 reporter lifecycles, still exactly one handler and zero NET leaked live entries.
expect(__signalHandlerInstalledForTest()).toBe(installedBefore || true);
expect(__liveReporterCountForTest()).toBe(0);
expect(__liveReporterCountForTest()).toBe(liveBefore);
});
test('startHeartbeat() fires heartbeats and stop() clears', async () => {