feat(session): project the inherited-history boundary into the log

A plugin owning a standalone open/close bracket cannot tell a dead marker
from a live one: an unmatched `compact/start` reads identically whether the
previous writer died mid-compaction or a compaction is running now.
`Session.firstLiveSeq` already holds that answer exactly, but only in memory.

Append the log-only `session/inherited` event at that seq from the seeded
constructor — the single waist all six seeded-start paths pass through
(resume, configured startup on a persisted id, `sessions.fork()`, a subagent
fork child, `adopt()`'s live prefix, and a bare seeded `create`). Read it
through the new `isInheritedSeq(events, seq)`.

The constructor placement means persistence needs no changes: the marker is
already in `events` when a backend captures the creation seed, so it rides
the ordinary seed path with no load-time write. It also covers fork, where
the inherited bracket's owner may still be running — the case a
persistence-layer boundary could not reach.

Activity ordering excludes the boundary through `lastActivityTime()`, since
lazy resume makes browsing a pickup and the three call sites would otherwise
float every opened session to the top of a picker or list.
This commit is contained in:
Hypatia May
2026-07-30 11:38:51 +08:00
parent 2a53806275
commit b341155652
41 changed files with 850 additions and 122 deletions

View File

@@ -23,6 +23,7 @@ import {
type AgentLlmTarget,
} from '@deepseek-ai/dsh-agent'
import type { LlmModelInfo, LlmModelReasoningInfo, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { lastActivityTime } from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session'
import { foldGoal, type GoalPhase } from '@deepseek-ai/dsh-goal'
import { foldSessionTitle } from '@deepseek-ai/dsh-session-title'
@@ -512,7 +513,8 @@ export function summarizeResumeCandidate(
return {
record,
title,
lastActivityAt: snapshot.events.at(-1)?.time ?? snapshot.session.createdAt,
// Excludes a prior pickup's boundary, or every browsed session floats up.
lastActivityAt: lastActivityTime(snapshot.events) ?? snapshot.session.createdAt,
lastTurn: resumeTurnLabel(snapshot),
currentWorkspace: record.header.cwd === cwd,
workspaceLabel: formatWorkspace(record.header.cwd),

View File

@@ -35,6 +35,7 @@ import type { ContentBlock, MessageId } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-llm-retry'
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import {
lastActivityTime,
SessionId,
type SessionEvent,
type UserMessage,
@@ -985,7 +986,7 @@ export function createTuiChat(
const systemPrompt = displayText(renderPrompt(assembly)) || '(empty)'
const registeredTools = assembly.tools.map(tool => displayText(tool.name)).join(', ') || '(none)'
const events = agent.session.events
const latestActivity = events.at(-1)?.time ?? agent.session.header.createdAt
const latestActivity = lastActivityTime(events) ?? agent.session.header.createdAt
const usedContext = Math.max(0, Math.round(ctx.tokenMeter.measure(agent.session).totalTokens))
let context = `${formatDiagnosticNumber(usedContext)} used · capacity unknown`
const contextWindow = modelController.contextWindow()

View File

@@ -837,6 +837,9 @@ describe('TUI terminal-state snapshots', () => {
{ type: 'step/end', seq: 5, time: Date.parse(`${day}T00:00:06Z`), data: { turn: 1, step: 1 } },
{ type: 'turn/end', seq: 6, time: Date.parse(`${day}T00:00:07Z`), data: { turn: 1, reason: { kind: 'completed' } } },
{ type: 'session/title', seq: 7, time: Date.parse(`${day}T00:00:08Z`), data: { title, messageSeqs: [1], source: { kind: 'fallback' } } },
// A prior pickup, dated well after the work: the picker must still
// show the work's date, not the pickup's.
{ type: 'session/inherited', seq: 8, time: Date.parse('2026-07-23T07:59:00.000Z'), data: {} },
],
})
const harness = await setupSnapshot({