Skip to content
Merged
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/api/src/handlers/discord/callback-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ async function handleCancelCallback(input: {
run,
authUserId: mappedUserId,
allowDirectCancelWithoutSandbox: true,
// Provider Cancel is terminal: stop the turn and shut the sandbox down.
terminate: true,
cancelledBy: {
...(cancelledByName ? { name: cancelledByName } : {}),
source: 'discord',
Expand Down
20 changes: 20 additions & 0 deletions apps/preview-proxy/src/handlers/__tests__/auto-resume.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions apps/preview-proxy/src/handlers/auto-resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export async function triggerAutoResume(
const existingResume = await db.query.taskRuns.findFirst({
where: and(
eq(taskRuns.sourceRunId, taskRun.id),
// Cross-task spawned runs also point at their launching run through
// sourceRunId. Only a sibling run on this task can be its resume.
eq(taskRuns.taskId, taskRun.taskId),
inArray(taskRuns.status, [...activeRunStatuses]),
),
columns: { id: true, status: true },
Expand Down
173 changes: 87 additions & 86 deletions apps/web/src/lib/server/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
environments,
llmUsageEvents,
alias,
and,
desc,
eq,
inArray,
Expand Down Expand Up @@ -1438,6 +1439,9 @@ async function getCostAnalyticsRows(
now: Date,
): Promise<AnalyticsRow[]> {
const cutoff = getTimeCutoff(timePeriod, now);
const usageCutoffCondition = cutoff
? sql`coalesce(${llmUsageEvents.messageCompletedAt}, ${llmUsageEvents.createdAt}) >= ${cutoff}`
: undefined;
const usageRows = await db
.select({
id: llmUsageEvents.id,
Expand Down Expand Up @@ -1469,7 +1473,7 @@ async function getCostAnalyticsRows(
)
.leftJoin(taskRuns, eq(taskRuns.id, llmUsageEvents.runId))
.leftJoin(environments, eq(environments.id, llmUsageEvents.environmentId))
.where(isNull(tasks.deletedAt));
.where(and(isNull(tasks.deletedAt), usageCutoffCondition));

const environmentRows = await db
.select({ id: environments.id, name: environments.name })
Expand All @@ -1478,22 +1482,24 @@ async function getCostAnalyticsRows(
const environmentNameById = new Map(
environmentRows.map((environment) => [environment.id, environment.name]),
);
const taskIds = usageRows
.map((row) => row.taskId)
.filter((taskId): taskId is string => Boolean(taskId));
const pullRequestRows =
taskIds.length === 0
? []
: await db
.select({
taskId: taskPullRequests.taskId,
repository: taskPullRequests.repository,
prNumber: taskPullRequests.prNumber,
sourceControlProvider: taskPullRequests.sourceControlProvider,
host: taskPullRequests.host,
})
.from(taskPullRequests)
.where(inArray(taskPullRequests.taskId, taskIds));
// Fetch PR attribution with a relational join instead of expanding one bind
// parameter per usage event. A task can have many usage rows, so an inArray
// built from usageRows eventually exceeds PostgreSQL's parameter limit.
const pullRequestRows = await db
.selectDistinct({
taskId: taskPullRequests.taskId,
repository: taskPullRequests.repository,
prNumber: taskPullRequests.prNumber,
sourceControlProvider: taskPullRequests.sourceControlProvider,
host: taskPullRequests.host,
})
.from(taskPullRequests)
.innerJoin(
llmUsageEvents,
eq(llmUsageEvents.taskId, taskPullRequests.taskId),
)
.leftJoin(tasks, eq(tasks.id, llmUsageEvents.taskId))
.where(and(isNull(tasks.deletedAt), usageCutoffCondition));
const prKeysByTaskId = new Map<string, Set<string>>();
for (const pullRequest of pullRequestRows) {
if (pullRequest.prNumber === null || !pullRequest.repository) {
Expand All @@ -1512,78 +1518,73 @@ async function getCostAnalyticsRows(
prKeysByTaskId.set(pullRequest.taskId, keys);
}

return usageRows
.filter((row) => {
const timestamp = row.timestamp ?? row.createdAt;
return !cutoff || timestamp >= cutoff;
})
.map((row) => {
const isTask = Boolean(row.taskId);
const taskType = isTask
? getTaskTypeDimensionValue({
initiatorKind: row.initiatorKind,
initiatorAutomation: row.initiatorAutomation,
})
: createLabelBackedDimensionValue('Non-task inference');
const attributedUserId = row.userId ?? row.taskUserId;
const userDimension =
isTask && row.initiatorKind === 'automation'
? createLabelBackedDimensionValue(NO_VALUE_LABEL)
: attributedUserId
? getCanonicalUserDimensionValue({
id: attributedUserId,
name: row.userId ? row.eventUserName : row.taskUserName,
email: row.userId ? row.eventUserEmail : row.taskUserEmail,
})
: createLabelBackedDimensionValue(NO_VALUE_LABEL);
const timestamp = row.timestamp ?? row.createdAt;
const cost = Number(row.costMicroUsd ?? 0) / 1_000_000;
const provider = row.providerId ?? 'Unknown provider';
const model = row.modelId ?? 'Unknown model';
const runEnvironmentId =
row.runPayload &&
typeof row.runPayload === 'object' &&
'environmentId' in row.runPayload &&
typeof row.runPayload.environmentId === 'string'
? row.runPayload.environmentId
: null;
const project =
row.environmentName ??
(runEnvironmentId
? (environmentNameById.get(runEnvironmentId) ?? NO_PROJECT_LABEL)
: NO_PROJECT_LABEL);
return usageRows.map((row) => {
const isTask = Boolean(row.taskId);
const taskType = isTask
? getTaskTypeDimensionValue({
initiatorKind: row.initiatorKind,
initiatorAutomation: row.initiatorAutomation,
})
: createLabelBackedDimensionValue('Non-task inference');
const attributedUserId = row.userId ?? row.taskUserId;
const userDimension =
isTask && row.initiatorKind === 'automation'
? createLabelBackedDimensionValue(NO_VALUE_LABEL)
: attributedUserId
? getCanonicalUserDimensionValue({
id: attributedUserId,
name: row.userId ? row.eventUserName : row.taskUserName,
email: row.userId ? row.eventUserEmail : row.taskUserEmail,
})
: createLabelBackedDimensionValue(NO_VALUE_LABEL);
const timestamp = row.timestamp ?? row.createdAt;
const cost = Number(row.costMicroUsd ?? 0) / 1_000_000;
const provider = row.providerId ?? 'Unknown provider';
const model = row.modelId ?? 'Unknown model';
const runEnvironmentId =
row.runPayload &&
typeof row.runPayload === 'object' &&
'environmentId' in row.runPayload &&
typeof row.runPayload.environmentId === 'string'
? row.runPayload.environmentId
: null;
const project =
row.environmentName ??
(runEnvironmentId
? (environmentNameById.get(runEnvironmentId) ?? NO_PROJECT_LABEL)
: NO_PROJECT_LABEL);

return {
return {
id: row.id,
timestamp,
value: cost,
dimensions: {
user: userDimension,
taskType,
project: createLabelBackedDimensionValue(project),
provider: createLabelBackedDimensionValue(provider),
model: createLabelBackedDimensionValue(model),
},
details: {
id: row.id,
timestamp,
value: cost,
dimensions: {
user: userDimension,
taskType,
project: createLabelBackedDimensionValue(project),
provider: createLabelBackedDimensionValue(provider),
model: createLabelBackedDimensionValue(model),
},
details: {
id: row.id,
values: {
date: formatAnalyticsDateTime(timestamp),
user: userDimension.label,
taskType: taskType.label,
project,
provider,
model,
cost: cost.toFixed(2),
taskTitle: row.taskTitle ?? 'Non-task inference',
},
links: row.taskId ? { task: `/task/${row.taskId}` } : undefined,
},
meta: {
canonicalTaskId: row.taskId,
prKeys: row.taskId ? [...(prKeysByTaskId.get(row.taskId) ?? [])] : [],
values: {
date: formatAnalyticsDateTime(timestamp),
user: userDimension.label,
taskType: taskType.label,
project,
provider,
model,
cost: cost.toFixed(2),
taskTitle: row.taskTitle ?? 'Non-task inference',
},
} satisfies AnalyticsRow;
});
links: row.taskId ? { task: `/task/${row.taskId}` } : undefined,
},
meta: {
canonicalTaskId: row.taskId,
prKeys: row.taskId ? [...(prKeysByTaskId.get(row.taskId) ?? [])] : [],
},
} satisfies AnalyticsRow;
});
}

async function getPullRequestAnalyticsRows(
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/trpc/commands/setup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ export async function batchCreateEnvironmentsCommand(
name: environmentSpec.name,
config,
createdByUserId: userId,
isVerified: false,
verificationError: null,
})
.returning({ id: environments.id });

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2038,6 +2038,15 @@ export class OpenCodeServerHarness
const text = command.data.text ?? '';
this.stopHookReminderCount = 0;

// A soft cancel can race with the very first session creation and abort
// its dedicated controller before a session id exists. SendMessage is the
// resumable follow-up path, so give it a fresh controller instead of
// immediately replaying the already-aborted signal forever.
if (!this.sessionId && this.sessionCreateAbortController.signal.aborted) {
this.cancelRequestedBeforeSession = false;
this.resetSessionCreateAbortController();
}

if (command.data.workflowPhase) {
this.currentWorkflowPhase = command.data.workflowPhase;
}
Expand Down
Loading
Loading