feat(hub+web): include scratchlist in session export (#1235) (#1237)

Bump export schema to v2 with scratchlist text and attachment metadata
so operators keep notes when they export-then-delete. Markdown gets a
Scratchlist section; attachment bytes stay out of the JSON.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
HeavyGee
2026-07-30 23:21:08 +08:00
committed by GitHub
co-authored by Cursor
parent 99814e9668
commit a742fdf1a8
8 changed files with 174 additions and 18 deletions
+65
View File
@@ -190,6 +190,71 @@ describe('MessageService goal status filtering', () => {
})
})
it('includes scratchlist text and attachment metadata in chronological order (tiann/hapi#1235)', () => {
const store = makeStore()
const session = makeSession(store, 'session-export-scratchlist')
store.messages.addMessage(session.id, { role: 'user', content: 'Hello' })
const older = store.scratchlist.create(session.id, 'Park this idea', {
entryId: 'entry-older',
createdAt: 1_000,
attachments: [{
id: 'att-1',
filename: 'note.png',
mimeType: 'image/png',
size: 42,
path: 'hapi-hub:scratchlist/att-1'
}]
})
const newer = store.scratchlist.create(session.id, 'Follow up tomorrow', {
entryId: 'entry-newer',
createdAt: 2_000
})
expect(older.outcome).toBe('created')
expect(newer.outcome).toBe('created')
const service = new MessageService(store, makeIo(() => {}), makePublisher() as any)
const result = service.getSessionExport(session.id, toProtocolSession(session))
expect(result.type).toBe('success')
if (result.type !== 'success') throw new Error('Expected success export')
expect(result.payload.schemaVersion).toBe(2)
expect(result.payload.scratchlist).toEqual([
{
entryId: 'entry-older',
text: 'Park this idea',
createdAt: 1_000,
updatedAt: expect.any(Number),
attachments: [{
id: 'att-1',
filename: 'note.png',
mimeType: 'image/png',
size: 42,
path: 'hapi-hub:scratchlist/att-1'
}]
},
{
entryId: 'entry-newer',
text: 'Follow up tomorrow',
createdAt: 2_000,
updatedAt: expect.any(Number),
attachments: []
}
])
})
it('emits an empty scratchlist array when the session has no notes', () => {
const store = makeStore()
const session = makeSession(store, 'session-export-no-scratchlist')
const service = new MessageService(store, makeIo(() => {}), makePublisher() as any)
const result = service.getSessionExport(session.id, toProtocolSession(session))
expect(result.type).toBe('success')
if (result.type !== 'success') throw new Error('Expected success export')
expect(result.payload.scratchlist).toEqual([])
})
it('pages past hidden-only goal status rows', () => {
const store = makeStore()
const session = makeSession(store, 'goal-status-pagination')
+17 -1
View File
@@ -138,13 +138,29 @@ export class MessageService {
}
}
// Chronological ASC for archive readability (store list is DESC).
const scratchlist = this.store.scratchlist.list(sessionId)
.slice()
.sort((a, b) => {
if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt
return a.entryId < b.entryId ? -1 : a.entryId > b.entryId ? 1 : 0
})
.map((row) => ({
entryId: row.entryId,
text: row.text,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
attachments: row.attachments
}))
return {
type: 'success',
payload: {
schemaVersion: HAPI_SESSION_EXPORT_SCHEMA_VERSION,
exportedAt: Date.now(),
session,
messages
messages,
scratchlist
}
}
}
+9 -6
View File
@@ -137,10 +137,11 @@ function createApp(session: Session, opts?: {
getSessionExport: opts?.getSessionExport ?? (() => ({
type: 'success',
payload: {
schemaVersion: 1,
schemaVersion: 2,
exportedAt: 1_762_000_000_000,
session,
messages: []
messages: [],
scratchlist: []
}
})),
listSlashCommands: opts?.listSlashCommands ?? (async () => ({
@@ -191,10 +192,11 @@ describe('sessions routes', () => {
expect(response.status).toBe(200)
expect(await response.json()).toEqual({
schemaVersion: 1,
schemaVersion: 2,
exportedAt: 1_762_000_000_000,
session,
messages: []
messages: [],
scratchlist: []
})
})
@@ -224,10 +226,11 @@ describe('sessions routes', () => {
getSessionExport: () => ({
type: 'success',
payload: {
schemaVersion: 1,
schemaVersion: 2,
exportedAt: 1_762_000_000_000,
session,
messages
messages,
scratchlist: []
}
})
})