refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/session/session-title/README.md
|
||||
README.md: e7004eb2e03757cb28b4b5756a876e1dee4b6229
|
||||
README.zh.md: a5f29426e460266d347c9c86564cf9a84428405d
|
||||
README.md: e923bd9700180214f235c4971d4b020565bfee43
|
||||
README.zh.md: 8e84ac27d9f1509c8530b838eeb5eb2216d87199
|
||||
|
||||
@@ -15,7 +15,7 @@ Only text blocks from human `user/message` events are eligible. The first eligib
|
||||
|
||||
Automatic work never delays the main agent response. A provider starts only after a marked loop-built request's exact route matches the current logged `request/header`, including when the unchanged header needs no new snapshot. Its late completion appends a standalone log-only event directly through `Session` without opening a turn. Persistence observes that event eagerly and drains on ordinary lifecycle checkpoints; title publication itself does not force a flush. Automatic failures warn and retain the latest title. New all-message revisions, provider disposal, session disposal, and explicit refresh abort older work, and a stale completion cannot append. Concurrent explicit refreshes reserve their revision before provider work, while overlapping automatic and explicit fallback requests share one session-local in-flight append. The service and bundled model provider each append their own literal event type, so no generic title-write marker, cast, or settlement queue is needed. Service teardown cancels queued work and drains calls that ignore cancellation before unloading completes.
|
||||
|
||||
Forks inherit title events in their seed unchanged. The first-message cadence does not automatically retitle a child; the all-messages cadence may append a new revision after the child receives a later human prompt.
|
||||
Forks inherit title events in their seed unchanged. The first-prompt cadence does not automatically retitle a child; the all-messages cadence may append a new revision after the child receives a later human prompt.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -29,7 +29,7 @@ All limits are required; the library supplies no defaults.
|
||||
|
||||
## Provider contract
|
||||
|
||||
A provider supplies a branded stable id, automatic mode (`first-message` or `all-user-messages`), and `generate(request)`. The request carries the live session, all eligible messages through one fixed revision, the current logged main-request route when available, and cancellation. The result identifies a non-empty title, unique ordered source-message seqs from that request, and the optional provider/model route used to generate it. The service normalizes and validates the result before it becomes durable.
|
||||
A provider supplies a branded stable id, automatic mode (`first-prompt` or `all-prompts`), and `generate(request)`. The request carries the live session, all eligible messages through one fixed revision, the current logged main-request route when available, and cancellation. The result identifies a non-empty title, unique ordered source-message seqs from that request, and the optional provider/model route used to generate it. The service normalizes and validates the result before it becomes durable.
|
||||
|
||||
See the [session-title data structures](../../../docs/subsystems/session-title.md) and [implemented decision](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md).
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ fork 出的会话会原样继承种子中的标题事件。首消息节奏不会
|
||||
|
||||
## 提供方约定
|
||||
|
||||
提供方会提供带品牌类型的稳定 id、自动模式(`first-message` 或 `all-user-messages`)和 `generate(request)`。请求携带活跃会话、截至一次固定修订的所有符合条件消息、可用时当前已记录的主请求路由,以及取消信号。结果包含非空标题、该请求中互不重复且有序的来源消息 seq,以及生成该标题时使用的可选提供方/模型路由。服务会在结果持久保存前进行规范化和验证。
|
||||
提供方会提供带品牌类型的稳定 id、自动模式(`first-prompt` 或 `all-prompts`)和 `generate(request)`。请求携带活跃会话、截至一次固定修订的所有符合条件消息、可用时当前已记录的主请求路由,以及取消信号。结果包含非空标题、该请求中互不重复且有序的来源消息 seq,以及生成该标题时使用的可选提供方/模型路由。服务会在结果持久保存前进行规范化和验证。
|
||||
|
||||
参见[会话标题数据结构](../../../docs/subsystems/session-title.md)与[已实现决策](../../../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)。
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export interface SessionTitleUserMessage {
|
||||
}
|
||||
|
||||
/** Automatic generation cadence owned by a registered provider. */
|
||||
export type SessionTitleAutomaticMode = 'first-message' | 'all-user-messages'
|
||||
export type SessionTitleAutomaticMode = 'first-prompt' | 'all-prompts'
|
||||
|
||||
/** Immutable input supplied to one title-provider call. */
|
||||
export interface SessionTitleProviderRequest {
|
||||
@@ -467,7 +467,7 @@ export class SessionTitleService extends Service {
|
||||
const registration = this.registration
|
||||
if (registration !== undefined && !registration.closing) {
|
||||
const messages = collectSessionTitleMessages(session.events, event.seq)
|
||||
const shouldSchedule = registration.provider.automatic === 'all-user-messages'
|
||||
const shouldSchedule = registration.provider.automatic === 'all-prompts'
|
||||
|| (session.header.parentSession === undefined && messages.length === 1 && this.get(session) === undefined)
|
||||
if (shouldSchedule) {
|
||||
const state = this.stateFor(session)
|
||||
@@ -726,7 +726,7 @@ export class SessionTitleService extends Service {
|
||||
if (typeof candidate.id !== 'string' || candidate.id.length === 0) {
|
||||
throw new Error('session-title provider id must be a non-empty string')
|
||||
}
|
||||
if (candidate.automatic !== 'first-message' && candidate.automatic !== 'all-user-messages') {
|
||||
if (candidate.automatic !== 'first-prompt' && candidate.automatic !== 'all-prompts') {
|
||||
throw new Error('session-title provider automatic mode is invalid')
|
||||
}
|
||||
if (typeof candidate.generate !== 'function') {
|
||||
|
||||
@@ -61,7 +61,7 @@ export function normalizeSessionTitle(input: string, maxBytes: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the deterministic first-message fallback.
|
||||
* Derive the deterministic first-prompt fallback.
|
||||
* @param input - text from the first eligible human message.
|
||||
* @param maxWords - positive whitespace-delimited word cap.
|
||||
* @param maxBytes - positive UTF-8 byte cap.
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import * as SessionTitleInvariantCompanion from '@deepseek-ai/dsh-session-title/invariant'
|
||||
import InvariantService, { InvariantError } from '@deepseek-ai/dsh-invariants'
|
||||
import InvariantRegistry, { InvariantError } from '@deepseek-ai/dsh-invariants'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
async function setup(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(InvariantService, { enabled: true })
|
||||
await ctx.plugin(InvariantRegistry, { enabled: true })
|
||||
await ctx.plugin(SessionTitleInvariantCompanion)
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import SessionPersistenceSqlite from '@deepseek-ai/dsh-session-persistence-sqlite'
|
||||
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import SqliteSessionPersistence from '@deepseek-ai/dsh-session-persistence-sqlite'
|
||||
import SessionTitleService, { foldSessionTitle } from '@deepseek-ai/dsh-session-title'
|
||||
|
||||
const CONFIG = {
|
||||
@@ -57,14 +57,14 @@ describe('session title persistence round trips', () => {
|
||||
const id = SessionId('title-jsonl')
|
||||
const writer = new Context()
|
||||
await writer.plugin(SessionStore)
|
||||
await writer.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
|
||||
await writer.plugin(JsonlSessionPersistence, { root, compression: 'none' })
|
||||
await writer.plugin(SessionTitleService, CONFIG)
|
||||
await appendPersistedTitle(writer, id)
|
||||
await writer.fiber.dispose()
|
||||
|
||||
const reader = new Context()
|
||||
await reader.plugin(SessionStore)
|
||||
await reader.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
|
||||
await reader.plugin(JsonlSessionPersistence, { root, compression: 'none' })
|
||||
await expectPersistedTitle(reader, id)
|
||||
await reader.fiber.dispose()
|
||||
})
|
||||
@@ -76,14 +76,14 @@ describe('session title persistence round trips', () => {
|
||||
const id = SessionId('title-sqlite')
|
||||
const writer = new Context()
|
||||
await writer.plugin(SessionStore)
|
||||
await writer.plugin(SessionPersistenceSqlite, { path })
|
||||
await writer.plugin(SqliteSessionPersistence, { path })
|
||||
await writer.plugin(SessionTitleService, CONFIG)
|
||||
await appendPersistedTitle(writer, id)
|
||||
await writer.fiber.dispose()
|
||||
|
||||
const reader = new Context()
|
||||
await reader.plugin(SessionStore)
|
||||
await reader.plugin(SessionPersistenceSqlite, { path })
|
||||
await reader.plugin(SqliteSessionPersistence, { path })
|
||||
await expectPersistedTitle(reader, id)
|
||||
await reader.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import LlmService, { createUserMessage, deepFreeze, markAgentLoopRequest } from '@deepseek-ai/dsh-llm'
|
||||
import LlmRuntime, { createUserMessage, deepFreeze, markAgentLoopRequest } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionTitleService, {
|
||||
SessionTitleProviderId,
|
||||
@@ -48,7 +48,7 @@ function appendRoute(session: ReturnType<Context['sessions']['create']>, reason:
|
||||
}
|
||||
|
||||
describe('SessionTitleService provider lifecycle', () => {
|
||||
it('inherits title events across forks, skips first-message retitling, and lets all-messages update later', async () => {
|
||||
it('inherits title events across forks, skips first-prompt retitling, and lets all-messages update later', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionTitleService, CONFIG)
|
||||
@@ -71,7 +71,7 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
}))
|
||||
const disposeFirst = ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('fork-first'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate: firstGenerate,
|
||||
})
|
||||
child.append('turn/start', {
|
||||
@@ -91,7 +91,7 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
}))
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('fork-all'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
generate: allGenerate,
|
||||
})
|
||||
child.append('turn/start', {
|
||||
@@ -112,14 +112,14 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
expect(ctx.sessionTitle.get(parent)?.title).toBe('Inherited title prompt')
|
||||
})
|
||||
|
||||
it('runs a first-message provider once after the routed request and retries only through refresh', async () => {
|
||||
it('runs a first-prompt provider once after the routed request and retries only through refresh', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionTitleService, CONFIG)
|
||||
const requests: SessionTitleProviderRequest[] = []
|
||||
const provider: SessionTitleProvider = {
|
||||
id: SessionTitleProviderId('first-model'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
async generate(request) {
|
||||
requests.push(request)
|
||||
return {
|
||||
@@ -175,7 +175,7 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
let observedSignal: AbortSignal | undefined
|
||||
const first: SessionTitleProvider = {
|
||||
id: SessionTitleProviderId('winner'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
generate(request) {
|
||||
observedSignal = request.signal
|
||||
return pending.promise
|
||||
@@ -184,7 +184,7 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
const dispose = ctx.sessionTitle.register(first)
|
||||
expect(() => ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('duplicate'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate: async () => ({ title: 'duplicate', messageSeqs: [0] }),
|
||||
})).toThrow(/already registered/)
|
||||
|
||||
@@ -211,7 +211,7 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
|
||||
const replacement: SessionTitleProvider = {
|
||||
id: SessionTitleProviderId('replacement'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate: async () => ({ title: 'replacement', messageSeqs: [message.seq] }),
|
||||
}
|
||||
const disposeReplacement = ctx.sessionTitle.register(replacement)
|
||||
@@ -226,7 +226,7 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
const requests: SessionTitleProviderRequest[] = []
|
||||
const provider: SessionTitleProvider = {
|
||||
id: SessionTitleProviderId('all-model'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
generate(request) {
|
||||
requests.push(request)
|
||||
if (requests.length === 1) return firstResult.promise
|
||||
@@ -262,13 +262,13 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
|
||||
it('runs an all-messages revision when the next main request reuses its logged header', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionTitleService, CONFIG)
|
||||
const requests: SessionTitleProviderRequest[] = []
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('unchanged-route'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
async generate(request) {
|
||||
requests.push(request)
|
||||
return {
|
||||
@@ -316,7 +316,7 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
|
||||
it('ignores model streams that are not a matching loop request', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmRuntime)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionTitleService, CONFIG)
|
||||
const generate = vi.fn(async (request: SessionTitleProviderRequest): Promise<SessionTitleProviderResult> => ({
|
||||
@@ -325,7 +325,7 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
}))
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('request-filter'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
generate,
|
||||
})
|
||||
const options = { provider: 'main-route', model: 'chat-model', messages: [] }
|
||||
@@ -353,7 +353,7 @@ describe('SessionTitleService provider lifecycle', () => {
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
|
||||
const provider: SessionTitleProvider = {
|
||||
id: SessionTitleProviderId('failing'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
generate: async () => { throw new Error('title backend failed') },
|
||||
}
|
||||
ctx.sessionTitle.register(provider)
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('SessionTitleService.rename', () => {
|
||||
}))
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('pin-provider'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
generate,
|
||||
})
|
||||
const session = ctx.sessions.create(SessionId('rename-pin'))
|
||||
@@ -139,7 +139,7 @@ describe('SessionTitleService.rename', () => {
|
||||
})
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('deferred-provider'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
generate,
|
||||
})
|
||||
const session = ctx.sessions.create(SessionId('rename-supersede'))
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('SessionTitleService configuration and refresh boundaries', () => {
|
||||
}))
|
||||
withProvider.sessionTitle.register({
|
||||
id: SessionTitleProviderId('empty-provider'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate,
|
||||
})
|
||||
const providerEmpty = withProvider.sessions.create(SessionId('empty-provider'))
|
||||
@@ -94,7 +94,7 @@ describe('SessionTitleService configuration and refresh boundaries', () => {
|
||||
let observed: SessionTitleProviderRequest | undefined
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('explicit-no-route'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
async generate(request) {
|
||||
observed = request
|
||||
return { title: 'Explicit title', messageSeqs: [request.messages[0]!.seq] }
|
||||
@@ -117,7 +117,7 @@ describe('SessionTitleService configuration and refresh boundaries', () => {
|
||||
let callerSignal: AbortSignal | undefined
|
||||
callerCtx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('caller-cancel'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate(request) {
|
||||
callerSignal = request.signal
|
||||
return callerPending.promise
|
||||
@@ -139,7 +139,7 @@ describe('SessionTitleService configuration and refresh boundaries', () => {
|
||||
let disposeSignal: AbortSignal | undefined
|
||||
disposeCtx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('session-dispose'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate(request) {
|
||||
disposeSignal = request.signal
|
||||
return disposePending.promise
|
||||
@@ -215,7 +215,7 @@ describe('SessionTitleService configuration and refresh boundaries', () => {
|
||||
const results: Array<ReturnType<typeof deferred<SessionTitleProviderResult>>> = []
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('refresh-order'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate(request) {
|
||||
requests.push(request)
|
||||
const result = deferred<SessionTitleProviderResult>()
|
||||
@@ -293,7 +293,7 @@ describe('SessionTitleService configuration and refresh boundaries', () => {
|
||||
const requests: SessionTitleProviderRequest[] = []
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('service-unload'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
generate(request) {
|
||||
requests.push(request)
|
||||
return result.promise
|
||||
@@ -362,12 +362,12 @@ describe('SessionTitleService provider validation and stale scheduling', () => {
|
||||
expect(() => ctx.sessionTitle.register('provider' as never)).toThrow(/must be an object/)
|
||||
expect(() => ctx.sessionTitle.register({
|
||||
id: 1,
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate,
|
||||
} as unknown as SessionTitleProvider)).toThrow(/id must be a non-empty string/)
|
||||
expect(() => ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId(''),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate,
|
||||
})).toThrow(/id must be a non-empty string/)
|
||||
expect(() => ctx.sessionTitle.register({
|
||||
@@ -377,7 +377,7 @@ describe('SessionTitleService provider validation and stale scheduling', () => {
|
||||
})).toThrow(/automatic mode is invalid/)
|
||||
expect(() => ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('missing-generate'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate: undefined,
|
||||
} as unknown as SessionTitleProvider)).toThrow(/requires generate/)
|
||||
})
|
||||
@@ -390,7 +390,7 @@ describe('SessionTitleService provider validation and stale scheduling', () => {
|
||||
}))
|
||||
const dispose = ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('queued-dispose'),
|
||||
automatic: 'all-user-messages',
|
||||
automatic: 'all-prompts',
|
||||
generate,
|
||||
})
|
||||
const session = startSession(ctx, 'queued-dispose')
|
||||
@@ -414,7 +414,7 @@ describe('SessionTitleService provider validation and stale scheduling', () => {
|
||||
let result: unknown
|
||||
ctx.sessionTitle.register({
|
||||
id: SessionTitleProviderId('invalid-results'),
|
||||
automatic: 'first-message',
|
||||
automatic: 'first-prompt',
|
||||
generate: async () => result as SessionTitleProviderResult,
|
||||
})
|
||||
const session = startSession(ctx, 'invalid-results')
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"path": "../../util/brand"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../llm/llm"
|
||||
|
||||
Reference in New Issue
Block a user