mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
Merge pull request #7472 from deepseek-harness/turtle/fork-name-suffix-5f3435925211
fix(session-controller): give repeated forks distinct titles
This commit is contained in:
@@ -15,8 +15,8 @@
|
||||
en: 1fdf707322fb6996
|
||||
zh: 0204f7e9d4aa3cce
|
||||
/session-controller/use-this-package:
|
||||
en: 300f02be359a3170
|
||||
zh: ac7fb4abbe78eddf
|
||||
en: 425f679cb90f481b
|
||||
zh: 15ed208a442b5b3b
|
||||
/session-controller/client-references:
|
||||
en: e8979a803ec3e194
|
||||
zh: c567efcd971728ba
|
||||
@@ -33,8 +33,8 @@
|
||||
en: 22fc0c9dbedd3bb6
|
||||
zh: fad040ca55fd0e0a
|
||||
/session-controller/known-limitations-and-deferred-work:
|
||||
en: 5db0f9f69a8be926
|
||||
zh: 6ba1cc4a3850fcbc
|
||||
en: 23bc3db43e84daa6
|
||||
zh: 0242338ac7bd24fa
|
||||
/session-controller/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: e88aeb617ffb74ea
|
||||
|
||||
@@ -64,7 +64,7 @@ During uninterrupted following, transcript and steering echoes stay in Chat thro
|
||||
|
||||
The user-invocable `skills/list` metadata includes the winning provider’s optional instruction-file `path`. The composer can preview that file without loading every skill body or activating a cold Agent.
|
||||
|
||||
Fork copies the exact inclusive event prefix selected by `atSeq`, including a cut inside an open turn, into an independent Session without `parentSession`. Client placeholders likewise omit `parentSessionId`; list indentation applies only to subagent-origin rows, including when historical ordinary forks retain a parent id. Workspace attachment and optional title increments use the source selected for the operation. The child records its inherited marker before synthetic fork results and closing events. Omitting `atSeq` selects the latest completed turn and its standalone tail, stopping before the next turn or queued input; a nonexistent event is rejected. The chat action selects a completed turn.
|
||||
Fork copies the exact inclusive event prefix selected by `atSeq`, including a cut inside an open turn, into an independent Session without `parentSession`. Client placeholders likewise omit `parentSessionId`; list indentation applies only to subagent-origin rows, including when historical ordinary forks retain a parent id. Workspace attachment and optional title increments use the source selected for the operation; an increment skips numbers that other Host-listed titles already use. The child records its inherited marker before synthetic fork results and closing events. Omitting `atSeq` selects the latest completed turn and its standalone tail, stopping before the next turn or queued input; a nonexistent event is rejected. The chat action selects a completed turn.
|
||||
|
||||
A resume blocked by an existing write handle returns `session/writer-held` with the Session id; other resume failures retain `gateway/internal`.
|
||||
|
||||
@@ -129,6 +129,7 @@ No direct effect; model requests remain owned by the Agent and LLM packages.
|
||||
- The image byte cap does not validate decoded dimensions or pixel count.
|
||||
- A failed follow resumption remains visible to the caller instead of retrying indefinitely.
|
||||
- The raw browser upload is one streaming HTTP request without resumable offsets; a retry sends the file again from byte zero.
|
||||
- Fork title increments compare candidates with the Client list before the Host normalizes them: concurrent forks of one source, or a title the Host truncates at its byte limit, can still produce duplicate titles.
|
||||
- File-reference completion uses the shared Agent lookup and can resume a cold Session; the `skills/list` catalog is the non-activating alternative for skill metadata.
|
||||
- Accepted/running display memory lives only in Client memory and is lost on page reload.
|
||||
- That memory is not shared between tabs: one tab can show a converted row while another still shows `New Session` for the same Session.
|
||||
|
||||
@@ -64,7 +64,7 @@ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用
|
||||
|
||||
面向用户调用的 `skills/list` 元数据包含胜出提供方可选的指令文件 `path`。输入框可据此预览文件,无需加载每个 skill 的正文或激活冷态 Agent。
|
||||
|
||||
Fork 将 `atSeq` 选定的含边界事件前缀(包括开放轮次内的截点)复制到不带 `parentSession` 的独立 Session。客户端占位项同样省略 `parentSessionId`;列表缩进仅适用于 subagent 来源的行,历史普通 fork 即使保留父 ID 也不缩进。工作区绑定及可选标题递增使用本次操作选定的源会话。子会话在合成的 fork 结果和结束事件之前记录继承标记。省略 `atSeq` 时选择最近已结束轮次及其独立尾部,在下一轮次或排队输入之前停止;不存在的事件会被拒绝。聊天操作选择已结束轮次。
|
||||
Fork 将 `atSeq` 选定的含边界事件前缀(包括开放轮次内的截点)复制到不带 `parentSession` 的独立 Session。客户端占位项同样省略 `parentSessionId`;列表缩进仅适用于 subagent 来源的行,历史普通 fork 即使保留父 ID 也不缩进。工作区绑定及可选标题递增使用本次操作选定的源会话;递增时会跳过其他由 Host 列出的标题已使用的编号。子会话在合成的 fork 结果和结束事件之前记录继承标记。省略 `atSeq` 时选择最近已结束轮次及其独立尾部,在下一轮次或排队输入之前停止;不存在的事件会被拒绝。聊天操作选择已结束轮次。
|
||||
|
||||
恢复会话时若已有写句柄占用,返回 `session/writer-held`,并携带会话 id;其他恢复失败仍返回 `gateway/internal`。
|
||||
|
||||
@@ -129,6 +129,7 @@ Blockquote 作为普通 prompt 文本传递,不携带 quote 元数据。本地
|
||||
- 图片字节上限不校验解码后的尺寸或像素数。
|
||||
- follow 恢复失败会对调用方可见,而不会无限重试。
|
||||
- 浏览器原始字节上传使用一次不带断点续传偏移的流式 HTTP 请求;重试会从第零字节重新传输整个文件。
|
||||
- Fork 标题递增会在 Host 规范化候选标题之前,先将其与客户端列表比较:同一来源的并发 Fork,或被 Host 按字节上限截断的标题,仍可能产生重复标题。
|
||||
- 文件引用补全使用共享 Agent lookup,因此可能恢复冷 Session;`skills/list` 目录是不激活 Agent 的 skill 元数据读取路径。
|
||||
- 受理/运行的展示记忆只存在于客户端内存,页面重载后即丢失。
|
||||
- 该记忆不在 tab 之间共享:同一会话可能在一个 tab 中已转正,在另一个 tab 中仍显示为 `New Session`。
|
||||
|
||||
@@ -128,7 +128,8 @@ export interface ISessions {
|
||||
* seq (a real event seq the caller already knows; a cut inside an open
|
||||
* turn is balanced Host-side with synthetic closers, and omission selects
|
||||
* the latest completed-turn prefix), and whether to increment an
|
||||
* inherited durable title before resolving. `onCreated` observes the catalogued child before that optional rename.
|
||||
* inherited durable title, skipping numbers Host-listed titles already use,
|
||||
* before resolving. `onCreated` observes the catalogued child before that optional rename.
|
||||
* @returns the child session id.
|
||||
* @throws when the fork fails, or when a requested child-title rename fails after creation.
|
||||
*/
|
||||
|
||||
@@ -130,12 +130,24 @@ function displayTitleOf(title: string | undefined, cwd: string | undefined, id:
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment a trailing fork number while preserving its half-width or
|
||||
* full-width parentheses; an unnumbered title starts with ` (1)`.
|
||||
* Increment the fork number until the candidate is not taken.
|
||||
* @param title - source session's durable title.
|
||||
* @param taken - durable titles of Host-listed sessions.
|
||||
* @returns the title assigned to the fork child.
|
||||
*/
|
||||
function increasedForkTitle(title: string): string {
|
||||
function increasedForkTitle(title: string, taken: ReadonlySet<string>): string {
|
||||
let next = incrementedTitle(title)
|
||||
while (taken.has(next)) next = incrementedTitle(next)
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment a trailing fork number while preserving its half-width or
|
||||
* full-width parentheses; an unnumbered title starts with ` (1)`.
|
||||
* @param title - title to increment.
|
||||
* @returns the title with the next fork number.
|
||||
*/
|
||||
function incrementedTitle(title: string): string {
|
||||
const ascii = /^(.*?)\((\d+)\)$/u.exec(title)
|
||||
if (ascii?.[1] !== undefined && ascii[2] !== undefined) {
|
||||
return `${ascii[1]}(${BigInt(ascii[2]) + 1n})`
|
||||
@@ -455,7 +467,8 @@ export class ClientSessions implements ISessions {
|
||||
* seq (a real event seq the caller already knows; a cut inside an open
|
||||
* turn is balanced Host-side with synthetic closers, and omission selects
|
||||
* the latest completed-turn prefix), and whether to increment an
|
||||
* inherited durable title before resolving, plus permission to start source migration.
|
||||
* inherited durable title, skipping numbers Host-listed titles already use,
|
||||
* before resolving, plus permission to start source migration.
|
||||
* @returns the child session id.
|
||||
* @throws {SessionForkError} with the source id.
|
||||
* @throws {Error} when a requested child-title rename fails after creation.
|
||||
@@ -480,7 +493,9 @@ export class ClientSessions implements ISessions {
|
||||
const childId = result.value.sessionId
|
||||
opts.onCreated?.(childId)
|
||||
if (sourceTitle !== undefined) {
|
||||
const renamed = await this.manager.rename(childId, increasedForkTitle(sourceTitle))
|
||||
const list = this.list.getSnapshot()
|
||||
const taken = new Set(list.ids.flatMap(id => list.byId[id]?.title ?? []))
|
||||
const renamed = await this.manager.rename(childId, increasedForkTitle(sourceTitle, taken))
|
||||
if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`)
|
||||
}
|
||||
return childId
|
||||
|
||||
@@ -1010,6 +1010,29 @@ describe('fork', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('gives repeated forks distinct titles past numbers listed sessions use', async ({ bench }) => {
|
||||
const b = bench()
|
||||
for (const [id, value] of [['source', 'Roadmap'], ['sibling', 'Roadmap (2)']] as const) {
|
||||
b.svc.handleControlFrame({ type: 'projection', sessionId: sid(id), changes: [{ key: 'title', val: value }], seq: 2 })
|
||||
}
|
||||
await feedList(b, [{ id: 'source' }, { id: 'sibling' }])
|
||||
b.mock.remote.session.rename.mockImplementation((payload) => {
|
||||
const { title } = payload as { title: string }
|
||||
return Promise.resolve(ok({ title, seq: 3 }))
|
||||
})
|
||||
|
||||
for (const child of ['child-1', 'child-2']) {
|
||||
b.mock.remote.session.fork.mockResolvedValueOnce(ok({ sessionId: sid(child) }))
|
||||
await b.svc.fork({ sessionId: sid('source'), increaseTitle: true })
|
||||
}
|
||||
|
||||
expect(b.mock.remote.session.rename.mock.calls).toEqual([
|
||||
[{ sessionId: 'child-1', title: 'Roadmap (1)' }],
|
||||
[{ sessionId: 'child-2', title: 'Roadmap (3)' }],
|
||||
])
|
||||
expect(b.svc.list.getSnapshot().byId[sid('child-2')]?.title).toBe('Roadmap (3)')
|
||||
})
|
||||
|
||||
it('sends the exact boundary seq verbatim: callers pass real event seqs', async ({ bench }) => {
|
||||
const b = bench()
|
||||
await feedList(b, [{ id: 'source', cwd: '/work' }])
|
||||
|
||||
@@ -215,7 +215,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
{
|
||||
signature: 'fork(opts: { sessionId: SessionId atSeq?: number increaseTitle?: boolean onCreated?: (childId: SessionId) => void /** False refuses source migration; omission preserves the Host default. */ allowMigration?: boolean }): Promise<SessionId>',
|
||||
description: 'Fork a session from an exact inclusive prefix of the source; on resolution the child is catalogued and can be explicitly retained.',
|
||||
parameters: [{ name: 'opts', description: 'source session id, the optional exact inclusive boundary seq (a real event seq the caller already knows; a cut inside an open turn is balanced Host-side with synthetic closers, and omission selects the latest completed-turn prefix), and whether to increment an inherited durable title before resolving. `onCreated` observes the catalogued child before that optional rename.' }],
|
||||
parameters: [{ name: 'opts', description: 'source session id, the optional exact inclusive boundary seq (a real event seq the caller already knows; a cut inside an open turn is balanced Host-side with synthetic closers, and omission selects the latest completed-turn prefix), and whether to increment an inherited durable title, skipping numbers Host-listed titles already use, before resolving. `onCreated` observes the catalogued child before that optional rename.' }],
|
||||
returns: 'the child session id.',
|
||||
throws: ['when the fork fails, or when a requested child-title rename fails after creation.'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user