@arcadeai/hypotenuse 0.19.0 → 0.21.0
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/hypotenuse.js +664 -269
- package/package.json +1 -1
- package/skills/hypotenuse.md +4 -2
package/dist/hypotenuse.js
CHANGED
|
@@ -343,7 +343,7 @@ import { Command } from "commander";
|
|
|
343
343
|
// package.json
|
|
344
344
|
var package_default = {
|
|
345
345
|
name: "@arcadeai/hypotenuse",
|
|
346
|
-
version: "0.
|
|
346
|
+
version: "0.21.0",
|
|
347
347
|
description: "Hypotenuse \u2014 run coding agents on your machine for Linear agent sessions",
|
|
348
348
|
type: "module",
|
|
349
349
|
license: "Apache-2.0",
|
|
@@ -1097,172 +1097,55 @@ var RunnerLimits = z2.strictObject({
|
|
|
1097
1097
|
agents: z2.record(identifier, cap).optional()
|
|
1098
1098
|
});
|
|
1099
1099
|
// ../protocol/src/complete.ts
|
|
1100
|
-
import { z as z3 } from "zod";
|
|
1101
|
-
var COMPLETE_OUTCOMES = ["completed", "failed", "cancelled"];
|
|
1102
|
-
var CompleteRequest = z3.strictObject({
|
|
1103
|
-
workItemId: z3.number().int().positive(),
|
|
1104
|
-
outcome: z3.enum(COMPLETE_OUTCOMES),
|
|
1105
|
-
error: z3.string().max(4096).nullable().optional(),
|
|
1106
|
-
acpSessionId: z3.string().max(255).nullable().optional(),
|
|
1107
|
-
branch: z3.string().max(255).nullable().optional(),
|
|
1108
|
-
answersConsumed: z3.array(z3.number().int().positive()).max(100).optional().describe("The activity ids of delivered answers the turn took as answers; any other answer becomes the session's next prompt")
|
|
1109
|
-
});
|
|
1110
|
-
var CompleteResponse = z3.looseObject({
|
|
1111
|
-
alreadyTerminal: z3.boolean()
|
|
1112
|
-
});
|
|
1113
|
-
// ../protocol/src/connect.ts
|
|
1114
|
-
import { z as z5 } from "zod";
|
|
1115
|
-
|
|
1116
|
-
// ../protocol/src/heartbeat.ts
|
|
1117
1100
|
import { z as z4 } from "zod";
|
|
1118
|
-
var ACTIVE_STATES = ["starting", "running", "cancelling"];
|
|
1119
|
-
var ActiveItem = z4.strictObject({
|
|
1120
|
-
workItemId: z4.number().int().positive(),
|
|
1121
|
-
agentSessionId: z4.number().int().positive(),
|
|
1122
|
-
role: RunnerRole,
|
|
1123
|
-
agentId: z4.string().min(1).max(64),
|
|
1124
|
-
state: z4.enum(ACTIVE_STATES)
|
|
1125
|
-
});
|
|
1126
|
-
var MachineId = z4.string().regex(/^sha256:[0-9a-f]{1,64}$/, "sha256:<hex>").describe("A stable hash of the OS machine id, `sha256:<hex>`");
|
|
1127
|
-
var HeartbeatRequest = z4.strictObject({
|
|
1128
|
-
protocolVersion: z4.number().int().positive(),
|
|
1129
|
-
daemonVersion: z4.string().trim().min(1).max(64),
|
|
1130
|
-
platform: z4.string().trim().min(1).max(64),
|
|
1131
|
-
hostname: z4.string().trim().min(1).max(256),
|
|
1132
|
-
draining: z4.boolean(),
|
|
1133
|
-
machineId: MachineId,
|
|
1134
|
-
limits: RunnerLimits,
|
|
1135
|
-
active: z4.array(ActiveItem).max(1000),
|
|
1136
|
-
capabilities: RunnerCapabilities,
|
|
1137
|
-
dryRun: z4.boolean().optional().describe("Validate and answer without recording anything: no presence, no capabilities, no leases, no offers")
|
|
1138
|
-
});
|
|
1139
|
-
var DryRunRunner = z4.looseObject({
|
|
1140
|
-
id: z4.number().int().positive(),
|
|
1141
|
-
name: z4.string(),
|
|
1142
|
-
projectId: z4.number().int().positive(),
|
|
1143
|
-
projectSlug: z4.string()
|
|
1144
|
-
});
|
|
1145
|
-
var OFFER_KINDS = [
|
|
1146
|
-
"session_created",
|
|
1147
|
-
"prompt",
|
|
1148
|
-
"dispatch",
|
|
1149
|
-
"review"
|
|
1150
|
-
];
|
|
1151
|
-
var Offer = z4.looseObject({
|
|
1152
|
-
workItemId: z4.number().int().positive(),
|
|
1153
|
-
agentSessionId: z4.number().int().positive(),
|
|
1154
|
-
kind: z4.enum(OFFER_KINDS),
|
|
1155
|
-
role: RunnerRole,
|
|
1156
|
-
offerExpiresAt: z4.number().int()
|
|
1157
|
-
});
|
|
1158
|
-
var CANCEL_REASONS = [
|
|
1159
|
-
"stop",
|
|
1160
|
-
"dashboard",
|
|
1161
|
-
"delegate_removed",
|
|
1162
|
-
"issue_closed",
|
|
1163
|
-
"unassigned",
|
|
1164
|
-
"integration_revoked",
|
|
1165
|
-
"superseded"
|
|
1166
|
-
];
|
|
1167
|
-
var Cancel = z4.looseObject({
|
|
1168
|
-
workItemId: z4.number().int().positive(),
|
|
1169
|
-
reason: z4.union([z4.enum(CANCEL_REASONS), z4.string().min(1).max(64)]),
|
|
1170
|
-
by: z4.string().min(1).max(200).optional(),
|
|
1171
|
-
said: z4.string().min(1).max(2000).optional()
|
|
1172
|
-
});
|
|
1173
|
-
var STOP_QUOTE_MAX = 500;
|
|
1174
|
-
function stopQuote(said) {
|
|
1175
|
-
const text = said?.trim();
|
|
1176
|
-
if (!text || /^stop[.!]*$/i.test(text))
|
|
1177
|
-
return "";
|
|
1178
|
-
const cut = text.length > STOP_QUOTE_MAX ? `${text.slice(0, STOP_QUOTE_MAX)}\u2026` : text;
|
|
1179
|
-
return `
|
|
1180
1101
|
|
|
1181
|
-
> You said: ${cut.split(`
|
|
1182
|
-
`).join(`
|
|
1183
|
-
> `)}`;
|
|
1184
|
-
}
|
|
1185
|
-
var Answer = z4.looseObject({
|
|
1186
|
-
workItemId: z4.number().int().positive(),
|
|
1187
|
-
activityId: z4.number().int().positive(),
|
|
1188
|
-
body: z4.string()
|
|
1189
|
-
});
|
|
1190
|
-
var HeartbeatResponse = z4.looseObject({
|
|
1191
|
-
serverTime: z4.number().int(),
|
|
1192
|
-
heartbeatIntervalMs: z4.number().int().positive(),
|
|
1193
|
-
leaseMs: z4.number().int().positive(),
|
|
1194
|
-
offers: z4.array(Offer),
|
|
1195
|
-
cancels: z4.array(Cancel),
|
|
1196
|
-
unknownActive: z4.array(z4.number().int().positive()),
|
|
1197
|
-
answers: z4.array(Answer).default([]).describe("Replies to questions held turns are waiting on, oldest first"),
|
|
1198
|
-
runner: DryRunRunner.optional().describe("A dry run only: the runner the token is"),
|
|
1199
|
-
unknownSubscriptions: z4.looseObject({ teams: z4.array(z4.string()) }).nullable().optional().describe("A dry run only: subscribed team keys the project's Linear connection cannot see; null when it could not be asked")
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
// ../protocol/src/connect.ts
|
|
1203
|
-
var ConnectRequest = z5.strictObject({
|
|
1204
|
-
machineId: MachineId
|
|
1205
|
-
});
|
|
1206
|
-
var ConnectResponse = z5.looseObject({
|
|
1207
|
-
runnerId: z5.number().int().positive(),
|
|
1208
|
-
channel: z5.string().regex(/^runner:\d+$/),
|
|
1209
|
-
heartbeatIntervalMs: z5.number().int().positive(),
|
|
1210
|
-
leaseMs: z5.number().int().positive(),
|
|
1211
|
-
serverTime: z5.number().int()
|
|
1212
|
-
});
|
|
1213
|
-
var MACHINE_IN_USE = "MACHINE_IN_USE";
|
|
1214
|
-
var RUNNER_FRAME_EVENTS = ["work", "cancel", "revoked"];
|
|
1215
|
-
var RunnerFrame = z5.object({ event: z5.enum(RUNNER_FRAME_EVENTS) });
|
|
1216
|
-
function runnerChannel(runnerId) {
|
|
1217
|
-
return `runner:${runnerId}`;
|
|
1218
|
-
}
|
|
1219
1102
|
// ../protocol/src/events.ts
|
|
1220
|
-
import { z as
|
|
1103
|
+
import { z as z3 } from "zod";
|
|
1221
1104
|
var MAX_EVENTS_PER_BATCH = 100;
|
|
1222
1105
|
var MAX_EVENTS_BATCH_BYTES = 256 * 1024;
|
|
1223
1106
|
var NOT_YOUR_WORK = "NOT_YOUR_WORK";
|
|
1224
1107
|
var SEQ_REGRESSION = "SEQ_REGRESSION";
|
|
1225
|
-
var body =
|
|
1108
|
+
var body = z3.string().trim().min(1).max(64 * 1024);
|
|
1226
1109
|
var envelope = {
|
|
1227
|
-
eventId:
|
|
1228
|
-
seq:
|
|
1229
|
-
at:
|
|
1110
|
+
eventId: z3.uuid({ version: "v4" }).describe("Generated by the daemon; the idempotency key end to end (the Linear activity's client id)"),
|
|
1111
|
+
seq: z3.number().int().positive().describe("Per work item, from 1, never going backwards"),
|
|
1112
|
+
at: z3.number().int().describe("When the daemon saw it, epoch ms")
|
|
1230
1113
|
};
|
|
1231
|
-
var SelectMetadata =
|
|
1232
|
-
options:
|
|
1233
|
-
label:
|
|
1234
|
-
value:
|
|
1114
|
+
var SelectMetadata = z3.strictObject({
|
|
1115
|
+
options: z3.array(z3.strictObject({
|
|
1116
|
+
label: z3.string().trim().min(1).max(255).optional(),
|
|
1117
|
+
value: z3.string().trim().min(1).max(1024)
|
|
1235
1118
|
})).min(1).max(25)
|
|
1236
1119
|
});
|
|
1237
|
-
var AuthProvider =
|
|
1238
|
-
var AuthMetadata =
|
|
1239
|
-
url:
|
|
1120
|
+
var AuthProvider = z3.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/).describe("An agent id, `github`, or a git host");
|
|
1121
|
+
var AuthMetadata = z3.strictObject({
|
|
1122
|
+
url: z3.url().optional(),
|
|
1240
1123
|
provider: AuthProvider.optional(),
|
|
1241
|
-
userId:
|
|
1242
|
-
providerName:
|
|
1124
|
+
userId: z3.string().max(255).optional(),
|
|
1125
|
+
providerName: z3.string().max(255).optional()
|
|
1243
1126
|
}).refine((m) => m.url !== undefined || m.provider !== undefined, {
|
|
1244
1127
|
message: "an auth elicitation needs a url or a provider"
|
|
1245
1128
|
});
|
|
1246
|
-
var ThoughtEvent =
|
|
1129
|
+
var ThoughtEvent = z3.strictObject({
|
|
1247
1130
|
...envelope,
|
|
1248
|
-
type:
|
|
1131
|
+
type: z3.literal("thought"),
|
|
1249
1132
|
body,
|
|
1250
|
-
ephemeral:
|
|
1133
|
+
ephemeral: z3.boolean().default(false)
|
|
1251
1134
|
});
|
|
1252
|
-
var ActionEvent =
|
|
1135
|
+
var ActionEvent = z3.strictObject({
|
|
1253
1136
|
...envelope,
|
|
1254
|
-
type:
|
|
1255
|
-
action:
|
|
1256
|
-
parameter:
|
|
1257
|
-
result:
|
|
1258
|
-
ephemeral:
|
|
1137
|
+
type: z3.literal("action"),
|
|
1138
|
+
action: z3.string().trim().min(1).max(255),
|
|
1139
|
+
parameter: z3.string().trim().min(1).max(4096),
|
|
1140
|
+
result: z3.string().max(64 * 1024).optional(),
|
|
1141
|
+
ephemeral: z3.boolean().default(false)
|
|
1259
1142
|
});
|
|
1260
|
-
var ElicitationEvent =
|
|
1143
|
+
var ElicitationEvent = z3.strictObject({
|
|
1261
1144
|
...envelope,
|
|
1262
|
-
type:
|
|
1145
|
+
type: z3.literal("elicitation"),
|
|
1263
1146
|
body,
|
|
1264
|
-
signal:
|
|
1265
|
-
signalMetadata:
|
|
1147
|
+
signal: z3.enum(["select", "auth"]).optional(),
|
|
1148
|
+
signalMetadata: z3.record(z3.string(), z3.unknown()).optional()
|
|
1266
1149
|
}).superRefine((event, ctx) => {
|
|
1267
1150
|
if (!event.signal) {
|
|
1268
1151
|
if (event.signalMetadata) {
|
|
@@ -1284,14 +1167,14 @@ var ElicitationEvent = z6.strictObject({
|
|
|
1284
1167
|
});
|
|
1285
1168
|
}
|
|
1286
1169
|
});
|
|
1287
|
-
var ResponseEvent =
|
|
1170
|
+
var ResponseEvent = z3.strictObject({
|
|
1288
1171
|
...envelope,
|
|
1289
|
-
type:
|
|
1172
|
+
type: z3.literal("response"),
|
|
1290
1173
|
body
|
|
1291
1174
|
});
|
|
1292
|
-
var ErrorEvent =
|
|
1175
|
+
var ErrorEvent = z3.strictObject({
|
|
1293
1176
|
...envelope,
|
|
1294
|
-
type:
|
|
1177
|
+
type: z3.literal("error"),
|
|
1295
1178
|
body
|
|
1296
1179
|
});
|
|
1297
1180
|
var PLAN_ENTRY_STATUSES = [
|
|
@@ -1300,33 +1183,36 @@ var PLAN_ENTRY_STATUSES = [
|
|
|
1300
1183
|
"completed",
|
|
1301
1184
|
"canceled"
|
|
1302
1185
|
];
|
|
1303
|
-
var PlanEvent =
|
|
1186
|
+
var PlanEvent = z3.strictObject({
|
|
1304
1187
|
...envelope,
|
|
1305
|
-
type:
|
|
1306
|
-
entries:
|
|
1307
|
-
content:
|
|
1308
|
-
status:
|
|
1188
|
+
type: z3.literal("plan"),
|
|
1189
|
+
entries: z3.array(z3.strictObject({
|
|
1190
|
+
content: z3.string().trim().min(1).max(1024),
|
|
1191
|
+
status: z3.enum(PLAN_ENTRY_STATUSES)
|
|
1309
1192
|
})).max(100)
|
|
1310
1193
|
});
|
|
1311
|
-
var SummaryEvent =
|
|
1194
|
+
var SummaryEvent = z3.strictObject({
|
|
1312
1195
|
...envelope,
|
|
1313
|
-
type:
|
|
1314
|
-
title:
|
|
1196
|
+
type: z3.literal("summary"),
|
|
1197
|
+
title: z3.string().min(1).max(255).regex(/^[^\r\n\0]*$/, "one line").refine((t) => t.trim().length > 0, "not blank")
|
|
1315
1198
|
});
|
|
1316
|
-
var ExternalUrlEvent =
|
|
1199
|
+
var ExternalUrlEvent = z3.strictObject({
|
|
1317
1200
|
...envelope,
|
|
1318
|
-
type:
|
|
1319
|
-
label:
|
|
1320
|
-
url:
|
|
1201
|
+
type: z3.literal("external_url"),
|
|
1202
|
+
label: z3.string().trim().min(1).max(255),
|
|
1203
|
+
url: z3.url()
|
|
1321
1204
|
});
|
|
1322
|
-
var
|
|
1205
|
+
var TurnUsage = z3.strictObject({
|
|
1206
|
+
inputTokens: z3.number().int().min(0).optional(),
|
|
1207
|
+
outputTokens: z3.number().int().min(0).optional(),
|
|
1208
|
+
costUsd: z3.number().min(0).optional().describe("This turn's cost in US dollars, as the agent reported it")
|
|
1209
|
+
});
|
|
1210
|
+
var UsageEvent = z3.strictObject({
|
|
1323
1211
|
...envelope,
|
|
1324
|
-
type:
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
contextUsed: z6.number().int().min(0).optional(),
|
|
1329
|
-
contextSize: z6.number().int().min(0).optional()
|
|
1212
|
+
type: z3.literal("usage"),
|
|
1213
|
+
...TurnUsage.shape,
|
|
1214
|
+
contextUsed: z3.number().int().min(0).optional(),
|
|
1215
|
+
contextSize: z3.number().int().min(0).optional()
|
|
1330
1216
|
});
|
|
1331
1217
|
var STATUS_EVENT_STATES = [
|
|
1332
1218
|
"starting",
|
|
@@ -1334,12 +1220,12 @@ var STATUS_EVENT_STATES = [
|
|
|
1334
1220
|
"waiting_permission",
|
|
1335
1221
|
"finishing"
|
|
1336
1222
|
];
|
|
1337
|
-
var StatusEvent =
|
|
1223
|
+
var StatusEvent = z3.strictObject({
|
|
1338
1224
|
...envelope,
|
|
1339
|
-
type:
|
|
1340
|
-
state:
|
|
1225
|
+
type: z3.literal("status"),
|
|
1226
|
+
state: z3.enum(STATUS_EVENT_STATES)
|
|
1341
1227
|
});
|
|
1342
|
-
var SessionEvent =
|
|
1228
|
+
var SessionEvent = z3.discriminatedUnion("type", [
|
|
1343
1229
|
ThoughtEvent,
|
|
1344
1230
|
ActionEvent,
|
|
1345
1231
|
ElicitationEvent,
|
|
@@ -1351,16 +1237,137 @@ var SessionEvent = z6.discriminatedUnion("type", [
|
|
|
1351
1237
|
UsageEvent,
|
|
1352
1238
|
StatusEvent
|
|
1353
1239
|
]);
|
|
1354
|
-
var EventsRequest =
|
|
1355
|
-
workItemId:
|
|
1356
|
-
events:
|
|
1240
|
+
var EventsRequest = z3.strictObject({
|
|
1241
|
+
workItemId: z3.number().int().positive(),
|
|
1242
|
+
events: z3.array(SessionEvent).min(1).max(MAX_EVENTS_PER_BATCH)
|
|
1243
|
+
});
|
|
1244
|
+
var EventsResponse = z3.looseObject({
|
|
1245
|
+
accepted: z3.number().int().min(0),
|
|
1246
|
+
duplicates: z3.number().int().min(0),
|
|
1247
|
+
dropped: z3.number().int().min(0),
|
|
1248
|
+
cancelRequested: z3.boolean()
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// ../protocol/src/complete.ts
|
|
1252
|
+
var COMPLETE_OUTCOMES = ["completed", "failed", "cancelled"];
|
|
1253
|
+
var CompleteRequest = z4.strictObject({
|
|
1254
|
+
workItemId: z4.number().int().positive(),
|
|
1255
|
+
outcome: z4.enum(COMPLETE_OUTCOMES),
|
|
1256
|
+
error: z4.string().max(4096).nullable().optional(),
|
|
1257
|
+
acpSessionId: z4.string().max(255).nullable().optional(),
|
|
1258
|
+
branch: z4.string().max(255).nullable().optional(),
|
|
1259
|
+
answersConsumed: z4.array(z4.number().int().positive()).max(100).optional().describe("The activity ids of delivered answers the turn took as answers; any other answer becomes the session's next prompt"),
|
|
1260
|
+
usage: TurnUsage.optional().describe("What the turn spent, in full; recorded even when the turn was stopped and its events were dropped")
|
|
1261
|
+
});
|
|
1262
|
+
var CompleteResponse = z4.looseObject({
|
|
1263
|
+
alreadyTerminal: z4.boolean()
|
|
1264
|
+
});
|
|
1265
|
+
// ../protocol/src/connect.ts
|
|
1266
|
+
import { z as z6 } from "zod";
|
|
1267
|
+
|
|
1268
|
+
// ../protocol/src/heartbeat.ts
|
|
1269
|
+
import { z as z5 } from "zod";
|
|
1270
|
+
var ACTIVE_STATES = ["starting", "running", "cancelling"];
|
|
1271
|
+
var ActiveItem = z5.strictObject({
|
|
1272
|
+
workItemId: z5.number().int().positive(),
|
|
1273
|
+
agentSessionId: z5.number().int().positive(),
|
|
1274
|
+
role: RunnerRole,
|
|
1275
|
+
agentId: z5.string().min(1).max(64),
|
|
1276
|
+
state: z5.enum(ACTIVE_STATES)
|
|
1277
|
+
});
|
|
1278
|
+
var MachineId = z5.string().regex(/^sha256:[0-9a-f]{1,64}$/, "sha256:<hex>").describe("A stable hash of the OS machine id, `sha256:<hex>`");
|
|
1279
|
+
var HeartbeatRequest = z5.strictObject({
|
|
1280
|
+
protocolVersion: z5.number().int().positive(),
|
|
1281
|
+
daemonVersion: z5.string().trim().min(1).max(64),
|
|
1282
|
+
platform: z5.string().trim().min(1).max(64),
|
|
1283
|
+
hostname: z5.string().trim().min(1).max(256),
|
|
1284
|
+
draining: z5.boolean(),
|
|
1285
|
+
machineId: MachineId,
|
|
1286
|
+
limits: RunnerLimits,
|
|
1287
|
+
active: z5.array(ActiveItem).max(1000),
|
|
1288
|
+
capabilities: RunnerCapabilities,
|
|
1289
|
+
dryRun: z5.boolean().optional().describe("Validate and answer without recording anything: no presence, no capabilities, no leases, no offers")
|
|
1290
|
+
});
|
|
1291
|
+
var DryRunRunner = z5.looseObject({
|
|
1292
|
+
id: z5.number().int().positive(),
|
|
1293
|
+
name: z5.string(),
|
|
1294
|
+
projectId: z5.number().int().positive(),
|
|
1295
|
+
projectSlug: z5.string()
|
|
1296
|
+
});
|
|
1297
|
+
var OFFER_KINDS = [
|
|
1298
|
+
"session_created",
|
|
1299
|
+
"prompt",
|
|
1300
|
+
"dispatch",
|
|
1301
|
+
"review"
|
|
1302
|
+
];
|
|
1303
|
+
var Offer = z5.looseObject({
|
|
1304
|
+
workItemId: z5.number().int().positive(),
|
|
1305
|
+
agentSessionId: z5.number().int().positive(),
|
|
1306
|
+
kind: z5.enum(OFFER_KINDS),
|
|
1307
|
+
role: RunnerRole,
|
|
1308
|
+
offerExpiresAt: z5.number().int()
|
|
1309
|
+
});
|
|
1310
|
+
var CANCEL_REASONS = [
|
|
1311
|
+
"stop",
|
|
1312
|
+
"dashboard",
|
|
1313
|
+
"delegate_removed",
|
|
1314
|
+
"issue_closed",
|
|
1315
|
+
"unassigned",
|
|
1316
|
+
"integration_revoked",
|
|
1317
|
+
"superseded"
|
|
1318
|
+
];
|
|
1319
|
+
var Cancel = z5.looseObject({
|
|
1320
|
+
workItemId: z5.number().int().positive(),
|
|
1321
|
+
reason: z5.union([z5.enum(CANCEL_REASONS), z5.string().min(1).max(64)]),
|
|
1322
|
+
by: z5.string().min(1).max(200).optional(),
|
|
1323
|
+
said: z5.string().min(1).max(2000).optional()
|
|
1357
1324
|
});
|
|
1358
|
-
var
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1325
|
+
var STOP_QUOTE_MAX = 500;
|
|
1326
|
+
function stopQuote(said) {
|
|
1327
|
+
const text = said?.trim();
|
|
1328
|
+
if (!text || /^stop[.!]*$/i.test(text))
|
|
1329
|
+
return "";
|
|
1330
|
+
const cut = text.length > STOP_QUOTE_MAX ? `${text.slice(0, STOP_QUOTE_MAX)}\u2026` : text;
|
|
1331
|
+
return `
|
|
1332
|
+
|
|
1333
|
+
> You said: ${cut.split(`
|
|
1334
|
+
`).join(`
|
|
1335
|
+
> `)}`;
|
|
1336
|
+
}
|
|
1337
|
+
var Answer = z5.looseObject({
|
|
1338
|
+
workItemId: z5.number().int().positive(),
|
|
1339
|
+
activityId: z5.number().int().positive(),
|
|
1340
|
+
body: z5.string()
|
|
1341
|
+
});
|
|
1342
|
+
var HeartbeatResponse = z5.looseObject({
|
|
1343
|
+
serverTime: z5.number().int(),
|
|
1344
|
+
heartbeatIntervalMs: z5.number().int().positive(),
|
|
1345
|
+
leaseMs: z5.number().int().positive(),
|
|
1346
|
+
offers: z5.array(Offer),
|
|
1347
|
+
cancels: z5.array(Cancel),
|
|
1348
|
+
unknownActive: z5.array(z5.number().int().positive()),
|
|
1349
|
+
answers: z5.array(Answer).default([]).describe("Replies to questions held turns are waiting on, oldest first"),
|
|
1350
|
+
runner: DryRunRunner.optional().describe("A dry run only: the runner the token is"),
|
|
1351
|
+
unknownSubscriptions: z5.looseObject({ teams: z5.array(z5.string()) }).nullable().optional().describe("A dry run only: subscribed team keys the project's Linear connection cannot see; null when it could not be asked")
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
// ../protocol/src/connect.ts
|
|
1355
|
+
var ConnectRequest = z6.strictObject({
|
|
1356
|
+
machineId: MachineId
|
|
1363
1357
|
});
|
|
1358
|
+
var ConnectResponse = z6.looseObject({
|
|
1359
|
+
runnerId: z6.number().int().positive(),
|
|
1360
|
+
channel: z6.string().regex(/^runner:\d+$/),
|
|
1361
|
+
heartbeatIntervalMs: z6.number().int().positive(),
|
|
1362
|
+
leaseMs: z6.number().int().positive(),
|
|
1363
|
+
serverTime: z6.number().int()
|
|
1364
|
+
});
|
|
1365
|
+
var MACHINE_IN_USE = "MACHINE_IN_USE";
|
|
1366
|
+
var RUNNER_FRAME_EVENTS = ["work", "cancel", "revoked"];
|
|
1367
|
+
var RunnerFrame = z6.object({ event: z6.enum(RUNNER_FRAME_EVENTS) });
|
|
1368
|
+
function runnerChannel(runnerId) {
|
|
1369
|
+
return `runner:${runnerId}`;
|
|
1370
|
+
}
|
|
1364
1371
|
// ../protocol/src/otlp.ts
|
|
1365
1372
|
import protobuf from "protobufjs/light.js";
|
|
1366
1373
|
var SPAN_KIND = {
|
|
@@ -3922,9 +3929,9 @@ async function socketAnswers(socketPath, timeoutMs = 1500) {
|
|
|
3922
3929
|
import { spawn as spawn3 } from "child_process";
|
|
3923
3930
|
import {
|
|
3924
3931
|
closeSync as closeSync3,
|
|
3925
|
-
existsSync as
|
|
3932
|
+
existsSync as existsSync18,
|
|
3926
3933
|
openSync as openSync3,
|
|
3927
|
-
readFileSync as
|
|
3934
|
+
readFileSync as readFileSync12,
|
|
3928
3935
|
readSync,
|
|
3929
3936
|
rmSync as rmSync9,
|
|
3930
3937
|
statSync as statSync5
|
|
@@ -4159,8 +4166,8 @@ class DaemonLog {
|
|
|
4159
4166
|
|
|
4160
4167
|
// src/daemon/main.ts
|
|
4161
4168
|
import { spawn as spawn2 } from "child_process";
|
|
4162
|
-
import { createHash as
|
|
4163
|
-
import { existsSync as
|
|
4169
|
+
import { createHash as createHash3 } from "crypto";
|
|
4170
|
+
import { existsSync as existsSync17, readFileSync as readFileSync11, rmSync as rmSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
4164
4171
|
import { join as join10 } from "path";
|
|
4165
4172
|
|
|
4166
4173
|
// src/acp/host.ts
|
|
@@ -5740,6 +5747,11 @@ class TurnMapper {
|
|
|
5740
5747
|
lastPlan = "";
|
|
5741
5748
|
unknown = new Set;
|
|
5742
5749
|
lastActionSummary = null;
|
|
5750
|
+
costBaseline = null;
|
|
5751
|
+
replayedCost = null;
|
|
5752
|
+
costChecked = false;
|
|
5753
|
+
sessionCost = null;
|
|
5754
|
+
usage = {};
|
|
5743
5755
|
recorded = {};
|
|
5744
5756
|
constructor(options) {
|
|
5745
5757
|
this.options = options;
|
|
@@ -5751,8 +5763,14 @@ class TurnMapper {
|
|
|
5751
5763
|
return [...this.messages.values()].some((m) => m.trim());
|
|
5752
5764
|
}
|
|
5753
5765
|
update(update) {
|
|
5754
|
-
if (this.replaying)
|
|
5766
|
+
if (this.replaying) {
|
|
5767
|
+
if (update.sessionUpdate === "usage_update") {
|
|
5768
|
+
const cost = usdCost(update.cost);
|
|
5769
|
+
if (cost !== null)
|
|
5770
|
+
this.replayedCost = cost;
|
|
5771
|
+
}
|
|
5755
5772
|
return [];
|
|
5773
|
+
}
|
|
5756
5774
|
const kind = update.sessionUpdate;
|
|
5757
5775
|
switch (update.sessionUpdate) {
|
|
5758
5776
|
case "agent_thought_chunk": {
|
|
@@ -5838,14 +5856,10 @@ class TurnMapper {
|
|
|
5838
5856
|
return [{ type: "summary", title: truncate(title, 255) }];
|
|
5839
5857
|
}
|
|
5840
5858
|
case "usage_update": {
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
contextSize: Math.max(0, Math.round(update.size)),
|
|
5846
|
-
...update.cost && update.cost.currency.toUpperCase() === "USD" ? { costUsd: Math.max(0, update.cost.amount) } : {}
|
|
5847
|
-
}
|
|
5848
|
-
];
|
|
5859
|
+
this.usage.contextUsed = Math.max(0, Math.round(update.used));
|
|
5860
|
+
this.usage.contextSize = Math.max(0, Math.round(update.size));
|
|
5861
|
+
this.recordCost(usdCost(update.cost));
|
|
5862
|
+
return [{ type: "usage", ...this.usage }];
|
|
5849
5863
|
}
|
|
5850
5864
|
case "current_mode_update":
|
|
5851
5865
|
this.recorded.mode = update.currentModeId;
|
|
@@ -5861,6 +5875,47 @@ class TurnMapper {
|
|
|
5861
5875
|
return [];
|
|
5862
5876
|
}
|
|
5863
5877
|
}
|
|
5878
|
+
acceptReplay() {
|
|
5879
|
+
if (this.replayedCost !== null)
|
|
5880
|
+
this.costBaseline = this.replayedCost;
|
|
5881
|
+
this.replayedCost = null;
|
|
5882
|
+
}
|
|
5883
|
+
recordCost(cumulative) {
|
|
5884
|
+
if (cumulative === null)
|
|
5885
|
+
return;
|
|
5886
|
+
if (!this.costChecked) {
|
|
5887
|
+
this.costChecked = true;
|
|
5888
|
+
if (this.costBaseline !== null && cumulative < this.costBaseline)
|
|
5889
|
+
this.costBaseline = 0;
|
|
5890
|
+
}
|
|
5891
|
+
this.sessionCost = cumulative;
|
|
5892
|
+
if (this.costBaseline !== null)
|
|
5893
|
+
this.usage.costUsd = roundUsd(Math.max(0, cumulative - this.costBaseline));
|
|
5894
|
+
}
|
|
5895
|
+
tokens(tokens) {
|
|
5896
|
+
const count = (n) => typeof n === "number" && Number.isFinite(n) ? Math.max(0, Math.round(n)) : undefined;
|
|
5897
|
+
const input = count(tokens?.inputTokens);
|
|
5898
|
+
const output = count(tokens?.outputTokens);
|
|
5899
|
+
if (input === undefined && output === undefined)
|
|
5900
|
+
return [];
|
|
5901
|
+
if (input !== undefined)
|
|
5902
|
+
this.usage.inputTokens = input;
|
|
5903
|
+
if (output !== undefined)
|
|
5904
|
+
this.usage.outputTokens = output;
|
|
5905
|
+
return [{ type: "usage", ...this.usage }];
|
|
5906
|
+
}
|
|
5907
|
+
turnUsage() {
|
|
5908
|
+
const { inputTokens, outputTokens, costUsd } = this.usage;
|
|
5909
|
+
const usage = {
|
|
5910
|
+
...inputTokens !== undefined ? { inputTokens } : {},
|
|
5911
|
+
...outputTokens !== undefined ? { outputTokens } : {},
|
|
5912
|
+
...costUsd !== undefined ? { costUsd } : {}
|
|
5913
|
+
};
|
|
5914
|
+
return Object.keys(usage).length ? usage : undefined;
|
|
5915
|
+
}
|
|
5916
|
+
sessionCostUsd() {
|
|
5917
|
+
return this.sessionCost ?? this.costBaseline;
|
|
5918
|
+
}
|
|
5864
5919
|
tick() {
|
|
5865
5920
|
if (!this.thought)
|
|
5866
5921
|
return [];
|
|
@@ -5963,6 +6018,14 @@ function callText(call) {
|
|
|
5963
6018
|
return parts.join(`
|
|
5964
6019
|
`);
|
|
5965
6020
|
}
|
|
6021
|
+
function usdCost(cost) {
|
|
6022
|
+
if (!cost || cost.currency.toUpperCase() !== "USD")
|
|
6023
|
+
return null;
|
|
6024
|
+
return Number.isFinite(cost.amount) ? Math.max(0, cost.amount) : null;
|
|
6025
|
+
}
|
|
6026
|
+
function roundUsd(usd) {
|
|
6027
|
+
return Math.round(usd * 1e6) / 1e6;
|
|
6028
|
+
}
|
|
5966
6029
|
|
|
5967
6030
|
// src/acp/mcp-servers.ts
|
|
5968
6031
|
import { isAbsolute as isAbsolute7 } from "path";
|
|
@@ -6706,7 +6769,9 @@ class Turn {
|
|
|
6706
6769
|
outcome,
|
|
6707
6770
|
error: error ? error.slice(0, 4000) : null,
|
|
6708
6771
|
acpSessionId: this.acpSessionId,
|
|
6709
|
-
branch: this.branch
|
|
6772
|
+
branch: this.branch,
|
|
6773
|
+
usage: this.mapper.turnUsage(),
|
|
6774
|
+
acpCostUsd: this.acpSessionId ? this.mapper.sessionCostUsd() : null
|
|
6710
6775
|
};
|
|
6711
6776
|
}
|
|
6712
6777
|
async start() {
|
|
@@ -6881,6 +6946,7 @@ class Turn {
|
|
|
6881
6946
|
const directories = extra.length && caps.sessionCapabilities?.additionalDirectories ? { additionalDirectories: extra } : {};
|
|
6882
6947
|
const mcpServers = this.mcpServers(caps);
|
|
6883
6948
|
this.resumed = false;
|
|
6949
|
+
this.mapper.costBaseline = this.ctx.previousAcpCostUsd ?? null;
|
|
6884
6950
|
if (previousAcpSessionId && !assignment.fresh && assignment.kind === "prompt") {
|
|
6885
6951
|
try {
|
|
6886
6952
|
if (caps.sessionCapabilities?.resume) {
|
|
@@ -6905,6 +6971,7 @@ class Turn {
|
|
|
6905
6971
|
mcpServers,
|
|
6906
6972
|
...directories
|
|
6907
6973
|
}), 0, ""));
|
|
6974
|
+
this.mapper.acceptReplay();
|
|
6908
6975
|
this.resumed = true;
|
|
6909
6976
|
await this.applyMode(answer);
|
|
6910
6977
|
return;
|
|
@@ -6922,6 +6989,7 @@ class Turn {
|
|
|
6922
6989
|
}
|
|
6923
6990
|
const created = await this.otel.around("acp.session.new", { "hypotenuse.mcp.servers": mcpServers.length }, () => this.guard(conn.newSession({ cwd: this.cwd, mcpServers, ...directories }), 0, ""));
|
|
6924
6991
|
this.acpSessionId = created.sessionId;
|
|
6992
|
+
this.mapper.costBaseline = 0;
|
|
6925
6993
|
await this.applyMode(created);
|
|
6926
6994
|
}
|
|
6927
6995
|
resumed = false;
|
|
@@ -7038,6 +7106,7 @@ class Turn {
|
|
|
7038
7106
|
throw new TurnFailure(`${this.agentId} failed the turn: ${errorMessage(error)}${quoteTail(this.log.tail())}`);
|
|
7039
7107
|
});
|
|
7040
7108
|
const usage = answer.usage;
|
|
7109
|
+
this.emitAll(this.mapper.tokens(usage));
|
|
7041
7110
|
this.otel.promptEnded({
|
|
7042
7111
|
"gen_ai.response.finish_reasons": answer.stopReason,
|
|
7043
7112
|
"gen_ai.usage.input_tokens": usage?.inputTokens,
|
|
@@ -7308,9 +7377,131 @@ function registrationHeartbeat() {
|
|
|
7308
7377
|
};
|
|
7309
7378
|
}
|
|
7310
7379
|
|
|
7380
|
+
// src/daemon/config-restart.ts
|
|
7381
|
+
class ConfigRestart {
|
|
7382
|
+
deps;
|
|
7383
|
+
timeoutMs;
|
|
7384
|
+
pending = null;
|
|
7385
|
+
generation = 0;
|
|
7386
|
+
takenOver = false;
|
|
7387
|
+
constructor(deps, timeoutMs) {
|
|
7388
|
+
this.deps = deps;
|
|
7389
|
+
this.timeoutMs = timeoutMs;
|
|
7390
|
+
}
|
|
7391
|
+
get isPending() {
|
|
7392
|
+
return this.pending !== null;
|
|
7393
|
+
}
|
|
7394
|
+
get isTakenOver() {
|
|
7395
|
+
return this.takenOver;
|
|
7396
|
+
}
|
|
7397
|
+
saved(outcome, path) {
|
|
7398
|
+
if (this.takenOver)
|
|
7399
|
+
return null;
|
|
7400
|
+
if (this.pending !== null) {
|
|
7401
|
+
if (!outcome.needsRestart) {
|
|
7402
|
+
this.callOff(outcome.invalid ? `${path} is not valid now; the running config stays` : "the change that needed it was undone");
|
|
7403
|
+
}
|
|
7404
|
+
return null;
|
|
7405
|
+
}
|
|
7406
|
+
if (!outcome.needsRestart)
|
|
7407
|
+
return null;
|
|
7408
|
+
const problem = this.deps.startProblem();
|
|
7409
|
+
if (problem) {
|
|
7410
|
+
this.deps.log(`config restart not started: the daemon would not start on ${path} as it is now (${problem}); the running config stays`);
|
|
7411
|
+
return null;
|
|
7412
|
+
}
|
|
7413
|
+
const generation = ++this.generation;
|
|
7414
|
+
this.pending = generation;
|
|
7415
|
+
this.deps.pauseClaims();
|
|
7416
|
+
return this.wait(generation, path);
|
|
7417
|
+
}
|
|
7418
|
+
takeOver() {
|
|
7419
|
+
this.takenOver = true;
|
|
7420
|
+
if (this.pending === null)
|
|
7421
|
+
return;
|
|
7422
|
+
this.pending = null;
|
|
7423
|
+
this.deps.log("config restart called off: a stop or restart was asked for");
|
|
7424
|
+
}
|
|
7425
|
+
async wait(generation, path) {
|
|
7426
|
+
const idle = await this.deps.waitIdle(this.timeoutMs);
|
|
7427
|
+
if (!this.current(generation))
|
|
7428
|
+
return;
|
|
7429
|
+
const problem = this.deps.startProblem();
|
|
7430
|
+
if (problem) {
|
|
7431
|
+
this.callOff(`the daemon would not start on ${path} as it is now (${problem}); the running config stays`);
|
|
7432
|
+
return;
|
|
7433
|
+
}
|
|
7434
|
+
if (!idle)
|
|
7435
|
+
await this.deps.stopTurns();
|
|
7436
|
+
if (!this.current(generation))
|
|
7437
|
+
return;
|
|
7438
|
+
this.pending = null;
|
|
7439
|
+
this.deps.restart();
|
|
7440
|
+
}
|
|
7441
|
+
current(generation) {
|
|
7442
|
+
return this.pending === generation && !this.takenOver && !this.deps.exiting();
|
|
7443
|
+
}
|
|
7444
|
+
callOff(reason) {
|
|
7445
|
+
if (this.pending === null)
|
|
7446
|
+
return;
|
|
7447
|
+
this.pending = null;
|
|
7448
|
+
this.deps.resumeClaims();
|
|
7449
|
+
this.deps.log(`config restart called off: ${reason}`);
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
|
|
7453
|
+
// src/daemon/config-watch.ts
|
|
7454
|
+
import { createHash as createHash2 } from "crypto";
|
|
7455
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9, watch } from "fs";
|
|
7456
|
+
import { basename as basename3, dirname as dirname8 } from "path";
|
|
7457
|
+
var CONFIG_WATCH_DEBOUNCE_MS = 300;
|
|
7458
|
+
function digest(path) {
|
|
7459
|
+
if (!existsSync13(path))
|
|
7460
|
+
return null;
|
|
7461
|
+
try {
|
|
7462
|
+
return createHash2("sha256").update(readFileSync9(path)).digest("hex");
|
|
7463
|
+
} catch {
|
|
7464
|
+
return null;
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
function watchConfigFile(path, onChange, options = {}) {
|
|
7468
|
+
const name = basename3(path);
|
|
7469
|
+
let last = digest(path);
|
|
7470
|
+
let timer = null;
|
|
7471
|
+
let watcher;
|
|
7472
|
+
const settle = () => {
|
|
7473
|
+
timer = null;
|
|
7474
|
+
const now = digest(path);
|
|
7475
|
+
if (now === null || now === last)
|
|
7476
|
+
return;
|
|
7477
|
+
last = now;
|
|
7478
|
+
onChange();
|
|
7479
|
+
};
|
|
7480
|
+
try {
|
|
7481
|
+
watcher = watch(dirname8(path), (_event, file) => {
|
|
7482
|
+
if (file !== null && file !== name)
|
|
7483
|
+
return;
|
|
7484
|
+
if (timer)
|
|
7485
|
+
clearTimeout(timer);
|
|
7486
|
+
timer = setTimeout(settle, options.debounceMs ?? CONFIG_WATCH_DEBOUNCE_MS);
|
|
7487
|
+
});
|
|
7488
|
+
} catch (error) {
|
|
7489
|
+
options.onError?.(error);
|
|
7490
|
+
return null;
|
|
7491
|
+
}
|
|
7492
|
+
watcher.on("error", (error) => options.onError?.(error));
|
|
7493
|
+
return {
|
|
7494
|
+
close() {
|
|
7495
|
+
if (timer)
|
|
7496
|
+
clearTimeout(timer);
|
|
7497
|
+
watcher.close();
|
|
7498
|
+
}
|
|
7499
|
+
};
|
|
7500
|
+
}
|
|
7501
|
+
|
|
7311
7502
|
// src/daemon/control-socket.ts
|
|
7312
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
7313
|
-
import { dirname as
|
|
7503
|
+
import { chmodSync as chmodSync4, existsSync as existsSync14, mkdirSync as mkdirSync8, rmSync as rmSync5 } from "fs";
|
|
7504
|
+
import { dirname as dirname9 } from "path";
|
|
7314
7505
|
var METHOD_NOT_FOUND = -32601;
|
|
7315
7506
|
var INVALID_PARAMS = -32602;
|
|
7316
7507
|
var INTERNAL_ERROR = -32603;
|
|
@@ -7335,8 +7526,8 @@ class ControlServer {
|
|
|
7335
7526
|
this.onError = onError;
|
|
7336
7527
|
}
|
|
7337
7528
|
listen() {
|
|
7338
|
-
mkdirSync8(
|
|
7339
|
-
if (
|
|
7529
|
+
mkdirSync8(dirname9(this.path), { recursive: true, mode: 448 });
|
|
7530
|
+
if (existsSync14(this.path))
|
|
7340
7531
|
rmSync5(this.path, { force: true });
|
|
7341
7532
|
const previous = process.umask(127);
|
|
7342
7533
|
try {
|
|
@@ -7502,11 +7693,11 @@ import {
|
|
|
7502
7693
|
closeSync as closeSync2,
|
|
7503
7694
|
mkdirSync as mkdirSync9,
|
|
7504
7695
|
openSync as openSync2,
|
|
7505
|
-
readFileSync as
|
|
7696
|
+
readFileSync as readFileSync10,
|
|
7506
7697
|
rmSync as rmSync6,
|
|
7507
7698
|
writeSync
|
|
7508
7699
|
} from "fs";
|
|
7509
|
-
import { dirname as
|
|
7700
|
+
import { dirname as dirname10 } from "path";
|
|
7510
7701
|
var STARTING_WINDOW_MS = 30000;
|
|
7511
7702
|
function looksLikeDaemon(args) {
|
|
7512
7703
|
const program = args.findIndex((arg) => {
|
|
@@ -7518,7 +7709,7 @@ function looksLikeDaemon(args) {
|
|
|
7518
7709
|
function processArgs(pid) {
|
|
7519
7710
|
try {
|
|
7520
7711
|
if (process.platform === "linux") {
|
|
7521
|
-
return
|
|
7712
|
+
return readFileSync10(`/proc/${pid}/cmdline`, "utf8").split("\x00").filter(Boolean);
|
|
7522
7713
|
}
|
|
7523
7714
|
if (process.platform === "win32") {
|
|
7524
7715
|
const out = Bun.spawnSync([
|
|
@@ -7553,7 +7744,7 @@ var realProbe = {
|
|
|
7553
7744
|
function readLock(path) {
|
|
7554
7745
|
let text;
|
|
7555
7746
|
try {
|
|
7556
|
-
text =
|
|
7747
|
+
text = readFileSync10(path, "utf8");
|
|
7557
7748
|
} catch {
|
|
7558
7749
|
return;
|
|
7559
7750
|
}
|
|
@@ -7577,7 +7768,7 @@ async function inspectLock(path, probe = realProbe, now = Date.now) {
|
|
|
7577
7768
|
return now() - record.startedAt < STARTING_WINDOW_MS ? { kind: "starting", record } : { kind: "hung", record };
|
|
7578
7769
|
}
|
|
7579
7770
|
function writeExclusive(path, record) {
|
|
7580
|
-
mkdirSync9(
|
|
7771
|
+
mkdirSync9(dirname10(path), { recursive: true, mode: 448 });
|
|
7581
7772
|
let fd;
|
|
7582
7773
|
try {
|
|
7583
7774
|
fd = openSync2(path, "wx", 384);
|
|
@@ -7655,8 +7846,8 @@ function childArgs(args) {
|
|
|
7655
7846
|
|
|
7656
7847
|
// src/daemon/state.ts
|
|
7657
7848
|
import { Database } from "bun:sqlite";
|
|
7658
|
-
import { chmodSync as chmodSync5, existsSync as
|
|
7659
|
-
import { dirname as
|
|
7849
|
+
import { chmodSync as chmodSync5, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
7850
|
+
import { dirname as dirname11 } from "path";
|
|
7660
7851
|
var LIVE_LOCAL_STATES = [
|
|
7661
7852
|
"claimed",
|
|
7662
7853
|
"running"
|
|
@@ -7714,7 +7905,8 @@ var MIGRATIONS = [
|
|
|
7714
7905
|
ALTER TABLE worktrees ADD COLUMN removed_at INTEGER;
|
|
7715
7906
|
ALTER TABLE local_sessions ADD COLUMN last_answer TEXT;
|
|
7716
7907
|
ALTER TABLE local_sessions ADD COLUMN ended_at INTEGER;
|
|
7717
|
-
ALTER TABLE local_sessions ADD COLUMN pull_requests TEXT NOT NULL DEFAULT '[]'
|
|
7908
|
+
ALTER TABLE local_sessions ADD COLUMN pull_requests TEXT NOT NULL DEFAULT '[]';`,
|
|
7909
|
+
`ALTER TABLE local_sessions ADD COLUMN acp_cost_usd REAL;`
|
|
7718
7910
|
];
|
|
7719
7911
|
function toSession(row) {
|
|
7720
7912
|
return {
|
|
@@ -7728,6 +7920,7 @@ function toSession(row) {
|
|
|
7728
7920
|
nextSeq: row.next_seq,
|
|
7729
7921
|
acpAgentId: row.acp_agent_id ?? null,
|
|
7730
7922
|
acpSessionId: row.acp_session_id ?? null,
|
|
7923
|
+
acpCostUsd: row.acp_cost_usd ?? null,
|
|
7731
7924
|
pid: row.pid ?? null,
|
|
7732
7925
|
worktreeId: row.worktree_id ?? null,
|
|
7733
7926
|
createdAt: row.created_at,
|
|
@@ -7778,9 +7971,9 @@ class StateDb {
|
|
|
7778
7971
|
this.path = path;
|
|
7779
7972
|
this.now = now;
|
|
7780
7973
|
if (path !== ":memory:") {
|
|
7781
|
-
mkdirSync10(
|
|
7974
|
+
mkdirSync10(dirname11(path), { recursive: true, mode: 448 });
|
|
7782
7975
|
}
|
|
7783
|
-
const fresh = path === ":memory:" || !
|
|
7976
|
+
const fresh = path === ":memory:" || !existsSync15(path);
|
|
7784
7977
|
this.db = new Database(path, { create: true, strict: true });
|
|
7785
7978
|
if (fresh && path !== ":memory:")
|
|
7786
7979
|
chmodSync5(path, 384);
|
|
@@ -7837,7 +8030,8 @@ class StateDb {
|
|
|
7837
8030
|
setSessionState(agentSessionId, state, extra = {}) {
|
|
7838
8031
|
this.db.query(`UPDATE local_sessions SET state = $state, updated_at = $now,
|
|
7839
8032
|
pid = CASE WHEN $setPid THEN $pid ELSE pid END,
|
|
7840
|
-
acp_session_id = CASE WHEN $setAcp THEN $acp ELSE acp_session_id END
|
|
8033
|
+
acp_session_id = CASE WHEN $setAcp THEN $acp ELSE acp_session_id END,
|
|
8034
|
+
acp_cost_usd = CASE WHEN $setCost THEN $cost ELSE acp_cost_usd END
|
|
7841
8035
|
WHERE agent_session_id = $id`).run({
|
|
7842
8036
|
state,
|
|
7843
8037
|
now: this.now(),
|
|
@@ -7845,7 +8039,9 @@ class StateDb {
|
|
|
7845
8039
|
setPid: extra.pid !== undefined ? 1 : 0,
|
|
7846
8040
|
pid: extra.pid ?? null,
|
|
7847
8041
|
setAcp: extra.acpSessionId !== undefined ? 1 : 0,
|
|
7848
|
-
acp: extra.acpSessionId ?? null
|
|
8042
|
+
acp: extra.acpSessionId ?? null,
|
|
8043
|
+
setCost: extra.acpCostUsd !== undefined ? 1 : 0,
|
|
8044
|
+
cost: extra.acpCostUsd ?? null
|
|
7849
8045
|
});
|
|
7850
8046
|
}
|
|
7851
8047
|
session(agentSessionId) {
|
|
@@ -8598,7 +8794,7 @@ function resolvedConfigSecrets(config, env) {
|
|
|
8598
8794
|
}
|
|
8599
8795
|
|
|
8600
8796
|
// src/worktree/cleanup.ts
|
|
8601
|
-
import { existsSync as
|
|
8797
|
+
import { existsSync as existsSync16, readdirSync, rmSync as rmSync7 } from "fs";
|
|
8602
8798
|
import { join as join9, relative as relative5, resolve as resolve4 } from "path";
|
|
8603
8799
|
|
|
8604
8800
|
// src/worktree/merge-state.ts
|
|
@@ -8736,14 +8932,14 @@ class WorktreeCleaner {
|
|
|
8736
8932
|
if (this.inUse(wt))
|
|
8737
8933
|
return { reason: "in use", overridable: [] };
|
|
8738
8934
|
const reasons = [];
|
|
8739
|
-
if (wt.kind === "worktree" &&
|
|
8935
|
+
if (wt.kind === "worktree" && existsSync16(wt.path)) {
|
|
8740
8936
|
const status = await this.git(["status", "--porcelain", "--", ".", `:(exclude)${KEEP_FILE}`], { cwd: wt.path });
|
|
8741
8937
|
if (status.code !== 0 || status.stdout.trim())
|
|
8742
8938
|
reasons.push("uncommitted changes");
|
|
8743
8939
|
if (await this.hasUnpushed(wt))
|
|
8744
8940
|
reasons.push("unpushed commits");
|
|
8745
8941
|
}
|
|
8746
|
-
if (
|
|
8942
|
+
if (existsSync16(join9(wt.path, KEEP_FILE)))
|
|
8747
8943
|
reasons.push("kept by request");
|
|
8748
8944
|
return { reason: reasons[0] ?? null, overridable: reasons };
|
|
8749
8945
|
}
|
|
@@ -8800,7 +8996,7 @@ class WorktreeCleaner {
|
|
|
8800
8996
|
}
|
|
8801
8997
|
}
|
|
8802
8998
|
const scratch = join9(root, SCRATCH);
|
|
8803
|
-
if (
|
|
8999
|
+
if (existsSync16(scratch)) {
|
|
8804
9000
|
for (const name of readdirSync(scratch)) {
|
|
8805
9001
|
const path = join9(scratch, name);
|
|
8806
9002
|
const id = Number(name);
|
|
@@ -8854,7 +9050,7 @@ class WorktreeCleaner {
|
|
|
8854
9050
|
if (!options.dryRun)
|
|
8855
9051
|
this.state.updateWorktree(wt.id, { keptReason: reason });
|
|
8856
9052
|
};
|
|
8857
|
-
if (!
|
|
9053
|
+
if (!existsSync16(wt.path)) {
|
|
8858
9054
|
if (!options.dryRun) {
|
|
8859
9055
|
if (wt.repoPath)
|
|
8860
9056
|
await this.git(["worktree", "prune"], { cwd: wt.repoPath });
|
|
@@ -10104,7 +10300,9 @@ class Supervisor {
|
|
|
10104
10300
|
...assignment,
|
|
10105
10301
|
agentId: assignment.agentId ?? this.agentFor(assignment.role)
|
|
10106
10302
|
}, this.config).agentId;
|
|
10107
|
-
const
|
|
10303
|
+
const local = state.session(assignment.agentSessionId);
|
|
10304
|
+
const previousAcpSessionId = local?.acpSessionId ?? null;
|
|
10305
|
+
const previousAcpCostUsd = local?.acpCostUsd ?? null;
|
|
10108
10306
|
state.claimSession({
|
|
10109
10307
|
agentSessionId: assignment.agentSessionId,
|
|
10110
10308
|
linearSessionId: assignment.externalSessionId,
|
|
@@ -10200,6 +10398,7 @@ class Supervisor {
|
|
|
10200
10398
|
stateDir: this.options.stateDir,
|
|
10201
10399
|
env: this.options.env,
|
|
10202
10400
|
previousAcpSessionId,
|
|
10401
|
+
previousAcpCostUsd,
|
|
10203
10402
|
onReply: (handler) => {
|
|
10204
10403
|
slot.onReply = handler;
|
|
10205
10404
|
},
|
|
@@ -10260,9 +10459,11 @@ class Supervisor {
|
|
|
10260
10459
|
this.slots.delete(slot.workItemId);
|
|
10261
10460
|
if (slot.nonce)
|
|
10262
10461
|
this.broker.end(slot.nonce);
|
|
10462
|
+
const acp = acpSessionAfterTurn(result, slot.silent, this.options.state.session(slot.agentSessionId)?.acpSessionId ?? null);
|
|
10263
10463
|
if (slot.silent) {
|
|
10264
10464
|
this.options.state.setSessionState(slot.agentSessionId, "interrupted", {
|
|
10265
|
-
pid: null
|
|
10465
|
+
pid: null,
|
|
10466
|
+
...acp
|
|
10266
10467
|
});
|
|
10267
10468
|
} else {
|
|
10268
10469
|
this.sink.complete({
|
|
@@ -10272,6 +10473,7 @@ class Supervisor {
|
|
|
10272
10473
|
error: result.error ?? null,
|
|
10273
10474
|
acpSessionId: result.acpSessionId ?? null,
|
|
10274
10475
|
branch: result.branch ?? null,
|
|
10476
|
+
...result.usage ? { usage: result.usage } : {},
|
|
10275
10477
|
...slot.consumed.length ? { answersConsumed: slot.consumed.slice(0, 100) } : {}
|
|
10276
10478
|
},
|
|
10277
10479
|
active: {
|
|
@@ -10283,10 +10485,7 @@ class Supervisor {
|
|
|
10283
10485
|
}
|
|
10284
10486
|
});
|
|
10285
10487
|
this.options.state.recordTurnEnd(slot.agentSessionId, slot.lastAnswer);
|
|
10286
|
-
this.options.state.setSessionState(slot.agentSessionId, result.outcome === "completed" ? "idle" : result.outcome === "cancelled" ? "cancelled" : "failed", {
|
|
10287
|
-
pid: null,
|
|
10288
|
-
...result.acpSessionId ? { acpSessionId: result.acpSessionId } : {}
|
|
10289
|
-
});
|
|
10488
|
+
this.options.state.setSessionState(slot.agentSessionId, result.outcome === "completed" ? "idle" : result.outcome === "cancelled" ? "cancelled" : "failed", { pid: null, ...acp });
|
|
10290
10489
|
}
|
|
10291
10490
|
this.options.log.info(`turn ended: ${slot.silent ? "taken away" : result.outcome}`, {
|
|
10292
10491
|
session: slot.agentSessionId,
|
|
@@ -10368,6 +10567,14 @@ class Supervisor {
|
|
|
10368
10567
|
this.link.beatNow();
|
|
10369
10568
|
return { drained: this.slots.size === 0 && flushed, cancelled };
|
|
10370
10569
|
}
|
|
10570
|
+
pauseClaims() {
|
|
10571
|
+
this.draining = true;
|
|
10572
|
+
this.link.beatNow();
|
|
10573
|
+
}
|
|
10574
|
+
resumeClaims() {
|
|
10575
|
+
this.draining = false;
|
|
10576
|
+
this.link.beatNow();
|
|
10577
|
+
}
|
|
10371
10578
|
async waitIdle(timeoutMs) {
|
|
10372
10579
|
const deadline = Date.now() + timeoutMs;
|
|
10373
10580
|
while (this.slots.size > 0) {
|
|
@@ -10525,6 +10732,17 @@ class Supervisor {
|
|
|
10525
10732
|
this.telemetryQueue.stop();
|
|
10526
10733
|
}
|
|
10527
10734
|
}
|
|
10735
|
+
function acpSessionAfterTurn(result, silent, recordedAcpSessionId) {
|
|
10736
|
+
if (!result.acpSessionId)
|
|
10737
|
+
return {};
|
|
10738
|
+
if (!silent) {
|
|
10739
|
+
return {
|
|
10740
|
+
acpSessionId: result.acpSessionId,
|
|
10741
|
+
acpCostUsd: result.acpCostUsd ?? null
|
|
10742
|
+
};
|
|
10743
|
+
}
|
|
10744
|
+
return result.acpSessionId === recordedAcpSessionId ? { acpCostUsd: result.acpCostUsd ?? null } : {};
|
|
10745
|
+
}
|
|
10528
10746
|
function changedKeys(a, b, prefix = "") {
|
|
10529
10747
|
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
10530
10748
|
if (isObject(a) && isObject(b)) {
|
|
@@ -10593,18 +10811,19 @@ function revokedMarker(stateDir) {
|
|
|
10593
10811
|
return join10(stateDir, "revoked.json");
|
|
10594
10812
|
}
|
|
10595
10813
|
function fingerprint(token) {
|
|
10596
|
-
return
|
|
10814
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
10597
10815
|
}
|
|
10598
10816
|
function wasRevoked(stateDir, token) {
|
|
10599
10817
|
const file = revokedMarker(stateDir);
|
|
10600
|
-
if (!
|
|
10818
|
+
if (!existsSync17(file))
|
|
10601
10819
|
return false;
|
|
10602
10820
|
try {
|
|
10603
|
-
return JSON.parse(
|
|
10821
|
+
return JSON.parse(readFileSync11(file, "utf8")).token === fingerprint(token);
|
|
10604
10822
|
} catch {
|
|
10605
10823
|
return false;
|
|
10606
10824
|
}
|
|
10607
10825
|
}
|
|
10826
|
+
var CONFIG_RESTART_DRAIN_MS = 10 * 60000;
|
|
10608
10827
|
function readDaemonConfig(options, env) {
|
|
10609
10828
|
const path = resolveConfigPath(options.config, env);
|
|
10610
10829
|
const checked = checkConfig(path, { env, offline: true });
|
|
@@ -10677,19 +10896,55 @@ async function runDaemon(options) {
|
|
|
10677
10896
|
const machine = realMachine(env);
|
|
10678
10897
|
const api = new DaemonApi(origin, runner.token, (line) => log.debug(line));
|
|
10679
10898
|
let exiting = false;
|
|
10899
|
+
let restartScheduled = false;
|
|
10900
|
+
let configHandOff = null;
|
|
10680
10901
|
let supervisor;
|
|
10681
10902
|
let control;
|
|
10903
|
+
let watcher = null;
|
|
10682
10904
|
const teardown = async (drain) => {
|
|
10683
10905
|
if (drain) {
|
|
10684
10906
|
await supervisor.drain({ timeoutMs: 1e4, cancel: false });
|
|
10685
10907
|
} else {
|
|
10686
10908
|
await supervisor.sink.drain(2000);
|
|
10687
10909
|
}
|
|
10910
|
+
watcher?.close();
|
|
10688
10911
|
supervisor.close();
|
|
10689
10912
|
control.close();
|
|
10690
10913
|
releaseLock(paths.pidfile);
|
|
10691
10914
|
state.close();
|
|
10692
10915
|
};
|
|
10916
|
+
const restartSelf = (fromConfig = false) => {
|
|
10917
|
+
const service = env.HYPOTENUSE_SERVICE === "1";
|
|
10918
|
+
const answer = {
|
|
10919
|
+
restarting: true,
|
|
10920
|
+
mode: service ? "service" : "re-exec"
|
|
10921
|
+
};
|
|
10922
|
+
if (restartScheduled)
|
|
10923
|
+
return answer;
|
|
10924
|
+
restartScheduled = true;
|
|
10925
|
+
const handOff = setTimeout(() => {
|
|
10926
|
+
configHandOff = null;
|
|
10927
|
+
if (exiting)
|
|
10928
|
+
return;
|
|
10929
|
+
exiting = true;
|
|
10930
|
+
log.info(service ? "exiting for the service manager to restart" : "re-executing");
|
|
10931
|
+
teardown(false).then(() => {
|
|
10932
|
+
if (service)
|
|
10933
|
+
process.exit(EXIT_RESTART);
|
|
10934
|
+
const [command, ...prefix] = selfCommand();
|
|
10935
|
+
const child = spawn2(command, [...prefix, ...childArgs(process.argv.slice(2))], {
|
|
10936
|
+
detached: true,
|
|
10937
|
+
stdio: "ignore",
|
|
10938
|
+
env
|
|
10939
|
+
});
|
|
10940
|
+
child.unref();
|
|
10941
|
+
process.exit(0);
|
|
10942
|
+
});
|
|
10943
|
+
}, 50);
|
|
10944
|
+
if (fromConfig)
|
|
10945
|
+
configHandOff = handOff;
|
|
10946
|
+
return answer;
|
|
10947
|
+
};
|
|
10693
10948
|
const requestExit = (exit) => {
|
|
10694
10949
|
if (exiting)
|
|
10695
10950
|
return;
|
|
@@ -10725,6 +10980,31 @@ async function runDaemon(options) {
|
|
|
10725
10980
|
...Object.values(credentials.servers).flatMap((s) => s.login ? [s.login.token] : [])
|
|
10726
10981
|
]
|
|
10727
10982
|
});
|
|
10983
|
+
const configRestart = new ConfigRestart({
|
|
10984
|
+
pauseClaims: () => supervisor.pauseClaims(),
|
|
10985
|
+
resumeClaims: () => supervisor.resumeClaims(),
|
|
10986
|
+
waitIdle: (ms) => supervisor.waitIdle(ms),
|
|
10987
|
+
stopTurns: async () => {
|
|
10988
|
+
await supervisor.drain({ timeoutMs: 0, cancel: true });
|
|
10989
|
+
},
|
|
10990
|
+
startProblem: () => {
|
|
10991
|
+
const read = readDaemonConfig(options, env);
|
|
10992
|
+
if (!read.config)
|
|
10993
|
+
return "the config is not valid";
|
|
10994
|
+
const file = credentialsPath(env);
|
|
10995
|
+
try {
|
|
10996
|
+
resolveDaemonRunner(read.config, loadCredentials(file), file);
|
|
10997
|
+
} catch (error) {
|
|
10998
|
+
return error instanceof Error ? error.message : String(error);
|
|
10999
|
+
}
|
|
11000
|
+
return null;
|
|
11001
|
+
},
|
|
11002
|
+
exiting: () => exiting,
|
|
11003
|
+
restart: () => {
|
|
11004
|
+
restartSelf(true);
|
|
11005
|
+
},
|
|
11006
|
+
log: (message) => log.info(message)
|
|
11007
|
+
}, CONFIG_RESTART_DRAIN_MS);
|
|
10728
11008
|
control = new ControlServer(paths.socket, {
|
|
10729
11009
|
status: () => supervisor.status({ pid: process.pid, configPath: paths.config }),
|
|
10730
11010
|
"agents.list": (params) => supervisor.agents(Boolean(params.all)),
|
|
@@ -10742,30 +11022,23 @@ async function runDaemon(options) {
|
|
|
10742
11022
|
const report = await supervisor.cleaner.run(cleanupParams(params));
|
|
10743
11023
|
return report;
|
|
10744
11024
|
},
|
|
10745
|
-
drain: (params) =>
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
11025
|
+
drain: (params) => {
|
|
11026
|
+
configRestart.takeOver();
|
|
11027
|
+
if (configHandOff) {
|
|
11028
|
+
clearTimeout(configHandOff);
|
|
11029
|
+
configHandOff = null;
|
|
11030
|
+
restartScheduled = false;
|
|
11031
|
+
log.info("config restart called off: a stop or restart was asked for");
|
|
11032
|
+
}
|
|
11033
|
+
return supervisor.drain({
|
|
11034
|
+
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : 600000,
|
|
11035
|
+
cancel: params.cancel === true
|
|
11036
|
+
});
|
|
11037
|
+
},
|
|
10749
11038
|
reload: () => supervisor.reload(),
|
|
10750
11039
|
restart: () => {
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
exiting = true;
|
|
10754
|
-
log.info(service ? "exiting for the service manager to restart" : "re-executing");
|
|
10755
|
-
teardown(false).then(() => {
|
|
10756
|
-
if (service)
|
|
10757
|
-
process.exit(EXIT_RESTART);
|
|
10758
|
-
const [command, ...prefix] = selfCommand();
|
|
10759
|
-
const child = spawn2(command, [...prefix, ...childArgs(process.argv.slice(2))], {
|
|
10760
|
-
detached: true,
|
|
10761
|
-
stdio: "ignore",
|
|
10762
|
-
env
|
|
10763
|
-
});
|
|
10764
|
-
child.unref();
|
|
10765
|
-
process.exit(0);
|
|
10766
|
-
});
|
|
10767
|
-
}, 50);
|
|
10768
|
-
return { restarting: true, mode: service ? "service" : "re-exec" };
|
|
11040
|
+
configRestart.takeOver();
|
|
11041
|
+
return restartSelf();
|
|
10769
11042
|
},
|
|
10770
11043
|
"mcp.hello": (params) => supervisor.broker.hello(params),
|
|
10771
11044
|
"mcp.call": (params) => supervisor.broker.call(params)
|
|
@@ -10801,6 +11074,19 @@ async function runDaemon(options) {
|
|
|
10801
11074
|
});
|
|
10802
11075
|
report({ ready: true, pid: process.pid });
|
|
10803
11076
|
rmSync8(revokedMarker(paths.stateDir), { force: true });
|
|
11077
|
+
watcher = watchConfigFile(paths.config, () => {
|
|
11078
|
+
if (exiting || restartScheduled || configRestart.isTakenOver)
|
|
11079
|
+
return;
|
|
11080
|
+
log.info(`${paths.config} changed; reloading`);
|
|
11081
|
+
const result = supervisor.reload();
|
|
11082
|
+
const needsRestart = !result.error && result.requiresRestart.length > 0;
|
|
11083
|
+
if (needsRestart && !configRestart.isPending) {
|
|
11084
|
+
log.info("restarting to apply the config change once running turns finish", { keys: result.requiresRestart.join(",") });
|
|
11085
|
+
}
|
|
11086
|
+
configRestart.saved({ needsRestart, invalid: Boolean(result.error) }, paths.config);
|
|
11087
|
+
}, {
|
|
11088
|
+
onError: (error) => log.warn(`not watching ${paths.config} for changes (\`hypotenuse daemon reload\` still applies them): ${error instanceof Error ? error.message : String(error)}`)
|
|
11089
|
+
});
|
|
10804
11090
|
}
|
|
10805
11091
|
|
|
10806
11092
|
// src/commands/daemon.ts
|
|
@@ -10962,22 +11248,22 @@ async function stopDaemon(ctx, options) {
|
|
|
10962
11248
|
}
|
|
10963
11249
|
async function tailLog(file, options) {
|
|
10964
11250
|
const keep = (line) => !options.session || line.includes(` session=${options.session}`);
|
|
10965
|
-
if (!
|
|
11251
|
+
if (!existsSync18(file)) {
|
|
10966
11252
|
if (!options.follow)
|
|
10967
11253
|
throw new Error(`${file} does not exist yet`);
|
|
10968
11254
|
} else {
|
|
10969
|
-
const all =
|
|
11255
|
+
const all = readFileSync12(file, "utf8").split(`
|
|
10970
11256
|
`).filter(Boolean).filter(keep);
|
|
10971
11257
|
for (const line of all.slice(-options.lines))
|
|
10972
11258
|
console.log(paintLogLine(line));
|
|
10973
11259
|
}
|
|
10974
11260
|
if (!options.follow)
|
|
10975
11261
|
return;
|
|
10976
|
-
let offset =
|
|
11262
|
+
let offset = existsSync18(file) ? statSync5(file).size : 0;
|
|
10977
11263
|
let partial = "";
|
|
10978
11264
|
for (;; ) {
|
|
10979
11265
|
await Bun.sleep(500);
|
|
10980
|
-
if (!
|
|
11266
|
+
if (!existsSync18(file))
|
|
10981
11267
|
continue;
|
|
10982
11268
|
const size = statSync5(file).size;
|
|
10983
11269
|
if (size < offset)
|
|
@@ -11072,7 +11358,7 @@ function registerDaemonCommands(program) {
|
|
|
11072
11358
|
const opts = this.opts();
|
|
11073
11359
|
const located = locateDaemon(ctx);
|
|
11074
11360
|
const sessionLog = opts.session && /^\d+$/.test(opts.session) ? sessionLogPath(located.stateDir, Number(opts.session)) : null;
|
|
11075
|
-
if (sessionLog &&
|
|
11361
|
+
if (sessionLog && existsSync18(sessionLog)) {
|
|
11076
11362
|
await tailLog(sessionLog, {
|
|
11077
11363
|
lines: Number(opts.lines) || 200,
|
|
11078
11364
|
follow: Boolean(opts.follow)
|
|
@@ -11538,9 +11824,9 @@ function registerCheckUpdateCommand(program) {
|
|
|
11538
11824
|
}
|
|
11539
11825
|
|
|
11540
11826
|
// src/commands/config.ts
|
|
11541
|
-
import { existsSync as
|
|
11827
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
11542
11828
|
import { hostname as hostname5 } from "os";
|
|
11543
|
-
import { dirname as
|
|
11829
|
+
import { dirname as dirname12, resolve as resolve5 } from "path";
|
|
11544
11830
|
|
|
11545
11831
|
// src/config/template.toml
|
|
11546
11832
|
var template_default = `#:schema https://www.hypotenuse.cloud/schemas/config-v1.json
|
|
@@ -11687,7 +11973,7 @@ function registerConfigCommands(program) {
|
|
|
11687
11973
|
await runCommand(this, async (ctx) => {
|
|
11688
11974
|
const opts = this.opts();
|
|
11689
11975
|
const path = ctx.configPath;
|
|
11690
|
-
if (
|
|
11976
|
+
if (existsSync19(path) && !opts.force) {
|
|
11691
11977
|
throw new Error(`${path} already exists; pass --force to overwrite it`);
|
|
11692
11978
|
}
|
|
11693
11979
|
const machine = realMachine(ctx.env);
|
|
@@ -11704,7 +11990,7 @@ function registerConfigCommands(program) {
|
|
|
11704
11990
|
repositories: repositoryBlocks(repos, ctx.env),
|
|
11705
11991
|
cpus: navigator.hardwareConcurrency || 1
|
|
11706
11992
|
});
|
|
11707
|
-
mkdirSync11(
|
|
11993
|
+
mkdirSync11(dirname12(resolve5(path)), { recursive: true, mode: 448 });
|
|
11708
11994
|
writeFileSync6(path, text);
|
|
11709
11995
|
if (!ctx.json)
|
|
11710
11996
|
console.log(`Wrote ${tildify(path, ctx.env)}.`);
|
|
@@ -11744,15 +12030,118 @@ function registerConfigCommands(program) {
|
|
|
11744
12030
|
});
|
|
11745
12031
|
}
|
|
11746
12032
|
|
|
12033
|
+
// src/commands/cost.ts
|
|
12034
|
+
var COST_DIMENSIONS = [
|
|
12035
|
+
"session",
|
|
12036
|
+
"root-session",
|
|
12037
|
+
"issue",
|
|
12038
|
+
"runner",
|
|
12039
|
+
"owner",
|
|
12040
|
+
"issuer",
|
|
12041
|
+
"role",
|
|
12042
|
+
"agent",
|
|
12043
|
+
"repository",
|
|
12044
|
+
"day"
|
|
12045
|
+
];
|
|
12046
|
+
function usd(usd) {
|
|
12047
|
+
if (usd === null)
|
|
12048
|
+
return "-";
|
|
12049
|
+
if (usd !== 0 && Math.abs(usd) < 0.01)
|
|
12050
|
+
return `$${usd.toPrecision(2)}`;
|
|
12051
|
+
return `$${usd.toFixed(2)}`;
|
|
12052
|
+
}
|
|
12053
|
+
function describeTotals(totals) {
|
|
12054
|
+
const turns = `${totals.turns} turn${totals.turns === 1 ? "" : "s"}`;
|
|
12055
|
+
const unpriced = totals.unpricedTurns ? ` (${totals.unpricedTurns} unpriced)` : "";
|
|
12056
|
+
const tokens = `${totals.inputTokens.toLocaleString("en-US")} in / ${totals.outputTokens.toLocaleString("en-US")} out`;
|
|
12057
|
+
return `${usd(totals.costUsd)} over ${turns}${unpriced}, ${tokens}`;
|
|
12058
|
+
}
|
|
12059
|
+
function withFilters(command) {
|
|
12060
|
+
return command.option("--session <id>", "only this agent session's turns").option("--root-session <id>", "only this manager session's turns and its dispatched workers'").option("--runner <id>", "only turns this runner ran").option("--owner <userId>", "only turns on runners this user owns").option("--issuer <userId>", "only turns this user asked for").option("--role <role>", "only turns in this role: manager or worker").option("--since <time>", "only turns that ended since: ISO-8601, epoch ms, or 7d / 12h / 30m").option("--until <time>", "only turns that ended before: ISO-8601, epoch ms, or 7d / 12h / 30m");
|
|
12061
|
+
}
|
|
12062
|
+
function filterQuery(command) {
|
|
12063
|
+
const opts = command.opts();
|
|
12064
|
+
return {
|
|
12065
|
+
agentSessionId: opts.session,
|
|
12066
|
+
rootAgentSessionId: opts.rootSession,
|
|
12067
|
+
runnerId: opts.runner,
|
|
12068
|
+
runnerUserId: opts.owner,
|
|
12069
|
+
issuerUserId: opts.issuer,
|
|
12070
|
+
role: opts.role,
|
|
12071
|
+
since: opts.since ? String(parseTime(opts.since)) : undefined,
|
|
12072
|
+
until: opts.until ? String(parseTime(opts.until)) : undefined
|
|
12073
|
+
};
|
|
12074
|
+
}
|
|
12075
|
+
function registerCostCommands(program) {
|
|
12076
|
+
const cost = program.command("cost").description("What the selected project's agent sessions cost, and whom it is attributed to");
|
|
12077
|
+
withPaging(withFilters(cost.command("list").description("List each turn's cost, newest first, with its session, runner, and role"))).action(async function() {
|
|
12078
|
+
await runCommand(this, async (ctx) => {
|
|
12079
|
+
const projectId = await ctx.projectId();
|
|
12080
|
+
const result = await ctx.signedInClient().request("GET", "/costs", {
|
|
12081
|
+
query: { projectId, ...filterQuery(this), ...pageQuery(this) }
|
|
12082
|
+
});
|
|
12083
|
+
emit(ctx, result, () => `${table(result.costRecords.map((r) => ({
|
|
12084
|
+
ended: when(r.endedAt),
|
|
12085
|
+
issue: r.issueIdentifier ?? "-",
|
|
12086
|
+
session: r.agentSessionId,
|
|
12087
|
+
role: r.role ?? "-",
|
|
12088
|
+
kind: r.kind,
|
|
12089
|
+
runner: r.runnerName,
|
|
12090
|
+
agent: r.agentId ?? "-",
|
|
12091
|
+
ending: r.ending,
|
|
12092
|
+
tokens: `${r.inputTokens ?? "-"} / ${r.outputTokens ?? "-"}`,
|
|
12093
|
+
cost: usd(r.costUsd)
|
|
12094
|
+
})), [
|
|
12095
|
+
{ key: "ended" },
|
|
12096
|
+
{ key: "issue" },
|
|
12097
|
+
{ key: "session" },
|
|
12098
|
+
{ key: "role" },
|
|
12099
|
+
{ key: "kind" },
|
|
12100
|
+
{ key: "runner" },
|
|
12101
|
+
{ key: "agent" },
|
|
12102
|
+
{ key: "ending" },
|
|
12103
|
+
{ key: "tokens" },
|
|
12104
|
+
{ key: "cost" }
|
|
12105
|
+
])}
|
|
12106
|
+
total ${describeTotals(result.total)}${pageFooter(result.pagination)}`);
|
|
12107
|
+
});
|
|
12108
|
+
});
|
|
12109
|
+
withPaging(withFilters(cost.command("summary").description("Total the turns' cost by one dimension, most expensive first").option("--by <dimension>", `what to group by: ${COST_DIMENSIONS.join(", ")}`, "session"))).action(async function() {
|
|
12110
|
+
await runCommand(this, async (ctx) => {
|
|
12111
|
+
const projectId = await ctx.projectId();
|
|
12112
|
+
const { by } = this.opts();
|
|
12113
|
+
const result = await ctx.signedInClient().request("GET", "/costs/summary", {
|
|
12114
|
+
query: { projectId, by, ...filterQuery(this), ...pageQuery(this) }
|
|
12115
|
+
});
|
|
12116
|
+
emit(ctx, result, () => `${table(result.groups.map((g) => ({
|
|
12117
|
+
[result.by]: g.label ?? g.key ?? "(none)",
|
|
12118
|
+
turns: g.turns,
|
|
12119
|
+
unpriced: g.unpricedTurns,
|
|
12120
|
+
"input tokens": g.inputTokens,
|
|
12121
|
+
"output tokens": g.outputTokens,
|
|
12122
|
+
cost: usd(g.costUsd)
|
|
12123
|
+
})), [
|
|
12124
|
+
{ key: result.by, maxWidth: 50 },
|
|
12125
|
+
{ key: "turns" },
|
|
12126
|
+
{ key: "unpriced" },
|
|
12127
|
+
{ key: "input tokens" },
|
|
12128
|
+
{ key: "output tokens" },
|
|
12129
|
+
{ key: "cost" }
|
|
12130
|
+
])}
|
|
12131
|
+
total ${describeTotals(result.total)}${pageFooter(result.pagination)}`);
|
|
12132
|
+
});
|
|
12133
|
+
});
|
|
12134
|
+
}
|
|
12135
|
+
|
|
11747
12136
|
// src/commands/doctor.ts
|
|
11748
|
-
import { existsSync as
|
|
12137
|
+
import { existsSync as existsSync21, statfsSync as statfsSync2, statSync as statSync6 } from "fs";
|
|
11749
12138
|
import { hostname as hostname6 } from "os";
|
|
11750
|
-
import { dirname as
|
|
12139
|
+
import { dirname as dirname14 } from "path";
|
|
11751
12140
|
|
|
11752
12141
|
// src/commands/service.ts
|
|
11753
|
-
import { existsSync as
|
|
12142
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync12, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
11754
12143
|
import { homedir as homedir3, userInfo } from "os";
|
|
11755
|
-
import { dirname as
|
|
12144
|
+
import { dirname as dirname13, join as join11 } from "path";
|
|
11756
12145
|
|
|
11757
12146
|
// src/daemon/units/launchd.plist
|
|
11758
12147
|
var launchd_default = `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -11888,7 +12277,7 @@ function registerServiceCommands(program) {
|
|
|
11888
12277
|
console.log(text);
|
|
11889
12278
|
return;
|
|
11890
12279
|
}
|
|
11891
|
-
mkdirSync12(
|
|
12280
|
+
mkdirSync12(dirname13(path), { recursive: true });
|
|
11892
12281
|
mkdirSync12(paths.stateDir, { recursive: true, mode: 448 });
|
|
11893
12282
|
writeFileSync7(path, text);
|
|
11894
12283
|
if (platform === "darwin") {
|
|
@@ -11909,7 +12298,7 @@ function registerServiceCommands(program) {
|
|
|
11909
12298
|
service.command("uninstall").description("Stop the service and remove its unit file").action(async function() {
|
|
11910
12299
|
await runCommand(this, async (ctx) => {
|
|
11911
12300
|
const path = unitPath(process.platform, ctx.env);
|
|
11912
|
-
if (!path || !
|
|
12301
|
+
if (!path || !existsSync20(path)) {
|
|
11913
12302
|
emit(ctx, { removed: false }, () => "No service is installed.");
|
|
11914
12303
|
return;
|
|
11915
12304
|
}
|
|
@@ -11927,7 +12316,7 @@ function registerServiceCommands(program) {
|
|
|
11927
12316
|
service.command("status").description("Show whether the service is installed and loaded, and the daemon's status").action(async function() {
|
|
11928
12317
|
await runCommand(this, async (ctx) => {
|
|
11929
12318
|
const path = unitPath(process.platform, ctx.env);
|
|
11930
|
-
const installed = Boolean(path &&
|
|
12319
|
+
const installed = Boolean(path && existsSync20(path));
|
|
11931
12320
|
let manager = "not installed";
|
|
11932
12321
|
if (installed && process.platform === "darwin") {
|
|
11933
12322
|
manager = run(["launchctl", "print", `gui/${userInfo().uid}/${LAUNCHD_LABEL}`], true).includes("state = running") ? "running" : "loaded, not running";
|
|
@@ -12015,7 +12404,7 @@ async function runDoctor(ctx, options) {
|
|
|
12015
12404
|
return list.checks;
|
|
12016
12405
|
}
|
|
12017
12406
|
let runner = null;
|
|
12018
|
-
if (!
|
|
12407
|
+
if (!existsSync21(ctx.credentialsPath)) {
|
|
12019
12408
|
list.fail("credentials", `${tildify(ctx.credentialsPath, ctx.env)} does not exist \u2014 \`hypotenuse login\`, then \`hypotenuse runner register\``);
|
|
12020
12409
|
} else {
|
|
12021
12410
|
try {
|
|
@@ -12078,7 +12467,7 @@ async function runDoctor(ctx, options) {
|
|
|
12078
12467
|
list.warn("daemon", "not running \u2014 `hypotenuse daemon start` or `hypotenuse service install`");
|
|
12079
12468
|
}
|
|
12080
12469
|
const unit = unitPath(process.platform, ctx.env);
|
|
12081
|
-
if (unit &&
|
|
12470
|
+
if (unit && existsSync21(unit))
|
|
12082
12471
|
list.ok("daemon", `service unit ${tildify(unit, ctx.env)}`);
|
|
12083
12472
|
else if (unit)
|
|
12084
12473
|
list.warn("daemon", "no service unit \u2014 `hypotenuse service install` starts the daemon at login");
|
|
@@ -12146,8 +12535,8 @@ async function runDoctor(ctx, options) {
|
|
|
12146
12535
|
`).at(-1) ?? ""}`);
|
|
12147
12536
|
}
|
|
12148
12537
|
let probePath = config.sessions.worktree_root;
|
|
12149
|
-
while (!
|
|
12150
|
-
probePath =
|
|
12538
|
+
while (!existsSync21(probePath) && dirname14(probePath) !== probePath)
|
|
12539
|
+
probePath = dirname14(probePath);
|
|
12151
12540
|
try {
|
|
12152
12541
|
const fs = statfsSync2(probePath);
|
|
12153
12542
|
const free = fs.bavail * fs.bsize;
|
|
@@ -13511,6 +13900,11 @@ function describeSession(session, activities, tree = {}) {
|
|
|
13511
13900
|
if (tree.issuer?.user) {
|
|
13512
13901
|
lines.push(`issuer ${tree.issuer.user.name}`);
|
|
13513
13902
|
}
|
|
13903
|
+
if (tree.cost?.turns)
|
|
13904
|
+
lines.push(`cost ${describeTotals(tree.cost)}`);
|
|
13905
|
+
if (tree.treeCost && tree.treeCost.turns > (tree.cost?.turns ?? 0)) {
|
|
13906
|
+
lines.push(`tree cost ${describeTotals(tree.treeCost)} with its workers`);
|
|
13907
|
+
}
|
|
13514
13908
|
if (session.summary)
|
|
13515
13909
|
lines.push(`summary ${session.summary}`);
|
|
13516
13910
|
if (session.externalUrl)
|
|
@@ -13737,7 +14131,7 @@ function registerSessionCommands(program) {
|
|
|
13737
14131
|
}
|
|
13738
14132
|
|
|
13739
14133
|
// src/commands/skill.ts
|
|
13740
|
-
import { existsSync as
|
|
14134
|
+
import { existsSync as existsSync22, readFileSync as readFileSync13 } from "fs";
|
|
13741
14135
|
import { join as join13 } from "path";
|
|
13742
14136
|
var SKILL_FILE = "hypotenuse.md";
|
|
13743
14137
|
async function readSkill() {
|
|
@@ -13746,8 +14140,8 @@ async function readSkill() {
|
|
|
13746
14140
|
return embedded.text();
|
|
13747
14141
|
for (const up of [[".."], ["..", ".."]]) {
|
|
13748
14142
|
const path = join13(import.meta.dir, ...up, "skills", SKILL_FILE);
|
|
13749
|
-
if (
|
|
13750
|
-
return
|
|
14143
|
+
if (existsSync22(path))
|
|
14144
|
+
return readFileSync13(path, "utf8");
|
|
13751
14145
|
}
|
|
13752
14146
|
throw new Error(`This install has no skills/${SKILL_FILE}; reinstall hypotenuse.`);
|
|
13753
14147
|
}
|
|
@@ -14245,6 +14639,7 @@ function buildProgram() {
|
|
|
14245
14639
|
registerIntegrationCommands(program);
|
|
14246
14640
|
registerSessionCommands(program);
|
|
14247
14641
|
registerEventCommands(program);
|
|
14642
|
+
registerCostCommands(program);
|
|
14248
14643
|
registerRunnerCommands(program);
|
|
14249
14644
|
registerRoutingCommands(program);
|
|
14250
14645
|
registerTelemetryCommands(program);
|