mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix: cr
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 .agents/notes/implemented/feature/2026-07-28-todo-plan-clears-on-next-turn.md
|
||||
2026-07-28-todo-plan-clears-on-next-turn.md: 64c7207d334689817c3a392a0866158e4c1b927d
|
||||
2026-07-28-todo-plan-clears-on-next-turn.zh.md: 9df4d952888df9e83f84deb3f2d587b5f9e2ff77
|
||||
2026-07-28-todo-plan-clears-on-next-turn.md: daf25fd92eae4c6429a07c6864a20a828fa9dbcd
|
||||
2026-07-28-todo-plan-clears-on-next-turn.zh.md: 10b1c1c6b15d54b5cc30f4bbe36bb5e23befa3b6
|
||||
|
||||
@@ -28,4 +28,4 @@ Host `backscanTodos` (and the fixture parallel) walks the full log from the tail
|
||||
|
||||
## Consequences
|
||||
|
||||
Interactive hosts and the history projection share one lifetime rule; reopening a session restores a plan only when no later turn has started. Partial supersession of the session-long standing-plan wording in [web todo display](2026-07-23-web-todo-display.md) and [`todo_write` tool](2026-06-29-todo-write-tool.md): event-sourcing, last-write-wins replacement, and the two render surfaces stay there; this note owns turn-boundary clearance. Coverage: client session specs for live clear + replay empty, host history projection after a post-write `turn/start`, and the existing web todo-display snapshot (fixture turn 65 remains the log tip, so the strip still pins).
|
||||
Interactive hosts and the history projection share one lifetime rule; reopening a session restores a plan only when no later turn has started. Partial supersession of the session-long standing-plan wording in [web todo display](2026-07-23-web-todo-display.md) and [`todo_write` tool](2026-06-29-todo-write-tool.md): event-sourcing, last-write-wins replacement, and the two render surfaces stay there; this note owns turn-boundary clearance. Coverage: client session specs for live clear + replay empty, host history projection after a post-write `turn/start`, the standing-plan web todo-display snapshot (fixture turn 65 remains the log tip), plus assembled web/TUI snapshots that start the next turn and pin the strip gone.
|
||||
|
||||
@@ -28,4 +28,4 @@ Host 的 `backscanTodos`(以及 fixture 平行实现)从日志尾部向前
|
||||
|
||||
## Consequences
|
||||
|
||||
交互式宿主与 history 投影共用同一生命周期规则;重开会话仅在其后没有更新的 turn 启动时恢复计划。对 [web todo 展示](2026-07-23-web-todo-display.md) 与 [`todo_write` 工具](2026-06-29-todo-write-tool.md) 中「会话级站立计划」表述构成部分取代:事件溯源、后写覆盖与两个渲染面仍归那两份 Note;本 Note 拥有 turn 边界清空。覆盖:客户端 session 对实时清空 + 回放为空的用例、host 在写入后的 `turn/start` 之后的 history 投影,以及既有的 web todo-display 快照(fixture 第 65 轮仍是日志末端,计划条仍被钉住)。
|
||||
交互式宿主与 history 投影共用同一生命周期规则;重开会话仅在其后没有更新的 turn 启动时恢复计划。对 [web todo 展示](2026-07-23-web-todo-display.md) 与 [`todo_write` 工具](2026-06-29-todo-write-tool.md) 中「会话级站立计划」表述构成部分取代:事件溯源、后写覆盖与两个渲染面仍归那两份 Note;本 Note 拥有 turn 边界清空。覆盖:客户端 session 对实时清空 + 回放为空的用例、host 在写入后的 `turn/start` 之后的 history 投影、站立计划的 web todo-display 快照(fixture 第 65 轮仍是日志末端),以及开启下一 turn 并钉住计划条消失的 assembled web/TUI 快照。
|
||||
|
||||
@@ -189,3 +189,31 @@ it('collapses the plan strip to the count summary and restores it', async () =>
|
||||
fireEvent.click(header)
|
||||
expect(panel.querySelectorAll('li')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('hides the plan strip when the next turn starts', async () => {
|
||||
boot()
|
||||
await openFixtureSession()
|
||||
expect(document.querySelector('[data-testid="todo-panel"]')).not.toBeNull()
|
||||
|
||||
const composer = await screen.findByPlaceholderText('Message the agent', {}, { timeout: 10_000 })
|
||||
fireEvent.change(composer, { target: { value: '下一轮清空计划' } })
|
||||
fireEvent.keyDown(composer, { key: 'Enter' })
|
||||
|
||||
await screen.findByText('下一轮清空计划', { exact: true }, { timeout: 10_000 })
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[data-testid="todo-panel"]')).toBeNull()
|
||||
}, { timeout: 10_000 })
|
||||
|
||||
expect({
|
||||
promptVisible: screen.getByText('下一轮清空计划', { exact: true }).textContent,
|
||||
panelGone: document.querySelector('[data-testid="todo-panel"]') === null,
|
||||
// Historical todo_write row stays in the flow; only the dock strip clears.
|
||||
rowStillPresent: document.querySelector('[data-sample="todo-row"]') !== null,
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"panelGone": true,
|
||||
"promptVisible": "下一轮清空计划",
|
||||
"rowStillPresent": true,
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
@@ -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/client/runtime/README.md
|
||||
README.md: 16c1124ec812b9f030ce8266a16cdb8f5db0e6cc
|
||||
README.zh.md: a3d2a2dfdd1662afee65ec45e26b1ef1029f44b5
|
||||
README.md: acbe04a9710250618623d47c175baccc9c312a2c
|
||||
README.zh.md: 002f0678bc8b339fba66f543eab1a06a8ee123f8
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects, list/scope/history state; WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (`connectWorkspace`). The runtime fans the shared Host stream into both managers. Client sessions are always Host-born (Session+Agent+cwd in one `session.create`); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Contract: api-contracts v3 §4. `ConversationSnapshot` carries `todos` — the session's current todo projection: taken from the tail history page's full-log value (host-computed, independent of the page window), preserved across an older-page prepend, and overwritten by each live `todo/write` (last write wins). A tail response that omits the field means the log holds no `todo/write`, so the list resets to empty — a plan the log never kept (a write lost to a host crash) disappears on the next open or resync.
|
||||
Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects, list/scope/history state; WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (`connectWorkspace`). The runtime fans the shared Host stream into both managers. Client sessions are always Host-born (Session+Agent+cwd in one `session.create`); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Contract: api-contracts v3 §4. `ConversationSnapshot` carries `todos` — the standing plan (latest `todo/write` with no later `turn/start`): taken from the tail history page's full-log projection (host-computed, independent of the page window), preserved across an older-page prepend when the window never re-determines it, overwritten by each live `todo/write`, and cleared on each live `turn/start`. A tail response that omits the field means no plan stands (never wrote one, or a later turn retired it), so the list resets to empty — including a plan the log never kept (a write lost to a host crash) on the next open or resync.
|
||||
|
||||
## Workspace and Session lists
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
客户端 cordis 启动与不依赖 React 的对象服务:SlotsService 包装 SlotCore 并提供 renderer 数据源;SessionsService 拥有 Session 对象、列表/scope/history 状态;WorkspacesService 依赖 SessionsService,拥有 Workspace 对象、列表/操作、默认目标派生,以及 New Session 空会话复用入口(`connectWorkspace`)。运行时把共享 Host 流分发给两个 manager。客户端 Session 一律由 Host 出生(一次 `session.create` 同瞬产出 Session+Agent+cwd);客户端不持有任何实体化之前的会话状态——Agent scope(host dsh-scope 的客户端镜像,以 agent/session 共用 id 为键)在会话行进入列表镜像时出生,随 prune 死亡。契约:api-contracts v3 §4。`ConversationSnapshot` 携带 `todos`——会话当前的 todo 投影:取自尾页 history 携带的全量 log 值(host 计算,独立于分页窗口),跨往前翻页保留,并被每次实时 `todo/write` 覆盖(后写胜出)。尾页响应省略该字段即表示 log 中没有任何 `todo/write`,因此列表复位为空——log 从未留下的计划(写入因 host 崩溃丢失)会在下一次打开或 resync 时消失。
|
||||
客户端 cordis 启动与不依赖 React 的对象服务:SlotsService 包装 SlotCore 并提供 renderer 数据源;SessionsService 拥有 Session 对象、列表/scope/history 状态;WorkspacesService 依赖 SessionsService,拥有 Workspace 对象、列表/操作、默认目标派生,以及 New Session 空会话复用入口(`connectWorkspace`)。运行时把共享 Host 流分发给两个 manager。客户端 Session 一律由 Host 出生(一次 `session.create` 同瞬产出 Session+Agent+cwd);客户端不持有任何实体化之前的会话状态——Agent scope(host dsh-scope 的客户端镜像,以 agent/session 共用 id 为键)在会话行进入列表镜像时出生,随 prune 死亡。契约:api-contracts v3 §4。`ConversationSnapshot` 携带 `todos`——站立计划(其后没有更晚 `turn/start` 的最近一次 `todo/write`):取自尾页 history 携带的全量 log 投影(host 计算,独立于分页窗口);窗口本身未再判定计划时跨往前翻页保留;被每次实时 `todo/write` 覆盖,并在每次实时 `turn/start` 清空。尾页响应省略该字段即表示当前没有站立计划(从未写入,或更晚的 turn 已退役),因此列表复位为空——也包括 log 从未留下的计划(写入因 host 崩溃丢失)在下一次打开或 resync 时消失。
|
||||
|
||||
## Workspace 与 Session 列表
|
||||
|
||||
|
||||
@@ -243,7 +243,8 @@ export interface ConversationSnapshot {
|
||||
*/
|
||||
blank: boolean
|
||||
lastAgentError: string | null
|
||||
/** Current whole-list `todo/write` projection — the tail page's full-log value, then each live
|
||||
* write (last write wins); empty = the log holds no plan. */
|
||||
/** Standing plan strip: the latest `todo/write` with no later `turn/start`. Seeded from the
|
||||
* tail history page, overwritten by each live write, cleared on each live `turn/start`;
|
||||
* empty when no plan stands. */
|
||||
todos: readonly TodoItem[]
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export const historyEntrySchema = z.object({
|
||||
view: toolEventViewSchema.optional(),
|
||||
}) satisfies z.ZodType<Wire<HistoryEntry>>
|
||||
|
||||
/** One todo item of the tail page's session-level projection (the todo/write payload shape). */
|
||||
/** One todo item of the tail page's standing-plan projection (the todo/write payload shape). */
|
||||
export const todoItemSchema = z.object({
|
||||
content: z.string(),
|
||||
status: z.union([z.literal('pending'), z.literal('in_progress'), z.literal('completed')]),
|
||||
|
||||
@@ -149,10 +149,11 @@ export interface SessionsApi {
|
||||
* Each entry pairs the raw SessionEvent with the host-computed view (tool events whose
|
||||
* presenter produced one, evaluated against the registry at pagination time); the client
|
||||
* rebuilds the surface from the events with the shared fold.
|
||||
* The tail page (beforeSeq absent) also carries `todos` — the session's current todo
|
||||
* projection (latest `todo/write` over the FULL log, independent of the page window) —
|
||||
* so a paged client restores the plan without walking history; absent when the session
|
||||
* never wrote one. Older pages omit it (the projection is session-level, not per-page).
|
||||
* The tail page (beforeSeq absent) also carries `todos` — the standing plan over the
|
||||
* FULL log (latest `todo/write` with no later `turn/start`), independent of the page
|
||||
* window — so a paged client restores the plan without walking history. Absent when no
|
||||
* plan stands (never wrote one, or a later `turn/start` retired it). Older pages omit
|
||||
* it (the projection is session-level, not per-page).
|
||||
*/
|
||||
history(request: RpcRequest<{ sessionId: SessionId; beforeSeq?: number; maxMessages?: number }>):
|
||||
Promise<RpcResponse<{ events: HistoryEntry[]; hasMore: boolean; todos?: TodoItem[] }>>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
terminal 96x36 buffer=normal length=36 base=0 viewport=0
|
||||
lifecycle started=1 stopped=0 progress=inactive
|
||||
title "DSH snapshot"
|
||||
cursor hidden column=7 viewportRow=15 bufferRow=15
|
||||
viewport
|
||||
0| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-blue bold
|
||||
style 10-16 bold
|
||||
1| " Snapshot agent ready."
|
||||
style 1-21 fg=bright-black
|
||||
2| " main-session"
|
||||
style 1-12 dim
|
||||
3| <blank>
|
||||
4| "Assistant "
|
||||
style 0-8 fg=bright-magenta bold underline
|
||||
5| "Tracking the steps. "
|
||||
6| "Model wait 0.0s · Completed 2026-07-21 14:45:00 "
|
||||
style 0-46 dim
|
||||
7| <blank>
|
||||
8| "You "
|
||||
style 0-2 fg=bright-blue bold underline
|
||||
9| "Plan the work. "
|
||||
10| <blank>
|
||||
11| "You "
|
||||
style 0-2 fg=bright-blue bold underline
|
||||
12| "Next question. "
|
||||
13| <blank>
|
||||
14| "/workspace/project (tui-staging) deepseek-v4-flash ↑0 ↓0 0% context"
|
||||
style 0-17 fg=bright-blue bold
|
||||
style 18-31 fg=bright-black
|
||||
style 34-50 fg=bright-black
|
||||
style 53-57 fg=bright-black
|
||||
style 60-69 fg=bright-black
|
||||
15| " dsh > "
|
||||
style 1-3 fg=bright-blue bold
|
||||
style 5-6 fg=bright-black
|
||||
style 7-7 inverse
|
||||
16-35| <blank>
|
||||
@@ -57,6 +57,7 @@ const CHECKPOINTS = [
|
||||
'resume-sessions',
|
||||
'status-diagnostics',
|
||||
'status-diagnostics-narrow',
|
||||
'todo-plan-cleared',
|
||||
] as const
|
||||
|
||||
// Real-loop scenarios own their assertions in separate snapshot suites but
|
||||
@@ -299,6 +300,34 @@ describe('TUI terminal-state snapshots', () => {
|
||||
await disposeSnapshot(harness)
|
||||
})
|
||||
|
||||
it('clears the plan strip when the next turn starts', async () => {
|
||||
// Freeze Completed-at formatting: the first turn ends before the next starts,
|
||||
// so the assistant timing line still appears without a Plan strip below it.
|
||||
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date(2026, 6, 21, 14, 45, 0).getTime())
|
||||
const harness = await setupSnapshot({
|
||||
beforeMount(session) {
|
||||
appendUser(session, 'Plan the work.')
|
||||
appendAssistant(session, [{ type: 'text', text: 'Tracking the steps.' }])
|
||||
session.append('todo/write', {
|
||||
todos: [
|
||||
{ content: 'read code', status: 'completed' },
|
||||
{ content: 'write tests', status: 'in_progress' },
|
||||
],
|
||||
})
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
session.append('turn/start', {
|
||||
turn: 2,
|
||||
trigger: { kind: 'message', source: { kind: 'user' } },
|
||||
})
|
||||
appendUser(session, 'Next question.')
|
||||
},
|
||||
})
|
||||
await checkpoint('todo-plan-cleared', harness.terminal)
|
||||
nowSpy.mockRestore()
|
||||
await disposeSnapshot(harness)
|
||||
})
|
||||
|
||||
it('pins failed-stream retraction, scheduled retry, and eventual success', async () => {
|
||||
const harness = await setupSnapshot()
|
||||
await renderAfter(harness, () => {
|
||||
|
||||
Reference in New Issue
Block a user