Skip to content

Commit 9c019bc

Browse files
authored
Merge pull request #34 from AltimateAI/worktree-observability
feat: two-tier telemetry and slash command tracking
2 parents 4f4844f + 3a5787a commit 9c019bc

File tree

4 files changed

+365
-4
lines changed

4 files changed

+365
-4
lines changed

packages/altimate-code/src/bridge/client.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { existsSync } from "fs"
1111
import path from "path"
1212
import { ensureEngine, enginePythonPath } from "./engine"
1313
import type { BridgeMethod, BridgeMethods } from "./protocol"
14+
import { Telemetry } from "@/telemetry"
1415

1516
/** Resolve the Python interpreter to use for the engine sidecar.
1617
* Exported for testing — not part of the public API. */
@@ -48,20 +49,55 @@ export namespace Bridge {
4849
method: M,
4950
params: (typeof BridgeMethods)[M] extends { params: infer P } ? P : never,
5051
): Promise<(typeof BridgeMethods)[M] extends { result: infer R } ? R : never> {
52+
const startTime = Date.now()
5153
if (!child || child.exitCode !== null) {
5254
if (restartCount >= MAX_RESTARTS) throw new Error("Python bridge failed after max restarts")
5355
await start()
5456
}
5557
const id = ++requestId
5658
const request = JSON.stringify({ jsonrpc: "2.0", method, params, id })
5759
return new Promise((resolve, reject) => {
58-
pending.set(id, { resolve, reject })
60+
pending.set(id, {
61+
resolve: (value: any) => {
62+
Telemetry.track({
63+
type: "bridge_call",
64+
timestamp: Date.now(),
65+
session_id: Telemetry.getContext().sessionId,
66+
method,
67+
status: "success",
68+
duration_ms: Date.now() - startTime,
69+
})
70+
resolve(value)
71+
},
72+
reject: (reason: any) => {
73+
Telemetry.track({
74+
type: "bridge_call",
75+
timestamp: Date.now(),
76+
session_id: Telemetry.getContext().sessionId,
77+
method,
78+
status: "error",
79+
duration_ms: Date.now() - startTime,
80+
error: String(reason).slice(0, 500),
81+
})
82+
reject(reason)
83+
},
84+
})
5985
child!.stdin!.write(request + "\n")
6086

6187
setTimeout(() => {
6288
if (pending.has(id)) {
6389
pending.delete(id)
64-
reject(new Error(`Bridge timeout: ${method} (${CALL_TIMEOUT_MS}ms)`))
90+
const error = new Error(`Bridge timeout: ${method} (${CALL_TIMEOUT_MS}ms)`)
91+
Telemetry.track({
92+
type: "bridge_call",
93+
timestamp: Date.now(),
94+
session_id: Telemetry.getContext().sessionId,
95+
method,
96+
status: "error",
97+
duration_ms: Date.now() - startTime,
98+
error: error.message,
99+
})
100+
reject(error)
65101
}
66102
}, CALL_TIMEOUT_MS)
67103
})

packages/altimate-code/src/session/processor.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Config } from "@/config/config"
1515
import { SessionCompaction } from "./compaction"
1616
import { PermissionNext } from "@/permission/next"
1717
import { Question } from "@/question"
18+
import { Telemetry } from "@/telemetry"
1819

