diff --git a/apps/bullmq/src/scheduled-jobs/__tests__/brain-outbox-drain.test.ts b/apps/bullmq/src/scheduled-jobs/__tests__/brain-outbox-drain.test.ts
index b28f6d091f..2efa82e8b8 100644
--- a/apps/bullmq/src/scheduled-jobs/__tests__/brain-outbox-drain.test.ts
+++ b/apps/bullmq/src/scheduled-jobs/__tests__/brain-outbox-drain.test.ts
@@ -61,6 +61,7 @@ vi.mock('@roomote/db/server', async (importOriginal) => {
releaseFastAgentMemoryEvents: mockReleaseFastEvents,
getBrainSyncState: mockGetSyncState,
upsertBrainSyncState: vi.fn(),
+ deleteBrainSyncStateFamily: vi.fn(),
};
});
@@ -82,12 +83,14 @@ beforeEach(() => {
});
});
+import { personIdentitySlug } from '../brain-collectors/identity';
import {
brainCollectorsJob,
brainOutboxDrainJob,
buildFastMemoryPage,
buildPullRequestFactPage,
buildMemoryPage,
+ resolveTaskMemoryRequest,
callBrainWriteTool,
isBrainUnreachable,
drainBrainHistoricalIngestion,
@@ -443,9 +446,92 @@ describe('task memory page identity', () => {
completedAt: new Date('2026-08-13T10:00:00Z'),
environmentName: null,
agentSummary: 'Used the durable approach.',
+ initiator: { kind: 'user' as const, userId: 'user-1', name: 'Sam Lee' },
+ workflow: 'standard' as const,
+ request: null,
pullRequests: [],
};
+ it('links a linked member to their person page', () => {
+ const page = buildMemoryPage({ ...base, runId: 101 });
+ const slug = personIdentitySlug('user-1');
+
+ expect(slug).toMatch(/^people\/roomote-member-[0-9a-f]{16}$/);
+ expect(page.content).toContain('\ninitiated_by: "Sam Lee"\n');
+ expect(page.content).toContain('\nroomote_user_id: user-1\n');
+ expect(page.content).toContain(
+ `\ninitiated_by_person: ${JSON.stringify(slug)}\n`,
+ );
+ expect(page.content).toContain(`\nInitiated by [Sam Lee](${slug}).\n`);
+ });
+
+ it('names an unlinked human without inventing a person page', () => {
+ const page = buildMemoryPage({
+ ...base,
+ runId: 101,
+ initiator: { kind: 'user', userId: null, name: 'octocat' },
+ });
+
+ expect(page.content).toContain('\ninitiated_by: "octocat"\n');
+ expect(page.content).not.toContain('roomote_user_id');
+ expect(page.content).not.toContain('initiated_by_person');
+ expect(page.content).toContain('\nInitiated by octocat.\n');
+ });
+
+ it('names the automation that started a task', () => {
+ const page = buildMemoryPage({
+ ...base,
+ runId: 101,
+ initiator: { kind: 'automation', automation: 'issue_fixer' },
+ });
+
+ expect(page.content).toContain('\ninitiated_by_automation: issue_fixer\n');
+ expect(page.content).not.toContain('initiated_by:');
+ expect(page.content).toContain(
+ '\nInitiated by the issue_fixer automation.\n',
+ );
+ });
+
+ it('never links a person to a review the member merely triggered', () => {
+ const page = buildMemoryPage({
+ ...base,
+ runId: 101,
+ workflow: 'pr_review',
+ });
+
+ expect(page.content).not.toContain('Initiated by');
+ expect(page.content).not.toContain('initiated_by');
+ expect(page.content).not.toContain('roomote_user_id');
+ });
+
+ it('carries the request the member made, ahead of the outcome', () => {
+ const page = buildMemoryPage({
+ ...base,
+ runId: 101,
+ request: 'Make the flaky upload test deterministic.',
+ });
+
+ const request = page.content.indexOf('## Request');
+ const summary = page.content.indexOf('Used the durable approach.');
+
+ expect(page.content).toContain(
+ '## Request\n\nMake the flaky upload test deterministic.\n',
+ );
+ expect(request).toBeGreaterThan(-1);
+ expect(request).toBeLessThan(summary);
+ });
+
+ it('omits the initiator line when nothing is known about them', () => {
+ const page = buildMemoryPage({
+ ...base,
+ runId: 101,
+ initiator: { kind: 'user', userId: null, name: null },
+ });
+
+ expect(page.content).not.toContain('Initiated by');
+ expect(page.content).not.toContain('initiated_by');
+ });
+
it('keeps separate runs of the same task distinct', () => {
const first = buildMemoryPage({ ...base, runId: 101 });
const followUp = buildMemoryPage({ ...base, runId: 102 });
@@ -484,6 +570,71 @@ describe('task memory page identity', () => {
});
});
+describe('resolveTaskMemoryRequest', () => {
+ it('reads the visible prompt from the launch payload', () => {
+ expect(
+ resolveTaskMemoryRequest(
+ { description: ' Fix the login redirect loop. ' },
+ 'standard',
+ ),
+ ).toBe('Fix the login redirect loop.');
+ expect(
+ resolveTaskMemoryRequest({ text: 'Ship the banner.' }, 'standard'),
+ ).toBe('Ship the banner.');
+ });
+
+ it('reads a Linear-launched request the way the agent prompt did', () => {
+ const issue = {
+ issueTitle: 'Login redirect loop',
+ issueDescription: 'Users bounce between /login and /home.',
+ };
+
+ expect(
+ resolveTaskMemoryRequest(
+ { ...issue, commentBody: '@roomote please fix this' },
+ 'standard',
+ ),
+ ).toBe('@roomote please fix this');
+ expect(resolveTaskMemoryRequest(issue, 'standard')).toBe(
+ 'Users bounce between /login and /home.',
+ );
+ expect(
+ resolveTaskMemoryRequest({ issueTitle: issue.issueTitle }, 'standard'),
+ ).toBe('Login redirect loop');
+ });
+
+ it('leaves out generated, hidden, and non-standard prompts', () => {
+ expect(
+ resolveTaskMemoryRequest({ description: 'Review this PR.' }, 'pr_review'),
+ ).toBeNull();
+ expect(
+ resolveTaskMemoryRequest(
+ { description: 'Set up.', visibleInTranscript: false },
+ 'standard',
+ ),
+ ).toBeNull();
+ expect(
+ resolveTaskMemoryRequest(
+ { description: 'bootstrap go' },
+ 'standard',
+ ),
+ ).toBeNull();
+ expect(resolveTaskMemoryRequest({}, 'standard')).toBeNull();
+ });
+
+ it('bounds a long request and says where the rest lives', () => {
+ const request = resolveTaskMemoryRequest(
+ { description: 'x'.repeat(2_000) },
+ 'standard',
+ );
+
+ expect(request).toHaveLength(
+ 1_500 + '\n\n_Request truncated; open the task for the rest._'.length,
+ );
+ expect(request?.endsWith('open the task for the rest._')).toBe(true);
+ });
+});
+
describe('task memory pull request outcomes', () => {
const base = {
runId: 7,
@@ -492,6 +643,9 @@ describe('task memory pull request outcomes', () => {
completedAt: new Date('2026-08-13T10:00:00Z'),
environmentName: null,
agentSummary: 'Opened a PR with the durable approach.',
+ initiator: { kind: 'automation' as const, automation: 'issue_fixer' },
+ workflow: 'standard' as const,
+ request: null,
};
const pr = {
repository: 'owner/repo',
diff --git a/apps/bullmq/src/scheduled-jobs/brain-outbox-drain.ts b/apps/bullmq/src/scheduled-jobs/brain-outbox-drain.ts
index 0ca6cf9c76..96c3fb7991 100644
--- a/apps/bullmq/src/scheduled-jobs/brain-outbox-drain.ts
+++ b/apps/bullmq/src/scheduled-jobs/brain-outbox-drain.ts
@@ -13,6 +13,7 @@ import {
settleBrainMemoryEvent,
releaseBrainMemoryEvents,
releaseFastAgentMemoryEvents,
+ deleteBrainSyncStateFamily,
settleFastAgentMemoryEvent,
pullRequestFacts,
taskPullRequests,
@@ -38,12 +39,20 @@ import {
BRAIN_PAGE_TYPES,
type PullRequestStatus,
RunStatus,
+ type TaskWorkflow,
+ ACP_ENVELOPE_EVENT_TYPES,
+ isSystemInjectedAcpPromptText,
+ normalizeTranscriptUserText,
brainNamespacePrefix,
getLinkedEnvironmentIdFromPayload,
renderBrainFrontmatter,
} from '@roomote/types';
import { runBrainCollectors } from './brain-collectors';
+import {
+ brainSafeIdentityValue,
+ personIdentitySlug,
+} from './brain-collectors/identity';
import { drainMemoryOutboxBatch } from './memory-outbox-drain';
import {
runSlackDayPageCensus,
@@ -54,6 +63,25 @@ import { slackPublicChannelsCollector } from './brain-collectors/slack-public-ch
const LOG_PREFIX = '[brainOutboxDrain]';
/** Sync-state key for the one-time task-history backfill. */
const TASK_MEMORY_COLLECTOR_ID = BRAIN_COLLECTOR_IDS.taskMemories;
+/**
+ * Task-memory sync-state rows left behind by version bumps. A bump replays
+ * history under the new id (the backfill checkpoint lives on the row), and
+ * the old row would otherwise linger and count as the source's history
+ * cutoff forever. Extend when bumping again.
+ */
+const SUPERSEDED_TASK_MEMORY_COLLECTOR_IDS = ['task-memory:effective-date-v2'];
+/**
+ * How far back a version bump re-puts memories that already reached the
+ * Brain. Older pages keep correct content and pick the new shape up if a
+ * linked pull request later changes state.
+ */
+const LINKABLE_REPLAY_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
+/**
+ * Bound on the request excerpt a task memory carries. The ask is usually a
+ * few sentences; a pasted log or spec should not dominate the page's
+ * embedding, and the task itself remains the place to read the rest.
+ */
+const TASK_REQUEST_CHAR_CAP = 1_500;
const CLAIM_BATCH_SIZE = 10;
// Backfill can enqueue a deployment's whole task history at once; drain up
// to this many batches per tick so the backlog clears in minutes, not hours.
@@ -302,12 +330,132 @@ function describePullRequestOutcome(
}
}
+/**
+ * The user's own request, as the web transcript would show it: the launch
+ * payload's visible prompt with Roomote's surface wrappers stripped. Only
+ * standard-workflow tasks carry one; a review or conflict-resolution run's
+ * prompt is generated, not asked. Bootstrap prompts the harness injected and
+ * prompts the launch path marked hidden are not the user's words and are
+ * left out. Treated as evidence like every other ingested text, never as
+ * instructions.
+ */
+export function resolveTaskMemoryRequest(
+ payload: Record,
+ workflow: TaskWorkflow,
+): string | null {
+ if (workflow !== 'standard') {
+ return null;
+ }
+
+ if (payload.visibleInTranscript === false) {
+ return null;
+ }
+
+ // Same precedence as the prompt the agent actually received
+ // (getInitialTaskPrompt): web and chat launches carry `description` or
+ // `text`; a Linear-launched task carries the triggering comment, else the
+ // issue body, else its title.
+ const raw =
+ [
+ payload.description,
+ payload.text,
+ payload.commentBody,
+ payload.issueDescription,
+ payload.issueTitle,
+ ].find(
+ (value): value is string =>
+ typeof value === 'string' && value.trim() !== '',
+ ) ?? null;
+
+ if (!raw || isSystemInjectedAcpPromptText(raw)) {
+ return null;
+ }
+
+ const text = normalizeTranscriptUserText(
+ raw,
+ ACP_ENVELOPE_EVENT_TYPES.UserPrompt,
+ )?.trim();
+
+ if (!text) {
+ return null;
+ }
+
+ return text.length > TASK_REQUEST_CHAR_CAP
+ ? `${text.slice(0, TASK_REQUEST_CHAR_CAP)}\n\n_Request truncated; open the task for the rest._`
+ : text;
+}
+
/**
* Build the memory page for a completed run. Deliberately deterministic and
* conservative: only structured, known-safe fields (title, repos, PRs,
* timestamps, provenance). LLM distillation of decisions/rationale layers on
* top of this later; it must never widen what raw data can reach the brain.
*/
+/**
+ * Who started the task. On a standard-workflow task, a linked Roomote member
+ * gets a link to their person page so recall can answer "what has X been
+ * working on"; an unlinked human from an integration surface (a Slack or
+ * GitHub user with no Roomote account) keeps only the display name the
+ * surface reported; an automation names its key. Other workflows (PR
+ * reviews, conflict resolution, scans, snapshots) never link a person: the
+ * human who happened to trigger a review is not its author, and a person
+ * page full of reviews says nothing about what they worked on.
+ */
+type TaskMemoryInitiator =
+ | {
+ kind: 'user';
+ userId: string | null;
+ /** The Roomote member's name, or the surface-reported display name. */
+ name: string | null;
+ }
+ | { kind: 'automation'; automation: string };
+
+function describeInitiator(
+ initiator: TaskMemoryInitiator,
+ workflow: TaskWorkflow,
+): {
+ fields: string[];
+ line: string | null;
+} {
+ if (initiator.kind === 'automation') {
+ return {
+ fields: [`initiated_by_automation: ${initiator.automation}`],
+ line: `Initiated by the ${initiator.automation} automation.`,
+ };
+ }
+
+ if (workflow !== 'standard') {
+ return { fields: [], line: null };
+ }
+
+ const name = initiator.name ? brainSafeIdentityValue(initiator.name) : '';
+
+ if (initiator.userId) {
+ const slug = personIdentitySlug(initiator.userId);
+ const title = name || 'Roomote member';
+
+ return {
+ fields: [
+ `initiated_by: ${JSON.stringify(title)}`,
+ `roomote_user_id: ${initiator.userId}`,
+ // Same convention as person aliases: the person page's slug, so the
+ // Brain can walk from the task to the member and back.
+ `initiated_by_person: ${JSON.stringify(slug)}`,
+ ],
+ line: `Initiated by [${title}](${slug}).`,
+ };
+ }
+
+ if (name) {
+ return {
+ fields: [`initiated_by: ${JSON.stringify(name)}`],
+ line: `Initiated by ${name}.`,
+ };
+ }
+
+ return { fields: [], line: null };
+}
+
export function buildMemoryPage(input: {
runId: number;
taskId: string;
@@ -315,6 +463,10 @@ export function buildMemoryPage(input: {
completedAt: Date | null;
environmentName: string | null;
agentSummary: string | null;
+ initiator: TaskMemoryInitiator;
+ workflow: TaskWorkflow;
+ /** Already bounded and workflow-gated; see resolveTaskMemoryRequest. */
+ request: string | null;
pullRequests: Array<{
repository: string | null;
prNumber: number | null;
@@ -327,6 +479,7 @@ export function buildMemoryPage(input: {
const completed = completedAtIso ?? 'unknown';
const completedDate = completedAtIso?.slice(0, 10);
const outcome = summarizePullRequestOutcome(input.pullRequests);
+ const initiator = describeInitiator(input.initiator, input.workflow);
const prLines = input.pullRequests.map((pr) => {
const label =
pr.repository && pr.prNumber
@@ -346,6 +499,7 @@ export function buildMemoryPage(input: {
fields: [
`roomote_task_id: ${input.taskId}`,
`roomote_run_id: ${input.runId}`,
+ ...initiator.fields,
// GBrain derives effective_date from this conventional field. Keep
// the full timestamp below as provenance, but make backfilled pages
// sort and filter by when the task completed rather than when it
@@ -367,6 +521,8 @@ export function buildMemoryPage(input: {
'',
`# ${input.taskTitle}`,
'',
+ ...(initiator.line ? [initiator.line, ''] : []),
+ ...(input.request ? ['## Request', '', input.request, ''] : []),
// The agent that did the work writes the substance when it can; the
// deterministic completion line is the floor, not the ceiling.
...(input.agentSummary
@@ -470,6 +626,10 @@ async function resolveReadyBrain(): Promise<{
* restarts (there is no connect action to hang this off anymore).
*/
async function backfillTaskHistoryOnce(): Promise {
+ for (const collectorId of SUPERSEDED_TASK_MEMORY_COLLECTOR_IDS) {
+ await deleteBrainSyncStateFamily(db, collectorId);
+ }
+
const state = await getBrainSyncState(db, TASK_MEMORY_COLLECTOR_ID);
if (state?.backfillCompletedAt) {
@@ -477,7 +637,9 @@ async function backfillTaskHistoryOnce(): Promise {
}
const enqueued = await backfillBrainMemoryEvents(db, {
- requeueCompleted: true,
+ requeueLinkable: {
+ completedAfter: new Date(Date.now() - LINKABLE_REPLAY_WINDOW_MS),
+ },
});
await upsertBrainSyncState(db, TASK_MEMORY_COLLECTOR_ID, {
@@ -613,7 +775,7 @@ async function drainOneBatch(connection: {
async prepare(event) {
const run = await db.query.taskRuns.findFirst({
where: eq(taskRuns.id, event.runId),
- with: { task: true },
+ with: { task: { with: { initiatorUser: true } } },
});
if (!run) {
@@ -671,13 +833,29 @@ async function drainOneBatch(connection: {
environmentName = environment?.name ?? null;
}
+ const task = run.task;
+ const initiator: TaskMemoryInitiator =
+ task.initiatorKind === 'automation' && task.initiatorAutomation
+ ? { kind: 'automation', automation: task.initiatorAutomation }
+ : {
+ kind: 'user',
+ userId: task.initiatorUser?.id ?? null,
+ name: task.initiatorUser?.name ?? task.actorDisplayName,
+ };
+
const page = buildMemoryPage({
environmentName,
agentSummary: event.agentSummary,
runId: run.id,
taskId: run.taskId,
- taskTitle: run.task.title,
+ taskTitle: task.title,
completedAt: run.completedAt,
+ initiator,
+ workflow: task.workflow,
+ request: resolveTaskMemoryRequest(
+ run.payload as Record,
+ task.workflow,
+ ),
pullRequests: prRows.map((pr) => ({
repository: pr.repository,
prNumber: pr.prNumber,
diff --git a/apps/docs/memory.mdx b/apps/docs/memory.mdx
index 0926547d21..bfba7dfa41 100644
--- a/apps/docs/memory.mdx
+++ b/apps/docs/memory.mdx
@@ -17,11 +17,16 @@ Roomote task.
Roomote fills Memory from what it can already see:
-- **completed Roomote tasks**, including a short memory the agent writes about
- its own work: what it decided, why, and what is still open. When a pull
+- **completed Roomote tasks**, including the request that started the task
+ and a short memory the agent writes about its own work: what it decided,
+ why, and what is still open. The request is bounded and only recorded for
+ tasks a person asked for, never for generated work such as reviews. When a pull
request the task opened later merges or closes unmerged, the task's memory is
refreshed with that outcome, so recall can tell work that shipped from work
- that was abandoned
+ that was abandoned. Each memory also records who started the task: a linked
+ Roomote member is connected to their person page, so recall can answer what
+ someone has been working on. Automated work such as pull request reviews
+ names the automation instead of linking a person
- **pull requests** from your connected source-control provider
- **public Slack channels** the Roomote bot has been added to
- **public Discord server channels and active public threads** the Roomote bot
diff --git a/packages/db/src/lib/__tests__/brain.test.ts b/packages/db/src/lib/__tests__/brain.test.ts
index 088e06f24c..3615526801 100644
--- a/packages/db/src/lib/__tests__/brain.test.ts
+++ b/packages/db/src/lib/__tests__/brain.test.ts
@@ -15,6 +15,7 @@ import {
tasks,
taskRuns,
taskFactory,
+ userFactory,
brainMemoryEvents,
brainCollectorItems,
brainSyncState,
@@ -47,8 +48,11 @@ import { runMemoryOutboxLifecycleContract } from './memory-outbox-lifecycle.cont
const createdTaskIds: string[] = [];
-async function makeCompletedRun(completedAt?: Date) {
- const task = await taskFactory.create({ state: 'active' });
+async function makeCompletedRun(
+ completedAt?: Date,
+ taskParams: Parameters[0] = {},
+) {
+ const task = await taskFactory.create({ state: 'active', ...taskParams });
createdTaskIds.push(task.id);
const [run] = await db
@@ -723,25 +727,53 @@ describe('backfillBrainMemoryEvents', () => {
expect(runningEvents).toHaveLength(0);
});
- it('requeues completed memories for a one-time metadata replay', async () => {
- const completed = await makeCompletedRun();
- await saveBrainAgentSummary(db, completed.id, 'Keep this summary.');
- const claimed = await claimPendingBrainMemoryEvents(db, 10);
- const event = claimed.find((row) => row.runId === completed.id);
- await settleBrainMemoryEvent(db, event!.id, event!.revision, 'done');
+ it('requeues only linkable recent memories for a one-time metadata replay', async () => {
+ const user = await userFactory.create();
+ const now = Date.now();
+ const recent = new Date(now - 24 * 60 * 60 * 1000);
+ const stale = new Date(now - 400 * 24 * 60 * 60 * 1000);
+ const linkable = await makeCompletedRun(recent, {
+ initiatorUserId: user.id,
+ });
+ const tooOld = await makeCompletedRun(stale, {
+ initiatorUserId: user.id,
+ });
+ const review = await makeCompletedRun(recent, {
+ initiatorUserId: user.id,
+ workflow: 'pr_review',
+ });
+ const unlinked = await makeCompletedRun(recent);
+ await saveBrainAgentSummary(db, linkable.id, 'Keep this summary.');
+ // Every fixture run reaches the Brain once before the replay is asked for.
+ await backfillBrainMemoryEvents(db);
+ const claimed = await claimPendingBrainMemoryEvents(db, 1_000);
+ for (const event of claimed) {
+ await settleBrainMemoryEvent(db, event.id, event.revision, 'done');
+ }
+
+ await backfillBrainMemoryEvents(db, {
+ requeueLinkable: {
+ completedAfter: new Date(now - 90 * 24 * 60 * 60 * 1000),
+ },
+ });
- await backfillBrainMemoryEvents(db, { requeueCompleted: true });
+ const statusOf = async (runId: number) => {
+ const [row] = await db
+ .select()
+ .from(brainMemoryEvents)
+ .where(eq(brainMemoryEvents.runId, runId));
+ return row;
+ };
- const [requeued] = await db
- .select()
- .from(brainMemoryEvents)
- .where(eq(brainMemoryEvents.runId, completed.id));
- expect(requeued).toMatchObject({
+ expect(await statusOf(linkable.id)).toMatchObject({
status: 'pending',
attempts: 0,
lastError: null,
agentSummary: 'Keep this summary.',
});
+ expect((await statusOf(tooOld.id))?.status).toBe('done');
+ expect((await statusOf(review.id))?.status).toBe('done');
+ expect((await statusOf(unlinked.id))?.status).toBe('done');
});
});
diff --git a/packages/db/src/lib/brain.ts b/packages/db/src/lib/brain.ts
index f45570ccb0..c264d3aa53 100644
--- a/packages/db/src/lib/brain.ts
+++ b/packages/db/src/lib/brain.ts
@@ -25,6 +25,7 @@ import {
brainMemoryEvents,
brainSyncState,
taskRuns,
+ tasks,
} from '../schema';
import { runInTransactionIfAvailable } from './transaction-utils';
import { createMemoryOutboxLifecycle } from './memory-outbox-lifecycle';
@@ -465,19 +466,31 @@ export async function requeueBrainMemoryEventsForTasks(
* connecting the brain sucks in the deployment's task history rather than
* only learning from tasks completed after enablement. Idempotent via the
* unique(runId) constraint; the drainer distills the backlog batch by batch.
+ *
+ * `requeueLinkable` re-puts memories already in the Brain so a page-shape
+ * change reaches them. It is deliberately narrow: only standard-workflow
+ * tasks a linked Roomote member started, completed after the given time.
+ * Reviews, conflict resolution, scans, and automation-started work gain
+ * nothing from the replay, and a deployment's whole history re-embedded at
+ * once is a burst every managed tenant would land on the shared embedder
+ * together.
*/
export async function backfillBrainMemoryEvents(
database: DatabaseOrTransaction,
- options: { requeueCompleted?: boolean } = {},
+ options: { requeueLinkable?: { completedAfter: Date } } = {},
): Promise {
- const requeued = options.requeueCompleted
+ const requeued = options.requeueLinkable
? ((await database.execute(
sql`UPDATE ${brainMemoryEvents} AS event
SET status = 'pending', attempts = 0, last_error = NULL, updated_at = now()
FROM ${taskRuns} AS run
+ JOIN ${tasks} AS task ON task.id = run.task_id
WHERE event.run_id = run.id
AND event.status = 'done'
AND run.status = 'completed'
+ AND run.completed_at > ${options.requeueLinkable.completedAfter.toISOString()}::timestamptz
+ AND task.workflow = 'standard'
+ AND task.initiator_user_id IS NOT NULL
RETURNING event.id`,
)) as unknown as Array<{ id: string }>)
: [];
diff --git a/packages/types/src/brain.test.ts b/packages/types/src/brain.test.ts
index 7e799ca8f9..1cc57b8bac 100644
--- a/packages/types/src/brain.test.ts
+++ b/packages/types/src/brain.test.ts
@@ -91,9 +91,9 @@ describe('resolveBrainSourceIdForCollector', () => {
});
it('maps the outbox-fed checkpoints back to their sources', () => {
- expect(
- resolveBrainSourceIdForCollector('task-memory:effective-date-v2'),
- ).toBe('task-memories');
+ expect(resolveBrainSourceIdForCollector('task-memory:initiator-v3')).toBe(
+ 'task-memories',
+ );
expect(
resolveBrainSourceIdForCollector('pull-request-facts:occurrence-date-v3'),
).toBe('pull-request-facts');
diff --git a/packages/types/src/brain.ts b/packages/types/src/brain.ts
index 6686faee78..4a43de4497 100644
--- a/packages/types/src/brain.ts
+++ b/packages/types/src/brain.ts
@@ -114,7 +114,7 @@ export function brainNamespaceLabel(id: BrainNamespaceBucketId): string {
* superseded version's rows.
*/
export const BRAIN_COLLECTOR_IDS = {
- taskMemories: 'task-memory:effective-date-v2',
+ taskMemories: 'task-memory:initiator-v3',
pullRequestFacts: 'pull-request-facts:occurrence-date-v3',
personIdentities: 'person-identities:members:occurrence-date-v2',
ripplingWorkers: 'rippling-workers',