Merge remote-tracking branch 'origin/master' into codex/basic-session-search

Conflict resolutions:

- `session.list`: master's projection columns fold into the PR's cancellable,
  batched `listVisibleSessionSummaries`, which `session.search` shares as its
  visibility baseline; master's goal helpers stay beside it.
- Client sessions face: master narrowed `ctx.sessions` to `ISessions`, so the
  search verb and its protocol-constant bound are declared there and the
  test-runtime double implements them (recorded, empty page unless a scenario
  stubs hits).
- `WorkspaceBrowser`: master's per-row Rename wiring rides the PR's search
  results view; the tree keeps the PR's query-free derivations.
- `dsh web` bin: the PR's shutdown-handlers-before-readiness order with
  master's boot-time LAN address snapshot.
- `session-query-sqlite`: master's `SCHEMA_VERSION` 7 stands; the PR's bump
  carried no schema change.
- Specs: master wraps assistant/steering message payloads and requires an
  `application/json` carrier request, so the search fixtures and tests follow.
- Web aria goldens keep master's recording plus the PR's search placeholder;
  the navigation-panes inventory keeps master's terminal-card golden next to
  the PR's search-results golden.
This commit is contained in:
Hypatia May
2026-07-30 09:40:38 +08:00
1904 changed files with 80499 additions and 17610 deletions

View File

@@ -1,3 +1,4 @@
import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context, type Fiber } from 'cordis'
import { DatabaseSync } from 'node:sqlite'
@@ -44,7 +45,9 @@ function messageEvents(text: string, time = 1): SessionEvent[] {
type: 'user/message',
seq: 0,
time,
data: { content: [{ type: 'text', text }], source: { kind: 'user' } },
data: createUserMessage({
content: [{ type: 'text', text }], source: { kind: 'user' },
}),
surfaceOp: 'append',
}]
}
@@ -146,6 +149,11 @@ class TestPersistence extends SessionPersistence {
return structuredClone(entry)
}
async readFrom(id: SessionIdType, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
const whole = await this.inspect(id, signal)
return { meta: whole.meta, events: whole.events.filter(event => event.seq >= fromSeq) }
}
async list(): Promise<SessionHeader[]> {
TestPersistence.listStarted?.()
await TestPersistence.listGate
@@ -276,7 +284,9 @@ describe('SQLite session search', () => {
})
session.append(
'user/message',
{ content: [{ type: 'text', text: 'An AI helper' }], source: { kind: 'user' } },
createUserMessage({
content: [{ type: 'text', text: 'An AI helper' }], source: { kind: 'user' },
}),
{ surfaceOp: 'append' },
)
@@ -297,11 +307,13 @@ describe('SQLite session search', () => {
{
turn: 1,
step: 1,
content: [
{ type: 'reasoning', text: 'private-chain-marker' },
{ type: 'text', text: 'visible-answer-marker' },
],
provenance: { provider: 'mock', model: 'mock' },
message: createAssistantMessage({
content: [
{ type: 'reasoning', text: 'private-chain-marker' },
{ type: 'text', text: 'visible-answer-marker' },
],
source: { provider: 'mock', model: 'mock' },
}),
},
{ surfaceOp: 'append' },
)
@@ -321,9 +333,13 @@ describe('SQLite session search', () => {
const ctx = await liveContext({ path: ':memory:', defaultLimit: 10, maxLimit: 20 })
const parent = SessionId('parent')
const events: SessionEvent[] = [
{ type: 'user/message', seq: 0, time: 10, data: { content: [{ type: 'text', text: 'needle original' }], source: { kind: 'user' } }, surfaceOp: 'append' },
{ type: 'user/message', seq: 0, time: 10, data: createUserMessage({
content: [{ type: 'text', text: 'needle original' }], source: { kind: 'user' },
}), surfaceOp: 'append' },
{ type: 'assistant/chunk', seq: 1, time: 11, data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'needle raw' } } },
{ type: 'user/message', seq: 2, time: 12, data: { content: [{ type: 'text', text: 'needle summary' }], source: { kind: 'plugin', plugin: 'test' } }, surfaceOp: { op: 'replace', start: 0, end: 0 }, sourceEventSeqs: [0] },
{ type: 'user/message', seq: 2, time: 12, data: createUserMessage({
content: [{ type: 'text', text: 'needle summary' }], source: { kind: 'plugin', plugin: 'test' },
}), surfaceOp: { op: 'replace', start: 0, end: 0 }, sourceEventSeqs: [0] },
{ type: 'turn/end', seq: 3, time: 13, data: { turn: 1, reason: { kind: 'error', step: 1, message: 'needle failure' } } },
]
ctx.sessions.create(SessionId('a'), { seed: events, meta: { cwd: '/a', parentSession: parent, createdAt: 20 } })
@@ -542,7 +558,9 @@ describe('SQLite session search', () => {
cursor: eventPage.nextCursor,
})).rejects.toThrow(expectCode('SESSION_QUERY_INVALID_CURSOR'))
target.append('user/message', { content: [{ type: 'text', text: 'needle four' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
target.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'needle four' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
await expect(ctx.sessionQuery.searchEvents({
sessionId: target.id,
query: 'needle',
@@ -718,7 +736,9 @@ describe('SQLite reconciliation and source lifecycle', () => {
await expect(ctx.sessionQuery.searchSessions({ query: 'durable' }))
.resolves.toMatchObject({ items: [{ header: durable, live: false, persisted: true }] })
const live = ctx.sessions.prepare(shared.id, { meta: { createdAt: 10, cwd: '/work' } })
live.append('user/message', { content: [{ type: 'text', text: 'live needle' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
live.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'live needle' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
const detach = ctx.sessions.enter(live)
ctx.sessions.announce(live)
@@ -1151,7 +1171,9 @@ describe('SQLite reconciliation and source lifecycle', () => {
await ctx.sessionQuery.searchEvents({ sessionId: live.id, query: 'base' })
const db = (ctx.sessionQuery as unknown as { _db: DatabaseSync })._db
db.exec('PRAGMA query_only = ON')
live.append('user/message', { content: [{ type: 'text', text: 'retry needle' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
live.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'retry needle' }], source: { kind: 'user' },
}), { surfaceOp: 'append' })
await expect(ctx.sessionQuery.searchEvents({ sessionId: live.id, query: 'needle' }))
.rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED'))
db.exec('PRAGMA query_only = OFF')