From 5cfae1ce622dfb7aa829b3ec606876902afd0705 Mon Sep 17 00:00:00 2001 From: Svetoslav Dekov Date: Tue, 8 Sep 2026 09:24:24 +0000 Subject: [PATCH] chore: Fix seed assistant threads so the assistant page can list them (no-changelog) (#37863) Co-authored-by: Claude Fable 5.1 --- scripts/instance-seeding/seedHistory.mjs | 30 +++++++++++----- scripts/instance-seeding/seedHistory.test.mjs | 34 +++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/scripts/instance-seeding/seedHistory.mjs b/scripts/instance-seeding/seedHistory.mjs index 2500091b786..22f7640f130 100644 --- a/scripts/instance-seeding/seedHistory.mjs +++ b/scripts/instance-seeding/seedHistory.mjs @@ -298,12 +298,20 @@ function main() { // workflow, so a "what was I working on?" probe has something to find. const threadId = uuid(); const threadAt = NOW - Math.floor(random() * DAYS * 86400e3); + // `resourceId` is the owning user: the assistant page lists threads by it, so a + // workflow id here makes the thread exist but never show. The workflow goes in + // `sourceContext`, where the runtime also puts it. insThread.run( threadId, - wf.id, + user.id, project.id, `Working on ${short}`, - `{${THREAD_MARKER}}`, + JSON.stringify({ + seeded: true, + source: 'assistant_page', + origin: 'internal', + sourceContext: { workflowId: wf.id }, + }), iso(threadAt), iso(threadAt + 600e3), ); @@ -329,16 +337,20 @@ function main() { `Added a Data Table insert named "Record Run" at the end of ${short}, writing to \`automation_runs\`, matching the other workflows here.`, ], ]; - for (const [role, content] of turns) { + // `content` holds the whole agent message as JSON, the way the runtime stores it. + // A plain string is dropped on read, so the thread would open empty. + for (const [i, [role, text]] of turns.entries()) { + const messageId = uuid(); + const createdAt = iso(threadAt + i * 30e3); insMessage.run( - uuid(), + messageId, threadId, - content, + JSON.stringify({ id: messageId, role, content: [{ type: 'text', text }], createdAt }), role, - 'message', - wf.id, - iso(threadAt), - iso(threadAt), + null, + user.id, + createdAt, + createdAt, ); msgCount++; } diff --git a/scripts/instance-seeding/seedHistory.test.mjs b/scripts/instance-seeding/seedHistory.test.mjs index 75789c146e6..901a5539824 100644 --- a/scripts/instance-seeding/seedHistory.test.mjs +++ b/scripts/instance-seeding/seedHistory.test.mjs @@ -192,6 +192,40 @@ describe('seedHistory cleanup', () => { assert.ok(c.seededActivity > 0, 'activity written'); }); + // The assistant page lists threads by `resourceId = user id` and drops any message + // whose `content` is not an agent-message JSON object, so both must match the runtime. + it('writes threads and messages the assistant page can read', () => { + const dbFile = fixture(); + run(dbFile); + const db = new DatabaseSync(dbFile, { readOnly: true }); + const threads = db + .prepare( + `SELECT resourceId FROM instance_ai_threads WHERE metadata LIKE '%"seeded":true%' AND projectId = 'p-seed'`, + ) + .all(); + assert.ok(threads.length > 0); + assert.ok( + threads.every((t) => t.resourceId === 'u1'), + 'threads owned by the first user', + ); + const messages = db + .prepare( + `SELECT m.content, m.resourceId, m.type FROM instance_ai_messages m + JOIN instance_ai_threads t ON t.id = m.threadId + WHERE t.metadata LIKE '%"seeded":true%' AND t.projectId = 'p-seed'`, + ) + .all(); + assert.ok(messages.length > 0); + for (const m of messages) { + assert.equal(m.resourceId, 'u1'); + assert.equal(m.type, null); + const parsed = JSON.parse(m.content); + assert.ok(['user', 'assistant'].includes(parsed.role)); + assert.equal(parsed.content[0].type, 'text'); + } + db.close(); + }); + it('leaves rows it did not create alone', () => { const dbFile = fixture(); run(dbFile);