@cat-factory/node-server 0.88.0 → 0.89.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/container.js +1 -1
- package/dist/container.js.map +1 -1
- package/dist/db/schema.d.ts +46 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +10 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/repositories/drizzle.d.ts +2 -2
- package/dist/repositories/drizzle.d.ts.map +1 -1
- package/dist/repositories/drizzle.js +77 -27
- package/dist/repositories/drizzle.js.map +1 -1
- package/drizzle/20260708102010_icy_yellow_claw/migration.sql +1 -0
- package/drizzle/20260708102010_icy_yellow_claw/snapshot.json +15006 -0
- package/drizzle/20260708165154_bizarre_gorgon/migration.sql +2 -0
- package/drizzle/20260708165154_bizarre_gorgon/snapshot.json +14924 -0
- package/package.json +19 -19
|
@@ -2,7 +2,7 @@ import { LLM_WARNING_FINISH_REASONS } from '@cat-factory/kernel';
|
|
|
2
2
|
import { agentRunKindSchema, decodeInitiativeRow, isWebSearchProvider, } from '@cat-factory/contracts';
|
|
3
3
|
import { decodeEnum, tryDecodeRows, blockInsertValues, blockPatchToColumns, parseIssueIntakeColumn, serializeIssueIntakeColumn, rowToBlock, rowToExecution, executionToDetail, rowToPipeline, rowToSandboxExperiment, rowToSandboxFixture, rowToSandboxGrade, rowToSandboxPromptVersion, rowToSandboxRun, rowToWorkspace, } from '@cat-factory/server';
|
|
4
4
|
import { and, asc, count, desc, eq, gte, inArray, isNull, lt, ne, notInArray, or, sql, } from 'drizzle-orm';
|
|
5
|
-
import { accountInvitations, passwordResetTokens, accountSettings, localSettings, accounts, agentContextSnapshots, agentSearchQueries, agentRuns, blocks, consensusSessions, incidentEnrichmentConnections, observabilityConnections, packageRegistryConnections, emailConnections, llmCallMetrics, provisioningLog, sharedStacks, memberships,
|
|
5
|
+
import { accountInvitations, passwordResetTokens, accountSettings, localSettings, accounts, agentContextSnapshots, agentSearchQueries, agentRuns, blocks, consensusSessions, incidentEnrichmentConnections, observabilityConnections, packageRegistryConnections, emailConnections, llmCallMetrics, provisioningLog, sharedStacks, memberships, riskPolicies, releaseHealthConfigs, testSecrets, pipelineScheduleRuns, pipelineSchedules, pipelines, requirementReviews, docInterviewSessions, kaizenGradings, kaizenVerifiedCombos, clarityReviews, binaryArtifacts, brainstormSessions, initiatives, sandboxPromptVersions, sandboxFixtures, sandboxExperiments, sandboxRuns, sandboxGrades, services, tokenUsage, trackerSettings, modelPresets, userIdentities, users, userSettings, workspaceFragmentDefaults, workspaceServices, workspaceSettings, workspaces, } from '../db/schema.js';
|
|
6
6
|
// Drizzle/Postgres implementations of the core kernel repository ports. The
|
|
7
7
|
// row<->domain mapping is the SAME shared mapping the Cloudflare D1 repos use
|
|
8
8
|
// (@cat-factory/server), so behaviour matches across stores; this layer only owns
|
|
@@ -222,6 +222,7 @@ class DrizzlePipelineRepository {
|
|
|
222
222
|
gating: pipeline.gating ? JSON.stringify(pipeline.gating) : null,
|
|
223
223
|
follow_ups: pipeline.followUps ? JSON.stringify(pipeline.followUps) : null,
|
|
224
224
|
tester_quality: pipeline.testerQuality ? JSON.stringify(pipeline.testerQuality) : null,
|
|
225
|
+
step_options: pipeline.stepOptions ? JSON.stringify(pipeline.stepOptions) : null,
|
|
225
226
|
labels: pipeline.labels ? JSON.stringify(pipeline.labels) : null,
|
|
226
227
|
archived: pipeline.archived ? 1 : null,
|
|
227
228
|
builtin: pipeline.builtin ? 1 : null,
|
|
@@ -246,6 +247,7 @@ class DrizzlePipelineRepository {
|
|
|
246
247
|
gating: pipeline.gating ? JSON.stringify(pipeline.gating) : null,
|
|
247
248
|
follow_ups: pipeline.followUps ? JSON.stringify(pipeline.followUps) : null,
|
|
248
249
|
tester_quality: pipeline.testerQuality ? JSON.stringify(pipeline.testerQuality) : null,
|
|
250
|
+
step_options: pipeline.stepOptions ? JSON.stringify(pipeline.stepOptions) : null,
|
|
249
251
|
labels: pipeline.labels ? JSON.stringify(pipeline.labels) : null,
|
|
250
252
|
archived: pipeline.archived ? 1 : null,
|
|
251
253
|
version: pipeline.version ?? null,
|
|
@@ -442,6 +444,21 @@ class DrizzleExecutionRepository {
|
|
|
442
444
|
return rows;
|
|
443
445
|
}
|
|
444
446
|
async markFailed(workspaceId, id, failure) {
|
|
447
|
+
// Guard against clobbering a row that already reached a terminal state: a `stopRun`
|
|
448
|
+
// racing a run that just merged (`done`) or already failed must not overwrite it. This
|
|
449
|
+
// is the authoritative first-write-wins / no-re-fail-a-merged-run check — `failRun`'s
|
|
450
|
+
// in-memory guard reads a snapshot that can be stale by the time this write lands
|
|
451
|
+
// (race-audit 2.3). Mirrors the D1 `AND status NOT IN ('done','failed')`.
|
|
452
|
+
//
|
|
453
|
+
// BUMP `rev` on the terminal write so it participates in the driver's optimistic
|
|
454
|
+
// concurrency: a `casPersist` from an in-flight driver iteration that loaded the run
|
|
455
|
+
// BEFORE this `stopRun`/`failRun` still holds the pre-fail `rev`, so bumping it here makes
|
|
456
|
+
// that stale write miss its `rev = ?` guard → `RunContendedError` → re-drive → the reload
|
|
457
|
+
// sees `failed` and no-ops. Without the bump `markFailed` left `rev` untouched, so a stale
|
|
458
|
+
// `casPersist` writing a non-terminal status (`pollGate` pending, dispatch, …) would MATCH
|
|
459
|
+
// the unchanged `rev` and RESURRECT the stopped run as `running` (race-audit 2.3, the
|
|
460
|
+
// driver-clobbers-terminal direction — the dual of the SQL status guard above). Mirrors the
|
|
461
|
+
// D1 `rev = rev + 1`.
|
|
445
462
|
await this.db
|
|
446
463
|
.update(agentRuns)
|
|
447
464
|
.set({
|
|
@@ -449,8 +466,9 @@ class DrizzleExecutionRepository {
|
|
|
449
466
|
error: failure.message,
|
|
450
467
|
failure: JSON.stringify(failure),
|
|
451
468
|
updated_at: this.clock.now(),
|
|
469
|
+
rev: sql `${agentRuns.rev} + 1`,
|
|
452
470
|
})
|
|
453
|
-
.where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.id, id), this.isExecution));
|
|
471
|
+
.where(and(eq(agentRuns.workspace_id, workspaceId), eq(agentRuns.id, id), this.isExecution, notInArray(agentRuns.status, ['done', 'failed'])));
|
|
454
472
|
}
|
|
455
473
|
}
|
|
456
474
|
class DrizzleAgentRunRepository {
|
|
@@ -969,9 +987,41 @@ class DrizzleTokenUsageRepository {
|
|
|
969
987
|
input_tokens: usage.inputTokens,
|
|
970
988
|
output_tokens: usage.outputTokens,
|
|
971
989
|
cost_estimate: usage.costEstimate,
|
|
990
|
+
billing: usage.billing,
|
|
991
|
+
vendor: usage.vendor,
|
|
972
992
|
created_at: usage.createdAt,
|
|
973
993
|
});
|
|
974
994
|
}
|
|
995
|
+
async usageBreakdownForWorkspace(workspaceId, epochMs) {
|
|
996
|
+
// One GROUP BY over the workspace's current period — both billing kinds (the report
|
|
997
|
+
// shows total usage). Never a per-model loop. sum() of int columns is bigint; cast +
|
|
998
|
+
// coerce like the totals rollups. Ordered heaviest-first in SQL, mirroring the D1 repo.
|
|
999
|
+
const rows = await this.db
|
|
1000
|
+
.select({
|
|
1001
|
+
billing: tokenUsage.billing,
|
|
1002
|
+
vendor: tokenUsage.vendor,
|
|
1003
|
+
provider: tokenUsage.provider,
|
|
1004
|
+
model: tokenUsage.model,
|
|
1005
|
+
input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
|
|
1006
|
+
output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
|
|
1007
|
+
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
1008
|
+
calls: sql `count(*)::bigint`,
|
|
1009
|
+
})
|
|
1010
|
+
.from(tokenUsage)
|
|
1011
|
+
.where(and(eq(tokenUsage.workspace_id, workspaceId), gte(tokenUsage.created_at, epochMs)))
|
|
1012
|
+
.groupBy(tokenUsage.billing, tokenUsage.vendor, tokenUsage.provider, tokenUsage.model)
|
|
1013
|
+
.orderBy(sql `(coalesce(sum(${tokenUsage.input_tokens}), 0) + coalesce(sum(${tokenUsage.output_tokens}), 0)) desc`);
|
|
1014
|
+
return rows.map((r) => ({
|
|
1015
|
+
billing: (r.billing === 'subscription' ? 'subscription' : 'metered'),
|
|
1016
|
+
vendor: r.vendor,
|
|
1017
|
+
provider: r.provider,
|
|
1018
|
+
model: r.model,
|
|
1019
|
+
inputTokens: Number(r.input ?? 0),
|
|
1020
|
+
outputTokens: Number(r.output ?? 0),
|
|
1021
|
+
costEstimate: r.cost ?? 0,
|
|
1022
|
+
calls: Number(r.calls ?? 0),
|
|
1023
|
+
}));
|
|
1024
|
+
}
|
|
975
1025
|
async totalsSince(epochMs) {
|
|
976
1026
|
// sum() of int columns is bigint in Postgres — cast to bigint (NOT int4, which
|
|
977
1027
|
// overflows past ~2.1B tokens) and coerce: node-postgres returns bigint as a
|
|
@@ -984,7 +1034,7 @@ class DrizzleTokenUsageRepository {
|
|
|
984
1034
|
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
985
1035
|
})
|
|
986
1036
|
.from(tokenUsage)
|
|
987
|
-
.where(gte(tokenUsage.created_at, epochMs));
|
|
1037
|
+
.where(and(gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
|
|
988
1038
|
return {
|
|
989
1039
|
inputTokens: Number(row?.input ?? 0),
|
|
990
1040
|
outputTokens: Number(row?.output ?? 0),
|
|
@@ -999,7 +1049,7 @@ class DrizzleTokenUsageRepository {
|
|
|
999
1049
|
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
1000
1050
|
})
|
|
1001
1051
|
.from(tokenUsage)
|
|
1002
|
-
.where(and(eq(tokenUsage.workspace_id, workspaceId), gte(tokenUsage.created_at, epochMs)));
|
|
1052
|
+
.where(and(eq(tokenUsage.workspace_id, workspaceId), gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
|
|
1003
1053
|
return {
|
|
1004
1054
|
inputTokens: Number(row?.input ?? 0),
|
|
1005
1055
|
outputTokens: Number(row?.output ?? 0),
|
|
@@ -1014,7 +1064,7 @@ class DrizzleTokenUsageRepository {
|
|
|
1014
1064
|
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
1015
1065
|
})
|
|
1016
1066
|
.from(tokenUsage)
|
|
1017
|
-
.where(and(eq(tokenUsage.account_id, accountId), gte(tokenUsage.created_at, epochMs)));
|
|
1067
|
+
.where(and(eq(tokenUsage.account_id, accountId), gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
|
|
1018
1068
|
return {
|
|
1019
1069
|
inputTokens: Number(row?.input ?? 0),
|
|
1020
1070
|
outputTokens: Number(row?.output ?? 0),
|
|
@@ -1029,7 +1079,7 @@ class DrizzleTokenUsageRepository {
|
|
|
1029
1079
|
cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
|
|
1030
1080
|
})
|
|
1031
1081
|
.from(tokenUsage)
|
|
1032
|
-
.where(and(eq(tokenUsage.user_id, userId), gte(tokenUsage.created_at, epochMs)));
|
|
1082
|
+
.where(and(eq(tokenUsage.user_id, userId), gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
|
|
1033
1083
|
return {
|
|
1034
1084
|
inputTokens: Number(row?.input ?? 0),
|
|
1035
1085
|
outputTokens: Number(row?.output ?? 0),
|
|
@@ -2801,7 +2851,7 @@ class DrizzleInitiativeRepository {
|
|
|
2801
2851
|
.where(and(eq(initiatives.workspace_id, workspaceId), eq(initiatives.id, id)));
|
|
2802
2852
|
}
|
|
2803
2853
|
}
|
|
2804
|
-
function
|
|
2854
|
+
function rowToRiskPolicy(row) {
|
|
2805
2855
|
return {
|
|
2806
2856
|
id: row.id,
|
|
2807
2857
|
name: row.name,
|
|
@@ -2823,13 +2873,13 @@ function rowToMergePreset(row) {
|
|
|
2823
2873
|
}
|
|
2824
2874
|
/**
|
|
2825
2875
|
* Per-workspace merge threshold presets over Postgres (the Drizzle mirror of the
|
|
2826
|
-
* Worker's `
|
|
2876
|
+
* Worker's `D1RiskPolicyRepository`, migration 0024). Enforces the single-default
|
|
2827
2877
|
* invariant: promoting a preset to default demotes every other in the workspace
|
|
2828
2878
|
* before the upsert. The default preset cannot be removed (the service keeps that
|
|
2829
2879
|
* rule too; the DELETE also guards `is_default = 0`). Behaviourally identical to the
|
|
2830
2880
|
* D1 repo so the cross-runtime conformance suite asserts the same preset resolution.
|
|
2831
2881
|
*/
|
|
2832
|
-
class
|
|
2882
|
+
class DrizzleRiskPolicyRepository {
|
|
2833
2883
|
db;
|
|
2834
2884
|
constructor(db) {
|
|
2835
2885
|
this.db = db;
|
|
@@ -2837,27 +2887,27 @@ class DrizzleMergePresetRepository {
|
|
|
2837
2887
|
async get(workspaceId, id) {
|
|
2838
2888
|
const rows = await this.db
|
|
2839
2889
|
.select()
|
|
2840
|
-
.from(
|
|
2841
|
-
.where(and(eq(
|
|
2890
|
+
.from(riskPolicies)
|
|
2891
|
+
.where(and(eq(riskPolicies.workspace_id, workspaceId), eq(riskPolicies.id, id)))
|
|
2842
2892
|
.limit(1);
|
|
2843
|
-
return rows[0] ?
|
|
2893
|
+
return rows[0] ? rowToRiskPolicy(rows[0]) : null;
|
|
2844
2894
|
}
|
|
2845
2895
|
async list(workspaceId) {
|
|
2846
2896
|
const rows = await this.db
|
|
2847
2897
|
.select()
|
|
2848
|
-
.from(
|
|
2849
|
-
.where(eq(
|
|
2850
|
-
.orderBy(
|
|
2851
|
-
return rows.map(
|
|
2898
|
+
.from(riskPolicies)
|
|
2899
|
+
.where(eq(riskPolicies.workspace_id, workspaceId))
|
|
2900
|
+
.orderBy(riskPolicies.created_at);
|
|
2901
|
+
return rows.map(rowToRiskPolicy);
|
|
2852
2902
|
}
|
|
2853
2903
|
async getDefault(workspaceId) {
|
|
2854
2904
|
const rows = await this.db
|
|
2855
2905
|
.select()
|
|
2856
|
-
.from(
|
|
2857
|
-
.where(and(eq(
|
|
2858
|
-
.orderBy(
|
|
2906
|
+
.from(riskPolicies)
|
|
2907
|
+
.where(and(eq(riskPolicies.workspace_id, workspaceId), eq(riskPolicies.is_default, 1)))
|
|
2908
|
+
.orderBy(riskPolicies.created_at)
|
|
2859
2909
|
.limit(1);
|
|
2860
|
-
return rows[0] ?
|
|
2910
|
+
return rows[0] ? rowToRiskPolicy(rows[0]) : null;
|
|
2861
2911
|
}
|
|
2862
2912
|
async upsert(workspaceId, preset) {
|
|
2863
2913
|
const values = {
|
|
@@ -2885,15 +2935,15 @@ class DrizzleMergePresetRepository {
|
|
|
2885
2935
|
// Promoting this preset to default demotes any other default first.
|
|
2886
2936
|
if (preset.isDefault) {
|
|
2887
2937
|
await tx
|
|
2888
|
-
.update(
|
|
2938
|
+
.update(riskPolicies)
|
|
2889
2939
|
.set({ is_default: 0 })
|
|
2890
|
-
.where(and(eq(
|
|
2940
|
+
.where(and(eq(riskPolicies.workspace_id, workspaceId), sql `${riskPolicies.id} <> ${preset.id}`));
|
|
2891
2941
|
}
|
|
2892
2942
|
await tx
|
|
2893
|
-
.insert(
|
|
2943
|
+
.insert(riskPolicies)
|
|
2894
2944
|
.values(values)
|
|
2895
2945
|
.onConflictDoUpdate({
|
|
2896
|
-
target: [
|
|
2946
|
+
target: [riskPolicies.workspace_id, riskPolicies.id],
|
|
2897
2947
|
set: {
|
|
2898
2948
|
name: values.name,
|
|
2899
2949
|
max_complexity: values.max_complexity,
|
|
@@ -2915,8 +2965,8 @@ class DrizzleMergePresetRepository {
|
|
|
2915
2965
|
}
|
|
2916
2966
|
async remove(workspaceId, id) {
|
|
2917
2967
|
await this.db
|
|
2918
|
-
.delete(
|
|
2919
|
-
.where(and(eq(
|
|
2968
|
+
.delete(riskPolicies)
|
|
2969
|
+
.where(and(eq(riskPolicies.workspace_id, workspaceId), eq(riskPolicies.id, id), eq(riskPolicies.is_default, 0)));
|
|
2920
2970
|
}
|
|
2921
2971
|
}
|
|
2922
2972
|
// Shape-guarded parsers matching the D1 mirror (`D1SharedStackRepository`) EXACTLY, so a
|
|
@@ -3929,7 +3979,7 @@ export function createDrizzleRepositories(db, clock) {
|
|
|
3929
3979
|
clarityReviewRepository: new DrizzleClarityReviewRepository(db),
|
|
3930
3980
|
brainstormSessionRepository: new DrizzleBrainstormSessionRepository(db),
|
|
3931
3981
|
initiativeRepository: new DrizzleInitiativeRepository(db),
|
|
3932
|
-
|
|
3982
|
+
riskPolicyRepository: new DrizzleRiskPolicyRepository(db),
|
|
3933
3983
|
sharedStackRepository: new DrizzleSharedStackRepository(db),
|
|
3934
3984
|
workspaceSettingsRepository: new DrizzleWorkspaceSettingsRepository(db),
|
|
3935
3985
|
userSettingsRepository: new DrizzleUserSettingsRepository(db),
|