Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dd34521
fix(server): accept up to 20 MCP OAuth redirect URIs so Google can re…
bk-agent-01 Oct 10, 2026
9a3153e
Merge pull request #296 from iamtushar324/fix/mcp-oauth-redirect-limit
bk-agent-01 Oct 10, 2026
64c1bb6
Merge the agent toolkit stack into bkmain
bk-agent-01 Oct 11, 2026
3ca2eeb
Merge pull request #306 from iamtushar324/promote/agent-toolkit-2026-…
bk-agent-01 Oct 11, 2026
c06d3c4
fix(mcp): t3_answer sees Codex async questions and says which questio…
bk-agent-01 Oct 12, 2026
f9a4941
fix(mcp): keep approval details beside async questions; a retry answe…
bk-agent-01 Oct 12, 2026
4bb4763
fix(mcp): require requestId with clientRequestId when answering; fix …
bk-agent-01 Oct 12, 2026
152e21d
Merge pull request #309 from iamtushar324/mcp/async-questions
bk-agent-01 Oct 12, 2026
697556e
Merge stage into bkmain: Running shelf, collapse-all, Claude subagent…
bk-agent-01 Oct 13, 2026
cb9eb33
Merge pull request #312 from iamtushar324/promote/stage-2026-10-13
bk-agent-01 Oct 13, 2026
4d6ac8d
chore: promote stage to bkmain (TEC-1306 settled shelf follows the se…
bk-agent-01 Oct 14, 2026
c7811a1
Merge pull request #314 from iamtushar324/promote/stage-2026-10-14
bk-agent-01 Oct 14, 2026
c0b8d2f
feat(server): bkt3 product metrics in PostHog (TEC-1631)
bk-agent-01 Oct 14, 2026
71fa6f3
chore(deploy): script for bkt3 PostHog dashboards and alerts (TEC-1631)
bk-agent-01 Oct 14, 2026
3585a02
fix(server,web): review fixes for bkt3 analytics (TEC-1631)
bk-agent-01 Oct 14, 2026
b8a1fc0
chore(web): mark fork analytics edits (TEC-1631)
bk-agent-01 Oct 14, 2026
a3ac8b6
chore(web): mark fork analytics edits (TEC-1631)
bk-agent-01 Oct 14, 2026
20539ad
fix(lint): analytics naming for repo lint rules (TEC-1631)
bk-agent-01 Oct 14, 2026
0bfa1be
Merge pull request #315: bkt3 product metrics in PostHog (TEC-1631)
bk-agent-01 Oct 14, 2026
564ef6f
fix(web): the BK sidebar drops the PLAN tag once plan mode is off
bk-agent-01 Oct 15, 2026
e418cdf
feat(web): plan cards render like chat, go wide, and take comments
bk-agent-01 Oct 15, 2026
ea8ab3d
chore(web): mark the plan citation predicate as a fork edit
bk-agent-01 Oct 15, 2026
37b7f30
fix: keep replay input unchanged and fix plan citation types
bk-agent-01 Oct 15, 2026
c7f6953
fix(web): review fixes for plan cards
bk-agent-01 Oct 15, 2026
06e40b2
fix(server,web): questions left by a failed run can be answered or di…
bk-agent-01 Oct 15, 2026
5837955
chore(web): mark the plan review lookup as a fork edit
bk-agent-01 Oct 15, 2026
b1e5f22
fix(server,web): review fixes for orphaned questions
bk-agent-01 Oct 15, 2026
6d04dc4
Merge pull request #321: promote plan cards and stuck-question fix to…
bk-agent-01 Oct 16, 2026
d095635
fix(server): fast session-webhook ack (Toolyard callbacks no longer t…
bk-agent-01 Oct 16, 2026
e6c620a
fix(server): stop the pull-request sweep and plan-flag reads from fre…
bk-agent-01 Oct 19, 2026
b7af1e6
Merge pull request #326 from iamtushar324/promote/bkt3-pr-sweep-freezes
bk-agent-01 Oct 19, 2026
36e81bb
Merge bkmain (b7af1e60) into stage before promoting stage to bkt3
bk-agent-01 Oct 19, 2026
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
433 changes: 433 additions & 0 deletions apps/server/scripts/analytics-backfill.ts

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions apps/server/src/auth/McpOAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,3 +595,32 @@ it("matches loopback redirects on everything but the port, and https redirects e
expect(matches("https://bot.example/cb", "https://bot.example/cb?x=1")).toBe(false);
expect(matches("https://bot.example/cb", "https://evil.example/cb")).toBe(false);
});

// T3-CUSTOM(expbkt3): BEGIN — hosted connectors such as Google's register many redirect URIs.
it.live("registers a hosted client with up to 20 https redirect URIs", () =>
withRoutes((handler) =>
Effect.gen(function* () {
const registerMany = (count: number) =>
handler(
at("/oauth/mcp/register", {
method: "POST",
headers: { "content-type": "application/json" },
body: encodeJson({
client_name: "Google",
redirect_uris: Array.from(
{ length: count },
(_, index) => `https://connector${index}.example/oauth-redirect`,
),
}),
}),
);

expect((yield* registerMany(8)).status).toBe(201);
expect((yield* registerMany(20)).status).toBe(201);
const tooMany = yield* registerMany(21);
expect(tooMany.status).toBe(400);
expect(yield* json<unknown>(tooMany)).toMatchObject({ error: "invalid_redirect_uri" });
}),
),
);
// T3-CUSTOM(expbkt3): END
3 changes: 2 additions & 1 deletion apps/server/src/auth/McpOAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ import {
const SIGNING_SECRET_NAME = "mcp-oauth-signing-key";
const AUTHORIZATION_CODE_TTL_MS = 60_000;
const MAX_CLIENT_NAME_LENGTH = 100;
const MAX_REDIRECT_URIS = 5;
// T3-CUSTOM(expbkt3): Google's MCP connector registers more than upstream's 5 redirect URIs.
const MAX_REDIRECT_URIS = 20;
const MAX_REDIRECT_URI_LENGTH = 512;
const DEFAULT_CLIENT_NAME = "MCP client";
const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { resolveServiceLauncherMode } from "../cloud/serviceLauncherClient.ts";
import * as ServerConfig from "../config.ts";
import * as ProcessRunner from "../processRunner.ts";
import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";
// T3-CUSTOM(expbkt3): product analytics config for clients (TEC-1631).
import { loadClientAnalyticsConfig } from "./clientAnalyticsConfig.ts";
import { detectServerEnvironmentMachineKind } from "./ServerEnvironmentMachine.ts";

export class ServerEnvironmentIdPersistenceError extends Schema.TaggedError<ServerEnvironmentIdPersistenceError>()(
Expand Down Expand Up @@ -204,6 +206,8 @@ export const make = Effect.gen(function* () {
// the fd and correctly do not advertise.
const desktopAppUpdate =
serverSelfUpdate === "desktop-managed" && serverConfig.desktopTelemetryControlFd !== undefined;
// T3-CUSTOM(expbkt3): read once at startup, like the rest of the descriptor.
const analytics = yield* loadClientAnalyticsConfig;

const descriptor: ExecutionEnvironmentDescriptor = {
environmentId,
Expand Down Expand Up @@ -282,6 +286,8 @@ export const make = Effect.gen(function* () {
...(desktopAppUpdate ? { desktopAppUpdate: true } : {}),
serverBrowser: true,
},
// T3-CUSTOM(expbkt3): absent unless an explicit company PostHog key is set.
...(analytics === null ? {} : { analytics }),
};

return ServerEnvironment.of({
Expand Down
45 changes: 45 additions & 0 deletions apps/server/src/environment/clientAnalyticsConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from "@effect/vitest";
import * as Option from "effect/Option";

import { resolveClientAnalyticsConfig } from "./clientAnalyticsConfig.ts";

const base = {
posthogKey: Option.some("phc_company"),
posthogHost: Option.none<string>(),
enabled: true,
instance: Option.some("bkt3"),
};

describe("resolveClientAnalyticsConfig", () => {
it("advertises an explicit key with the default host and the instance", () => {
expect(resolveClientAnalyticsConfig(base)).toEqual({
enabled: true,
posthogKey: "phc_company",
posthogHost: "https://us.i.posthog.com",
instance: "bkt3",
});
});

it("stays absent without an explicit key, so clients never use upstream's project", () => {
expect(resolveClientAnalyticsConfig({ ...base, posthogKey: Option.none() })).toBeNull();
expect(resolveClientAnalyticsConfig({ ...base, posthogKey: Option.some(" ") })).toBeNull();
});

it("stays absent when telemetry is off", () => {
expect(resolveClientAnalyticsConfig({ ...base, enabled: false })).toBeNull();
});

it("omits a blank instance and honours a custom host", () => {
expect(
resolveClientAnalyticsConfig({
...base,
instance: Option.some(""),
posthogHost: Option.some("https://eu.i.posthog.com"),
}),
).toEqual({
enabled: true,
posthogKey: "phc_company",
posthogHost: "https://eu.i.posthog.com",
});
});
});
57 changes: 57 additions & 0 deletions apps/server/src/environment/clientAnalyticsConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* T3-CUSTOM(expbkt3): product analytics config handed to clients (TEC-1631).
*
* The web and desktop clients send their own PostHog events, to the same
* company project the server reports to. They learn the key from the
* environment descriptor at runtime, so no key is baked into the bundle and
* each environment decides for itself.
*
* Only an explicitly set `T3CODE_POSTHOG_KEY` is advertised. The server's own
* telemetry falls back to upstream's project key; clients must never send
* Beknown usage there, so with no explicit key the field is absent and the
* client stays off.
*
* @module clientAnalyticsConfig
*/
import type { ExecutionEnvironmentAnalytics } from "@t3tools/contracts";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";

const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";

const ClientAnalyticsEnvConfig = Config.all({
posthogKey: Config.String("T3CODE_POSTHOG_KEY").pipe(Config.option),
posthogHost: Config.String("T3CODE_POSTHOG_HOST").pipe(Config.option),
enabled: Config.Boolean("T3CODE_TELEMETRY_ENABLED").pipe(Config.withDefault(true)),
instance: Config.String("T3CODE_ANALYTICS_INSTANCE").pipe(Config.option),
});

function nonEmpty(value: Option.Option<string>): string | null {
const trimmed = Option.getOrNull(value)?.trim() ?? "";
return trimmed.length > 0 ? trimmed : null;
}

/** Pure mapping from env values to the descriptor field; `null` means omit it. */
export function resolveClientAnalyticsConfig(input: {
readonly posthogKey: Option.Option<string>;
readonly posthogHost: Option.Option<string>;
readonly enabled: boolean;
readonly instance: Option.Option<string>;
}): ExecutionEnvironmentAnalytics | null {
const posthogKey = nonEmpty(input.posthogKey);
if (!input.enabled || posthogKey === null) return null;
const instance = nonEmpty(input.instance);
return {
enabled: true,
posthogKey,
posthogHost: nonEmpty(input.posthogHost) ?? DEFAULT_POSTHOG_HOST,
...(instance === null ? {} : { instance }),
};
}

/** Reads the env once; a bad value disables client analytics, never the server. */
export const loadClientAnalyticsConfig: Effect.Effect<ExecutionEnvironmentAnalytics | null> =
Effect.gen(function* () {
return resolveClientAnalyticsConfig(yield* ClientAnalyticsEnvConfig);
}).pipe(Effect.orElseSucceed(() => null));
4 changes: 3 additions & 1 deletion apps/server/src/mcp/McpToolAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type DispatchModeRefusal,
} from "../orchestration-v2/DispatchModeLimit.ts";
import * as McpInvocationContext from "./McpInvocationContext.ts";
import { withMcpToolCallAnalytics } from "./mcpToolAnalytics.expbkt3.ts"; // T3-CUSTOM(expbkt3)
import { resolveInteractionMode, resolveRuntimeMode } from "./OrchestratorMcpService.ts";
import {
assertFullAccess,
Expand Down Expand Up @@ -300,7 +301,8 @@ export class HandlersLayer<Tools extends Record<string, Tool.Any>, EX = never, R
}

const checkedHandlers = <Handlers>(declarations: Declarations<Handlers>): Handlers =>
Struct.map(declarations, checkedHandler);
// T3-CUSTOM(expbkt3): count every tool call by name (TEC-1631).
withMcpToolCallAnalytics(Struct.map(declarations, checkedHandler));

/** `Toolkit.toLayer` for handlers that all declare their access. */
export const toLayer = <Tools extends Record<string, Tool.Any>, EX = never, RX = never>(
Expand Down
40 changes: 40 additions & 0 deletions apps/server/src/mcp/mcpToolAnalytics.expbkt3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* T3-CUSTOM(expbkt3): counts every T3 MCP tool call by name (TEC-1631).
*
* `McpToolAccess.toLayer` is the one place every `/mcp` toolkit's handlers are
* built, keyed by tool name, so wrapping them there covers every tool. Only
* the name, caller kind, outcome and duration are recorded, never arguments.
*/
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";

import { type McpToolCaller, withMcpToolAnalytics } from "../telemetry/serverEvents.ts";
import * as McpInvocationContext from "./McpInvocationContext.ts";

const currentCaller: Effect.Effect<McpToolCaller | undefined> = Effect.serviceOption(
McpInvocationContext.McpInvocationContext,
).pipe(
Effect.map((scope) =>
Option.isNone(scope)
? undefined
: { principal: scope.value.principal, actorUserId: scope.value.actorUserId },
),
);

/** `handlers` with each Effect-returning handler timed and recorded under its key. */
export function withMcpToolCallAnalytics<Handlers>(handlers: Handlers): Handlers {
const wrapped: Record<string, unknown> = {};
for (const [toolName, handler] of Object.entries(handlers as Record<string, unknown>)) {
wrapped[toolName] =
typeof handler === "function"
? (...args: ReadonlyArray<unknown>) => {
const result = (handler as (...params: ReadonlyArray<unknown>) => unknown)(...args);
return Effect.isEffect(result)
? // @effect-diagnostics-next-line anyUnknownInErrorContext:off -- handlers are heterogeneous; types pass through unchanged.
withMcpToolAnalytics(toolName, currentCaller, result)
: result;
}
: handler;
}
return wrapped as Handlers;
}
11 changes: 10 additions & 1 deletion apps/server/src/mcp/toolkits/control/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ import { hasUserWideScope, resolveMcpSessionTarget } from "../../mcpSessionTarge
import { boundSessionDetail } from "./sessionDetail.ts";
import { T3ControlToolkit, T3ControlToolError, type T3CreateSessionTool } from "./tools.ts";
import type { Tool } from "effect/ai";
import { recordLinearLinked } from "../../../telemetry/serverEvents.ts";
import { CurrentOrchestrationActorUserId } from "../../../orchestration-v2/forkActor.expbkt3.ts";

const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
const decodeOrchestrationCommand = Schema.decodeUnknownEffect(OrchestrationCommandSchema);
Expand Down Expand Up @@ -1662,10 +1664,17 @@ const handlers = {
const after = yield* readLinearLinks(operation, sessionId);
const urls = links.map((link) => link.url);
const has = (list: typeof before, url: string) => list.some((entry) => entry.url === url);
const added = urls.filter((url) => !has(before, url) && has(after, url));
// Product analytics: how many items were linked, never which (TEC-1631).
yield* recordLinearLinked({
threadId: sessionId,
count: added.length,
userId: yield* CurrentOrchestrationActorUserId,
});
return {
sessionId,
linearLinks: after,
added: urls.filter((url) => !has(before, url) && has(after, url)),
added,
alreadyLinked: urls.filter((url) => has(before, url)),
// Left out because the session already holds the maximum number of tags.
notAdded: urls.filter((url) => !has(before, url) && !has(after, url)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
settledBackgroundWakeRequest,
settledBackgroundWakeRun,
} from "./restartBackgroundResume.expbkt3.ts";
// T3-CUSTOM(expbkt3): session.recovery.* product analytics (TEC-1631).
import { withSessionRecoveryAnalytics } from "../telemetry/serverEvents.ts";
import {
cancelledRosterTaskWork,
cancelledTurnItemWork,
Expand Down Expand Up @@ -834,9 +836,12 @@ export const make = Effect.gen(function* () {
Effect.mapError((cause) => new ProviderRuntimeRecoveryError({ operation: "reconcile", cause })),
);

const recover = Effect.gen(function* () {
return (yield* reconcile("startup")) satisfies ProviderRuntimeRecoverySummary;
});
// T3-CUSTOM(expbkt3): session.recovery.* product analytics (TEC-1631).
const recover = withSessionRecoveryAnalytics(
Effect.gen(function* () {
return (yield* reconcile("startup")) satisfies ProviderRuntimeRecoverySummary;
}),
);

return ProviderRuntimeRecoveryService.of({ reconcile, prepareForShutdown, recover });
});
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/provider/ProviderInstanceEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { ProviderInstanceEnvironment } from "@t3tools/contracts";

import { expandHomePath } from "../pathExpansion.ts";
// T3-CUSTOM(expbkt3): agents never inherit the server's analytics settings (TEC-1631).
import { withoutAnalyticsEnvironment } from "../telemetry/analyticsEnvironment.ts";

export function mergeProviderInstanceEnvironment(
environment: ProviderInstanceEnvironment | undefined,
baseEnv: NodeJS.ProcessEnv = process.env,
): NodeJS.ProcessEnv {
if (!environment || environment.length === 0) {
return baseEnv;
return withoutAnalyticsEnvironment(baseEnv); // T3-CUSTOM(expbkt3): TEC-1631.
}

const next: NodeJS.ProcessEnv = { ...baseEnv };
Expand All @@ -18,5 +20,5 @@ export function mergeProviderInstanceEnvironment(
? expandHomePath(variable.value)
: variable.value;
}
return next;
return withoutAnalyticsEnvironment(next); // T3-CUSTOM(expbkt3): TEC-1631.
}
11 changes: 11 additions & 0 deletions apps/server/src/scheduledTasks/ScheduledTaskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
type WebhookRequest,
} from "./webhookTemplate.ts";
import { constantTimeEquals, verifyWebhookSignature } from "./webhookVerification.ts";
import { recordScheduledTaskRan } from "../telemetry/serverEvents.ts"; // T3-CUSTOM(expbkt3)
// T3-CUSTOM(expbkt3): BK sidebar custom group of the threads a task launches (XFN-59).
import {
deleteScheduledTaskCustomGroup,
Expand Down Expand Up @@ -904,6 +905,16 @@ export const layer = Layer.effect(
lastRunError,
runCount: scheduleSource.runCount + 1,
};
// T3-CUSTOM(expbkt3): product analytics for each finished run (TEC-1631).
yield* recordScheduledTaskRan({
taskId: task.id,
scheduleType: scheduleSource.schedule.type,
trigger,
creationSource: active.creationSource,
status: lastRunStatus,
durationMs: DateTime.toEpochMillis(completedAt) - DateTime.toEpochMillis(startedAt),
ownerUserId: forkOwner,
});
if (current !== null) {
// startedAtIso in the guard ensures this writes only to the row this
// run marked as running — a task deleted mid-run and recreated with
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/scheduledTasks/webhookRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
import * as Metrics from "../observability/Metrics.ts";
import * as RelayDeliveryProof from "./RelayDeliveryProof.ts";
import * as ScheduledTaskService from "./ScheduledTaskService.ts";
import { recordWebhookDelivered } from "../telemetry/serverEvents.ts"; // T3-CUSTOM(expbkt3)

/** Largest request body a webhook accepts. The relay enforces the same cap. */
export const WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
Expand Down Expand Up @@ -111,6 +112,8 @@ export const layer = HttpApiBuilder.group(
Effect.as({ _tag: "error" as const }),
),
),
// T3-CUSTOM(expbkt3): product analytics, outcome tag only (TEC-1631).
Effect.tap((delivery) => recordWebhookDelivered("scheduled_task", delivery._tag)),
);

switch (result._tag) {
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import * as SqlitePersistence from "./persistence/Sqlite.ts";
import * as PullRequestFilesViewed from "./persistence/PullRequestFilesViewed.ts";
import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
import * as AnalyticsService from "./telemetry/AnalyticsService.ts";
import * as SessionActivityReactor from "./telemetry/sessionActivity/SessionActivityReactor.ts"; // T3-CUSTOM(expbkt3): TEC-1631.
import * as ProviderEventIngestor from "./orchestration-v2/ProviderEventIngestor.ts";
import * as ModelManifest from "./provider/ModelManifest.ts";
import * as ResetCreditCoordinator from "./provider/resetCreditCoordinator.ts";
Expand Down Expand Up @@ -656,6 +657,7 @@ const layerRuntimeCoreDependenciesBase = Layer.mergeAll(
ProviderUsageLimitsIngestion.layer,
layerProviderInstallationRefresh,
ReplayMarkers.layer,
SessionActivityReactor.layer, // T3-CUSTOM(expbkt3): session time-in-state metrics (TEC-1631).
).pipe(
// Core Services
Layer.provideMerge(layerOrchestrationApplication),
Expand Down
Loading
Loading