@tarquinen/opencode-dcp 3.2.1-beta0 → 3.2.2-beta0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analysis/tokens.ts +225 -0
- package/lib/config.ts +1071 -0
- package/lib/logger.ts +235 -0
- package/lib/messages/query.ts +56 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +260 -0
- package/lib/state/state.ts +180 -0
- package/lib/state/tool-cache.ts +98 -0
- package/lib/state/types.ts +108 -0
- package/lib/state/utils.ts +310 -0
- package/lib/token-utils.ts +162 -0
- package/package.json +13 -2
- package/tui/data/context.ts +177 -0
- package/tui/index.tsx +34 -0
- package/tui/routes/summary.tsx +175 -0
- package/tui/shared/names.ts +9 -0
- package/tui/shared/theme.ts +58 -0
- package/tui/shared/types.ts +38 -0
- package/tui/slots/sidebar-content.tsx +502 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { SessionState, ToolParameterEntry, WithParts } from "./types"
|
|
2
|
+
import type { Logger } from "../logger"
|
|
3
|
+
import { loadSessionState, saveSessionState } from "./persistence"
|
|
4
|
+
import {
|
|
5
|
+
isSubAgentSession,
|
|
6
|
+
findLastCompactionTimestamp,
|
|
7
|
+
countTurns,
|
|
8
|
+
resetOnCompaction,
|
|
9
|
+
createPruneMessagesState,
|
|
10
|
+
loadPruneMessagesState,
|
|
11
|
+
loadPruneMap,
|
|
12
|
+
collectTurnNudgeAnchors,
|
|
13
|
+
} from "./utils"
|
|
14
|
+
import { getLastUserMessage } from "../messages/query"
|
|
15
|
+
|
|
16
|
+
export const checkSession = async (
|
|
17
|
+
client: any,
|
|
18
|
+
state: SessionState,
|
|
19
|
+
logger: Logger,
|
|
20
|
+
messages: WithParts[],
|
|
21
|
+
manualModeDefault: boolean,
|
|
22
|
+
): Promise<void> => {
|
|
23
|
+
const lastUserMessage = getLastUserMessage(messages)
|
|
24
|
+
if (!lastUserMessage) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lastSessionId = lastUserMessage.info.sessionID
|
|
29
|
+
|
|
30
|
+
if (state.sessionId === null || state.sessionId !== lastSessionId) {
|
|
31
|
+
logger.info(`Session changed: ${state.sessionId} -> ${lastSessionId}`)
|
|
32
|
+
try {
|
|
33
|
+
await ensureSessionInitialized(
|
|
34
|
+
client,
|
|
35
|
+
state,
|
|
36
|
+
lastSessionId,
|
|
37
|
+
logger,
|
|
38
|
+
messages,
|
|
39
|
+
manualModeDefault,
|
|
40
|
+
)
|
|
41
|
+
} catch (err: any) {
|
|
42
|
+
logger.error("Failed to initialize session state", { error: err.message })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lastCompactionTimestamp = findLastCompactionTimestamp(messages)
|
|
47
|
+
if (lastCompactionTimestamp > state.lastCompaction) {
|
|
48
|
+
state.lastCompaction = lastCompactionTimestamp
|
|
49
|
+
resetOnCompaction(state)
|
|
50
|
+
logger.info("Detected compaction - reset stale state", {
|
|
51
|
+
timestamp: lastCompactionTimestamp,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
saveSessionState(state, logger).catch((error) => {
|
|
55
|
+
logger.warn("Failed to persist state reset after compaction", {
|
|
56
|
+
error: error instanceof Error ? error.message : String(error),
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
state.currentTurn = countTurns(state, messages)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createSessionState(): SessionState {
|
|
65
|
+
return {
|
|
66
|
+
sessionId: null,
|
|
67
|
+
isSubAgent: false,
|
|
68
|
+
manualMode: false,
|
|
69
|
+
compressPermission: undefined,
|
|
70
|
+
pendingManualTrigger: null,
|
|
71
|
+
prune: {
|
|
72
|
+
tools: new Map<string, number>(),
|
|
73
|
+
messages: createPruneMessagesState(),
|
|
74
|
+
},
|
|
75
|
+
nudges: {
|
|
76
|
+
contextLimitAnchors: new Set<string>(),
|
|
77
|
+
turnNudgeAnchors: new Set<string>(),
|
|
78
|
+
iterationNudgeAnchors: new Set<string>(),
|
|
79
|
+
},
|
|
80
|
+
stats: {
|
|
81
|
+
pruneTokenCounter: 0,
|
|
82
|
+
totalPruneTokens: 0,
|
|
83
|
+
},
|
|
84
|
+
toolParameters: new Map<string, ToolParameterEntry>(),
|
|
85
|
+
subAgentResultCache: new Map<string, string>(),
|
|
86
|
+
toolIdList: [],
|
|
87
|
+
messageIds: {
|
|
88
|
+
byRawId: new Map<string, string>(),
|
|
89
|
+
byRef: new Map<string, string>(),
|
|
90
|
+
nextRef: 1,
|
|
91
|
+
},
|
|
92
|
+
lastCompaction: 0,
|
|
93
|
+
currentTurn: 0,
|
|
94
|
+
variant: undefined,
|
|
95
|
+
modelContextLimit: undefined,
|
|
96
|
+
systemPromptTokens: undefined,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resetSessionState(state: SessionState): void {
|
|
101
|
+
state.sessionId = null
|
|
102
|
+
state.isSubAgent = false
|
|
103
|
+
state.manualMode = false
|
|
104
|
+
state.compressPermission = undefined
|
|
105
|
+
state.pendingManualTrigger = null
|
|
106
|
+
state.prune = {
|
|
107
|
+
tools: new Map<string, number>(),
|
|
108
|
+
messages: createPruneMessagesState(),
|
|
109
|
+
}
|
|
110
|
+
state.nudges = {
|
|
111
|
+
contextLimitAnchors: new Set<string>(),
|
|
112
|
+
turnNudgeAnchors: new Set<string>(),
|
|
113
|
+
iterationNudgeAnchors: new Set<string>(),
|
|
114
|
+
}
|
|
115
|
+
state.stats = {
|
|
116
|
+
pruneTokenCounter: 0,
|
|
117
|
+
totalPruneTokens: 0,
|
|
118
|
+
}
|
|
119
|
+
state.toolParameters.clear()
|
|
120
|
+
state.subAgentResultCache.clear()
|
|
121
|
+
state.toolIdList = []
|
|
122
|
+
state.messageIds = {
|
|
123
|
+
byRawId: new Map<string, string>(),
|
|
124
|
+
byRef: new Map<string, string>(),
|
|
125
|
+
nextRef: 1,
|
|
126
|
+
}
|
|
127
|
+
state.lastCompaction = 0
|
|
128
|
+
state.currentTurn = 0
|
|
129
|
+
state.variant = undefined
|
|
130
|
+
state.modelContextLimit = undefined
|
|
131
|
+
state.systemPromptTokens = undefined
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function ensureSessionInitialized(
|
|
135
|
+
client: any,
|
|
136
|
+
state: SessionState,
|
|
137
|
+
sessionId: string,
|
|
138
|
+
logger: Logger,
|
|
139
|
+
messages: WithParts[],
|
|
140
|
+
manualModeEnabled: boolean,
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
if (state.sessionId === sessionId) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// logger.info("session ID = " + sessionId)
|
|
147
|
+
// logger.info("Initializing session state", { sessionId: sessionId })
|
|
148
|
+
|
|
149
|
+
resetSessionState(state)
|
|
150
|
+
state.manualMode = manualModeEnabled ? "active" : false
|
|
151
|
+
state.sessionId = sessionId
|
|
152
|
+
|
|
153
|
+
const isSubAgent = await isSubAgentSession(client, sessionId)
|
|
154
|
+
state.isSubAgent = isSubAgent
|
|
155
|
+
// logger.info("isSubAgent = " + isSubAgent)
|
|
156
|
+
|
|
157
|
+
state.lastCompaction = findLastCompactionTimestamp(messages)
|
|
158
|
+
state.currentTurn = countTurns(state, messages)
|
|
159
|
+
state.nudges.turnNudgeAnchors = collectTurnNudgeAnchors(messages)
|
|
160
|
+
|
|
161
|
+
const persisted = await loadSessionState(sessionId, logger)
|
|
162
|
+
if (persisted === null) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
state.prune.tools = loadPruneMap(persisted.prune.tools)
|
|
167
|
+
state.prune.messages = loadPruneMessagesState(persisted.prune.messages)
|
|
168
|
+
state.nudges.contextLimitAnchors = new Set<string>(persisted.nudges.contextLimitAnchors || [])
|
|
169
|
+
state.nudges.turnNudgeAnchors = new Set<string>([
|
|
170
|
+
...state.nudges.turnNudgeAnchors,
|
|
171
|
+
...(persisted.nudges.turnNudgeAnchors || []),
|
|
172
|
+
])
|
|
173
|
+
state.nudges.iterationNudgeAnchors = new Set<string>(
|
|
174
|
+
persisted.nudges.iterationNudgeAnchors || [],
|
|
175
|
+
)
|
|
176
|
+
state.stats = {
|
|
177
|
+
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
|
|
178
|
+
totalPruneTokens: persisted.stats?.totalPruneTokens || 0,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { SessionState, ToolStatus, WithParts } from "./index"
|
|
2
|
+
import type { Logger } from "../logger"
|
|
3
|
+
import { PluginConfig } from "../config"
|
|
4
|
+
import { isMessageCompacted } from "./utils"
|
|
5
|
+
import { countToolTokens } from "../token-utils"
|
|
6
|
+
|
|
7
|
+
const MAX_TOOL_CACHE_SIZE = 1000
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sync tool parameters from session messages.
|
|
11
|
+
*/
|
|
12
|
+
export function syncToolCache(
|
|
13
|
+
state: SessionState,
|
|
14
|
+
config: PluginConfig,
|
|
15
|
+
logger: Logger,
|
|
16
|
+
messages: WithParts[],
|
|
17
|
+
): void {
|
|
18
|
+
try {
|
|
19
|
+
logger.info("Syncing tool parameters from OpenCode messages")
|
|
20
|
+
|
|
21
|
+
let turnCounter = 0
|
|
22
|
+
|
|
23
|
+
for (const msg of messages) {
|
|
24
|
+
if (isMessageCompacted(state, msg)) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
if (part.type === "step-start") {
|
|
31
|
+
turnCounter++
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (part.type !== "tool" || !part.callID) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const turnProtectionEnabled = config.turnProtection.enabled
|
|
40
|
+
const turnProtectionTurns = config.turnProtection.turns
|
|
41
|
+
const isProtectedByTurn =
|
|
42
|
+
turnProtectionEnabled &&
|
|
43
|
+
turnProtectionTurns > 0 &&
|
|
44
|
+
state.currentTurn - turnCounter < turnProtectionTurns
|
|
45
|
+
|
|
46
|
+
if (state.toolParameters.has(part.callID)) {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (isProtectedByTurn) {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tokenCount = countToolTokens(part)
|
|
55
|
+
|
|
56
|
+
state.toolParameters.set(part.callID, {
|
|
57
|
+
tool: part.tool,
|
|
58
|
+
parameters: part.state?.input ?? {},
|
|
59
|
+
status: part.state.status as ToolStatus | undefined,
|
|
60
|
+
error: part.state.status === "error" ? part.state.error : undefined,
|
|
61
|
+
turn: turnCounter,
|
|
62
|
+
tokenCount,
|
|
63
|
+
})
|
|
64
|
+
logger.info(
|
|
65
|
+
`Cached tool id: ${part.callID} (turn ${turnCounter}${tokenCount !== undefined ? `, ${tokenCount} tokens` : ""})`,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logger.info(
|
|
71
|
+
`Synced cache - size: ${state.toolParameters.size}, currentTurn: ${state.currentTurn}`,
|
|
72
|
+
)
|
|
73
|
+
trimToolParametersCache(state)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
logger.warn("Failed to sync tool parameters from OpenCode", {
|
|
76
|
+
error: error instanceof Error ? error.message : String(error),
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Trim the tool parameters cache to prevent unbounded memory growth.
|
|
83
|
+
* Uses FIFO eviction - removes oldest entries first.
|
|
84
|
+
*/
|
|
85
|
+
export function trimToolParametersCache(state: SessionState): void {
|
|
86
|
+
if (state.toolParameters.size <= MAX_TOOL_CACHE_SIZE) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const keysToRemove = Array.from(state.toolParameters.keys()).slice(
|
|
91
|
+
0,
|
|
92
|
+
state.toolParameters.size - MAX_TOOL_CACHE_SIZE,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
for (const key of keysToRemove) {
|
|
96
|
+
state.toolParameters.delete(key)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Message, Part } from "@opencode-ai/sdk/v2"
|
|
2
|
+
|
|
3
|
+
export interface WithParts {
|
|
4
|
+
info: Message
|
|
5
|
+
parts: Part[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ToolStatus = "pending" | "running" | "completed" | "error"
|
|
9
|
+
|
|
10
|
+
export interface ToolParameterEntry {
|
|
11
|
+
tool: string
|
|
12
|
+
parameters: any
|
|
13
|
+
status?: ToolStatus
|
|
14
|
+
error?: string
|
|
15
|
+
turn: number
|
|
16
|
+
tokenCount?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SessionStats {
|
|
20
|
+
pruneTokenCounter: number
|
|
21
|
+
totalPruneTokens: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PrunedMessageEntry {
|
|
25
|
+
tokenCount: number
|
|
26
|
+
allBlockIds: number[]
|
|
27
|
+
activeBlockIds: number[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CompressionMode = "range" | "message"
|
|
31
|
+
|
|
32
|
+
export interface CompressionBlock {
|
|
33
|
+
blockId: number
|
|
34
|
+
runId: number
|
|
35
|
+
active: boolean
|
|
36
|
+
deactivatedByUser: boolean
|
|
37
|
+
compressedTokens: number
|
|
38
|
+
summaryTokens: number
|
|
39
|
+
mode?: CompressionMode
|
|
40
|
+
topic: string
|
|
41
|
+
batchTopic?: string
|
|
42
|
+
startId: string
|
|
43
|
+
endId: string
|
|
44
|
+
anchorMessageId: string
|
|
45
|
+
compressMessageId: string
|
|
46
|
+
includedBlockIds: number[]
|
|
47
|
+
consumedBlockIds: number[]
|
|
48
|
+
parentBlockIds: number[]
|
|
49
|
+
directMessageIds: string[]
|
|
50
|
+
directToolIds: string[]
|
|
51
|
+
effectiveMessageIds: string[]
|
|
52
|
+
effectiveToolIds: string[]
|
|
53
|
+
createdAt: number
|
|
54
|
+
deactivatedAt?: number
|
|
55
|
+
deactivatedByBlockId?: number
|
|
56
|
+
summary: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PruneMessagesState {
|
|
60
|
+
byMessageId: Map<string, PrunedMessageEntry>
|
|
61
|
+
blocksById: Map<number, CompressionBlock>
|
|
62
|
+
activeBlockIds: Set<number>
|
|
63
|
+
activeByAnchorMessageId: Map<string, number>
|
|
64
|
+
nextBlockId: number
|
|
65
|
+
nextRunId: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Prune {
|
|
69
|
+
tools: Map<string, number>
|
|
70
|
+
messages: PruneMessagesState
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface PendingManualTrigger {
|
|
74
|
+
sessionId: string
|
|
75
|
+
prompt: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface MessageIdState {
|
|
79
|
+
byRawId: Map<string, string>
|
|
80
|
+
byRef: Map<string, string>
|
|
81
|
+
nextRef: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface Nudges {
|
|
85
|
+
contextLimitAnchors: Set<string>
|
|
86
|
+
turnNudgeAnchors: Set<string>
|
|
87
|
+
iterationNudgeAnchors: Set<string>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SessionState {
|
|
91
|
+
sessionId: string | null
|
|
92
|
+
isSubAgent: boolean
|
|
93
|
+
manualMode: false | "active" | "compress-pending"
|
|
94
|
+
compressPermission: "ask" | "allow" | "deny" | undefined
|
|
95
|
+
pendingManualTrigger: PendingManualTrigger | null
|
|
96
|
+
prune: Prune
|
|
97
|
+
nudges: Nudges
|
|
98
|
+
stats: SessionStats
|
|
99
|
+
toolParameters: Map<string, ToolParameterEntry>
|
|
100
|
+
subAgentResultCache: Map<string, string>
|
|
101
|
+
toolIdList: string[]
|
|
102
|
+
messageIds: MessageIdState
|
|
103
|
+
lastCompaction: number
|
|
104
|
+
currentTurn: number
|
|
105
|
+
variant: string | undefined
|
|
106
|
+
modelContextLimit: number | undefined
|
|
107
|
+
systemPromptTokens: number | undefined
|
|
108
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompressionBlock,
|
|
3
|
+
PruneMessagesState,
|
|
4
|
+
PrunedMessageEntry,
|
|
5
|
+
SessionState,
|
|
6
|
+
WithParts,
|
|
7
|
+
} from "./types"
|
|
8
|
+
import { isIgnoredUserMessage, messageHasCompress } from "../messages/query"
|
|
9
|
+
import { countTokens } from "../token-utils"
|
|
10
|
+
|
|
11
|
+
export const isMessageCompacted = (state: SessionState, msg: WithParts): boolean => {
|
|
12
|
+
if (msg.info.time.created < state.lastCompaction) {
|
|
13
|
+
return true
|
|
14
|
+
}
|
|
15
|
+
const pruneEntry = state.prune.messages.byMessageId.get(msg.info.id)
|
|
16
|
+
if (pruneEntry && pruneEntry.activeBlockIds.length > 0) {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PersistedPruneMessagesState {
|
|
23
|
+
byMessageId?: Record<string, PrunedMessageEntry>
|
|
24
|
+
blocksById?: Record<string, CompressionBlock>
|
|
25
|
+
activeBlockIds?: number[]
|
|
26
|
+
activeByAnchorMessageId?: Record<string, number>
|
|
27
|
+
nextBlockId?: number
|
|
28
|
+
nextRunId?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function isSubAgentSession(client: any, sessionID: string): Promise<boolean> {
|
|
32
|
+
try {
|
|
33
|
+
const result = await client.session.get({ path: { id: sessionID } })
|
|
34
|
+
return !!result.data?.parentID
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function findLastCompactionTimestamp(messages: WithParts[]): number {
|
|
41
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
42
|
+
const msg = messages[i]
|
|
43
|
+
if (msg.info.role === "assistant" && msg.info.summary === true) {
|
|
44
|
+
return msg.info.time.created
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function countTurns(state: SessionState, messages: WithParts[]): number {
|
|
51
|
+
let turnCount = 0
|
|
52
|
+
for (const msg of messages) {
|
|
53
|
+
if (isMessageCompacted(state, msg)) {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
if (part.type === "step-start") {
|
|
59
|
+
turnCount++
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return turnCount
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function loadPruneMap(obj?: Record<string, number>): Map<string, number> {
|
|
67
|
+
if (!obj || typeof obj !== "object") {
|
|
68
|
+
return new Map()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const entries = Object.entries(obj).filter(
|
|
72
|
+
(entry): entry is [string, number] =>
|
|
73
|
+
typeof entry[0] === "string" && typeof entry[1] === "number",
|
|
74
|
+
)
|
|
75
|
+
return new Map(entries)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createPruneMessagesState(): PruneMessagesState {
|
|
79
|
+
return {
|
|
80
|
+
byMessageId: new Map<string, PrunedMessageEntry>(),
|
|
81
|
+
blocksById: new Map<number, CompressionBlock>(),
|
|
82
|
+
activeBlockIds: new Set<number>(),
|
|
83
|
+
activeByAnchorMessageId: new Map<string, number>(),
|
|
84
|
+
nextBlockId: 1,
|
|
85
|
+
nextRunId: 1,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function loadPruneMessagesState(
|
|
90
|
+
persisted?: PersistedPruneMessagesState,
|
|
91
|
+
): PruneMessagesState {
|
|
92
|
+
const state = createPruneMessagesState()
|
|
93
|
+
if (!persisted || typeof persisted !== "object") {
|
|
94
|
+
return state
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof persisted.nextBlockId === "number" && Number.isInteger(persisted.nextBlockId)) {
|
|
98
|
+
state.nextBlockId = Math.max(1, persisted.nextBlockId)
|
|
99
|
+
}
|
|
100
|
+
if (typeof persisted.nextRunId === "number" && Number.isInteger(persisted.nextRunId)) {
|
|
101
|
+
state.nextRunId = Math.max(1, persisted.nextRunId)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (persisted.byMessageId && typeof persisted.byMessageId === "object") {
|
|
105
|
+
for (const [messageId, entry] of Object.entries(persisted.byMessageId)) {
|
|
106
|
+
if (!entry || typeof entry !== "object") {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const tokenCount = typeof entry.tokenCount === "number" ? entry.tokenCount : 0
|
|
111
|
+
const allBlockIds = Array.isArray(entry.allBlockIds)
|
|
112
|
+
? [
|
|
113
|
+
...new Set(
|
|
114
|
+
entry.allBlockIds.filter(
|
|
115
|
+
(id): id is number => Number.isInteger(id) && id > 0,
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
]
|
|
119
|
+
: []
|
|
120
|
+
const activeBlockIds = Array.isArray(entry.activeBlockIds)
|
|
121
|
+
? [
|
|
122
|
+
...new Set(
|
|
123
|
+
entry.activeBlockIds.filter(
|
|
124
|
+
(id): id is number => Number.isInteger(id) && id > 0,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
: []
|
|
129
|
+
|
|
130
|
+
state.byMessageId.set(messageId, {
|
|
131
|
+
tokenCount,
|
|
132
|
+
allBlockIds,
|
|
133
|
+
activeBlockIds,
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (persisted.blocksById && typeof persisted.blocksById === "object") {
|
|
139
|
+
for (const [blockIdStr, block] of Object.entries(persisted.blocksById)) {
|
|
140
|
+
const blockId = Number.parseInt(blockIdStr, 10)
|
|
141
|
+
if (!Number.isInteger(blockId) || blockId < 1 || !block || typeof block !== "object") {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const toNumberArray = (value: unknown): number[] =>
|
|
146
|
+
Array.isArray(value)
|
|
147
|
+
? [
|
|
148
|
+
...new Set(
|
|
149
|
+
value.filter(
|
|
150
|
+
(item): item is number => Number.isInteger(item) && item > 0,
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
]
|
|
154
|
+
: []
|
|
155
|
+
const toStringArray = (value: unknown): string[] =>
|
|
156
|
+
Array.isArray(value)
|
|
157
|
+
? [...new Set(value.filter((item): item is string => typeof item === "string"))]
|
|
158
|
+
: []
|
|
159
|
+
|
|
160
|
+
state.blocksById.set(blockId, {
|
|
161
|
+
blockId,
|
|
162
|
+
runId:
|
|
163
|
+
typeof block.runId === "number" &&
|
|
164
|
+
Number.isInteger(block.runId) &&
|
|
165
|
+
block.runId > 0
|
|
166
|
+
? block.runId
|
|
167
|
+
: blockId,
|
|
168
|
+
active: block.active === true,
|
|
169
|
+
deactivatedByUser: block.deactivatedByUser === true,
|
|
170
|
+
compressedTokens:
|
|
171
|
+
typeof block.compressedTokens === "number" &&
|
|
172
|
+
Number.isFinite(block.compressedTokens)
|
|
173
|
+
? Math.max(0, block.compressedTokens)
|
|
174
|
+
: 0,
|
|
175
|
+
summaryTokens:
|
|
176
|
+
typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens)
|
|
177
|
+
? Math.max(0, block.summaryTokens)
|
|
178
|
+
: typeof block.summary === "string"
|
|
179
|
+
? countTokens(block.summary)
|
|
180
|
+
: 0,
|
|
181
|
+
mode: block.mode === "range" || block.mode === "message" ? block.mode : undefined,
|
|
182
|
+
topic: typeof block.topic === "string" ? block.topic : "",
|
|
183
|
+
batchTopic:
|
|
184
|
+
typeof block.batchTopic === "string"
|
|
185
|
+
? block.batchTopic
|
|
186
|
+
: typeof block.topic === "string"
|
|
187
|
+
? block.topic
|
|
188
|
+
: "",
|
|
189
|
+
startId: typeof block.startId === "string" ? block.startId : "",
|
|
190
|
+
endId: typeof block.endId === "string" ? block.endId : "",
|
|
191
|
+
anchorMessageId:
|
|
192
|
+
typeof block.anchorMessageId === "string" ? block.anchorMessageId : "",
|
|
193
|
+
compressMessageId:
|
|
194
|
+
typeof block.compressMessageId === "string" ? block.compressMessageId : "",
|
|
195
|
+
includedBlockIds: toNumberArray(block.includedBlockIds),
|
|
196
|
+
consumedBlockIds: toNumberArray(block.consumedBlockIds),
|
|
197
|
+
parentBlockIds: toNumberArray(block.parentBlockIds),
|
|
198
|
+
directMessageIds: toStringArray(block.directMessageIds),
|
|
199
|
+
directToolIds: toStringArray(block.directToolIds),
|
|
200
|
+
effectiveMessageIds: toStringArray(block.effectiveMessageIds),
|
|
201
|
+
effectiveToolIds: toStringArray(block.effectiveToolIds),
|
|
202
|
+
createdAt: typeof block.createdAt === "number" ? block.createdAt : 0,
|
|
203
|
+
deactivatedAt:
|
|
204
|
+
typeof block.deactivatedAt === "number" ? block.deactivatedAt : undefined,
|
|
205
|
+
deactivatedByBlockId:
|
|
206
|
+
typeof block.deactivatedByBlockId === "number" &&
|
|
207
|
+
Number.isInteger(block.deactivatedByBlockId)
|
|
208
|
+
? block.deactivatedByBlockId
|
|
209
|
+
: undefined,
|
|
210
|
+
summary: typeof block.summary === "string" ? block.summary : "",
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (Array.isArray(persisted.activeBlockIds)) {
|
|
216
|
+
for (const blockId of persisted.activeBlockIds) {
|
|
217
|
+
if (!Number.isInteger(blockId) || blockId < 1) {
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
state.activeBlockIds.add(blockId)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (
|
|
225
|
+
persisted.activeByAnchorMessageId &&
|
|
226
|
+
typeof persisted.activeByAnchorMessageId === "object"
|
|
227
|
+
) {
|
|
228
|
+
for (const [anchorMessageId, blockId] of Object.entries(
|
|
229
|
+
persisted.activeByAnchorMessageId,
|
|
230
|
+
)) {
|
|
231
|
+
if (typeof blockId !== "number" || !Number.isInteger(blockId) || blockId < 1) {
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
state.activeByAnchorMessageId.set(anchorMessageId, blockId)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const [blockId, block] of state.blocksById) {
|
|
239
|
+
if (block.active) {
|
|
240
|
+
state.activeBlockIds.add(blockId)
|
|
241
|
+
if (block.anchorMessageId) {
|
|
242
|
+
state.activeByAnchorMessageId.set(block.anchorMessageId, blockId)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (blockId >= state.nextBlockId) {
|
|
246
|
+
state.nextBlockId = blockId + 1
|
|
247
|
+
}
|
|
248
|
+
if (block.runId >= state.nextRunId) {
|
|
249
|
+
state.nextRunId = block.runId + 1
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return state
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function collectTurnNudgeAnchors(messages: WithParts[]): Set<string> {
|
|
257
|
+
const anchors = new Set<string>()
|
|
258
|
+
let pendingUserMessageId: string | null = null
|
|
259
|
+
|
|
260
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
261
|
+
const message = messages[i]
|
|
262
|
+
|
|
263
|
+
if (messageHasCompress(message)) {
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (message.info.role === "user") {
|
|
268
|
+
if (!isIgnoredUserMessage(message)) {
|
|
269
|
+
pendingUserMessageId = message.info.id
|
|
270
|
+
}
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (message.info.role === "assistant" && pendingUserMessageId) {
|
|
275
|
+
anchors.add(message.info.id)
|
|
276
|
+
anchors.add(pendingUserMessageId)
|
|
277
|
+
pendingUserMessageId = null
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return anchors
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function getActiveSummaryTokenUsage(state: SessionState): number {
|
|
285
|
+
let total = 0
|
|
286
|
+
for (const blockId of state.prune.messages.activeBlockIds) {
|
|
287
|
+
const block = state.prune.messages.blocksById.get(blockId)
|
|
288
|
+
if (!block || !block.active) {
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
total += block.summaryTokens
|
|
292
|
+
}
|
|
293
|
+
return total
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function resetOnCompaction(state: SessionState): void {
|
|
297
|
+
state.toolParameters.clear()
|
|
298
|
+
state.prune.tools = new Map<string, number>()
|
|
299
|
+
state.prune.messages = createPruneMessagesState()
|
|
300
|
+
state.messageIds = {
|
|
301
|
+
byRawId: new Map<string, string>(),
|
|
302
|
+
byRef: new Map<string, string>(),
|
|
303
|
+
nextRef: 1,
|
|
304
|
+
}
|
|
305
|
+
state.nudges = {
|
|
306
|
+
contextLimitAnchors: new Set<string>(),
|
|
307
|
+
turnNudgeAnchors: new Set<string>(),
|
|
308
|
+
iterationNudgeAnchors: new Set<string>(),
|
|
309
|
+
}
|
|
310
|
+
}
|