1920
export namespace SessionProcessor {
2021
const DOOM_LOOP_THRESHOLD = 3
@@ -34,11 +35,16 @@ export namespace SessionProcessor {
3435
let blocked = false
3536
let attempt = 0
3637
let needsCompaction = false
38+
let stepStartTime = Date.now()
39+
let toolCallCounter = 0
3740

3841
const result = {
3942
get message() {
4043
return input.assistantMessage
4144
},
45+
get toolCallCount() {
46+
return toolCallCounter
47+
},
4248
partFromToolCall(toolCallID: string) {
4349
return toolcalls[toolCallID]
4450
},
@@ -195,7 +201,17 @@ export namespace SessionProcessor {
195201
attachments: value.output.attachments,
196202
},
197203
})
198-
204+
toolCallCounter++
205+
Telemetry.track({
206+
type: "tool_call",
207+
timestamp: Date.now(),
208+
session_id: input.sessionID,
209+
message_id: input.assistantMessage.id,
210+
tool_name: match.tool,
211+
tool_type: match.tool.startsWith("mcp__") ? "mcp" : "standard",
212+
status: "success",
213+
duration_ms: Date.now() - match.state.time.start,
214+
})
199215
delete toolcalls[value.toolCallId]
200216
}
201217
break
@@ -216,7 +232,18 @@ export namespace SessionProcessor {
216232
},
217233
},
218234
})
219-
235+
toolCallCounter++
236+
Telemetry.track({
237+
type: "tool_call",
238+
timestamp: Date.now(),
239+
session_id: input.sessionID,
240+
message_id: input.assistantMessage.id,
241+
tool_name: match.tool,
242+
tool_type: match.tool.startsWith("mcp__") ? "mcp" : "standard",
243+
status: "error",
244+
duration_ms: Date.now() - match.state.time.start,
245+
error: (value.error as any).toString().slice(0, 500),
246+
})
220247
if (
221248
value.error instanceof PermissionNext.RejectedError ||
222249
value.error instanceof Question.RejectedError
@@ -231,6 +258,7 @@ export namespace SessionProcessor {
231258
throw value.error
232259

233260
case "start-step":
261+
stepStartTime = Date.now()
234262
snapshot = await Snapshot.track()
235263
await Session.updatePart({
236264
id: Identifier.ascending("part"),
@@ -261,6 +289,25 @@ export namespace SessionProcessor {
261289
cost: usage.cost,
262290
})
263291
await Session.updateMessage(input.assistantMessage)
292+
Telemetry.track({
293+
type: "generation",
294+
timestamp: Date.now(),
295+
session_id: input.sessionID,
296+
message_id: input.assistantMessage.id,
297+
model_id: input.model.id,
298+
provider_id: input.model.providerID,
299+
agent: input.assistantMessage.agent ?? "",
300+
finish_reason: value.finishReason,
301+
tokens: {
302+
input: usage.tokens.input,
303+
output: usage.tokens.output,
304+
reasoning: usage.tokens.reasoning,
305+
cache_read: usage.tokens.cache.read,
306+
cache_write: usage.tokens.cache.write,
307+
},
308+
cost: usage.cost,
309+
duration_ms: Date.now() - stepStartTime,
310+
})
264311
if (snapshot) {
265312
const patch = await Snapshot.patch(snapshot)
266313
if (patch.files.length) {
@@ -352,6 +399,14 @@ export namespace SessionProcessor {
352399
error: e,
353400
stack: JSON.stringify(e.stack),
354401
})
402+
Telemetry.track({
403+
type: "error",
404+
timestamp: Date.now(),
405+
session_id: input.sessionID,
406+
error_name: e?.name ?? "UnknownError",
407+
error_message: (e?.message ?? String(e)).slice(0, 1000),
408+
context: "processor",
409+
})
355410
const error = MessageV2.fromError(e, { providerID: input.model.providerID })
356411
if (MessageV2.ContextOverflowError.isInstance(error)) {
357412
// TODO: Handle context overflow error

packages/altimate-code/src/session/prompt.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { LLM } from "./llm"
4545
import { iife } from "@/util/iife"
4646
import { Shell } from "@/shell/shell"
4747
import { Truncate } from "@/tool/truncation"
48+
import { Telemetry } from "@/telemetry"
4849

4950
// @ts-ignore
5051
globalThis.AI_SDK_LOG_WARNINGS = false
@@ -290,7 +291,14 @@ export namespace SessionPrompt {
290291
let structuredOutput: unknown | undefined
291292

292293
let step = 0
294+
const sessionStartTime = Date.now()
295+
let sessionTotalCost = 0
296+
let sessionTotalTokens = 0
297+
let toolCallCount = 0
293298
const session = await Session.get(sessionID)
299+
await Telemetry.init()
300+
Telemetry.setContext({ sessionId: sessionID, projectId: Instance.project?.id ?? "" })
301+
try {
294302
while (true) {
295303
SessionStatus.set(sessionID, { type: "busy" })
296304
log.info("loop", { step, sessionID })
@@ -624,6 +632,15 @@ export namespace SessionPrompt {
624632
sessionID: sessionID,
625633
messageID: lastUser.id,
626634
})
635+
Telemetry.track({
636+
type: "session_start",
637+
timestamp: Date.now(),
638+
session_id: sessionID,
639+
model_id: model.id,
640+
provider_id: model.providerID,
641+
agent: lastUser.agent,
642+
project_id: Instance.project?.id ?? "",
643+
})
627644
}
628645

629646
// Ephemerally wrap queued user messages with a reminder to stay on track
@@ -680,6 +697,13 @@ export namespace SessionPrompt {
680697
toolChoice: format.type === "json_schema" ? "required" : undefined,
681698
})
682699

700+
sessionTotalCost += processor.message.cost
701+
sessionTotalTokens +=
702+
(processor.message.tokens?.input ?? 0) +
703+
(processor.message.tokens?.output ?? 0) +
704+
(processor.message.tokens?.reasoning ?? 0)
705+
toolCallCount += processor.toolCallCount
706+
683707
// If structured output was captured, save it and exit immediately
684708
// This takes priority because the StructuredOutput tool was called successfully
685709
if (structuredOutput !== undefined) {
@@ -716,6 +740,18 @@ export namespace SessionPrompt {
716740
continue
717741
}
718742
SessionCompaction.prune({ sessionID })
743+
} finally {
744+
Telemetry.track({
745+
type: "session_end",
746+
timestamp: Date.now(),
747+
session_id: sessionID,
748+
total_cost: sessionTotalCost,
749+
total_tokens: sessionTotalTokens,
750+
tool_call_count: toolCallCount,
751+
duration_ms: Date.now() - sessionStartTime,
752+
})
753+
await Telemetry.shutdown()
754+
}
719755
for await (const item of MessageV2.stream(sessionID)) {
720756
if (item.info.role === "user") continue
721757
const queued = state()[sessionID]?.callbacks ?? []
@@ -1886,6 +1922,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
18861922
messageID: result.info.id,
18871923
})
18881924

1925+
Telemetry.track({
1926+
type: "command",
1927+
timestamp: Date.now(),
1928+
session_id: input.sessionID,
1929+
command_name: input.command,
1930+
command_source: command.source ?? "unknown",
1931+
message_id: result.info.id,
1932+
})
1933+
18891934
return result
18901935
}
18911936

0 commit comments

Comments
 (0)