Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,66 @@ export namespace Telemetry {
kind: "skills" | "memory"
reason: "auth" | "not_found" | "network" | "other"
}
// Workspace engine (see workspace/engine-telemetry.ts)
| {
/** A session's engine attach settled at a new outcome. */
type: "workspace_engine_attach"
timestamp: number
session_id: string
outcome: "attached" | "engine_missing" | "engine_too_old" | "connect_failed" | "disabled" | "unbound"
/** The workspace the boundary settled for; absent for unbound and disabled. */
workspace_id?: number
available?: number
declared?: number
missing?: number
extensions?: number
engine_version?: string
found_version?: string
error_class?: "link_unreadable" | "probe_failed" | "not_connected" | "other"
}
| {
type: "workspace_engine_install_prompt"
timestamp: number
session_id: string
action: "shown" | "accepted" | "installed" | "install_failed" | "copied" | "declined"
reason: "engine-missing" | "engine-too-old"
workspace_id?: number
declared?: number
found_version?: string
surface?: "headless" | "dialog" | "bus" | "toast"
node_detected?: boolean
node_major?: number
}
| {
/** A session's precedence snapshot settled at a new state. */
type: "workspace_engine_precedence"
timestamp: number
session_id: string
enabled: boolean
reason?: string
shadowed_types: number
shadowed_connections: number
extensions: number
escape_hatch: boolean
workspace_id?: number
}
| {
/** A datamate tool call refused because the engine is off for that workspace. */
type: "workspace_engine_redirect"
timestamp: number
session_id: string
tool: string
operation: string
outcome: "refused"
workspace_id?: number
}
| {
type: "workspace_engine_error"
timestamp: number
session_id: string
stage: "attach" | "install" | "precedence"
message: string
}
// altimate_change end
| {
type: "mcp_server_census"
Expand Down
42 changes: 24 additions & 18 deletions packages/opencode/src/altimate/tools/datamate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ export const DatamateManagerTool = Tool.define("datamate_manager", {
.optional()
.describe("Server name to remove (for 'remove'). Use 'list-config' or 'status' to find names."),
}),
async execute(args): Promise<{ title: string; metadata: Record<string, unknown>; output: string }> {
async execute(args, ctx): Promise<{ title: string; metadata: Record<string, unknown>; output: string }> {
// altimate_change start — a linked project gets its integrations from the
// workspace's own engine only. The tool is kept out of the model's catalog
// there (datamate-manager-gate.ts); this covers callers that run it
// directly. Every operation is refused before anything is looked up or
// written, so the refusal does not depend on the API being reachable.
const workspace = await disablingWorkspace()
if (workspace) return refusal(args.operation, workspace)
if (workspace) return refusal(args.operation, workspace, ctx.sessionID)
// altimate_change end
if (args.operation !== "status" && args.operation !== "list-config") {
const configured = await AltimateApi.isConfigured()
Expand All @@ -106,17 +106,17 @@ export const DatamateManagerTool = Tool.define("datamate_manager", {
case "list-integrations":
return handleListIntegrations()
case "add":
return handleAdd(args)
return handleAdd(args, "add", ctx.sessionID)
case "create":
return handleCreate(args)
return handleCreate(args, ctx.sessionID)
case "edit":
return handleEdit(args)
case "delete":
return handleDelete(args)
case "status":
return handleStatus()
case "remove":
return handleRemove(args)
return handleRemove(args, ctx.sessionID)
case "list-config":
return handleListConfig()
}
Expand Down Expand Up @@ -250,6 +250,8 @@ function mergeRefreshedEntry(
async function handleAdd(
args: { datamate_id?: string; name?: string; scope?: "project" | "global" },
operation = "add",
/** The calling session, for the refusal's telemetry row. */
sessionID?: string,
) {
if (!args.datamate_id) {
return {
Expand Down Expand Up @@ -315,7 +317,7 @@ async function handleAdd(
// it, as a boundary starts the engine's, or it could replace the engine's.
using lock = await holdDirectoryLock()
const off = await disablingWorkspace()
if (off) return refusal(operation, off)
if (off) return refusal(operation, off, sessionID)
// altimate_change end

if (transport !== null) {
Expand Down Expand Up @@ -452,7 +454,7 @@ async function handleAdd(
else await removeMcpFromConfig(serverName, configPath)
again.release()
if (prior && prior.enabled !== false) await MCP.add(serverName, prior).catch(() => {})
return refusal(operation, linked)
return refusal(operation, linked, sessionID)
}
}
// altimate_change end
Expand Down Expand Up @@ -492,14 +494,17 @@ async function handleAdd(
}
}

async function handleCreate(args: {
name?: string
description?: string
integration_ids?: string[]
memory_enabled?: boolean
privacy?: string
scope?: "project" | "global"
}) {
async function handleCreate(
args: {
name?: string
description?: string
integration_ids?: string[]
memory_enabled?: boolean
privacy?: string
scope?: "project" | "global"
},
sessionID?: string,
Comment thread
ralphstodomingo marked this conversation as resolved.
) {
if (!args.name) {
return {
title: "Datamate create: FAILED",
Expand All @@ -514,7 +519,7 @@ async function handleCreate(args: {
// altimate_change start — checked again before the datamate is created: a
// boundary may have linked the project since the check in `execute`.
const off = await disablingWorkspace()
if (off) return refusal("create", off)
if (off) return refusal("create", off, sessionID)
// The account the datamate is created in, kept for deleting it again: ids
// are per tenant, and the credentials can change while the request runs.
const creds = await AltimateApi.getCredentials()
Expand All @@ -535,6 +540,7 @@ async function handleCreate(args: {
const added = await handleAdd(
{ datamate_id: created.id, name: `datamate-${slugify(args.name)}`, scope: args.scope },
"create",
sessionID,
)
if (!("managedBy" in added.metadata)) return added
const rolledBack = await AltimateApi.deleteDatamate(created.id, creds).then(
Expand Down Expand Up @@ -691,7 +697,7 @@ async function handleStatus() {
}
}

async function handleRemove(args: { server_name?: string; scope?: "project" | "global" }) {
async function handleRemove(args: { server_name?: string; scope?: "project" | "global" }, sessionID?: string) {
if (!args.server_name) {
return {
title: "Datamate remove: FAILED",
Expand All @@ -706,7 +712,7 @@ async function handleRemove(args: { server_name?: string; scope?: "project" | "g
// check at the top of `execute` must not have its engine's key removed.
using _lock = await holdDirectoryLock()
const off = await disablingWorkspace()
if (off) return refusal("remove", off)
if (off) return refusal("remove", off, sessionID)
// altimate_change end
// Fully remove from runtime state (disconnect + purge from MCP list)
// altimate_change start — MCP.remove (was disconnect): delete the status entry + publish
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
// key. Null for an unlinked project, an unreadable binding with no engine
// running, workspaces disabled, `altimate serve`, and organisation-managed
// config that owns the `datamate` key.
import { trackRedirect } from "./engine-telemetry"
import { linkedWorkspaceLoaded, managedWorkspace, settledOutcome, settledWorkspace } from "./engine-overlay"
import { installCommand } from "./engine-offer"
import { log } from "./engine-seams"
Expand Down Expand Up @@ -52,7 +53,8 @@ export async function hiddenToolIds(): Promise<ReadonlySet<string>> {

/** What `datamate_manager` answers in a linked project, whatever the operation.
* Nothing is looked up or written before it. */
export function refusal(operation: string, workspace: { id: string; name: string }) {
export function refusal(operation: string, workspace: { id: string; name: string }, sessionID?: string) {
trackRedirect("datamate", operation, Number(workspace.id), sessionID) // telemetry: the engine is off for this workspace
return {
title: `Datamate ${operation}: off in a project linked to a workspace`,
metadata: { operation, managedBy: workspace.id },
Expand Down
68 changes: 62 additions & 6 deletions packages/opencode/src/altimate/workspace/engine-offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// the detail here from disk and PATH rather than from the overlay's memory.
// Offer, never install on the flow's own account — `installEngine` only ever
// runs from an explicit "Install now".
import { trackInstallPrompt } from "./engine-telemetry"
import { execFile, type ChildProcess } from "node:child_process"
import launch from "cross-spawn"
import { Process } from "@/util/process"
Expand Down Expand Up @@ -272,15 +273,61 @@ export async function installEngine(): Promise<InstallResult> {
return { ok: false, error: detail || `npm exited with code ${run.code ?? "unknown"}` }
}

/** What the bus carries with the offer command: the offer's telemetry fields, so the surface that
* prints the offer in another process (an attached run) reports `shown` with them. No node probe
* rides along: it would sit on the dispatch path, ahead of the turn boundary. */
export type OfferDetail = Pick<EngineOffer, "reason" | "workspaceId" | "declared" | "found">

export function encodeOfferDetail(offer: EngineOffer): string {
const detail: OfferDetail = {
reason: offer.reason,
workspaceId: offer.workspaceId,
declared: offer.declared,
found: offer.found,
}
return JSON.stringify(detail)
}

/** The detail back from the bus, or undefined for anything that is not one. */
export function decodeOfferDetail(raw: unknown): OfferDetail | undefined {
if (typeof raw !== "string") return undefined
try {
const v = JSON.parse(raw) as Partial<Record<keyof OfferDetail, unknown>>
const reason = v.reason === "engine-missing" || v.reason === "engine-too-old" ? v.reason : undefined
const workspaceId = typeof v.workspaceId === "string" ? v.workspaceId : undefined
const declared = typeof v.declared === "number" ? v.declared : v.declared === undefined ? undefined : NaN
const found = typeof v.found === "string" ? v.found : v.found === undefined ? undefined : ""
if (!reason || workspaceId === undefined || Number.isNaN(declared) || found === "") {
return undefined
}
return { reason, workspaceId, declared, found }
} catch {
return undefined
}
}

/** An attached run printed the offer it read off the bus: that process is the surface that showed
* it, so it reports `shown` (surface `bus`) from the fields the event carried. False when the
* event carried none that decode, and nothing is reported. */
export function trackPrintedOffer(raw: unknown, sessionID: string): boolean {
const detail = decodeOfferDetail(raw)
if (!detail) return false
trackInstallPrompt("shown", detail, { surface: "bus" }, sessionID)
return true
}

/** Ask the TUI to raise the offer. False when the bus is unavailable. The
* session is carried so an attached headless run, which reads the same event
* stream, prints the offer raised for its own session only. */
async function publishOffer(sessionID: string): Promise<boolean> {
if (syncInternals.publishOffer) return syncInternals.publishOffer(sessionID)
* stream, prints the offer raised for its own session only; the offer's fields
* ride along so that run reports `shown` from them (the TUI dialog re-derives
* the offer and reports its own). */
async function publishOffer(offer: EngineOffer, sessionID: string): Promise<boolean> {
const detail = encodeOfferDetail(offer)
if (syncInternals.publishOffer) return syncInternals.publishOffer(sessionID, detail)
try {
await AppRuntime.runPromise(
EventV2Bridge.Service.use((events) =>
events.publish(TuiEvent.CommandExecute, { command: OFFER_COMMAND, sessionID }),
events.publish(TuiEvent.CommandExecute, { command: OFFER_COMMAND, sessionID, arguments: detail }),
),
)
return true
Expand Down Expand Up @@ -320,11 +367,20 @@ export function toolsNeed(declared: number | undefined): string {
/** Offer via the dialog surface when there is one; otherwise print (headless)
* or toast (bus unavailable). Exactly one of these happens. */
export async function offerOrNotify(offer: EngineOffer, toast: Toast, sessionID: string): Promise<void> {
// Telemetry: the offer was shown, on which surface, and whether Node was there to install with.
const shown = (surface: "headless" | "dialog" | "bus" | "toast") =>
void nodeMajor()
.catch(() => null)
.then((major) => trackInstallPrompt("shown", offer, { surface, nodeMajor: major }, sessionID))
if (isHeadless()) {
printLine(describeOfferLine(offer))
shown("headless")
return
}
// A dialog, whether taken here or over the bus, reports `shown` itself when it renders: the TUI
// may hold the offer back (its 7-day latch), so dispatch is not display. The toast reports here
// only when it was shown.
if (offerInstall(offer)) return
if (await publishOffer(sessionID)) return
await notify(toast)
if (await publishOffer(offer, sessionID)) return
if (await notify(toast)) shown("toast")
}
Loading
Loading