mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
* feat: /goal命令能力支持,参考codex实现 * fix: 修复promp和提示词不一致的问题 * fix: 修复 goal 功能多项 AI 审查问题 - prompt 中 update 行为描述与运行时不一致(no-op → error) - src/commands/goal/ 使用相对路径导入,改为 src/* 别名 - /goal 命令标记 bridgeSafe 但含交互式对话框,改为 false - useGoalContinuation 中 origin 使用 as unknown as string 强转,改为直接传字符串 - ResumeConversation 路径缺少 goal hydration,补齐恢复逻辑 - onCancel 在非查询状态下误暂停 goal,加 queryGuard 守卫 - resumeGoal 允许从终态恢复,收紧为仅允许 paused 状态 - buildGoalContextBlock 生成畸形 XML 属性,改为合法 budget 属性 * fix: 修复剩余AI审查的问题 * fix: 防止goal状态丢失 * fix: 修复Biome规范错误问题 * fix: 修复部分情况下goal无法启动的问题 * fix: 增加断网后状态默认设置为PAUSE机制、完成暂停-恢复状态切换,且正常进行前端渲染。设置达到max turn后处理逻辑。 * fix: 修复终端异常断开情况,resume续跑;修复用户消息排队信息被goal输出信息覆盖的问题。 * fix: apply biome formatting to pass CI lint check Co-authored-by: Cursor <cursoragent@cursor.com> * fix: skip slash command echo in setUserInputOnProcessing to prevent UI flash Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: moyu <moyu@kingsoft.com> Co-authored-by: Cursor <cursoragent@cursor.com>
405 lines
13 KiB
TypeScript
405 lines
13 KiB
TypeScript
import type { UUID } from 'crypto'
|
|
import type { FileHistorySnapshot } from 'src/utils/fileHistory.js'
|
|
import type { ContentReplacementRecord } from 'src/utils/toolResultStorage.js'
|
|
import type { AgentId } from './ids.js'
|
|
import type { Message } from './message.js'
|
|
import type { QueueOperationMessage } from './messageQueueTypes.js'
|
|
|
|
export type SerializedMessage = Message & {
|
|
cwd: string
|
|
userType: string
|
|
entrypoint?: string // CLAUDE_CODE_ENTRYPOINT — distinguishes cli/sdk-ts/sdk-py/etc.
|
|
sessionId: string
|
|
timestamp: string
|
|
version: string
|
|
gitBranch?: string
|
|
slug?: string // Session slug for files like plans (used for resume)
|
|
}
|
|
|
|
export type LogOption = {
|
|
date: string
|
|
messages: SerializedMessage[]
|
|
fullPath?: string
|
|
value: number
|
|
created: Date
|
|
modified: Date
|
|
firstPrompt: string
|
|
messageCount: number
|
|
fileSize?: number // File size in bytes (for display)
|
|
isSidechain: boolean
|
|
isLite?: boolean // True for lite logs (messages not loaded)
|
|
sessionId?: string // Session ID for lite logs
|
|
teamName?: string // Team name if this is a spawned agent session
|
|
agentName?: string // Agent's custom name (from /rename or swarm)
|
|
agentColor?: string // Agent's color (from /rename or swarm)
|
|
agentSetting?: string // Agent definition used (from --agent flag or settings.agent)
|
|
isTeammate?: boolean // Whether this session was created by a swarm teammate
|
|
leafUuid?: UUID // If given, this uuid must appear in the DB
|
|
summary?: string // Optional conversation summary
|
|
customTitle?: string // Optional user-set custom title
|
|
tag?: string // Optional tag for the session (searchable in /resume)
|
|
fileHistorySnapshots?: FileHistorySnapshot[] // Optional file history snapshots
|
|
attributionSnapshots?: AttributionSnapshotMessage[] // Optional attribution snapshots
|
|
contextCollapseCommits?: ContextCollapseCommitEntry[] // Ordered — commit B may reference commit A's summary
|
|
contextCollapseSnapshot?: ContextCollapseSnapshotEntry // Last-wins — staged queue + spawn state
|
|
gitBranch?: string // Git branch at the end of the session
|
|
projectPath?: string // Original project directory path
|
|
prNumber?: number // GitHub PR number linked to this session
|
|
prUrl?: string // Full URL to the linked PR
|
|
prRepository?: string // Repository in "owner/repo" format
|
|
mode?: 'coordinator' | 'normal' // Session mode for coordinator/normal detection
|
|
worktreeSession?: PersistedWorktreeSession | null // Worktree state at session end (null = exited, undefined = never entered)
|
|
contentReplacements?: ContentReplacementRecord[] // Replacement decisions for resume reconstruction
|
|
goal?: GoalState // Active goal state at session end (for resume)
|
|
}
|
|
|
|
export type SummaryMessage = {
|
|
type: 'summary'
|
|
leafUuid: UUID
|
|
summary: string
|
|
}
|
|
|
|
export type CustomTitleMessage = {
|
|
type: 'custom-title'
|
|
sessionId: UUID
|
|
customTitle: string
|
|
}
|
|
|
|
/**
|
|
* AI-generated session title. Distinct from CustomTitleMessage so that:
|
|
* - User renames (custom-title) always win over AI titles in read preference
|
|
* - reAppendSessionMetadata never re-appends AI titles (they're ephemeral/
|
|
* regeneratable; re-appending would clobber user renames on resume)
|
|
* - VS Code's onlyIfNoCustomTitle CAS check only matches user titles,
|
|
* allowing AI to overwrite its own previous AI title but not user titles
|
|
*/
|
|
export type AiTitleMessage = {
|
|
type: 'ai-title'
|
|
sessionId: UUID
|
|
aiTitle: string
|
|
}
|
|
|
|
export type LastPromptMessage = {
|
|
type: 'last-prompt'
|
|
sessionId: UUID
|
|
lastPrompt: string
|
|
}
|
|
|
|
/**
|
|
* Periodic fork-generated summary of what the agent is currently doing.
|
|
* Written every min(5 steps, 2min) by forking the main thread mid-turn so
|
|
* `claude ps` can show something more useful than the last user prompt
|
|
* (which is often "ok go" or "fix it").
|
|
*/
|
|
export type TaskSummaryMessage = {
|
|
type: 'task-summary'
|
|
sessionId: UUID
|
|
summary: string
|
|
timestamp: string
|
|
}
|
|
|
|
export type TagMessage = {
|
|
type: 'tag'
|
|
sessionId: UUID
|
|
tag: string
|
|
}
|
|
|
|
export type AgentNameMessage = {
|
|
type: 'agent-name'
|
|
sessionId: UUID
|
|
agentName: string
|
|
}
|
|
|
|
export type AgentColorMessage = {
|
|
type: 'agent-color'
|
|
sessionId: UUID
|
|
agentColor: string
|
|
}
|
|
|
|
export type AgentSettingMessage = {
|
|
type: 'agent-setting'
|
|
sessionId: UUID
|
|
agentSetting: string
|
|
}
|
|
|
|
/**
|
|
* PR link message stored in session transcript.
|
|
* Links a session to a GitHub pull request for tracking and navigation.
|
|
*/
|
|
export type PRLinkMessage = {
|
|
type: 'pr-link'
|
|
sessionId: UUID
|
|
prNumber: number
|
|
prUrl: string
|
|
prRepository: string // e.g., "owner/repo"
|
|
timestamp: string // ISO timestamp when linked
|
|
}
|
|
|
|
export type ModeEntry = {
|
|
type: 'mode'
|
|
sessionId: UUID
|
|
mode: 'coordinator' | 'normal'
|
|
}
|
|
|
|
/**
|
|
* Lifecycle states for a persistent thread goal.
|
|
* - active: agent should auto-continue toward the objective
|
|
* - paused: user temporarily halted progress
|
|
* - blocked: model reported the same blocker for >=3 consecutive turns
|
|
* - budget_limited: tokensUsed >= tokenBudget (auto-transition)
|
|
* - usage_limited: provider rate/usage limit triggered (auto-transition)
|
|
* - max_turns: auto-continuation reached MAX_GOAL_TURNS safety cap
|
|
* - complete: model audit confirmed objective achieved
|
|
*/
|
|
export type GoalStatus =
|
|
| 'active'
|
|
| 'paused'
|
|
| 'blocked'
|
|
| 'budget_limited'
|
|
| 'usage_limited'
|
|
| 'max_turns'
|
|
| 'complete'
|
|
|
|
/**
|
|
* Per-session goal state. Persisted to the JSONL transcript as a `goal`
|
|
* entry on every mutation; last-wins on read.
|
|
*
|
|
* Timing fields handle pause correctly: `getActiveElapsedMs(state)`
|
|
* = accumulatedActiveMs + (now - startTime if active, else 0).
|
|
*
|
|
* `turnsExecuted` is a defensive upper bound for the auto-continuation
|
|
* loop so a runaway goal cannot spin indefinitely.
|
|
*
|
|
* `blockedAttempts` + `lastBlockReason` implement CODEX's "blocked
|
|
* only after 3 consecutive same-reason attempts" audit rule.
|
|
*/
|
|
export type GoalState = {
|
|
objective: string
|
|
status: GoalStatus
|
|
tokenBudget: number | null
|
|
tokensUsed: number
|
|
startTime: number
|
|
pausedAt: number | null
|
|
accumulatedActiveMs: number
|
|
blockedAttempts: number
|
|
lastBlockReason: string | null
|
|
createdAt: number
|
|
updatedAt: number
|
|
turnsExecuted: number
|
|
}
|
|
|
|
/**
|
|
* JSONL entry representing a goal-state checkpoint. Written on every
|
|
* mutation (set / pause / resume / complete / token update). Readers
|
|
* use the latest entry by sessionId as the authoritative state.
|
|
*/
|
|
export type GoalMetadataEntry = {
|
|
type: 'goal'
|
|
sessionId: UUID
|
|
state: GoalState
|
|
timestamp: string
|
|
}
|
|
|
|
/**
|
|
* JSONL entry signalling the user explicitly cleared the goal.
|
|
* Distinct from `complete` (which preserves the achievement). Readers
|
|
* encountering this entry after a `goal` entry should treat the goal
|
|
* as absent.
|
|
*/
|
|
export type GoalClearedEntry = {
|
|
type: 'goal-cleared'
|
|
sessionId: UUID
|
|
timestamp: string
|
|
}
|
|
|
|
/**
|
|
* Worktree session state persisted to the transcript for resume.
|
|
* Subset of WorktreeSession from utils/worktree.ts — excludes ephemeral
|
|
* fields (creationDurationMs, usedSparsePaths) that are only used for
|
|
* first-run analytics.
|
|
*/
|
|
export type PersistedWorktreeSession = {
|
|
originalCwd: string
|
|
worktreePath: string
|
|
worktreeName: string
|
|
worktreeBranch?: string
|
|
originalBranch?: string
|
|
originalHeadCommit?: string
|
|
sessionId: string
|
|
tmuxSessionName?: string
|
|
hookBased?: boolean
|
|
}
|
|
|
|
/**
|
|
* Records whether the session is currently inside a worktree created by
|
|
* EnterWorktree or --worktree. Last-wins: an enter writes the session,
|
|
* an exit writes null. On --resume, restored only if the worktreePath
|
|
* still exists on disk (the /exit dialog may have removed it).
|
|
*/
|
|
export type WorktreeStateEntry = {
|
|
type: 'worktree-state'
|
|
sessionId: UUID
|
|
worktreeSession: PersistedWorktreeSession | null
|
|
}
|
|
|
|
/**
|
|
* Records content blocks whose in-context representation was replaced with a
|
|
* smaller stub (the full content was persisted elsewhere). Replayed on resume
|
|
* for prompt cache stability. Written once per enforcement pass that replaces
|
|
* at least one block. When agentId is set, the record belongs to a subagent
|
|
* sidechain (AgentTool resume reads these); when absent, it's main-thread
|
|
* (/resume reads these).
|
|
*/
|
|
export type ContentReplacementEntry = {
|
|
type: 'content-replacement'
|
|
sessionId: UUID
|
|
agentId?: AgentId
|
|
replacements: ContentReplacementRecord[]
|
|
}
|
|
|
|
export type FileHistorySnapshotMessage = {
|
|
type: 'file-history-snapshot'
|
|
messageId: UUID
|
|
snapshot: FileHistorySnapshot
|
|
isSnapshotUpdate: boolean
|
|
}
|
|
|
|
/**
|
|
* Per-file attribution state tracking Claude's character contributions.
|
|
*/
|
|
export type FileAttributionState = {
|
|
contentHash: string // SHA-256 hash of file content
|
|
claudeContribution: number // Characters written by Claude
|
|
mtime: number // File modification time
|
|
}
|
|
|
|
/**
|
|
* Attribution snapshot message stored in session transcript.
|
|
* Tracks character-level contributions by Claude for commit attribution.
|
|
*/
|
|
export type AttributionSnapshotMessage = {
|
|
type: 'attribution-snapshot'
|
|
messageId: UUID
|
|
surface: string // Client surface (cli, ide, web, api)
|
|
fileStates: Record<string, FileAttributionState>
|
|
promptCount?: number // Total prompts in session
|
|
promptCountAtLastCommit?: number // Prompts at last commit
|
|
permissionPromptCount?: number // Total permission prompts shown
|
|
permissionPromptCountAtLastCommit?: number // Permission prompts at last commit
|
|
escapeCount?: number // Total ESC presses (cancelled permission prompts)
|
|
escapeCountAtLastCommit?: number // ESC presses at last commit
|
|
}
|
|
|
|
export type TranscriptMessage = SerializedMessage & {
|
|
parentUuid: UUID | null
|
|
logicalParentUuid?: UUID | null // Preserves logical parent when parentUuid is nullified for session breaks
|
|
isSidechain: boolean
|
|
gitBranch?: string
|
|
agentId?: string // Agent ID for sidechain transcripts to enable resuming agents
|
|
teamName?: string // Team name if this is a spawned agent session
|
|
agentName?: string // Agent's custom name (from /rename or swarm)
|
|
agentColor?: string // Agent's color (from /rename or swarm)
|
|
promptId?: string // Correlates with OTel prompt.id for user prompt messages
|
|
}
|
|
|
|
export type SpeculationAcceptMessage = {
|
|
type: 'speculation-accept'
|
|
timestamp: string
|
|
timeSavedMs: number
|
|
}
|
|
|
|
/**
|
|
* Persisted context-collapse commit. The archived messages themselves are
|
|
* NOT persisted — they're already in the transcript as ordinary user/
|
|
* assistant messages. We only persist enough to reconstruct the splice
|
|
* instruction (boundary uuids) and the summary placeholder (which is NOT
|
|
* in the transcript because it's never yielded to the REPL).
|
|
*
|
|
* On restore, the store reconstructs CommittedCollapse with archived=[];
|
|
* projectView lazily fills the archive the first time it finds the span.
|
|
*
|
|
* Discriminator is obfuscated to match the gate name. sessionStorage.ts
|
|
* isn't feature-gated (it's the generic transcript plumbing used by every
|
|
* entry type), so a descriptive string here would leak into external builds
|
|
* via the appendEntry dispatch / loadTranscriptFile parser even though
|
|
* nothing in an external build ever writes or reads this entry.
|
|
*/
|
|
export type ContextCollapseCommitEntry = {
|
|
type: 'marble-origami-commit'
|
|
sessionId: UUID
|
|
/** 16-digit collapse ID. Max across entries reseeds the ID counter. */
|
|
collapseId: string
|
|
/** The summary placeholder's uuid — registerSummary() needs it. */
|
|
summaryUuid: string
|
|
/** Full <collapsed id="...">text</collapsed> string for the placeholder. */
|
|
summaryContent: string
|
|
/** Plain summary text for ctx_inspect. */
|
|
summary: string
|
|
/** Span boundaries — projectView finds these in the resumed Message[]. */
|
|
firstArchivedUuid: string
|
|
lastArchivedUuid: string
|
|
}
|
|
|
|
/**
|
|
* Snapshot of the staged queue and spawn trigger state. Unlike commits
|
|
* (append-only, replay-all), snapshots are last-wins — only the most
|
|
* recent snapshot entry is applied on restore. Written after every
|
|
* ctx-agent spawn resolves (when staged contents may have changed).
|
|
*
|
|
* Staged boundaries are UUIDs (session-stable), not collapse IDs (which
|
|
* reset with the uuidToId bimap). Restoring a staged span issues fresh
|
|
* collapse IDs for those messages on the next decorate/display, but the
|
|
* span itself resolves correctly.
|
|
*/
|
|
export type ContextCollapseSnapshotEntry = {
|
|
type: 'marble-origami-snapshot'
|
|
sessionId: UUID
|
|
staged: Array<{
|
|
startUuid: string
|
|
endUuid: string
|
|
summary: string
|
|
risk: number
|
|
stagedAt: number
|
|
}>
|
|
/** Spawn trigger state — so the +interval clock picks up where it left off. */
|
|
armed: boolean
|
|
lastSpawnTokens: number
|
|
}
|
|
|
|
export type Entry =
|
|
| TranscriptMessage
|
|
| SummaryMessage
|
|
| CustomTitleMessage
|
|
| AiTitleMessage
|
|
| LastPromptMessage
|
|
| TaskSummaryMessage
|
|
| TagMessage
|
|
| AgentNameMessage
|
|
| AgentColorMessage
|
|
| AgentSettingMessage
|
|
| PRLinkMessage
|
|
| FileHistorySnapshotMessage
|
|
| AttributionSnapshotMessage
|
|
| QueueOperationMessage
|
|
| SpeculationAcceptMessage
|
|
| ModeEntry
|
|
| WorktreeStateEntry
|
|
| ContentReplacementEntry
|
|
| ContextCollapseCommitEntry
|
|
| ContextCollapseSnapshotEntry
|
|
| GoalMetadataEntry
|
|
| GoalClearedEntry
|
|
|
|
export function sortLogs(logs: LogOption[]): LogOption[] {
|
|
return logs.sort((a, b) => {
|
|
// Sort by modified date (newest first)
|
|
const modifiedDiff = b.modified.getTime() - a.modified.getTime()
|
|
if (modifiedDiff !== 0) {
|
|
return modifiedDiff
|
|
}
|
|
|
|
// If modified dates are equal, sort by created date (newest first)
|
|
return b.created.getTime() - a.created.getTime()
|
|
})
|
|
}
|