oh-my-opencode-dashboard 0.0.4 → 0.1.0

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.
@@ -155,4 +155,111 @@ describe('toDashboardPayload', () => {
155
155
  // #then: should handle non-array steps gracefully
156
156
  expect(payload.planProgress.steps).toEqual([])
157
157
  })
158
- })
158
+
159
+ it('should parse mainSession.sessionId from camel or snake keys', () => {
160
+ // #given: server JSON with main session id in camel and snake case
161
+ const camelJson = {
162
+ mainSession: {
163
+ agent: "sisyphus",
164
+ currentTool: "dashboard_start",
165
+ currentModel: "anthropic/claude-opus-4-5",
166
+ lastUpdatedLabel: "just now",
167
+ session: "test-session",
168
+ sessionId: "ses_main",
169
+ statusPill: "busy",
170
+ },
171
+ }
172
+
173
+ const snakeJson = {
174
+ main_session: {
175
+ agent: "sisyphus",
176
+ current_tool: "dashboard_start",
177
+ current_model: "anthropic/claude-opus-4-5",
178
+ last_updated: "just now",
179
+ session: "test-session",
180
+ session_id: "ses_snake",
181
+ status: "busy",
182
+ },
183
+ }
184
+
185
+ // #when: converting to dashboard payload
186
+ const camelPayload = toDashboardPayload(camelJson)
187
+ const snakePayload = toDashboardPayload(snakeJson)
188
+
189
+ // #then: sessionId should be preserved
190
+ expect(camelPayload.mainSession.sessionId).toBe("ses_main")
191
+ expect(snakePayload.mainSession.sessionId).toBe("ses_snake")
192
+ })
193
+
194
+ it('should preserve mainSessionTasks from server JSON', () => {
195
+ // #given: server JSON with mainSessionTasks
196
+ const serverJson = {
197
+ mainSession: {
198
+ agent: "sisyphus",
199
+ currentTool: "dashboard_start",
200
+ currentModel: "anthropic/claude-opus-4-5",
201
+ lastUpdatedLabel: "just now",
202
+ session: "test-session",
203
+ sessionId: "ses_main",
204
+ statusPill: "busy",
205
+ },
206
+ planProgress: {
207
+ name: "test-plan",
208
+ completed: 0,
209
+ total: 0,
210
+ path: "/tmp/test-plan.md",
211
+ statusPill: "not started",
212
+ steps: [],
213
+ },
214
+ mainSessionTasks: [
215
+ {
216
+ id: "main-session",
217
+ description: "Main session",
218
+ subline: "ses_main",
219
+ agent: "sisyphus",
220
+ lastModel: "anthropic/claude-opus-4-5",
221
+ sessionId: "ses_main",
222
+ status: "running",
223
+ toolCalls: 3,
224
+ lastTool: "delegate_task",
225
+ timeline: "2026-01-01T00:00:00Z: 2m",
226
+ },
227
+ ],
228
+ backgroundTasks: [],
229
+ timeSeries: {
230
+ windowMs: 300000,
231
+ buckets: 150,
232
+ bucketMs: 2000,
233
+ anchorMs: 1640995200000,
234
+ serverNowMs: 1640995500000,
235
+ series: [
236
+ {
237
+ id: "overall-main",
238
+ label: "Overall",
239
+ tone: "muted",
240
+ values: new Array(150).fill(0),
241
+ },
242
+ ],
243
+ },
244
+ }
245
+
246
+ // #when
247
+ const payload = toDashboardPayload(serverJson)
248
+
249
+ // #then
250
+ expect(payload.mainSessionTasks).toEqual([
251
+ {
252
+ id: "main-session",
253
+ description: "Main session",
254
+ subline: "ses_main",
255
+ agent: "sisyphus",
256
+ lastModel: "anthropic/claude-opus-4-5",
257
+ sessionId: "ses_main",
258
+ status: "running",
259
+ toolCalls: 3,
260
+ lastTool: "delegate_task",
261
+ timeline: "2026-01-01T00:00:00Z: 2m",
262
+ },
263
+ ])
264
+ })
265
+ })
@@ -0,0 +1,191 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { computeToolCallsFetchPlan, toggleIdInSet } from "./App"
3
+
4
+ describe('computeToolCallsFetchPlan', () => {
5
+ it('should not fetch when sessionId is missing', () => {
6
+ // #given: no sessionId
7
+ const params = {
8
+ sessionId: null,
9
+ status: "running",
10
+ cachedState: "idle" as const,
11
+ cachedDataOk: false,
12
+ isExpanded: true
13
+ }
14
+
15
+ // #when
16
+ const result = computeToolCallsFetchPlan(params)
17
+
18
+ // #then
19
+ expect(result.shouldFetch).toBe(false)
20
+ expect(result.force).toBe(false)
21
+ })
22
+
23
+ it('should not fetch when not expanded', () => {
24
+ // #given: expanded is false
25
+ const params = {
26
+ sessionId: "session-123",
27
+ status: "done",
28
+ cachedState: "idle" as const,
29
+ cachedDataOk: false,
30
+ isExpanded: false
31
+ }
32
+
33
+ // #when
34
+ const result = computeToolCallsFetchPlan(params)
35
+
36
+ // #then
37
+ expect(result.shouldFetch).toBe(false)
38
+ expect(result.force).toBe(false)
39
+ })
40
+
41
+ it('should force fetch when status is "running" and expanded', () => {
42
+ // #given: running status
43
+ const params = {
44
+ sessionId: "session-123",
45
+ status: "running",
46
+ cachedState: "ok" as const,
47
+ cachedDataOk: true,
48
+ isExpanded: true
49
+ }
50
+
51
+ // #when
52
+ const result = computeToolCallsFetchPlan(params)
53
+
54
+ // #then
55
+ expect(result.shouldFetch).toBe(true)
56
+ expect(result.force).toBe(true)
57
+ })
58
+
59
+ it('should not fetch when non-running status and cached data is ok', () => {
60
+ // #given: done status with good cache
61
+ const params = {
62
+ sessionId: "session-123",
63
+ status: "done",
64
+ cachedState: "ok" as const,
65
+ cachedDataOk: true,
66
+ isExpanded: true
67
+ }
68
+
69
+ // #when
70
+ const result = computeToolCallsFetchPlan(params)
71
+
72
+ // #then
73
+ expect(result.shouldFetch).toBe(false)
74
+ expect(result.force).toBe(false)
75
+ })
76
+
77
+ it('should not fetch when already loading', () => {
78
+ // #given: loading state
79
+ const params = {
80
+ sessionId: "session-123",
81
+ status: "done",
82
+ cachedState: "loading" as const,
83
+ cachedDataOk: false,
84
+ isExpanded: true
85
+ }
86
+
87
+ // #when
88
+ const result = computeToolCallsFetchPlan(params)
89
+
90
+ // #then
91
+ expect(result.shouldFetch).toBe(false)
92
+ expect(result.force).toBe(false)
93
+ })
94
+
95
+ it('should fetch when non-running, not cached, not loading, and expanded', () => {
96
+ // #given: done status with no cache
97
+ const params = {
98
+ sessionId: "session-123",
99
+ status: "done",
100
+ cachedState: "idle" as const,
101
+ cachedDataOk: false,
102
+ isExpanded: true
103
+ }
104
+
105
+ // #when
106
+ const result = computeToolCallsFetchPlan(params)
107
+
108
+ // #then
109
+ expect(result.shouldFetch).toBe(true)
110
+ expect(result.force).toBe(false)
111
+ })
112
+
113
+ it('should handle case-insensitive status values', () => {
114
+ // #given: RUNNING in uppercase
115
+ const params = {
116
+ sessionId: "session-123",
117
+ status: "RUNNING",
118
+ cachedState: "idle" as const,
119
+ cachedDataOk: false,
120
+ isExpanded: true
121
+ }
122
+
123
+ // #when
124
+ const result = computeToolCallsFetchPlan(params)
125
+
126
+ // #then
127
+ expect(result.shouldFetch).toBe(true)
128
+ expect(result.force).toBe(true)
129
+ })
130
+
131
+ it('should handle whitespace in status values', () => {
132
+ // #given: running with whitespace
133
+ const params = {
134
+ sessionId: "session-123",
135
+ status: " running ",
136
+ cachedState: "idle" as const,
137
+ cachedDataOk: false,
138
+ isExpanded: true
139
+ }
140
+
141
+ // #when
142
+ const result = computeToolCallsFetchPlan(params)
143
+
144
+ // #then
145
+ expect(result.shouldFetch).toBe(true)
146
+ expect(result.force).toBe(true)
147
+ })
148
+ })
149
+
150
+ describe('toggleIdInSet', () => {
151
+ it('should add id when not present in set', () => {
152
+ // #given: empty set
153
+ const currentSet = new Set<string>()
154
+
155
+ // #when
156
+ const result = toggleIdInSet("task-1", currentSet)
157
+
158
+ // #then
159
+ expect(result.has("task-1")).toBe(true)
160
+ expect(result.size).toBe(1)
161
+ })
162
+
163
+ it('should remove id when already present in set', () => {
164
+ // #given: set with id already present
165
+ const currentSet = new Set(["task-1", "task-2"])
166
+
167
+ // #when
168
+ const result = toggleIdInSet("task-1", currentSet)
169
+
170
+ // #then
171
+ expect(result.has("task-1")).toBe(false)
172
+ expect(result.has("task-2")).toBe(true)
173
+ expect(result.size).toBe(1)
174
+ })
175
+
176
+ it('should not modify original set', () => {
177
+ // #given: original set
178
+ const originalSet = new Set(["task-1"])
179
+
180
+ // #when
181
+ const result = toggleIdInSet("task-2", originalSet)
182
+
183
+ // #then
184
+ expect(originalSet.has("task-1")).toBe(true)
185
+ expect(originalSet.has("task-2")).toBe(false)
186
+ expect(originalSet.size).toBe(1)
187
+ expect(result.has("task-1")).toBe(true)
188
+ expect(result.has("task-2")).toBe(true)
189
+ expect(result.size).toBe(2)
190
+ })
191
+ })
@@ -981,7 +981,16 @@ describe("deriveBackgroundTasks", () => {
981
981
  "utf8"
982
982
  )
983
983
 
984
- const readdirSync = vi.fn(fs.readdirSync)
984
+ // Create typed wrapper that records calls and delegates to fs.readdirSync
985
+ const readdirCalls: Array<[fs.PathLike]> = []
986
+ const readdirSync = ((path: fs.PathLike, options?: any): any => {
987
+ if (typeof options === 'object' && options.withFileTypes) {
988
+ return fs.readdirSync(path, options)
989
+ }
990
+ readdirCalls.push([path])
991
+ return fs.readdirSync(path, 'utf8')
992
+ }) as typeof fs.readdirSync
993
+
985
994
  const fsLike = {
986
995
  readFileSync: fs.readFileSync,
987
996
  readdirSync,
@@ -993,7 +1002,7 @@ describe("deriveBackgroundTasks", () => {
993
1002
  const rows = deriveBackgroundTasks({ storage, mainSessionId, fs: fsLike })
994
1003
 
995
1004
  // #then
996
- const backgroundReads = readdirSync.mock.calls.filter((call) => call[0] === childMsgDir)
1005
+ const backgroundReads = readdirCalls.filter((call) => call[0] === childMsgDir)
997
1006
  expect(rows.length).toBe(2)
998
1007
  expect(backgroundReads.length).toBe(1)
999
1008
  })
@@ -0,0 +1,161 @@
1
+ import * as os from "node:os"
2
+ import * as path from "node:path"
3
+ import * as fs from "node:fs"
4
+ import { describe, expect, it } from "vitest"
5
+ import { deriveToolCalls, MAX_TOOL_CALL_MESSAGES, MAX_TOOL_CALLS } from "./tool-calls"
6
+ import { getStorageRoots } from "./session"
7
+
8
+ function mkStorageRoot(): string {
9
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "omo-storage-"))
10
+ fs.mkdirSync(path.join(root, "session"), { recursive: true })
11
+ fs.mkdirSync(path.join(root, "message"), { recursive: true })
12
+ fs.mkdirSync(path.join(root, "part"), { recursive: true })
13
+ return root
14
+ }
15
+
16
+ function writeMessageMeta(opts: {
17
+ storageRoot: string
18
+ sessionId: string
19
+ messageId: string
20
+ created?: number
21
+ }): void {
22
+ const storage = getStorageRoots(opts.storageRoot)
23
+ const msgDir = path.join(storage.message, opts.sessionId)
24
+ fs.mkdirSync(msgDir, { recursive: true })
25
+ const meta: Record<string, unknown> = {
26
+ id: opts.messageId,
27
+ sessionID: opts.sessionId,
28
+ role: "assistant",
29
+ }
30
+ if (typeof opts.created === "number") {
31
+ meta.time = { created: opts.created }
32
+ }
33
+ fs.writeFileSync(path.join(msgDir, `${opts.messageId}.json`), JSON.stringify(meta), "utf8")
34
+ }
35
+
36
+ function writeToolPart(opts: {
37
+ storageRoot: string
38
+ sessionId: string
39
+ messageId: string
40
+ callId: string
41
+ tool: string
42
+ state?: Record<string, unknown>
43
+ }): void {
44
+ const storage = getStorageRoots(opts.storageRoot)
45
+ const partDir = path.join(storage.part, opts.messageId)
46
+ fs.mkdirSync(partDir, { recursive: true })
47
+ fs.writeFileSync(
48
+ path.join(partDir, `${opts.callId}.json`),
49
+ JSON.stringify({
50
+ id: `part_${opts.callId}`,
51
+ sessionID: opts.sessionId,
52
+ messageID: opts.messageId,
53
+ type: "tool",
54
+ callID: opts.callId,
55
+ tool: opts.tool,
56
+ state: opts.state ?? { status: "completed", input: {} },
57
+ }),
58
+ "utf8"
59
+ )
60
+ }
61
+
62
+ function hasBannedKeys(value: unknown, banned: Set<string>): boolean {
63
+ if (!value || typeof value !== "object") return false
64
+ if (Array.isArray(value)) {
65
+ return value.some((item) => hasBannedKeys(item, banned))
66
+ }
67
+ for (const [key, child] of Object.entries(value)) {
68
+ if (banned.has(key)) return true
69
+ if (hasBannedKeys(child, banned)) return true
70
+ }
71
+ return false
72
+ }
73
+
74
+ describe("deriveToolCalls", () => {
75
+ it("orders tool calls deterministically and sorts null timestamps last", () => {
76
+ const storageRoot = mkStorageRoot()
77
+ const storage = getStorageRoots(storageRoot)
78
+ const sessionId = "ses_main"
79
+
80
+ writeMessageMeta({ storageRoot, sessionId, messageId: "msg_0", created: 500 })
81
+ writeToolPart({ storageRoot, sessionId, messageId: "msg_0", callId: "call_a", tool: "read" })
82
+
83
+ writeMessageMeta({ storageRoot, sessionId, messageId: "msg_1", created: 1000 })
84
+ writeToolPart({ storageRoot, sessionId, messageId: "msg_1", callId: "call_a", tool: "bash" })
85
+
86
+ writeMessageMeta({ storageRoot, sessionId, messageId: "msg_2", created: 1000 })
87
+ writeToolPart({ storageRoot, sessionId, messageId: "msg_2", callId: "call_b", tool: "grep" })
88
+ writeToolPart({ storageRoot, sessionId, messageId: "msg_2", callId: "call_a", tool: "grep" })
89
+
90
+ writeMessageMeta({ storageRoot, sessionId, messageId: "msg_3" })
91
+ writeToolPart({ storageRoot, sessionId, messageId: "msg_3", callId: "call_z", tool: "read" })
92
+
93
+ const result = deriveToolCalls({ storage, sessionId })
94
+ expect(result.toolCalls.map((row) => `${row.messageId}:${row.callId}`)).toEqual([
95
+ "msg_1:call_a",
96
+ "msg_2:call_a",
97
+ "msg_2:call_b",
98
+ "msg_0:call_a",
99
+ "msg_3:call_z",
100
+ ])
101
+ expect(result.toolCalls[0].createdAtMs).toBe(1000)
102
+ expect(result.toolCalls[4].createdAtMs).toBe(null)
103
+ expect(result.truncated).toBe(false)
104
+ })
105
+
106
+ it("caps message scan and tool call output", () => {
107
+ const storageRoot = mkStorageRoot()
108
+ const storage = getStorageRoots(storageRoot)
109
+ const sessionId = "ses_main"
110
+
111
+ const totalMessages = MAX_TOOL_CALL_MESSAGES + 5
112
+ for (let i = 0; i < totalMessages; i += 1) {
113
+ const suffix = String(i).padStart(3, "0")
114
+ const messageId = `msg_${suffix}`
115
+ writeMessageMeta({ storageRoot, sessionId, messageId, created: i })
116
+ writeToolPart({ storageRoot, sessionId, messageId, callId: `call_${suffix}_a`, tool: "bash" })
117
+ writeToolPart({ storageRoot, sessionId, messageId, callId: `call_${suffix}_b`, tool: "read" })
118
+ }
119
+
120
+ const result = deriveToolCalls({ storage, sessionId })
121
+ expect(result.toolCalls.length).toBe(MAX_TOOL_CALLS)
122
+ expect(result.truncated).toBe(true)
123
+
124
+ const messageIds = new Set(result.toolCalls.map((row) => row.messageId))
125
+ for (let i = 0; i < 5; i += 1) {
126
+ const suffix = String(i).padStart(3, "0")
127
+ expect(messageIds.has(`msg_${suffix}`)).toBe(false)
128
+ }
129
+ for (let i = totalMessages - 5; i < totalMessages; i += 1) {
130
+ const suffix = String(i).padStart(3, "0")
131
+ expect(messageIds.has(`msg_${suffix}`)).toBe(true)
132
+ }
133
+ })
134
+
135
+ it("redacts tool call payload fields", () => {
136
+ const storageRoot = mkStorageRoot()
137
+ const storage = getStorageRoots(storageRoot)
138
+ const sessionId = "ses_main"
139
+
140
+ writeMessageMeta({ storageRoot, sessionId, messageId: "msg_1", created: 1000 })
141
+ writeToolPart({
142
+ storageRoot,
143
+ sessionId,
144
+ messageId: "msg_1",
145
+ callId: "call_secret",
146
+ tool: "bash",
147
+ state: {
148
+ status: "completed",
149
+ input: { prompt: "SECRET", nested: { output: "HIDDEN" } },
150
+ output: "NOPE",
151
+ error: "NOPE",
152
+ },
153
+ })
154
+
155
+ const result = deriveToolCalls({ storage, sessionId })
156
+ expect(result.toolCalls.length).toBe(1)
157
+
158
+ const banned = new Set(["prompt", "input", "output", "error", "state"])
159
+ expect(hasBannedKeys(result.toolCalls[0], banned)).toBe(false)
160
+ })
161
+ })
@@ -0,0 +1,157 @@
1
+ import * as fs from "node:fs"
2
+ import * as path from "node:path"
3
+ import type { OpenCodeStorageRoots, StoredMessageMeta } from "./session"
4
+ import { getMessageDir } from "./session"
5
+ import { assertAllowedPath } from "./paths"
6
+
7
+ type FsLike = Pick<typeof fs, "readFileSync" | "readdirSync" | "existsSync" | "statSync">
8
+
9
+ export const MAX_TOOL_CALL_MESSAGES = 200
10
+ export const MAX_TOOL_CALLS = 300
11
+
12
+ export type ToolCallSummary = {
13
+ sessionId: string
14
+ messageId: string
15
+ callId: string
16
+ tool: string
17
+ status: "pending" | "running" | "completed" | "error" | "unknown"
18
+ createdAtMs: number | null
19
+ }
20
+
21
+ export type ToolCallSummaryResult = {
22
+ toolCalls: ToolCallSummary[]
23
+ truncated: boolean
24
+ }
25
+
26
+ type StoredToolPartMeta = {
27
+ type?: string
28
+ callID?: string
29
+ tool?: string
30
+ state?: { status?: string }
31
+ }
32
+
33
+ function readJsonFile<T>(filePath: string, fsLike: FsLike): T | null {
34
+ try {
35
+ const content = fsLike.readFileSync(filePath, "utf8")
36
+ return JSON.parse(content) as T
37
+ } catch {
38
+ return null
39
+ }
40
+ }
41
+
42
+ function listJsonFiles(dir: string, fsLike: FsLike): string[] {
43
+ try {
44
+ return fsLike.readdirSync(dir).filter((f) => f.endsWith(".json"))
45
+ } catch {
46
+ return []
47
+ }
48
+ }
49
+
50
+ function readRecentMessageMetas(
51
+ messageDir: string,
52
+ maxMessages: number,
53
+ fsLike: FsLike
54
+ ): { metas: StoredMessageMeta[]; totalMessages: number } {
55
+ if (!messageDir || !fsLike.existsSync(messageDir)) return { metas: [], totalMessages: 0 }
56
+ const files = listJsonFiles(messageDir, fsLike)
57
+ const ranked = files
58
+ .map((f) => ({
59
+ f,
60
+ mtime: (() => {
61
+ try {
62
+ return fsLike.statSync(path.join(messageDir, f)).mtimeMs
63
+ } catch {
64
+ return 0
65
+ }
66
+ })(),
67
+ }))
68
+ .sort((a, b) => b.mtime - a.mtime)
69
+ .slice(0, maxMessages)
70
+
71
+ const metas: StoredMessageMeta[] = []
72
+ for (const item of ranked) {
73
+ const meta = readJsonFile<StoredMessageMeta>(path.join(messageDir, item.f), fsLike)
74
+ if (meta && typeof meta.id === "string") metas.push(meta)
75
+ }
76
+ return { metas, totalMessages: files.length }
77
+ }
78
+
79
+ function readToolPartsForMessage(
80
+ partStorage: string,
81
+ messageId: string,
82
+ fsLike: FsLike,
83
+ allowedRoots?: string[]
84
+ ): StoredToolPartMeta[] {
85
+ const partDir = path.join(partStorage, messageId)
86
+ if (allowedRoots && allowedRoots.length > 0) {
87
+ assertAllowedPath({ candidatePath: partDir, allowedRoots })
88
+ }
89
+ if (!fsLike.existsSync(partDir)) return []
90
+
91
+ const files = listJsonFiles(partDir, fsLike).sort()
92
+ const parts: StoredToolPartMeta[] = []
93
+ for (const file of files) {
94
+ const part = readJsonFile<StoredToolPartMeta>(path.join(partDir, file), fsLike)
95
+ if (part && part.type === "tool" && typeof part.tool === "string" && typeof part.callID === "string") {
96
+ parts.push(part)
97
+ }
98
+ }
99
+ return parts
100
+ }
101
+
102
+ function readStatus(value: StoredToolPartMeta["state"]): ToolCallSummary["status"] {
103
+ const status = value?.status
104
+ if (status === "pending" || status === "running" || status === "completed" || status === "error") {
105
+ return status
106
+ }
107
+ return "unknown"
108
+ }
109
+
110
+ export function deriveToolCalls(opts: {
111
+ storage: OpenCodeStorageRoots
112
+ sessionId: string
113
+ fs?: FsLike
114
+ allowedRoots?: string[]
115
+ }): ToolCallSummaryResult {
116
+ const fsLike: FsLike = opts.fs ?? fs
117
+ const messageDir = getMessageDir(opts.storage.message, opts.sessionId)
118
+ if (messageDir && opts.allowedRoots && opts.allowedRoots.length > 0) {
119
+ assertAllowedPath({ candidatePath: messageDir, allowedRoots: opts.allowedRoots })
120
+ }
121
+ const { metas, totalMessages } = readRecentMessageMetas(messageDir, MAX_TOOL_CALL_MESSAGES, fsLike)
122
+ const truncatedByMessages = totalMessages > MAX_TOOL_CALL_MESSAGES
123
+
124
+ const calls: Array<ToolCallSummary & { createdSortKey: number }> = []
125
+ for (const meta of metas) {
126
+ const createdAtMs = typeof meta.time?.created === "number" ? meta.time.created : null
127
+ const createdSortKey = createdAtMs ?? -Infinity
128
+ const parts = readToolPartsForMessage(opts.storage.part, meta.id, fsLike, opts.allowedRoots)
129
+ for (const part of parts) {
130
+ calls.push({
131
+ sessionId: opts.sessionId,
132
+ messageId: meta.id,
133
+ callId: part.callID ?? "",
134
+ tool: part.tool ?? "",
135
+ status: readStatus(part.state),
136
+ createdAtMs,
137
+ createdSortKey,
138
+ })
139
+ }
140
+ }
141
+
142
+ const truncatedByCalls = calls.length > MAX_TOOL_CALLS
143
+ const toolCalls = calls
144
+ .sort((a, b) => {
145
+ if (a.createdSortKey !== b.createdSortKey) return b.createdSortKey - a.createdSortKey
146
+ const messageCompare = String(a.messageId).localeCompare(String(b.messageId))
147
+ if (messageCompare !== 0) return messageCompare
148
+ return String(a.callId).localeCompare(String(b.callId))
149
+ })
150
+ .slice(0, MAX_TOOL_CALLS)
151
+ .map(({ createdSortKey, ...row }) => row)
152
+
153
+ return {
154
+ toolCalls,
155
+ truncated: truncatedByMessages || truncatedByCalls,
156
+ }
157
+ }