bopodev-db 0.1.30 → 0.1.32
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/repositories/company-assistant-chat.d.ts +56 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/legacy.d.ts +27 -0
- package/dist/schema.d.ts +3378 -1302
- package/package.json +1 -1
- package/src/migrations/0005_work_loops.sql +67 -0
- package/src/migrations/0006_company_assistant_chat.sql +20 -0
- package/src/migrations/0007_cost_ledger_company_assistant.sql +7 -0
- package/src/migrations/meta/_journal.json +21 -0
- package/src/repositories/company-assistant-chat.ts +126 -0
- package/src/repositories/index.ts +1 -0
- package/src/repositories/legacy.ts +90 -2
- package/src/schema.ts +102 -1
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
CREATE TABLE "work_loops" (
|
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
|
|
4
|
+
"project_id" text NOT NULL REFERENCES "projects"("id") ON DELETE CASCADE,
|
|
5
|
+
"parent_issue_id" text REFERENCES "issues"("id") ON DELETE SET NULL,
|
|
6
|
+
"goal_ids_json" text DEFAULT '[]' NOT NULL,
|
|
7
|
+
"title" text NOT NULL,
|
|
8
|
+
"description" text,
|
|
9
|
+
"assignee_agent_id" text NOT NULL REFERENCES "agents"("id") ON DELETE RESTRICT,
|
|
10
|
+
"priority" text DEFAULT 'medium' NOT NULL,
|
|
11
|
+
"status" text DEFAULT 'active' NOT NULL,
|
|
12
|
+
"concurrency_policy" text DEFAULT 'coalesce_if_active' NOT NULL,
|
|
13
|
+
"catch_up_policy" text DEFAULT 'skip_missed' NOT NULL,
|
|
14
|
+
"last_triggered_at" timestamp,
|
|
15
|
+
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
16
|
+
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
|
|
17
|
+
);
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
CREATE TABLE "work_loop_triggers" (
|
|
20
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
21
|
+
"company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
|
|
22
|
+
"work_loop_id" text NOT NULL REFERENCES "work_loops"("id") ON DELETE CASCADE,
|
|
23
|
+
"kind" text DEFAULT 'schedule' NOT NULL,
|
|
24
|
+
"label" text,
|
|
25
|
+
"enabled" boolean DEFAULT true NOT NULL,
|
|
26
|
+
"cron_expression" text NOT NULL,
|
|
27
|
+
"timezone" text DEFAULT 'UTC' NOT NULL,
|
|
28
|
+
"next_run_at" timestamp,
|
|
29
|
+
"last_fired_at" timestamp,
|
|
30
|
+
"last_result" text,
|
|
31
|
+
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
32
|
+
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
|
|
33
|
+
);
|
|
34
|
+
--> statement-breakpoint
|
|
35
|
+
CREATE TABLE "work_loop_runs" (
|
|
36
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
37
|
+
"company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
|
|
38
|
+
"work_loop_id" text NOT NULL REFERENCES "work_loops"("id") ON DELETE CASCADE,
|
|
39
|
+
"trigger_id" text REFERENCES "work_loop_triggers"("id") ON DELETE SET NULL,
|
|
40
|
+
"source" text NOT NULL,
|
|
41
|
+
"status" text DEFAULT 'received' NOT NULL,
|
|
42
|
+
"triggered_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
43
|
+
"idempotency_key" text,
|
|
44
|
+
"payload_json" text DEFAULT '{}' NOT NULL,
|
|
45
|
+
"linked_issue_id" text REFERENCES "issues"("id") ON DELETE SET NULL,
|
|
46
|
+
"coalesced_into_run_id" text REFERENCES "work_loop_runs"("id") ON DELETE SET NULL,
|
|
47
|
+
"failure_reason" text,
|
|
48
|
+
"completed_at" timestamp,
|
|
49
|
+
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
50
|
+
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
|
|
51
|
+
);
|
|
52
|
+
--> statement-breakpoint
|
|
53
|
+
ALTER TABLE "issues" ADD COLUMN "loop_id" text REFERENCES "work_loops"("id") ON DELETE SET NULL;
|
|
54
|
+
--> statement-breakpoint
|
|
55
|
+
ALTER TABLE "issues" ADD COLUMN "loop_run_id" text REFERENCES "work_loop_runs"("id") ON DELETE SET NULL;
|
|
56
|
+
--> statement-breakpoint
|
|
57
|
+
CREATE INDEX "work_loops_company_status_idx" ON "work_loops" ("company_id","status");
|
|
58
|
+
--> statement-breakpoint
|
|
59
|
+
CREATE INDEX "work_loop_triggers_company_next_run_idx" ON "work_loop_triggers" ("company_id","next_run_at");
|
|
60
|
+
--> statement-breakpoint
|
|
61
|
+
CREATE INDEX "work_loop_triggers_work_loop_idx" ON "work_loop_triggers" ("work_loop_id");
|
|
62
|
+
--> statement-breakpoint
|
|
63
|
+
CREATE INDEX "work_loop_runs_work_loop_created_idx" ON "work_loop_runs" ("work_loop_id","created_at");
|
|
64
|
+
--> statement-breakpoint
|
|
65
|
+
CREATE INDEX "work_loop_runs_trigger_idempotency_idx" ON "work_loop_runs" ("trigger_id","idempotency_key");
|
|
66
|
+
--> statement-breakpoint
|
|
67
|
+
CREATE INDEX "issues_loop_id_idx" ON "issues" ("loop_id");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
CREATE TABLE "company_assistant_threads" (
|
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
|
|
4
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
5
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
CREATE INDEX "company_assistant_threads_company_idx" ON "company_assistant_threads" ("company_id");
|
|
9
|
+
|
|
10
|
+
CREATE TABLE "company_assistant_messages" (
|
|
11
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
12
|
+
"thread_id" text NOT NULL REFERENCES "company_assistant_threads"("id") ON DELETE CASCADE,
|
|
13
|
+
"company_id" text NOT NULL REFERENCES "companies"("id") ON DELETE CASCADE,
|
|
14
|
+
"role" text NOT NULL,
|
|
15
|
+
"body" text NOT NULL,
|
|
16
|
+
"metadata_json" text,
|
|
17
|
+
"created_at" timestamp DEFAULT now() NOT NULL
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE INDEX "company_assistant_messages_thread_created_idx" ON "company_assistant_messages" ("thread_id","created_at");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ALTER TABLE "cost_ledger" ADD COLUMN "cost_category" text;
|
|
2
|
+
--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "cost_ledger" ADD COLUMN "assistant_thread_id" text REFERENCES "company_assistant_threads"("id") ON DELETE SET NULL;
|
|
4
|
+
--> statement-breakpoint
|
|
5
|
+
ALTER TABLE "cost_ledger" ADD COLUMN "assistant_message_id" text REFERENCES "company_assistant_messages"("id") ON DELETE SET NULL;
|
|
6
|
+
--> statement-breakpoint
|
|
7
|
+
CREATE INDEX "idx_cost_ledger_company_category_created" ON "cost_ledger" ("company_id", "cost_category", "created_at");
|
|
@@ -36,6 +36,27 @@
|
|
|
36
36
|
"when": 1742900000000,
|
|
37
37
|
"tag": "0004_agents_capabilities",
|
|
38
38
|
"breakpoints": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"idx": 5,
|
|
42
|
+
"version": "7",
|
|
43
|
+
"when": 1743000000000,
|
|
44
|
+
"tag": "0005_work_loops",
|
|
45
|
+
"breakpoints": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"idx": 6,
|
|
49
|
+
"version": "7",
|
|
50
|
+
"when": 1743100000000,
|
|
51
|
+
"tag": "0006_company_assistant_chat",
|
|
52
|
+
"breakpoints": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"idx": 7,
|
|
56
|
+
"version": "7",
|
|
57
|
+
"when": 1743200000000,
|
|
58
|
+
"tag": "0007_cost_ledger_company_assistant",
|
|
59
|
+
"breakpoints": true
|
|
39
60
|
}
|
|
40
61
|
]
|
|
41
62
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { and, asc, count, desc, eq, gte, lt } from "drizzle-orm";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import type { BopoDb } from "../client";
|
|
4
|
+
import { companyAssistantMessages, companyAssistantThreads } from "../schema";
|
|
5
|
+
|
|
6
|
+
export type AssistantMessageRole = "user" | "assistant" | "system";
|
|
7
|
+
|
|
8
|
+
export async function getOrCreateAssistantThread(db: BopoDb, companyId: string) {
|
|
9
|
+
const [existing] = await db
|
|
10
|
+
.select()
|
|
11
|
+
.from(companyAssistantThreads)
|
|
12
|
+
.where(eq(companyAssistantThreads.companyId, companyId))
|
|
13
|
+
.orderBy(desc(companyAssistantThreads.updatedAt))
|
|
14
|
+
.limit(1);
|
|
15
|
+
if (existing) {
|
|
16
|
+
return existing;
|
|
17
|
+
}
|
|
18
|
+
return createAssistantThreadRow(db, companyId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function createAssistantThreadRow(db: BopoDb, companyId: string) {
|
|
22
|
+
const id = nanoid(16);
|
|
23
|
+
const now = new Date();
|
|
24
|
+
await db.insert(companyAssistantThreads).values({
|
|
25
|
+
id,
|
|
26
|
+
companyId,
|
|
27
|
+
createdAt: now,
|
|
28
|
+
updatedAt: now
|
|
29
|
+
});
|
|
30
|
+
const [row] = await db.select().from(companyAssistantThreads).where(eq(companyAssistantThreads.id, id)).limit(1);
|
|
31
|
+
return row!;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** New empty thread; previous threads and messages remain in the database. */
|
|
35
|
+
export async function createAssistantThread(db: BopoDb, companyId: string) {
|
|
36
|
+
return createAssistantThreadRow(db, companyId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getAssistantThreadById(db: BopoDb, companyId: string, threadId: string) {
|
|
40
|
+
const [row] = await db
|
|
41
|
+
.select()
|
|
42
|
+
.from(companyAssistantThreads)
|
|
43
|
+
.where(and(eq(companyAssistantThreads.id, threadId), eq(companyAssistantThreads.companyId, companyId)))
|
|
44
|
+
.limit(1);
|
|
45
|
+
return row ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function touchAssistantThread(db: BopoDb, threadId: string) {
|
|
49
|
+
await db
|
|
50
|
+
.update(companyAssistantThreads)
|
|
51
|
+
.set({ updatedAt: new Date() })
|
|
52
|
+
.where(eq(companyAssistantThreads.id, threadId));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function insertAssistantMessage(
|
|
56
|
+
db: BopoDb,
|
|
57
|
+
input: {
|
|
58
|
+
threadId: string;
|
|
59
|
+
companyId: string;
|
|
60
|
+
role: AssistantMessageRole;
|
|
61
|
+
body: string;
|
|
62
|
+
metadataJson?: string | null;
|
|
63
|
+
}
|
|
64
|
+
) {
|
|
65
|
+
const id = nanoid(16);
|
|
66
|
+
await db.insert(companyAssistantMessages).values({
|
|
67
|
+
id,
|
|
68
|
+
threadId: input.threadId,
|
|
69
|
+
companyId: input.companyId,
|
|
70
|
+
role: input.role,
|
|
71
|
+
body: input.body,
|
|
72
|
+
metadataJson: input.metadataJson ?? null
|
|
73
|
+
});
|
|
74
|
+
await touchAssistantThread(db, input.threadId);
|
|
75
|
+
const [row] = await db.select().from(companyAssistantMessages).where(eq(companyAssistantMessages.id, id)).limit(1);
|
|
76
|
+
return row!;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function listAssistantMessages(db: BopoDb, threadId: string, limit = 100) {
|
|
80
|
+
const capped = Math.min(Math.max(1, limit), 200);
|
|
81
|
+
return db
|
|
82
|
+
.select()
|
|
83
|
+
.from(companyAssistantMessages)
|
|
84
|
+
.where(eq(companyAssistantMessages.threadId, threadId))
|
|
85
|
+
.orderBy(asc(companyAssistantMessages.createdAt))
|
|
86
|
+
.limit(capped);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Threads with at least one message in `[startInclusive, endExclusive)` on `created_at`. */
|
|
90
|
+
export async function listAssistantChatThreadStatsInCreatedAtRange(
|
|
91
|
+
db: BopoDb,
|
|
92
|
+
companyId: string,
|
|
93
|
+
startInclusive: Date,
|
|
94
|
+
endExclusive: Date
|
|
95
|
+
): Promise<Array<{ threadId: string; messageCount: number }>> {
|
|
96
|
+
const rows = await db
|
|
97
|
+
.select({
|
|
98
|
+
threadId: companyAssistantMessages.threadId,
|
|
99
|
+
messageCount: count()
|
|
100
|
+
})
|
|
101
|
+
.from(companyAssistantMessages)
|
|
102
|
+
.where(
|
|
103
|
+
and(
|
|
104
|
+
eq(companyAssistantMessages.companyId, companyId),
|
|
105
|
+
gte(companyAssistantMessages.createdAt, startInclusive),
|
|
106
|
+
lt(companyAssistantMessages.createdAt, endExclusive)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
.groupBy(companyAssistantMessages.threadId);
|
|
110
|
+
return rows.map((r) => ({
|
|
111
|
+
threadId: r.threadId,
|
|
112
|
+
messageCount: Number(r.messageCount) || 0
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Threads with at least one message in the UTC calendar month (for callers without local bounds). */
|
|
117
|
+
export async function listAssistantChatThreadStatsInUtcMonth(
|
|
118
|
+
db: BopoDb,
|
|
119
|
+
companyId: string,
|
|
120
|
+
year: number,
|
|
121
|
+
month1Based: number
|
|
122
|
+
): Promise<Array<{ threadId: string; messageCount: number }>> {
|
|
123
|
+
const startUtc = new Date(Date.UTC(year, month1Based - 1, 1, 0, 0, 0, 0));
|
|
124
|
+
const endExclusiveUtc = new Date(Date.UTC(year, month1Based, 1, 0, 0, 0, 0));
|
|
125
|
+
return listAssistantChatThreadStatsInCreatedAtRange(db, companyId, startUtc, endExclusiveUtc);
|
|
126
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, asc, desc, eq, gt, inArray, notInArray, sql } from "drizzle-orm";
|
|
1
|
+
import { and, asc, desc, eq, gt, gte, inArray, lt, notInArray, sql } from "drizzle-orm";
|
|
2
2
|
import { nanoid } from "nanoid";
|
|
3
3
|
import type { BopoDb } from "../client";
|
|
4
4
|
import {
|
|
@@ -521,6 +521,8 @@ export async function createIssue(
|
|
|
521
521
|
assigneeAgentId?: string | null;
|
|
522
522
|
labels?: string[];
|
|
523
523
|
tags?: string[];
|
|
524
|
+
loopId?: string | null;
|
|
525
|
+
loopRunId?: string | null;
|
|
524
526
|
}
|
|
525
527
|
) {
|
|
526
528
|
await assertProjectBelongsToCompany(db, input.companyId, input.projectId);
|
|
@@ -546,7 +548,9 @@ export async function createIssue(
|
|
|
546
548
|
priority: input.priority ?? "none",
|
|
547
549
|
assigneeAgentId: input.assigneeAgentId ?? null,
|
|
548
550
|
labelsJson: JSON.stringify(input.labels ?? []),
|
|
549
|
-
tagsJson: JSON.stringify(input.tags ?? [])
|
|
551
|
+
tagsJson: JSON.stringify(input.tags ?? []),
|
|
552
|
+
loopId: input.loopId ?? null,
|
|
553
|
+
loopRunId: input.loopRunId ?? null
|
|
550
554
|
})
|
|
551
555
|
.returning();
|
|
552
556
|
if (!row) {
|
|
@@ -1514,6 +1518,10 @@ export async function appendCost(
|
|
|
1514
1518
|
projectId?: string | null;
|
|
1515
1519
|
issueId?: string | null;
|
|
1516
1520
|
agentId?: string | null;
|
|
1521
|
+
/** Discriminator for reporting (e.g. `company_assistant`); null for heartbeat / legacy */
|
|
1522
|
+
costCategory?: string | null;
|
|
1523
|
+
assistantThreadId?: string | null;
|
|
1524
|
+
assistantMessageId?: string | null;
|
|
1517
1525
|
}
|
|
1518
1526
|
) {
|
|
1519
1527
|
const id = nanoid(14);
|
|
@@ -1521,6 +1529,9 @@ export async function appendCost(
|
|
|
1521
1529
|
id,
|
|
1522
1530
|
companyId: input.companyId,
|
|
1523
1531
|
runId: input.runId ?? null,
|
|
1532
|
+
costCategory: input.costCategory ?? null,
|
|
1533
|
+
assistantThreadId: input.assistantThreadId ?? null,
|
|
1534
|
+
assistantMessageId: input.assistantMessageId ?? null,
|
|
1524
1535
|
providerType: input.providerType,
|
|
1525
1536
|
runtimeModelId: input.runtimeModelId ?? null,
|
|
1526
1537
|
pricingProviderType: input.pricingProviderType ?? null,
|
|
@@ -1546,6 +1557,83 @@ export async function listCostEntries(db: BopoDb, companyId: string, limit = 200
|
|
|
1546
1557
|
.limit(limit);
|
|
1547
1558
|
}
|
|
1548
1559
|
|
|
1560
|
+
export type CostLedgerAggregate = {
|
|
1561
|
+
rowCount: number;
|
|
1562
|
+
tokenInput: number;
|
|
1563
|
+
tokenOutput: number;
|
|
1564
|
+
/** Sum of `usd_cost` as a decimal string (full precision from DB). */
|
|
1565
|
+
usdTotal: string;
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
/** Sum every ledger row for the company in `[startInclusive, endExclusive)` (typically UTC month). */
|
|
1569
|
+
export async function aggregateCompanyCostLedgerInRange(
|
|
1570
|
+
db: BopoDb,
|
|
1571
|
+
companyId: string,
|
|
1572
|
+
startInclusive: Date,
|
|
1573
|
+
endExclusive: Date
|
|
1574
|
+
): Promise<CostLedgerAggregate> {
|
|
1575
|
+
const [row] = await db
|
|
1576
|
+
.select({
|
|
1577
|
+
rowCount: sql<number>`count(*)::int`,
|
|
1578
|
+
tokenInput: sql<string>`coalesce(sum(${costLedger.tokenInput})::text, '0')`,
|
|
1579
|
+
tokenOutput: sql<string>`coalesce(sum(${costLedger.tokenOutput})::text, '0')`,
|
|
1580
|
+
usdTotal: sql<string>`coalesce(sum(${costLedger.usdCost})::text, '0')`
|
|
1581
|
+
})
|
|
1582
|
+
.from(costLedger)
|
|
1583
|
+
.where(
|
|
1584
|
+
and(
|
|
1585
|
+
eq(costLedger.companyId, companyId),
|
|
1586
|
+
gte(costLedger.createdAt, startInclusive),
|
|
1587
|
+
lt(costLedger.createdAt, endExclusive)
|
|
1588
|
+
)
|
|
1589
|
+
);
|
|
1590
|
+
|
|
1591
|
+
const parseBigIntish = (v: string) => {
|
|
1592
|
+
try {
|
|
1593
|
+
return Number(BigInt(v));
|
|
1594
|
+
} catch {
|
|
1595
|
+
return Number.parseInt(v, 10) || 0;
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1599
|
+
return {
|
|
1600
|
+
rowCount: Number(row?.rowCount ?? 0),
|
|
1601
|
+
tokenInput: parseBigIntish(String(row?.tokenInput ?? "0")),
|
|
1602
|
+
tokenOutput: parseBigIntish(String(row?.tokenOutput ?? "0")),
|
|
1603
|
+
usdTotal: String(row?.usdTotal ?? "0").trim() || "0"
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
export async function aggregateCompanyCostLedgerAllTime(
|
|
1608
|
+
db: BopoDb,
|
|
1609
|
+
companyId: string
|
|
1610
|
+
): Promise<CostLedgerAggregate> {
|
|
1611
|
+
const [row] = await db
|
|
1612
|
+
.select({
|
|
1613
|
+
rowCount: sql<number>`count(*)::int`,
|
|
1614
|
+
tokenInput: sql<string>`coalesce(sum(${costLedger.tokenInput})::text, '0')`,
|
|
1615
|
+
tokenOutput: sql<string>`coalesce(sum(${costLedger.tokenOutput})::text, '0')`,
|
|
1616
|
+
usdTotal: sql<string>`coalesce(sum(${costLedger.usdCost})::text, '0')`
|
|
1617
|
+
})
|
|
1618
|
+
.from(costLedger)
|
|
1619
|
+
.where(eq(costLedger.companyId, companyId));
|
|
1620
|
+
|
|
1621
|
+
const parseBigIntish = (v: string) => {
|
|
1622
|
+
try {
|
|
1623
|
+
return Number(BigInt(v));
|
|
1624
|
+
} catch {
|
|
1625
|
+
return Number.parseInt(v, 10) || 0;
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
return {
|
|
1630
|
+
rowCount: Number(row?.rowCount ?? 0),
|
|
1631
|
+
tokenInput: parseBigIntish(String(row?.tokenInput ?? "0")),
|
|
1632
|
+
tokenOutput: parseBigIntish(String(row?.tokenOutput ?? "0")),
|
|
1633
|
+
usdTotal: String(row?.usdTotal ?? "0").trim() || "0"
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1549
1637
|
export async function listHeartbeatRuns(db: BopoDb, companyId: string, limit = 100) {
|
|
1550
1638
|
return db
|
|
1551
1639
|
.select()
|
package/src/schema.ts
CHANGED
|
@@ -121,6 +121,75 @@ export const issues = pgTable("issues", {
|
|
|
121
121
|
externalLink: text("external_link"),
|
|
122
122
|
isClaimed: boolean("is_claimed").notNull().default(false),
|
|
123
123
|
claimedByHeartbeatRunId: text("claimed_by_heartbeat_run_id"),
|
|
124
|
+
/** Set when issue was created by a scheduled/manual work loop run (FK enforced in SQL migration). */
|
|
125
|
+
loopId: text("loop_id"),
|
|
126
|
+
loopRunId: text("loop_run_id"),
|
|
127
|
+
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
|
128
|
+
updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const workLoops = pgTable("work_loops", {
|
|
132
|
+
id: text("id").primaryKey(),
|
|
133
|
+
companyId: text("company_id")
|
|
134
|
+
.notNull()
|
|
135
|
+
.references(() => companies.id, { onDelete: "cascade" }),
|
|
136
|
+
projectId: text("project_id")
|
|
137
|
+
.notNull()
|
|
138
|
+
.references(() => projects.id, { onDelete: "cascade" }),
|
|
139
|
+
parentIssueId: text("parent_issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
140
|
+
goalIdsJson: text("goal_ids_json").notNull().default("[]"),
|
|
141
|
+
title: text("title").notNull(),
|
|
142
|
+
description: text("description"),
|
|
143
|
+
assigneeAgentId: text("assignee_agent_id")
|
|
144
|
+
.notNull()
|
|
145
|
+
.references(() => agents.id, { onDelete: "restrict" }),
|
|
146
|
+
priority: text("priority").notNull().default("medium"),
|
|
147
|
+
status: text("status").notNull().default("active"),
|
|
148
|
+
concurrencyPolicy: text("concurrency_policy").notNull().default("coalesce_if_active"),
|
|
149
|
+
catchUpPolicy: text("catch_up_policy").notNull().default("skip_missed"),
|
|
150
|
+
lastTriggeredAt: timestamp("last_triggered_at", { mode: "date" }),
|
|
151
|
+
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
|
152
|
+
updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export const workLoopTriggers = pgTable("work_loop_triggers", {
|
|
156
|
+
id: text("id").primaryKey(),
|
|
157
|
+
companyId: text("company_id")
|
|
158
|
+
.notNull()
|
|
159
|
+
.references(() => companies.id, { onDelete: "cascade" }),
|
|
160
|
+
workLoopId: text("work_loop_id")
|
|
161
|
+
.notNull()
|
|
162
|
+
.references(() => workLoops.id, { onDelete: "cascade" }),
|
|
163
|
+
kind: text("kind").notNull().default("schedule"),
|
|
164
|
+
label: text("label"),
|
|
165
|
+
enabled: boolean("enabled").notNull().default(true),
|
|
166
|
+
cronExpression: text("cron_expression").notNull(),
|
|
167
|
+
timezone: text("timezone").notNull().default("UTC"),
|
|
168
|
+
nextRunAt: timestamp("next_run_at", { mode: "date" }),
|
|
169
|
+
lastFiredAt: timestamp("last_fired_at", { mode: "date" }),
|
|
170
|
+
lastResult: text("last_result"),
|
|
171
|
+
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
|
172
|
+
updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export const workLoopRuns = pgTable("work_loop_runs", {
|
|
176
|
+
id: text("id").primaryKey(),
|
|
177
|
+
companyId: text("company_id")
|
|
178
|
+
.notNull()
|
|
179
|
+
.references(() => companies.id, { onDelete: "cascade" }),
|
|
180
|
+
workLoopId: text("work_loop_id")
|
|
181
|
+
.notNull()
|
|
182
|
+
.references(() => workLoops.id, { onDelete: "cascade" }),
|
|
183
|
+
triggerId: text("trigger_id").references(() => workLoopTriggers.id, { onDelete: "set null" }),
|
|
184
|
+
source: text("source").notNull(),
|
|
185
|
+
status: text("status").notNull().default("received"),
|
|
186
|
+
triggeredAt: timestamp("triggered_at", { mode: "date" }).defaultNow().notNull(),
|
|
187
|
+
idempotencyKey: text("idempotency_key"),
|
|
188
|
+
payloadJson: text("payload_json").notNull().default("{}"),
|
|
189
|
+
linkedIssueId: text("linked_issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
190
|
+
coalescedIntoRunId: text("coalesced_into_run_id"),
|
|
191
|
+
failureReason: text("failure_reason"),
|
|
192
|
+
completedAt: timestamp("completed_at", { mode: "date" }),
|
|
124
193
|
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
|
125
194
|
updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
|
|
126
195
|
});
|
|
@@ -297,6 +366,29 @@ export const attentionInboxStates = pgTable(
|
|
|
297
366
|
(table) => [primaryKey({ columns: [table.companyId, table.actorId, table.itemKey] })]
|
|
298
367
|
);
|
|
299
368
|
|
|
369
|
+
export const companyAssistantThreads = pgTable("company_assistant_threads", {
|
|
370
|
+
id: text("id").primaryKey(),
|
|
371
|
+
companyId: text("company_id")
|
|
372
|
+
.notNull()
|
|
373
|
+
.references(() => companies.id, { onDelete: "cascade" }),
|
|
374
|
+
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
|
375
|
+
updatedAt: timestamp("updated_at", { mode: "date" }).defaultNow().notNull()
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
export const companyAssistantMessages = pgTable("company_assistant_messages", {
|
|
379
|
+
id: text("id").primaryKey(),
|
|
380
|
+
threadId: text("thread_id")
|
|
381
|
+
.notNull()
|
|
382
|
+
.references(() => companyAssistantThreads.id, { onDelete: "cascade" }),
|
|
383
|
+
companyId: text("company_id")
|
|
384
|
+
.notNull()
|
|
385
|
+
.references(() => companies.id, { onDelete: "cascade" }),
|
|
386
|
+
role: text("role").notNull(),
|
|
387
|
+
body: text("body").notNull(),
|
|
388
|
+
metadataJson: text("metadata_json"),
|
|
389
|
+
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull()
|
|
390
|
+
});
|
|
391
|
+
|
|
300
392
|
export const costLedger = pgTable("cost_ledger", {
|
|
301
393
|
id: text("id").primaryKey(),
|
|
302
394
|
companyId: text("company_id")
|
|
@@ -306,6 +398,10 @@ export const costLedger = pgTable("cost_ledger", {
|
|
|
306
398
|
projectId: text("project_id").references(() => projects.id, { onDelete: "set null" }),
|
|
307
399
|
issueId: text("issue_id").references(() => issues.id, { onDelete: "set null" }),
|
|
308
400
|
agentId: text("agent_id").references(() => agents.id, { onDelete: "set null" }),
|
|
401
|
+
/** e.g. `company_assistant` for owner-assistant chat; null for heartbeat and legacy rows */
|
|
402
|
+
costCategory: text("cost_category"),
|
|
403
|
+
assistantThreadId: text("assistant_thread_id").references(() => companyAssistantThreads.id, { onDelete: "set null" }),
|
|
404
|
+
assistantMessageId: text("assistant_message_id").references(() => companyAssistantMessages.id, { onDelete: "set null" }),
|
|
309
405
|
providerType: text("provider_type").notNull(),
|
|
310
406
|
runtimeModelId: text("runtime_model_id"),
|
|
311
407
|
pricingProviderType: text("pricing_provider_type"),
|
|
@@ -445,6 +541,9 @@ export const schema = {
|
|
|
445
541
|
goals,
|
|
446
542
|
agents,
|
|
447
543
|
issues,
|
|
544
|
+
workLoops,
|
|
545
|
+
workLoopTriggers,
|
|
546
|
+
workLoopRuns,
|
|
448
547
|
issueGoals,
|
|
449
548
|
issueComments,
|
|
450
549
|
issueAttachments,
|
|
@@ -464,7 +563,9 @@ export const schema = {
|
|
|
464
563
|
templateVersions,
|
|
465
564
|
templateInstalls,
|
|
466
565
|
agentIssueLabels,
|
|
467
|
-
projectWorkspaces
|
|
566
|
+
projectWorkspaces,
|
|
567
|
+
companyAssistantThreads,
|
|
568
|
+
companyAssistantMessages
|
|
468
569
|
};
|
|
469
570
|
|
|
470
571
|
export const touchUpdatedAtSql = sql`CURRENT_TIMESTAMP`;
|