@saadnvd1/agent-os 0.3.555 → 0.3.557
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/README.md +1 -0
- package/app/api/experiments/[id]/route.ts +39 -0
- package/app/api/experiments/route.test.ts +51 -0
- package/app/api/experiments/route.ts +7 -0
- package/app/api/sessions/[id]/done/route.test.ts +70 -0
- package/app/api/sessions/[id]/done/route.ts +32 -6
- package/components/SessionList/useDoneAction.test.ts +149 -0
- package/components/SessionList/useDoneAction.ts +17 -12
- package/components/Settings/ExperimentsSection.test.ts +89 -0
- package/components/Settings/ExperimentsSection.tsx +225 -0
- package/components/Settings/SettingsDialog.test.ts +3 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Settings/commands.ts +9 -0
- package/components/Settings/entry-points.test.ts +1 -0
- package/data/done/index.ts +33 -7
- package/data/experiments/index.ts +67 -0
- package/lib/db/migrations.ts +33 -0
- package/lib/done/deadline.ts +45 -0
- package/lib/done/index.ts +6 -1
- package/lib/experiments/hooks.ts +31 -0
- package/lib/experiments/index.ts +19 -0
- package/lib/experiments/meter.ts +327 -0
- package/lib/experiments/registry.ts +37 -0
- package/lib/experiments/view.ts +19 -0
- package/lib/recipes/settle.ts +4 -1
- package/lib/settings-store.ts +22 -0
- package/lib/settings-url.ts +1 -0
- package/lib/tasks/finish.ts +2 -0
- package/lib/tasks/session.ts +2 -0
- package/lib/updates/settings.ts +2 -20
- package/package.json +1 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What experiments cost and whether they earn it. Every model call an
|
|
3
|
+
* experiment makes goes through experimentRun, which records it here and in
|
|
4
|
+
* the usage ledger; non-model work (scripts, suggestions shown, accepted,
|
|
5
|
+
* dismissed) is counted with countEvent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { db } from "../db";
|
|
9
|
+
import {
|
|
10
|
+
runClaude,
|
|
11
|
+
type ClaudeRun,
|
|
12
|
+
type ClaudeRunner,
|
|
13
|
+
} from "../orchestrator/claude-cli";
|
|
14
|
+
import type { RunUsage } from "../usage/check-runs";
|
|
15
|
+
import { recordLooseCall } from "../usage/calls";
|
|
16
|
+
import { dayKey } from "../usage/aggregate";
|
|
17
|
+
import { experimentOn } from ".";
|
|
18
|
+
import { getExperiment, type ExperimentId } from "./registry";
|
|
19
|
+
|
|
20
|
+
export type ExperimentEvent =
|
|
21
|
+
| "model"
|
|
22
|
+
| "script"
|
|
23
|
+
| "shown"
|
|
24
|
+
| "accepted"
|
|
25
|
+
| "dismissed"
|
|
26
|
+
| "applied"
|
|
27
|
+
| "error";
|
|
28
|
+
|
|
29
|
+
export interface MeterContext {
|
|
30
|
+
trigger: string;
|
|
31
|
+
sessionId?: string | null;
|
|
32
|
+
workspaceId?: string | null;
|
|
33
|
+
detail?: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const clip = (s: string | null | undefined, n = 120) =>
|
|
37
|
+
s ? s.slice(0, n) : null;
|
|
38
|
+
|
|
39
|
+
function insert(
|
|
40
|
+
id: ExperimentId,
|
|
41
|
+
event: ExperimentEvent,
|
|
42
|
+
ctx: MeterContext,
|
|
43
|
+
u?: RunUsage,
|
|
44
|
+
ok = true,
|
|
45
|
+
at = Date.now()
|
|
46
|
+
): number {
|
|
47
|
+
const info = db
|
|
48
|
+
.prepare(
|
|
49
|
+
`INSERT INTO experiment_usage (experiment_id, event, trigger, session_id, workspace_id, model, provider,
|
|
50
|
+
input_tokens, cache_write_tokens, cache_read_tokens, output_tokens, cost_usd, duration_ms, ok, detail, at)
|
|
51
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
52
|
+
)
|
|
53
|
+
.run(
|
|
54
|
+
id,
|
|
55
|
+
event,
|
|
56
|
+
ctx.trigger,
|
|
57
|
+
ctx.sessionId ?? null,
|
|
58
|
+
ctx.workspaceId ?? null,
|
|
59
|
+
u?.model ?? null,
|
|
60
|
+
u ? (u.provider ?? "anthropic") : null,
|
|
61
|
+
u?.inputTokens ?? 0,
|
|
62
|
+
u?.cacheWriteTokens ?? 0,
|
|
63
|
+
u?.cacheReadTokens ?? 0,
|
|
64
|
+
u?.outputTokens ?? 0,
|
|
65
|
+
u?.costUsd ?? 0,
|
|
66
|
+
u?.durationMs ?? null,
|
|
67
|
+
ok ? 1 : 0,
|
|
68
|
+
clip(ctx.detail),
|
|
69
|
+
at
|
|
70
|
+
);
|
|
71
|
+
return Number(info.lastInsertRowid);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function recordModel(id: ExperimentId, ctx: MeterContext, u: RunUsage) {
|
|
75
|
+
try {
|
|
76
|
+
const rowId = insert(id, "model", ctx, u);
|
|
77
|
+
recordLooseCall({
|
|
78
|
+
callKey: `experiment:${rowId}`,
|
|
79
|
+
at: Date.now(),
|
|
80
|
+
sessionId: ctx.sessionId ?? null,
|
|
81
|
+
taskId: null,
|
|
82
|
+
workspaceId: ctx.workspaceId ?? null,
|
|
83
|
+
projectId: null,
|
|
84
|
+
sessionName: getExperiment(id).name,
|
|
85
|
+
role: `experiment:${id}`,
|
|
86
|
+
kind: "helper",
|
|
87
|
+
agentType: null,
|
|
88
|
+
model: u.model,
|
|
89
|
+
provider: u.provider ?? "anthropic",
|
|
90
|
+
input: u.inputTokens,
|
|
91
|
+
cacheRead: u.cacheReadTokens,
|
|
92
|
+
cacheWrite5m: u.cacheWriteTokens,
|
|
93
|
+
cacheWrite1h: 0,
|
|
94
|
+
output: u.outputTokens,
|
|
95
|
+
source: "helper",
|
|
96
|
+
});
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.error(`[experiments] recording ${id}:`, e);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The only way an experiment calls a model. Returns null, having started
|
|
104
|
+
* nothing, when the experiment is off. A failed run is recorded too (its
|
|
105
|
+
* usage, or an error row when it reported none) and then thrown.
|
|
106
|
+
*/
|
|
107
|
+
export async function experimentRun(
|
|
108
|
+
id: ExperimentId,
|
|
109
|
+
ctx: MeterContext,
|
|
110
|
+
run: ClaudeRun,
|
|
111
|
+
runner: ClaudeRunner = runClaude
|
|
112
|
+
): Promise<unknown | null> {
|
|
113
|
+
if (!experimentOn(id)) return null;
|
|
114
|
+
let usage: RunUsage | null = null;
|
|
115
|
+
try {
|
|
116
|
+
const out = await runner({
|
|
117
|
+
...run,
|
|
118
|
+
onUsage: (u) => {
|
|
119
|
+
usage = u;
|
|
120
|
+
run.onUsage?.(u);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
if (usage) recordModel(id, ctx, usage);
|
|
124
|
+
return out;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
if (usage) recordModel(id, ctx, usage);
|
|
128
|
+
insert(
|
|
129
|
+
id,
|
|
130
|
+
"error",
|
|
131
|
+
{
|
|
132
|
+
...ctx,
|
|
133
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
134
|
+
},
|
|
135
|
+
undefined,
|
|
136
|
+
false
|
|
137
|
+
);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
console.error(`[experiments] recording ${id}:`, e);
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Non-model work. Does nothing when the experiment is off, unless `force`:
|
|
147
|
+
* a person deciding on a card that is already on screen is still counted.
|
|
148
|
+
*/
|
|
149
|
+
export function countEvent(
|
|
150
|
+
id: ExperimentId,
|
|
151
|
+
event: Exclude<ExperimentEvent, "model">,
|
|
152
|
+
ctx: MeterContext,
|
|
153
|
+
force = false
|
|
154
|
+
): void {
|
|
155
|
+
if (!force && !experimentOn(id)) return;
|
|
156
|
+
try {
|
|
157
|
+
insert(id, event, ctx);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.error(`[experiments] counting ${id}:`, e);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Whether this session already has this event, so a hook runs once.
|
|
164
|
+
export function hasEvent(
|
|
165
|
+
id: ExperimentId,
|
|
166
|
+
sessionId: string,
|
|
167
|
+
event: ExperimentEvent,
|
|
168
|
+
trigger?: string
|
|
169
|
+
): boolean {
|
|
170
|
+
return !!db
|
|
171
|
+
.prepare(
|
|
172
|
+
`SELECT 1 FROM experiment_usage WHERE experiment_id = ? AND session_id = ? AND event = ?
|
|
173
|
+
AND (? IS NULL OR trigger = ?) LIMIT 1`
|
|
174
|
+
)
|
|
175
|
+
.get(id, sessionId, event, trigger ?? null, trigger ?? null);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface WindowTotals {
|
|
179
|
+
calls: number;
|
|
180
|
+
tokens: number;
|
|
181
|
+
costUsd: number;
|
|
182
|
+
scripts: number;
|
|
183
|
+
shown: number;
|
|
184
|
+
accepted: number;
|
|
185
|
+
dismissed: number;
|
|
186
|
+
// accepted / (accepted + dismissed); null with no decisions yet.
|
|
187
|
+
acceptRate: number | null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface UsageSummary {
|
|
191
|
+
today: WindowTotals;
|
|
192
|
+
d7: WindowTotals;
|
|
193
|
+
d30: WindowTotals;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const DAY = 86_400_000;
|
|
197
|
+
|
|
198
|
+
export function startOfDay(now: number): number {
|
|
199
|
+
const d = new Date(now);
|
|
200
|
+
d.setHours(0, 0, 0, 0);
|
|
201
|
+
return d.getTime();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function windowTotals(id: ExperimentId, since: number): WindowTotals {
|
|
205
|
+
const r = db
|
|
206
|
+
.prepare(
|
|
207
|
+
`SELECT
|
|
208
|
+
coalesce(sum(event = 'model'), 0) AS calls,
|
|
209
|
+
coalesce(sum(input_tokens + cache_write_tokens + cache_read_tokens + output_tokens), 0) AS tokens,
|
|
210
|
+
coalesce(sum(cost_usd), 0) AS costUsd,
|
|
211
|
+
coalesce(sum(event = 'script'), 0) AS scripts,
|
|
212
|
+
coalesce(sum(event = 'shown'), 0) AS shown,
|
|
213
|
+
coalesce(sum(event = 'accepted'), 0) AS accepted,
|
|
214
|
+
coalesce(sum(event = 'dismissed'), 0) AS dismissed
|
|
215
|
+
FROM experiment_usage WHERE experiment_id = ? AND at >= ?`
|
|
216
|
+
)
|
|
217
|
+
.get(id, since) as Omit<WindowTotals, "acceptRate">;
|
|
218
|
+
const decided = r.accepted + r.dismissed;
|
|
219
|
+
return { ...r, acceptRate: decided ? r.accepted / decided : null };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function usageSummary(id: ExperimentId, now = Date.now()): UsageSummary {
|
|
223
|
+
const today = startOfDay(now);
|
|
224
|
+
return {
|
|
225
|
+
today: windowTotals(id, today),
|
|
226
|
+
d7: windowTotals(id, today - 6 * DAY),
|
|
227
|
+
d30: windowTotals(id, today - 29 * DAY),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface DayRow {
|
|
232
|
+
day: string;
|
|
233
|
+
calls: number;
|
|
234
|
+
tokens: number;
|
|
235
|
+
costUsd: number;
|
|
236
|
+
accepted: number;
|
|
237
|
+
dismissed: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface Breakdown {
|
|
241
|
+
key: string;
|
|
242
|
+
calls: number;
|
|
243
|
+
tokens: number;
|
|
244
|
+
costUsd: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface RecentEvent {
|
|
248
|
+
at: number;
|
|
249
|
+
event: string;
|
|
250
|
+
trigger: string;
|
|
251
|
+
model: string | null;
|
|
252
|
+
tokens: number;
|
|
253
|
+
costUsd: number;
|
|
254
|
+
ok: boolean;
|
|
255
|
+
detail: string | null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface UsageDetail {
|
|
259
|
+
days: DayRow[];
|
|
260
|
+
byTrigger: Breakdown[];
|
|
261
|
+
byModel: Breakdown[];
|
|
262
|
+
recent: RecentEvent[];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function usageDetail(id: ExperimentId, now = Date.now()): UsageDetail {
|
|
266
|
+
const since = startOfDay(now) - 29 * DAY;
|
|
267
|
+
const rows = db
|
|
268
|
+
.prepare(
|
|
269
|
+
`SELECT at, event, trigger, model, cost_usd AS costUsd,
|
|
270
|
+
input_tokens + cache_write_tokens + cache_read_tokens + output_tokens AS tokens
|
|
271
|
+
FROM experiment_usage WHERE experiment_id = ? AND at >= ?`
|
|
272
|
+
)
|
|
273
|
+
.all(id, since) as {
|
|
274
|
+
at: number;
|
|
275
|
+
event: string;
|
|
276
|
+
trigger: string;
|
|
277
|
+
model: string | null;
|
|
278
|
+
costUsd: number;
|
|
279
|
+
tokens: number;
|
|
280
|
+
}[];
|
|
281
|
+
const days = new Map<string, DayRow>();
|
|
282
|
+
for (let i = 29; i >= 0; i--) {
|
|
283
|
+
const day = dayKey(startOfDay(now) - i * DAY);
|
|
284
|
+
days.set(day, {
|
|
285
|
+
day,
|
|
286
|
+
calls: 0,
|
|
287
|
+
tokens: 0,
|
|
288
|
+
costUsd: 0,
|
|
289
|
+
accepted: 0,
|
|
290
|
+
dismissed: 0,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const trig = new Map<string, Breakdown>();
|
|
294
|
+
const mod = new Map<string, Breakdown>();
|
|
295
|
+
const add = (m: Map<string, Breakdown>, key: string, r: (typeof rows)[0]) => {
|
|
296
|
+
const b = m.get(key) ?? { key, calls: 0, tokens: 0, costUsd: 0 };
|
|
297
|
+
b.calls += r.event === "model" ? 1 : 0;
|
|
298
|
+
b.tokens += r.tokens;
|
|
299
|
+
b.costUsd += r.costUsd;
|
|
300
|
+
m.set(key, b);
|
|
301
|
+
};
|
|
302
|
+
for (const r of rows) {
|
|
303
|
+
const d = days.get(dayKey(r.at));
|
|
304
|
+
if (d) {
|
|
305
|
+
d.calls += r.event === "model" ? 1 : 0;
|
|
306
|
+
d.tokens += r.tokens;
|
|
307
|
+
d.costUsd += r.costUsd;
|
|
308
|
+
d.accepted += r.event === "accepted" ? 1 : 0;
|
|
309
|
+
d.dismissed += r.event === "dismissed" ? 1 : 0;
|
|
310
|
+
}
|
|
311
|
+
add(trig, r.trigger, r);
|
|
312
|
+
if (r.event === "model") add(mod, r.model ?? "unrecorded", r);
|
|
313
|
+
}
|
|
314
|
+
const recent = db
|
|
315
|
+
.prepare(
|
|
316
|
+
`SELECT at, event, trigger, model, input_tokens + cache_write_tokens + cache_read_tokens + output_tokens AS tokens,
|
|
317
|
+
cost_usd AS costUsd, ok, detail
|
|
318
|
+
FROM experiment_usage WHERE experiment_id = ? ORDER BY at DESC, id DESC LIMIT 50`
|
|
319
|
+
)
|
|
320
|
+
.all(id) as (Omit<RecentEvent, "ok"> & { ok: number })[];
|
|
321
|
+
return {
|
|
322
|
+
days: [...days.values()],
|
|
323
|
+
byTrigger: [...trig.values()],
|
|
324
|
+
byModel: [...mod.values()],
|
|
325
|
+
recent: recent.map((r) => ({ ...r, ok: !!r.ok })),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Every experiment AgentOS knows about. All are off until someone turns them
|
|
2
|
+
// on. To add one: an entry here, `experimentOn(id)` where it does its work,
|
|
3
|
+
// and `experimentRun(id, ...)` for any model call it makes.
|
|
4
|
+
|
|
5
|
+
export type ExperimentStatus = "experimental" | "beta";
|
|
6
|
+
|
|
7
|
+
export interface Experiment {
|
|
8
|
+
// Kebab-case and stable: it names the setting and keys the usage rows.
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
// Who owns it: "sessions", "tasks", "orchestrator"...
|
|
13
|
+
area: string;
|
|
14
|
+
status: ExperimentStatus;
|
|
15
|
+
// It shows suggestions, so an accept rate means something for it.
|
|
16
|
+
suggests?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const EXPERIMENTS = [
|
|
20
|
+
{
|
|
21
|
+
id: "learning-cards",
|
|
22
|
+
name: "Learning cards",
|
|
23
|
+
description:
|
|
24
|
+
"When a session ends, suggest saving it as a recipe or keeping a lesson, in the one place it belongs.",
|
|
25
|
+
area: "sessions",
|
|
26
|
+
status: "experimental",
|
|
27
|
+
suggests: true,
|
|
28
|
+
},
|
|
29
|
+
] as const satisfies readonly Experiment[];
|
|
30
|
+
|
|
31
|
+
export type ExperimentId = (typeof EXPERIMENTS)[number]["id"];
|
|
32
|
+
|
|
33
|
+
export const isExperimentId = (v: unknown): v is ExperimentId =>
|
|
34
|
+
EXPERIMENTS.some((e) => e.id === v);
|
|
35
|
+
|
|
36
|
+
export const getExperiment = (id: ExperimentId): Experiment =>
|
|
37
|
+
EXPERIMENTS.find((e) => e.id === id)!;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { experimentOn, experimentsLocked } from ".";
|
|
2
|
+
import { EXPERIMENTS, type Experiment, type ExperimentId } from "./registry";
|
|
3
|
+
import { usageSummary, type UsageSummary } from "./meter";
|
|
4
|
+
|
|
5
|
+
export interface ExperimentView extends Experiment {
|
|
6
|
+
on: boolean;
|
|
7
|
+
usage: UsageSummary;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const experimentView = (id: ExperimentId): ExperimentView => ({
|
|
11
|
+
...EXPERIMENTS.find((e) => e.id === id)!,
|
|
12
|
+
on: experimentOn(id),
|
|
13
|
+
usage: usageSummary(id),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const experimentsView = () => ({
|
|
17
|
+
locked: experimentsLocked(),
|
|
18
|
+
experiments: EXPERIMENTS.map((e) => experimentView(e.id)),
|
|
19
|
+
});
|
package/lib/recipes/settle.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { blockedReason } from "../tasks/state";
|
|
|
11
11
|
import { runClaude, type ClaudeRunner } from "../orchestrator/claude-cli";
|
|
12
12
|
import { releasePieces } from "../plans/release";
|
|
13
13
|
import { queueEvent } from "../orchestrator/events";
|
|
14
|
+
import { sessionEnded } from "../experiments/hooks";
|
|
14
15
|
import { untrusted } from "../orchestrator/untrusted";
|
|
15
16
|
import {
|
|
16
17
|
cleanOutputs,
|
|
@@ -172,8 +173,10 @@ function finish(
|
|
|
172
173
|
run.id
|
|
173
174
|
).changes;
|
|
174
175
|
idleSince.delete(run.id);
|
|
175
|
-
if (changed)
|
|
176
|
+
if (changed) {
|
|
176
177
|
tell(run, status, summary, result, session?.pr_number ?? run.pr_number);
|
|
178
|
+
if (run.session_id) void sessionEnded(run.session_id, "recipe-settled");
|
|
179
|
+
}
|
|
177
180
|
return changed === 1;
|
|
178
181
|
}
|
|
179
182
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getDb } from "@/lib/db";
|
|
2
|
+
|
|
3
|
+
// The settings table: a key and a string, shared by every feature.
|
|
4
|
+
export function getSetting(key: string): string | null {
|
|
5
|
+
const row = getDb()
|
|
6
|
+
.prepare(`SELECT value FROM settings WHERE key = ?`)
|
|
7
|
+
.get(key) as { value: string } | undefined;
|
|
8
|
+
return row?.value ?? null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function putSetting(key: string, value: string | null): void {
|
|
12
|
+
if (value === null) {
|
|
13
|
+
getDb().prepare(`DELETE FROM settings WHERE key = ?`).run(key);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
getDb()
|
|
17
|
+
.prepare(
|
|
18
|
+
`INSERT INTO settings (key, value) VALUES (?, ?)
|
|
19
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
20
|
+
)
|
|
21
|
+
.run(key, value);
|
|
22
|
+
}
|
package/lib/settings-url.ts
CHANGED
package/lib/tasks/finish.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { canSignOff } from "./state";
|
|
|
26
26
|
import { forgetPR, getTaskSession, prFor, projectPathFor } from "./session";
|
|
27
27
|
import { dropSessionDatabase } from "../project-config/database";
|
|
28
28
|
import { captureMergedTask } from "../session-logs";
|
|
29
|
+
import { sessionEnded } from "../experiments/hooks";
|
|
29
30
|
|
|
30
31
|
// Never deleted, whatever a setting says.
|
|
31
32
|
export const PROTECTED_BRANCHES = new Set(["main", "master", "HEAD"]);
|
|
@@ -163,6 +164,7 @@ export async function signOffTask(
|
|
|
163
164
|
pushMerged(id);
|
|
164
165
|
// Reads the plan file now: cleanup below removes the worktree.
|
|
165
166
|
void captureMergedTask(id, { head: opts.head ?? pr!.head ?? null });
|
|
167
|
+
void sessionEnded(id, "merged");
|
|
166
168
|
const head = opts.head ?? pr!.head;
|
|
167
169
|
resolveMergedTaskAsks(
|
|
168
170
|
id,
|
package/lib/tasks/session.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { findPRStrict } from "./gh";
|
|
|
16
16
|
import { currentBranch, git } from "../git-exec";
|
|
17
17
|
import { lookupPR } from "./pr-poll";
|
|
18
18
|
import { captureMergedTask } from "../session-logs";
|
|
19
|
+
import { sessionEnded } from "../experiments/hooks";
|
|
19
20
|
import { getOrchestrator } from "../orchestrator/home";
|
|
20
21
|
import {
|
|
21
22
|
pushCiFailed,
|
|
@@ -211,6 +212,7 @@ export async function prFor(
|
|
|
211
212
|
if (pr.state === "MERGED") {
|
|
212
213
|
if (session.pr_status !== "merged") pushMerged(session.id);
|
|
213
214
|
void captureMergedTask(session.id, { head: pr.head });
|
|
215
|
+
void sessionEnded(session.id, "merged");
|
|
214
216
|
}
|
|
215
217
|
}
|
|
216
218
|
// Only an answer just read from GitHub: a cached one may predate a
|
package/lib/updates/settings.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getSetting, putSetting } from "../settings-store";
|
|
2
2
|
|
|
3
3
|
// Whether this server shows the update banner, and the checks behind it.
|
|
4
4
|
// AGENTOS_UPDATE_BANNER=off pins it off, like the network settings.
|
|
@@ -17,22 +17,4 @@ export function bannerEnabled(env: Env = process.env): boolean {
|
|
|
17
17
|
export const setBannerEnabled = (on: boolean) =>
|
|
18
18
|
putSetting(KEY, on ? "1" : "0");
|
|
19
19
|
|
|
20
|
-
export
|
|
21
|
-
const row = getDb()
|
|
22
|
-
.prepare(`SELECT value FROM settings WHERE key = ?`)
|
|
23
|
-
.get(key) as { value: string } | undefined;
|
|
24
|
-
return row?.value ?? null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function putSetting(key: string, value: string | null): void {
|
|
28
|
-
if (value === null) {
|
|
29
|
-
getDb().prepare(`DELETE FROM settings WHERE key = ?`).run(key);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
getDb()
|
|
33
|
-
.prepare(
|
|
34
|
-
`INSERT INTO settings (key, value) VALUES (?, ?)
|
|
35
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
36
|
-
)
|
|
37
|
-
.run(key, value);
|
|
38
|
-
}
|
|
20
|
+
export { getSetting, putSetting } from "../settings-store";
|