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
48 changes: 39 additions & 9 deletions apps/server/src/audit/Services/AuditLogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,23 +91,53 @@ const makeAuditLogService = Effect.gen(function* () {
const query: AuditLogServiceShape["query"] = (input) =>
Effect.gen(function* () {
const conditions: Array<string> = [];
if (input.projectId) conditions.push(`project_id = '${input.projectId}'`);
if (input.threadId) conditions.push(`thread_id = '${input.threadId}'`);
if (input.category) conditions.push(`category = '${input.category}'`);
if (input.severity) conditions.push(`severity = '${input.severity}'`);
if (input.actor) conditions.push(`actor = '${input.actor}'`);
if (input.fromTimestamp) conditions.push(`timestamp >= '${input.fromTimestamp}'`);
if (input.toTimestamp) conditions.push(`timestamp <= '${input.toTimestamp}'`);
const params: Array<string | number> = [];

if (input.projectId) {
conditions.push("project_id = ?");
params.push(input.projectId);
}
if (input.threadId) {
conditions.push("thread_id = ?");
params.push(input.threadId);
}
if (input.category) {
conditions.push("category = ?");
params.push(input.category);
}
if (input.severity) {
conditions.push("severity = ?");
params.push(input.severity);
}
if (input.actor) {
conditions.push("actor = ?");
params.push(input.actor);
}
if (input.fromTimestamp) {
conditions.push("timestamp >= ?");
params.push(input.fromTimestamp);
}
if (input.toTimestamp) {
conditions.push("timestamp <= ?");
params.push(input.toTimestamp);
}

const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const limit = Math.max(1, input.limit);
const offset = Math.max(0, input.offset);

const countResult = yield* sql.unsafe<{ total: number }>(
`SELECT COUNT(*) as total FROM audit_log ${whereClause}`,
params,
);
const total = Number(countResult[0]?.total ?? 0);

const rows = yield* sql.unsafe<Record<string, unknown>>(
`SELECT id, timestamp, actor, actor_id, category, action, severity, project_id, thread_id, command_id, event_id, summary, detail, metadata FROM audit_log ${whereClause} ORDER BY timestamp DESC LIMIT ${input.limit} OFFSET ${input.offset}`,
`SELECT id, timestamp, actor, actor_id, category, action, severity, project_id, thread_id, command_id, event_id, summary, detail, metadata
FROM audit_log ${whereClause}
ORDER BY timestamp DESC
LIMIT ? OFFSET ?`,
[...params, limit, offset],
);

const entries: AuditEntry[] = rows.map((r) => ({
Expand All @@ -131,7 +161,7 @@ const makeAuditLogService = Effect.gen(function* () {
return {
entries,
total: total as AuditQueryResult["total"],
hasMore: input.offset + input.limit < total,
hasMore: offset + limit < total,
} satisfies AuditQueryResult;
}).pipe(Effect.orDie);

Expand Down
19 changes: 13 additions & 6 deletions apps/server/src/cost/Services/CostTrackingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,16 @@ const makeCostTrackingService = Effect.gen(function* () {
input.periodStart ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
const periodEnd = input.periodEnd ?? now;

const conditions: Array<string> = [
`created_at >= '${periodStart}'`,
`created_at <= '${periodEnd}'`,
];
if (input.projectId) conditions.push(`project_id = '${input.projectId}'`);
if (input.threadId) conditions.push(`thread_id = '${input.threadId}'`);
const conditions: Array<string> = ["created_at >= ?", "created_at <= ?"];
const params: Array<string> = [periodStart, periodEnd];
if (input.projectId) {
conditions.push("project_id = ?");
params.push(input.projectId);
}
if (input.threadId) {
conditions.push("thread_id = ?");
params.push(input.threadId);
}
const whereClause = `WHERE ${conditions.join(" AND ")}`;

const totals = yield* sql.unsafe<{
Expand All @@ -159,6 +163,7 @@ const makeCostTrackingService = Effect.gen(function* () {
total_thinking: number;
}>(
`SELECT COALESCE(SUM(cost_cents), 0) as total_cost, COALESCE(SUM(input_tokens), 0) as total_input, COALESCE(SUM(output_tokens), 0) as total_output, COALESCE(SUM(thinking_tokens), 0) as total_thinking FROM cost_entries ${whereClause}`,
params,
);
const byProvider = yield* sql.unsafe<{
provider: string;
Expand All @@ -167,9 +172,11 @@ const makeCostTrackingService = Effect.gen(function* () {
output_tokens: number;
}>(
`SELECT provider, COALESCE(SUM(cost_cents), 0) as cost_cents, COALESCE(SUM(input_tokens), 0) as input_tokens, COALESCE(SUM(output_tokens), 0) as output_tokens FROM cost_entries ${whereClause} GROUP BY provider`,
params,
);
const byThread = yield* sql.unsafe<{ threadId: string; cost_cents: number }>(
`SELECT thread_id as threadId, COALESCE(SUM(cost_cents), 0) as cost_cents FROM cost_entries ${whereClause} GROUP BY thread_id ORDER BY cost_cents DESC LIMIT 20`,
params,
);

const row = totals[0] ?? {
Expand Down
14 changes: 10 additions & 4 deletions apps/server/src/memory/Services/ProjectMemoryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ const makeProjectMemoryService = Effect.gen(function* () {
const search: ProjectMemoryServiceShape["search"] = (input) =>
Effect.gen(function* () {
const start = Date.now();
const kindClause = input.kind ? "AND m.kind = ?" : "";
const queryParams: Array<string | number> = input.kind
? [input.query, input.projectId, input.kind, input.limit]
: [input.query, input.projectId, input.limit];
// Use FTS5 for full-text search
const rows = yield* sql.unsafe<{
id: string;
Expand All @@ -77,18 +81,20 @@ const makeProjectMemoryService = Effect.gen(function* () {
JOIN memory_entries m ON m.rowid = fts.rowid
WHERE memory_fts MATCH ?
AND m.project_id = ?
${input.kind ? `AND m.kind = '${input.kind}'` : ""}
${kindClause}
AND (m.expires_at IS NULL OR m.expires_at > datetime('now'))
ORDER BY fts.rank
LIMIT ?`,
[input.query, input.projectId, input.limit],
queryParams,
);

// Increment access count
if (rows.length > 0) {
const ids = rows.map((r) => `'${r.id}'`).join(",");
const placeholders = rows.map(() => "?").join(",");
const ids = rows.map((r) => r.id);
yield* sql.unsafe(
`UPDATE memory_entries SET access_count = access_count + 1 WHERE id IN (${ids})`,
`UPDATE memory_entries SET access_count = access_count + 1 WHERE id IN (${placeholders})`,
ids,
);
}

Expand Down
45 changes: 23 additions & 22 deletions apps/server/src/presence/Services/PresenceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,37 +86,38 @@ const makePresenceService = Effect.gen(function* () {

const leave: PresenceServiceShape["leave"] = (input) =>
Effect.gen(function* () {
// We need participant id — we'll clean up all stale entries
const threadMap = presenceMap.get(input.threadId);
if (!threadMap) return;
for (const [participantId] of threadMap) {
threadMap.delete(participantId);
yield* PubSub.publish(pubsub, {
type: "presence.left" as const,
participantId: participantId as ParticipantId,
threadId: input.threadId,
});
const existed = threadMap.delete(input.participantId);
if (threadMap.size === 0) {
presenceMap.delete(input.threadId);
}
if (!existed) return;
yield* PubSub.publish(pubsub, {
type: "presence.left" as const,
participantId: input.participantId,
threadId: input.threadId,
});
});

const updateCursor: PresenceServiceShape["updateCursor"] = (input) =>
Effect.gen(function* () {
const threadMap = presenceMap.get(input.threadId);
if (!threadMap) return;
for (const [id, p] of threadMap) {
const updated = {
...p,
cursor: input.cursor as PresenceCursorKind,
lastSeenAt: new Date().toISOString(),
};
threadMap.set(id, updated);
yield* PubSub.publish(pubsub, {
type: "presence.cursor.updated" as const,
participantId: id as ParticipantId,
cursor: input.cursor as PresenceCursorKind,
threadId: input.threadId,
});
}
const participant = threadMap.get(input.participantId);
if (!participant) return;
const updated = {
...participant,
cursor: input.cursor as PresenceCursorKind,
lastSeenAt: new Date().toISOString(),
};
threadMap.set(input.participantId, updated);
yield* PubSub.publish(pubsub, {
type: "presence.cursor.updated" as const,
participantId: input.participantId,
cursor: input.cursor as PresenceCursorKind,
threadId: input.threadId,
});
});

const share: PresenceServiceShape["share"] = (input) =>
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ export type PresenceJoinInput = typeof PresenceJoinInput.Type;

export const PresenceLeaveInput = Schema.Struct({
threadId: ThreadId,
participantId: ParticipantId,
});
export type PresenceLeaveInput = typeof PresenceLeaveInput.Type;

export const PresenceUpdateCursorInput = Schema.Struct({
threadId: ThreadId,
participantId: ParticipantId,
cursor: PresenceCursorKind,
});
export type PresenceUpdateCursorInput = typeof PresenceUpdateCursorInput.Type;
Expand Down