@kubitnodes/stellaris 0.3.27 → 0.4.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/cli/index.js +415 -56
- package/dist/server/index.js +737 -112
- package/package.json +2 -2
- package/web/dist/assets/{BackLink-qSxOMeEG.js → BackLink-BLiSCuKv.js} +1 -1
- package/web/dist/assets/{Button-CxjWy598.js → Button-D4-iu6aX.js} +1 -1
- package/web/dist/assets/ChannelForms-DjviQfVm.js +1 -0
- package/web/dist/assets/{ChannelView-rqfk1Tyu.js → ChannelView-B-0H7Ps8.js} +2 -2
- package/web/dist/assets/CitizenView-gE5GgCBa.js +2 -0
- package/web/dist/assets/{CitizensView-Bgw6PRSo.js → CitizensView-DXD3qkq1.js} +1 -1
- package/web/dist/assets/{CliIcon-BB2NdLxN.js → CliIcon-DL4ZwgZv.js} +1 -1
- package/web/dist/assets/{CronsView-BrXrcfC6.js → CronsView-CX_G5D3h.js} +1 -1
- package/web/dist/assets/{FileTree-CHDmrOhY.js → FileTree-BFa_KVI8.js} +1 -1
- package/web/dist/assets/{FileView-DUMfKYRW.js → FileView-Cd9Ijarf.js} +2 -2
- package/web/dist/assets/KnowledgeView-D_a4pjGV.js +1 -0
- package/web/dist/assets/{Markdown-C7RbUEZE.js → Markdown-DPqvK--4.js} +4 -4
- package/web/dist/assets/{MessageItem-DgWh5pJF.js → MessageItem-WBTzWwyj.js} +1 -1
- package/web/dist/assets/MetricsView-YUkYixqT.js +1 -0
- package/web/dist/assets/NeedsYouView-CE_B9bZQ.js +1 -0
- package/web/dist/assets/Overview-BGUnf6K1.js +1 -0
- package/web/dist/assets/{Pane-BlIRwUtQ.js → Pane-C9TbEME4.js} +1 -1
- package/web/dist/assets/{Picker-BSOB2JM_.js → Picker-BK48MjLY.js} +1 -1
- package/web/dist/assets/{ProjectView-CVt28SoI.js → ProjectView-sL3aGAC6.js} +1 -1
- package/web/dist/assets/ProposalView-BhZR4MIR.js +1 -0
- package/web/dist/assets/{ProposalsView-C0r1ya8_.js → ProposalsView-DTGx8NDn.js} +1 -1
- package/web/dist/assets/{Quota-59QJZrPa.js → Quota-BkF7pa0u.js} +1 -1
- package/web/dist/assets/RunnersView-Bl62pXNw.js +1 -0
- package/web/dist/assets/{SkillFileList-Cs_f3RPo.js → SkillFileList-BUpKUkaG.js} +1 -1
- package/web/dist/assets/{SkillView-BvVx3LSL.js → SkillView-BQJApSh7.js} +1 -1
- package/web/dist/assets/{SocietyView-CJRrypjI.js → SocietyView-JSdVE50m.js} +1 -1
- package/web/dist/assets/{StickyList-BbGq82Jj.js → StickyList-DafbJutb.js} +1 -1
- package/web/dist/assets/{SubjectThread-DdkyhVqK.js → SubjectThread-Jyl5YCFL.js} +1 -1
- package/web/dist/assets/{TaskView-CyEWpCvS.js → TaskView-Jke2pVGT.js} +2 -2
- package/web/dist/assets/{TasksView-D10XP1uk.js → TasksView-zJTIaqEA.js} +1 -1
- package/web/dist/assets/{ThreadForms-D94Jj0ES.js → ThreadForms-DwUOktyP.js} +1 -1
- package/web/dist/assets/{ThreadItem-CYmdCPOf.js → ThreadItem-Blx8gSvO.js} +1 -1
- package/web/dist/assets/{ThreadView-D79G2zgF.js → ThreadView-CdQpBb7M.js} +1 -1
- package/web/dist/assets/citizen-BOn5BBRL.js +1 -0
- package/web/dist/assets/compose-D-X3BQgY.js +1 -0
- package/web/dist/assets/format-Dhd2NUB3.js +55 -0
- package/web/dist/assets/{governance-YialJVYK.js → governance-CWzi7hzX.js} +1 -1
- package/web/dist/assets/index-CA_njyMe.css +2 -0
- package/web/dist/assets/index-iplooQqq.js +24 -0
- package/web/dist/assets/{mentions-Buc5vs-j.js → mentions-S-r32HKM.js} +1 -1
- package/web/dist/assets/{tasks-DLTeaHys.js → tasks-D_Fy7dtN.js} +1 -1
- package/web/dist/assets/useMatch-DZXCHHaG.js +1 -0
- package/web/dist/index.html +9 -9
- package/web/dist/assets/ChannelForms-BVidWMId.js +0 -1
- package/web/dist/assets/CitizenView-xU99R8hS.js +0 -2
- package/web/dist/assets/KnowledgeView-Qog6c92V.js +0 -1
- package/web/dist/assets/MetricsView-DuOyISk1.js +0 -1
- package/web/dist/assets/NeedsYouView-CrmHXZAn.js +0 -1
- package/web/dist/assets/Overview-xatORJK6.js +0 -1
- package/web/dist/assets/ProposalView-DJ0aJm7J.js +0 -1
- package/web/dist/assets/RunnersView-CcAAfw6y.js +0 -1
- package/web/dist/assets/citizen-CgA4vJjy.js +0 -1
- package/web/dist/assets/compose-3ksQaHF6.js +0 -1
- package/web/dist/assets/format-DpQyO6r1.js +0 -55
- package/web/dist/assets/index-DIa8Dlav.css +0 -2
- package/web/dist/assets/index-DhuCp-VF.js +0 -23
- package/web/dist/assets/useMatch-DPEg8ohe.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// apps/server/dist/index.js
|
|
4
4
|
import { access as access2 } from "node:fs/promises";
|
|
5
5
|
import path11 from "node:path";
|
|
6
|
-
import { serve } from "@hono/node-server";
|
|
7
6
|
|
|
8
7
|
// packages/board-core/dist/board.js
|
|
9
8
|
import { chmod as chmod2, cp, mkdir as mkdir5, readdir as readdir4, readFile as readFile4, rename as rename3, rm as rm3, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
|
|
@@ -65,7 +64,8 @@ var VERB_NAMES = [
|
|
|
65
64
|
"update_cron",
|
|
66
65
|
"remove_cron",
|
|
67
66
|
"remove_skill",
|
|
68
|
-
"upload_image"
|
|
67
|
+
"upload_image",
|
|
68
|
+
"read_image"
|
|
69
69
|
];
|
|
70
70
|
var VerbNameSchema = z2.enum(VERB_NAMES);
|
|
71
71
|
var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
|
|
@@ -95,6 +95,7 @@ var USER_NAME = "user";
|
|
|
95
95
|
var MEMBER_VERBS = [
|
|
96
96
|
"post_message",
|
|
97
97
|
"upload_image",
|
|
98
|
+
"read_image",
|
|
98
99
|
"read_inbox",
|
|
99
100
|
"search",
|
|
100
101
|
"open_thread",
|
|
@@ -226,7 +227,12 @@ var CronScheduleSchema = z3.union([
|
|
|
226
227
|
var CronRunSchema = z3.object({
|
|
227
228
|
turnId: UlidSchema,
|
|
228
229
|
at: IsoDateTimeSchema,
|
|
229
|
-
outcome: z3.enum(["completed", "failed", "stopped"])
|
|
230
|
+
outcome: z3.enum(["completed", "failed", "stopped"]),
|
|
231
|
+
/**
|
|
232
|
+
* For a failed one, the first turn of the cron's citizen in its conversation that was shown the
|
|
233
|
+
* failure and completed, and when it ended.
|
|
234
|
+
*/
|
|
235
|
+
caughtUp: z3.object({ turnId: UlidSchema, at: IsoDateTimeSchema }).optional()
|
|
230
236
|
});
|
|
231
237
|
var CronFrontmatterSchema = z3.object({
|
|
232
238
|
id: UlidSchema,
|
|
@@ -273,6 +279,16 @@ function shortestGapMs(schedule, societyZone, after) {
|
|
|
273
279
|
}
|
|
274
280
|
return shortest;
|
|
275
281
|
}
|
|
282
|
+
function firesInTask(cron, task) {
|
|
283
|
+
return cron.scope === task.project && cron.thread === task.id;
|
|
284
|
+
}
|
|
285
|
+
function cronFailure(cron) {
|
|
286
|
+
return cron.lastRun?.outcome === "failed" && cron.lastRun.caughtUp === void 0 ? cron.lastRun : null;
|
|
287
|
+
}
|
|
288
|
+
function cronStoppedAt(cron) {
|
|
289
|
+
const times = [cron.paused?.at, cron.ended?.at, cronFailure(cron)?.at].filter((at) => at !== void 0);
|
|
290
|
+
return times.reduce((first, at) => first === null || Date.parse(at) < Date.parse(first) ? at : first, null);
|
|
291
|
+
}
|
|
276
292
|
function nextFire(cron, societyZone, floorMs = CRON_FLOOR_MS) {
|
|
277
293
|
if (cron.ended !== void 0 || cron.paused !== void 0) {
|
|
278
294
|
return null;
|
|
@@ -704,6 +720,7 @@ var MemberSchema = z5.object({
|
|
|
704
720
|
});
|
|
705
721
|
var RunnerOsSchema = z5.enum(["linux", "windows", "darwin"]);
|
|
706
722
|
var GitIdentityPartSchema = z5.enum(["name", "email"]);
|
|
723
|
+
var RunnerPauseSchema = z5.object({ at: IsoDateTimeSchema, by: NameSchema });
|
|
707
724
|
var RunnerSchema = z5.object({
|
|
708
725
|
name: NameSchema,
|
|
709
726
|
os: RunnerOsSchema,
|
|
@@ -722,8 +739,25 @@ var RunnerSchema = z5.object({
|
|
|
722
739
|
* Each CLI's plan quota as its login on this runner last reported it, from a turn's stream or a
|
|
723
740
|
* read; absent until one has.
|
|
724
741
|
*/
|
|
725
|
-
quotas: z5.partialRecord(CliKindSchema, CliQuotaSchema).optional()
|
|
742
|
+
quotas: z5.partialRecord(CliKindSchema, CliQuotaSchema).optional(),
|
|
743
|
+
/** The user's pause of the whole runner: no turn starts on it until the user resumes it. */
|
|
744
|
+
paused: RunnerPauseSchema.optional(),
|
|
745
|
+
/**
|
|
746
|
+
* The user's pauses of single CLIs on the runner, each apart from the runner's own: no turn of
|
|
747
|
+
* that CLI starts on it until the user resumes it.
|
|
748
|
+
*/
|
|
749
|
+
pausedClis: z5.partialRecord(CliKindSchema, RunnerPauseSchema).optional()
|
|
726
750
|
});
|
|
751
|
+
function runnerPause(runner, cli) {
|
|
752
|
+
return runner.paused ?? runner.pausedClis?.[cli];
|
|
753
|
+
}
|
|
754
|
+
function heldOn(runners2, runner, cli) {
|
|
755
|
+
if (runner === void 0 || cli === null) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
const record = runners2.find((each) => each.name === runner);
|
|
759
|
+
return record !== void 0 && runnerPause(record, cli) !== void 0;
|
|
760
|
+
}
|
|
727
761
|
var ProposalKindSchema = z5.enum([
|
|
728
762
|
"role",
|
|
729
763
|
"member",
|
|
@@ -1073,7 +1107,21 @@ function imageTypeOf(bytes) {
|
|
|
1073
1107
|
return null;
|
|
1074
1108
|
}
|
|
1075
1109
|
var ImagePathSchema = z6.string().min(1).max(4096).refine((value) => !value.includes("\0"), { message: "a path with no NUL character" });
|
|
1110
|
+
var ImageNameSchema = z6.string().max(1024);
|
|
1111
|
+
function imageDescription(name) {
|
|
1112
|
+
return name.replaceAll(/[[\]\\\r\n`]/g, "");
|
|
1113
|
+
}
|
|
1076
1114
|
var IMAGE_SCHEME = "image:";
|
|
1115
|
+
var ImageRefSchema = z6.string().regex(/^(?:image:)?[0-9A-HJKMNP-TV-Z]{26}$/, "expected an image's id or image:<id>");
|
|
1116
|
+
function imageIdOf(ref) {
|
|
1117
|
+
return ref.startsWith(IMAGE_SCHEME) ? ref.slice(IMAGE_SCHEME.length) : ref;
|
|
1118
|
+
}
|
|
1119
|
+
var ImageFileSchema = z6.object({
|
|
1120
|
+
id: UlidSchema,
|
|
1121
|
+
type: ImageTypeSchema,
|
|
1122
|
+
size: z6.number().int().positive(),
|
|
1123
|
+
path: ImagePathSchema
|
|
1124
|
+
});
|
|
1077
1125
|
var UploadedImageSchema = z6.object({
|
|
1078
1126
|
id: UlidSchema,
|
|
1079
1127
|
type: ImageTypeSchema,
|
|
@@ -1238,6 +1286,10 @@ var VerbInputs = {
|
|
|
1238
1286
|
upload_image: z7.object({
|
|
1239
1287
|
/** The image file on the machine your turn runs on: absolute, or relative to your working directory. */
|
|
1240
1288
|
path: ImagePathSchema
|
|
1289
|
+
}),
|
|
1290
|
+
read_image: z7.object({
|
|
1291
|
+
/** The image's id, or its `image:<id>` address as a post shows it. */
|
|
1292
|
+
image_id: ImageRefSchema
|
|
1241
1293
|
})
|
|
1242
1294
|
};
|
|
1243
1295
|
var VERB_DESCRIPTIONS = {
|
|
@@ -1249,7 +1301,7 @@ var VERB_DESCRIPTIONS = {
|
|
|
1249
1301
|
create_task: "Create a task in a project with its plan: stages of {name, role or agent, gate}. Without stages it gets one stage, work, that anyone in the project may take. File it in a channel of the project, such as a release's, or general by default; its thread opens there with it, under the task's id, for everything said about the work.",
|
|
1250
1302
|
claim_task: "Hold the task's current stage. Claims are leases renewed by every turn that touches the task.",
|
|
1251
1303
|
release_task: "Let go of the stage you hold so someone else can take it.",
|
|
1252
|
-
update_task: "Move a task back to an earlier stage (the current stage's holder, the user, the steward, or the concierge; anyone else asks the concierge in the task's thread), abandon it, set the tasks it is blocked by (blocked_by: its stage wakes nobody until they have all ended), or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
|
|
1304
|
+
update_task: "Move a task back to an earlier stage (the current stage's holder, the user, the steward, or the concierge; anyone else asks the concierge in the task's thread, with a mention), abandon it, set the tasks it is blocked by (blocked_by: its stage wakes nobody until they have all ended), or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
|
|
1253
1305
|
get_task: "Read a task with its plan, its brief, and the messages of its thread.",
|
|
1254
1306
|
subscribe: "Subscribe to a channel. Subscriptions feed your digest; they never wake you.",
|
|
1255
1307
|
unsubscribe: "Unsubscribe from a channel.",
|
|
@@ -1272,7 +1324,8 @@ var VERB_DESCRIPTIONS = {
|
|
|
1272
1324
|
update_cron: "Pause or resume a cron, or change its title, note, or schedule; resuming or rescheduling starts it afresh from now. Yours, or anyone's for the user, the steward, and the concierge.",
|
|
1273
1325
|
remove_cron: "End a cron for good, with a reason. Yours, or anyone's for the user, the steward, and the concierge. A cron also ends with its conversation, and a one-time cron once it fires.",
|
|
1274
1326
|
remove_skill: "Remove a skill from the society's skills, with a reason, when it is wrong, superseded, or no longer used: no skills index lists it from the next turn, and a notice in general says so with the reason. Its proposer's own copy stays in its home. Steward and user only; the board keeps its text aside for the user to restore.",
|
|
1275
|
-
upload_image: "Upload an image file, such as a screenshot or a chart, from the machine your turn runs on: a PNG, JPEG, GIF, or WebP of at most 8 MB, in your worktree, your home, or the system temp folder. Returns its id and the markdown that shows it in a post, ``; write what it shows as the alt text."
|
|
1327
|
+
upload_image: "Upload an image file, such as a screenshot or a chart, from the machine your turn runs on: a PNG, JPEG, GIF, or WebP of at most 8 MB, in your worktree, your home, or the system temp folder. Returns its id and the markdown that shows it in a post, ``; write what it shows as the alt text.",
|
|
1328
|
+
read_image: "Open an image the board keeps, as a post shows it, ``, by its id: the board writes it to a file of this turn's own on the machine it runs on, removed when the turn ends, and returns its path, type, and size. Open the path with your file tools to see the image."
|
|
1276
1329
|
};
|
|
1277
1330
|
|
|
1278
1331
|
// packages/shared/dist/events.js
|
|
@@ -1323,6 +1376,8 @@ var BOARD_EVENT_TYPES = [
|
|
|
1323
1376
|
"user.token_rotated",
|
|
1324
1377
|
"runner.changed",
|
|
1325
1378
|
"runner.quota",
|
|
1379
|
+
"runner.paused",
|
|
1380
|
+
"runner.resumed",
|
|
1326
1381
|
"ops.signal",
|
|
1327
1382
|
"paused.changed",
|
|
1328
1383
|
"wake.requested",
|
|
@@ -1472,6 +1527,11 @@ var RunnerHelloSchema = z9.object({
|
|
|
1472
1527
|
quotaReads: z9.boolean().default(false),
|
|
1473
1528
|
/** Whether the runner reads an image a turn running there uploads; one from before never does. */
|
|
1474
1529
|
imageReads: z9.boolean().default(false),
|
|
1530
|
+
/**
|
|
1531
|
+
* Whether the runner writes an image the board sends for a turn running there to open, for
|
|
1532
|
+
* `read_image`; one from before never does.
|
|
1533
|
+
*/
|
|
1534
|
+
imageWrites: z9.boolean().default(false),
|
|
1475
1535
|
/**
|
|
1476
1536
|
* Whether the runner carries a project's repository out to another runner and in from one, and
|
|
1477
1537
|
* removes its copy once the project has moved; one from before never does.
|
|
@@ -1593,6 +1653,12 @@ var TurnJobSchema = z9.object({
|
|
|
1593
1653
|
usageSoFar: UsageSchema.optional(),
|
|
1594
1654
|
/** The rendered charter, norms, memory core, and skills index, with `PATH_TOKENS` for places. */
|
|
1595
1655
|
instructions: z9.string(),
|
|
1656
|
+
/**
|
|
1657
|
+
* The hash of `instructions` without the first turn's section (`instructionsHash` in
|
|
1658
|
+
* `turn-host`): a warm process started on other instructions starts afresh, so a compaction in
|
|
1659
|
+
* it puts back the current ones. Absent from a server from before, when the text is compared.
|
|
1660
|
+
*/
|
|
1661
|
+
instructionsHash: z9.string().optional(),
|
|
1596
1662
|
prompt: z9.string(),
|
|
1597
1663
|
mcp: z9.object({ url: z9.string().min(1), token: z9.string().min(1) }),
|
|
1598
1664
|
limits: z9.object({
|
|
@@ -1777,6 +1843,13 @@ var TurnImageSchema = z9.discriminatedUnion("kind", [
|
|
|
1777
1843
|
}),
|
|
1778
1844
|
z9.object({ kind: z9.literal("refused"), why: ImageRefusalSchema, detail: z9.string() })
|
|
1779
1845
|
]);
|
|
1846
|
+
var TurnImageWriteSchema = z9.object({
|
|
1847
|
+
turnId: UlidSchema,
|
|
1848
|
+
id: UlidSchema,
|
|
1849
|
+
type: ImageTypeSchema,
|
|
1850
|
+
content: z9.string()
|
|
1851
|
+
});
|
|
1852
|
+
var TurnImageFileSchema = z9.object({ path: ImagePathSchema });
|
|
1780
1853
|
var WorkspaceConversationSchema = z9.discriminatedUnion("kind", [
|
|
1781
1854
|
z9.object({ kind: z9.literal("thread"), id: UlidSchema }),
|
|
1782
1855
|
z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema }),
|
|
@@ -1799,6 +1872,10 @@ var HeldWorkspaceSchema = z9.object({
|
|
|
1799
1872
|
var HeldWorkspacesSchema = z9.object({ workspaces: z9.array(HeldWorkspaceSchema) });
|
|
1800
1873
|
var SweepResultSchema = z9.object({ kept: z9.array(HeldWorkspaceSchema) });
|
|
1801
1874
|
var MOVE_PACK_LIMIT_BYTES = 256 * 1024 * 1024;
|
|
1875
|
+
var MOVE_FLOOR_BYTES_PER_SECOND = 16 * 1024;
|
|
1876
|
+
function moveTransferMs(bytes) {
|
|
1877
|
+
return 10 * 6e4 + Math.ceil(bytes / MOVE_FLOOR_BYTES_PER_SECOND) * 1e3;
|
|
1878
|
+
}
|
|
1802
1879
|
var RepoRefsSchema = z9.record(z9.string().regex(/^refs\/(heads|tags)\/\S+$/), z9.string().regex(/^[0-9a-f]{40}([0-9a-f]{24})?$/));
|
|
1803
1880
|
var ProjectExportSchema = z9.object({ move: UlidSchema, repo: ProjectRepoSchema });
|
|
1804
1881
|
var HeldWorkSchema = z9.object({
|
|
@@ -1822,7 +1899,9 @@ var ProjectExportedSchema = z9.discriminatedUnion("ok", [
|
|
|
1822
1899
|
var ProjectImportSchema = z9.object({
|
|
1823
1900
|
move: UlidSchema,
|
|
1824
1901
|
repo: ProjectRepoSchema,
|
|
1825
|
-
pullRequests: z9.boolean()
|
|
1902
|
+
pullRequests: z9.boolean(),
|
|
1903
|
+
/** The packs' size the old runner measured, which the clone's clock is sized from; the cap if unsaid. */
|
|
1904
|
+
bytes: z9.number().int().nonnegative().optional()
|
|
1826
1905
|
});
|
|
1827
1906
|
var ProjectImportedSchema = z9.object({ refs: RepoRefsSchema });
|
|
1828
1907
|
var ProjectReleaseSchema = z9.object({ slug: NameSchema });
|
|
@@ -1862,6 +1941,15 @@ var RunnerMessageSchema = z9.discriminatedUnion("type", [
|
|
|
1862
1941
|
* whose hello says `imageReads`.
|
|
1863
1942
|
*/
|
|
1864
1943
|
z9.object({ type: z9.literal("image"), request: z9.string().min(1), read: TurnImageReadSchema }),
|
|
1944
|
+
/**
|
|
1945
|
+
* Answered with a `TurnImageFile`, or null when the turn is not running there. Sent only to
|
|
1946
|
+
* runners whose hello says `imageWrites`.
|
|
1947
|
+
*/
|
|
1948
|
+
z9.object({
|
|
1949
|
+
type: z9.literal("image_write"),
|
|
1950
|
+
request: z9.string().min(1),
|
|
1951
|
+
write: TurnImageWriteSchema
|
|
1952
|
+
}),
|
|
1865
1953
|
/** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
|
|
1866
1954
|
z9.object({
|
|
1867
1955
|
type: z9.literal("steer"),
|
|
@@ -1927,6 +2015,7 @@ var TriggerKindSchema = z10.enum([
|
|
|
1927
2015
|
"closing",
|
|
1928
2016
|
"cron"
|
|
1929
2017
|
]);
|
|
2018
|
+
var HeartbeatCauseSchema = z10.enum(["unread", "held", "waiting"]);
|
|
1930
2019
|
var TriggerSchema = z10.object({
|
|
1931
2020
|
kind: TriggerKindSchema,
|
|
1932
2021
|
from: NameSchema.optional(),
|
|
@@ -1937,7 +2026,9 @@ var TriggerSchema = z10.object({
|
|
|
1937
2026
|
/** The channel the waking message was posted in. */
|
|
1938
2027
|
channel: ChannelRefSchema.optional(),
|
|
1939
2028
|
/** The cron that came due, for a `cron` wake. */
|
|
1940
|
-
cronId: UlidSchema.optional()
|
|
2029
|
+
cronId: UlidSchema.optional(),
|
|
2030
|
+
/** What a `heartbeat` woke for; absent on records from before the board kept it. */
|
|
2031
|
+
heartbeat: HeartbeatCauseSchema.optional()
|
|
1941
2032
|
});
|
|
1942
2033
|
var TurnThreadSchema = z10.object({ id: UlidSchema, task: z10.boolean() });
|
|
1943
2034
|
var TurnDispatchSchema = z10.object({
|
|
@@ -2019,6 +2110,12 @@ var TurnRecordSchema = z10.object({
|
|
|
2019
2110
|
* one. A resumed session's next turn measures its own usage from here.
|
|
2020
2111
|
*/
|
|
2021
2112
|
sessionUsage: UsageSchema.optional(),
|
|
2113
|
+
/**
|
|
2114
|
+
* The SHA-256 of the instructions the session holds after this turn, the first turn's section
|
|
2115
|
+
* left out (`instructionsHash` in `turn-host`): those it began with, or the latest a prompt
|
|
2116
|
+
* carried. Absent when the board does not know them, as for records from before it kept them.
|
|
2117
|
+
*/
|
|
2118
|
+
sessionInstructionsHash: z10.string().optional(),
|
|
2022
2119
|
/**
|
|
2023
2120
|
* The whole input of the turn's latest model call in the session's own thread, as the runner
|
|
2024
2121
|
* reported it: how large the session had grown. Absent for a turn that made no call.
|
|
@@ -2043,7 +2140,13 @@ var TurnRecordSchema = z10.object({
|
|
|
2043
2140
|
*/
|
|
2044
2141
|
sessionDropped: z10.boolean().optional(),
|
|
2045
2142
|
/** The crons whose fire started the turn, which record how it ended. */
|
|
2046
|
-
crons: z10.array(UlidSchema).optional()
|
|
2143
|
+
crons: z10.array(UlidSchema).optional(),
|
|
2144
|
+
/**
|
|
2145
|
+
* The failed fires its prompt named: each of its citizen's crons in its conversation whose latest
|
|
2146
|
+
* fire's turn failed with nothing catching it up (`cronFailure`), but those it carries, with that
|
|
2147
|
+
* turn. Completing it catches each up.
|
|
2148
|
+
*/
|
|
2149
|
+
cronFailures: z10.array(z10.object({ cron: UlidSchema, turnId: UlidSchema })).optional()
|
|
2047
2150
|
});
|
|
2048
2151
|
var RunningTurnSchema = z10.object({
|
|
2049
2152
|
turnId: UlidSchema,
|
|
@@ -2085,6 +2188,8 @@ var TurnHistoryEntrySchema = z10.object({
|
|
|
2085
2188
|
/** The project whose channels' conversation the turn was in, for a turn in the society scope. */
|
|
2086
2189
|
place: NameSchema.optional(),
|
|
2087
2190
|
trigger: z10.string(),
|
|
2191
|
+
/** What a `heartbeat` woke for; absent on turns logged before the board kept it. */
|
|
2192
|
+
heartbeat: HeartbeatCauseSchema.optional(),
|
|
2088
2193
|
exitReason: z10.string().nullable(),
|
|
2089
2194
|
costUsd: z10.number(),
|
|
2090
2195
|
/** The tokens the turn used; absent for turns logged before turns recorded them. */
|
|
@@ -2158,7 +2263,7 @@ var METRICS_WINDOW_MS = {
|
|
|
2158
2263
|
"7d": 7 * 24 * 36e5,
|
|
2159
2264
|
all: null
|
|
2160
2265
|
};
|
|
2161
|
-
var READ_VERBS = ["read_inbox", "search", "get_task"];
|
|
2266
|
+
var READ_VERBS = ["read_inbox", "search", "get_task", "read_image"];
|
|
2162
2267
|
function isBoardAction(tool) {
|
|
2163
2268
|
const verb = /^mcp__board__(.+)$/.exec(tool)?.[1];
|
|
2164
2269
|
return verb !== void 0 && !READ_VERBS.some((read2) => read2 === verb);
|
|
@@ -2172,12 +2277,19 @@ var MetricsSchema = z12.object({
|
|
|
2172
2277
|
/**
|
|
2173
2278
|
* Finished turns that changed nothing on the board. Reflections are left out, since they write
|
|
2174
2279
|
* memory, not the board; turns without a transcript are counted apart, their actions unknown.
|
|
2280
|
+
* Heartbeats are split by what each woke for, and those recorded before the board kept it are a
|
|
2281
|
+
* row without one.
|
|
2175
2282
|
*/
|
|
2176
2283
|
idle: z12.object({
|
|
2177
2284
|
turns: CountSchema,
|
|
2178
2285
|
idle: CountSchema,
|
|
2179
2286
|
unknown: CountSchema,
|
|
2180
|
-
byTrigger: z12.array(z12.object({
|
|
2287
|
+
byTrigger: z12.array(z12.object({
|
|
2288
|
+
trigger: z12.string(),
|
|
2289
|
+
heartbeat: HeartbeatCauseSchema.optional(),
|
|
2290
|
+
turns: CountSchema,
|
|
2291
|
+
idle: CountSchema
|
|
2292
|
+
})),
|
|
2181
2293
|
byAgent: z12.array(z12.object({
|
|
2182
2294
|
agent: NameSchema,
|
|
2183
2295
|
role: NameSchema.nullable(),
|
|
@@ -2355,7 +2467,7 @@ var RunnerConfigSchema = z13.object({
|
|
|
2355
2467
|
});
|
|
2356
2468
|
|
|
2357
2469
|
// packages/shared/dist/version.js
|
|
2358
|
-
var STELLARIS_VERSION = "0.
|
|
2470
|
+
var STELLARIS_VERSION = "0.4.0";
|
|
2359
2471
|
|
|
2360
2472
|
// packages/shared/dist/mentions.js
|
|
2361
2473
|
import { parse, postprocess, preprocess } from "micromark";
|
|
@@ -3119,7 +3231,8 @@ var TurnPayload = z14.object({
|
|
|
3119
3231
|
thread: z14.string().optional(),
|
|
3120
3232
|
channel: z14.string().optional(),
|
|
3121
3233
|
place: z14.string().optional(),
|
|
3122
|
-
trigger: z14.string()
|
|
3234
|
+
trigger: z14.string(),
|
|
3235
|
+
heartbeat: HeartbeatCauseSchema.optional().catch(void 0)
|
|
3123
3236
|
});
|
|
3124
3237
|
var TaskPayload = z14.object({
|
|
3125
3238
|
taskId: z14.string(),
|
|
@@ -3219,13 +3332,12 @@ function computeMetrics(input) {
|
|
|
3219
3332
|
const nothing = actions === 0 ? 1 : 0;
|
|
3220
3333
|
turns2 += 1;
|
|
3221
3334
|
idle += nothing;
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
}
|
|
3335
|
+
const { trigger, heartbeat } = turn.data;
|
|
3336
|
+
const key = heartbeat === void 0 ? trigger : `${trigger}\0${heartbeat}`;
|
|
3337
|
+
const row = turnRows.get(key) ?? { trigger, heartbeat, turns: 0, idle: 0 };
|
|
3338
|
+
turnRows.set(key, { ...row, turns: row.turns + 1, idle: row.idle + nothing });
|
|
3339
|
+
const own = agentRows.get(event.actor) ?? { turns: 0, idle: 0 };
|
|
3340
|
+
agentRows.set(event.actor, { turns: own.turns + 1, idle: own.idle + nothing });
|
|
3229
3341
|
break;
|
|
3230
3342
|
}
|
|
3231
3343
|
case "turn.started": {
|
|
@@ -3381,7 +3493,10 @@ function computeMetrics(input) {
|
|
|
3381
3493
|
turns: turns2,
|
|
3382
3494
|
idle,
|
|
3383
3495
|
unknown,
|
|
3384
|
-
byTrigger: [...turnRows].map((
|
|
3496
|
+
byTrigger: [...turnRows.values()].map(({ heartbeat, ...row }) => ({
|
|
3497
|
+
...row,
|
|
3498
|
+
...heartbeat === void 0 ? {} : { heartbeat }
|
|
3499
|
+
})).toSorted((a, b) => b.idle - a.idle || b.turns - a.turns),
|
|
3385
3500
|
byAgent: [...agentRows].map(([agent, row]) => ({ agent, role: citizens.get(agent)?.role ?? null, ...row })).toSorted((a, b) => b.idle - a.idle || b.turns - a.turns)
|
|
3386
3501
|
},
|
|
3387
3502
|
sentBack: {
|
|
@@ -3441,6 +3556,8 @@ var TurnHistoryPayloadSchema = z15.object({
|
|
|
3441
3556
|
channel: NameSchema.optional(),
|
|
3442
3557
|
place: NameSchema.optional(),
|
|
3443
3558
|
trigger: z15.string().default("manual"),
|
|
3559
|
+
// One a later board added reads as none, so the turn stays in the history.
|
|
3560
|
+
heartbeat: HeartbeatCauseSchema.optional().catch(void 0),
|
|
3444
3561
|
exitReason: z15.string().nullable().default(null),
|
|
3445
3562
|
costUsd: z15.number().default(0),
|
|
3446
3563
|
usage: UsageSchema.optional(),
|
|
@@ -3866,7 +3983,10 @@ var SignInsSchema = z16.object({
|
|
|
3866
3983
|
expiresAt: IsoDateTimeSchema
|
|
3867
3984
|
})).default([])
|
|
3868
3985
|
});
|
|
3869
|
-
var TurnStartPayloadSchema = z16.object({
|
|
3986
|
+
var TurnStartPayloadSchema = z16.object({
|
|
3987
|
+
turnId: UlidSchema,
|
|
3988
|
+
heartbeat: TurnHistoryPayloadSchema.shape.heartbeat
|
|
3989
|
+
});
|
|
3870
3990
|
var CursorsSchema = z16.object({
|
|
3871
3991
|
digest: z16.string().nullable(),
|
|
3872
3992
|
scopes: z16.record(z16.string(), z16.string()).default({}),
|
|
@@ -3938,6 +4058,9 @@ function skillFolderOf(proposal) {
|
|
|
3938
4058
|
function proposedFrom(origin, where) {
|
|
3939
4059
|
return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
|
|
3940
4060
|
}
|
|
4061
|
+
function latestLease(a, b) {
|
|
4062
|
+
return a === null ? b : b === null || a >= b ? a : b;
|
|
4063
|
+
}
|
|
3941
4064
|
function megabytes(bytes) {
|
|
3942
4065
|
return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
|
|
3943
4066
|
}
|
|
@@ -4077,6 +4200,7 @@ var Board = class _Board {
|
|
|
4077
4200
|
/** The turns in flight, which the server hosting them supplies; none until it does. */
|
|
4078
4201
|
turnsInFlight = () => [];
|
|
4079
4202
|
imageSource = null;
|
|
4203
|
+
imageSink = null;
|
|
4080
4204
|
newId = monotonicFactory();
|
|
4081
4205
|
events;
|
|
4082
4206
|
actionCounts = /* @__PURE__ */ new Map();
|
|
@@ -4127,10 +4251,13 @@ var Board = class _Board {
|
|
|
4127
4251
|
/**
|
|
4128
4252
|
* Applies each one-time alignment an existing society has not had yet, and remembers it, so a
|
|
4129
4253
|
* member that later chooses otherwise keeps its choice. A new alignment is a name and a method in
|
|
4130
|
-
* this list; once every board has applied it, both are deleted.
|
|
4254
|
+
* this list; once every board has applied it, both are deleted.
|
|
4131
4255
|
*/
|
|
4132
4256
|
async applyAlignments() {
|
|
4133
|
-
const alignments = [
|
|
4257
|
+
const alignments = [
|
|
4258
|
+
// Whoever may show an image in a post may open one a post shows.
|
|
4259
|
+
{ name: "image-reads-granted", apply: () => this.grantBeside("upload_image", "read_image") }
|
|
4260
|
+
];
|
|
4134
4261
|
const file = path8.join(this.paths.state(), "alignments.json");
|
|
4135
4262
|
const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
|
|
4136
4263
|
const pending = alignments.filter(({ name }) => !applied.includes(name));
|
|
@@ -4142,6 +4269,17 @@ var Board = class _Board {
|
|
|
4142
4269
|
}
|
|
4143
4270
|
await writeJson(file, { applied: [...applied, ...pending.map(({ name }) => name)] });
|
|
4144
4271
|
}
|
|
4272
|
+
/**
|
|
4273
|
+
* Grants `verb` once to every charter granting `beside`, the work roles' charters in practice;
|
|
4274
|
+
* seed roles have it from their seed.
|
|
4275
|
+
*/
|
|
4276
|
+
async grantBeside(beside, verb) {
|
|
4277
|
+
for (const charter of await this.listRoles()) {
|
|
4278
|
+
if (charter.verbs.includes(beside) && !charter.verbs.includes(verb)) {
|
|
4279
|
+
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4145
4283
|
/** Writes every missing seed charter and aligns the existing ones with their seed. */
|
|
4146
4284
|
async ensureSeedRoles() {
|
|
4147
4285
|
await ensureDir(this.paths.roles());
|
|
@@ -4909,6 +5047,46 @@ ${DASHBOARD_LINE}
|
|
|
4909
5047
|
return next;
|
|
4910
5048
|
});
|
|
4911
5049
|
}
|
|
5050
|
+
/**
|
|
5051
|
+
* Pauses or resumes a runner, the user's alone: the whole runner, or with `cli` that CLI on it
|
|
5052
|
+
* alone, each apart from the other. While either holds, no turn it covers starts there; their
|
|
5053
|
+
* wakes wait in the scheduler's queue, and a turn under way finishes. Pausing what is paused, or
|
|
5054
|
+
* resuming what is not, changes nothing and logs nothing.
|
|
5055
|
+
*/
|
|
5056
|
+
async setRunnerPaused(actor, name, change) {
|
|
5057
|
+
this.assertUser(actor, "only the user may pause or resume a runner");
|
|
5058
|
+
const cli = change.cli === void 0 ? void 0 : CliKindSchema.parse(change.cli);
|
|
5059
|
+
return this.mutex.run(async () => {
|
|
5060
|
+
const file = this.paths.runner(name);
|
|
5061
|
+
if (!await exists(file)) {
|
|
5062
|
+
throw new BoardError("NOT_FOUND", `runner ${name} not found`);
|
|
5063
|
+
}
|
|
5064
|
+
const doc = await readMarkdown(file, RunnerSchema);
|
|
5065
|
+
const { paused, pausedClis, ...rest } = doc.data;
|
|
5066
|
+
if (cli !== void 0 && change.paused && !doc.data.clis.includes(cli)) {
|
|
5067
|
+
throw new BoardError("INVALID_STATE", `runner ${name} has no ${cli} to pause`);
|
|
5068
|
+
}
|
|
5069
|
+
const current = cli === void 0 ? paused : pausedClis?.[cli];
|
|
5070
|
+
if (current !== void 0 === change.paused) {
|
|
5071
|
+
return doc.data;
|
|
5072
|
+
}
|
|
5073
|
+
const pause = change.paused ? { at: this.now().toISOString(), by: actor.name } : void 0;
|
|
5074
|
+
const clis = cli === void 0 ? { ...pausedClis } : { ...pausedClis, [cli]: pause };
|
|
5075
|
+
const kept = Object.entries(clis).filter(([, each]) => each !== void 0);
|
|
5076
|
+
const whole = cli === void 0 ? pause : paused;
|
|
5077
|
+
const next = RunnerSchema.parse({
|
|
5078
|
+
...rest,
|
|
5079
|
+
...whole === void 0 ? {} : { paused: whole },
|
|
5080
|
+
...kept.length === 0 ? {} : { pausedClis: Object.fromEntries(kept) }
|
|
5081
|
+
});
|
|
5082
|
+
await writeMarkdown(file, next, doc.body);
|
|
5083
|
+
await this.events.append(change.paused ? "runner.paused" : "runner.resumed", actor.name, {
|
|
5084
|
+
name,
|
|
5085
|
+
...cli === void 0 ? {} : { cli }
|
|
5086
|
+
});
|
|
5087
|
+
return next;
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
4912
5090
|
/**
|
|
4913
5091
|
* Logs an operations signal as an `ops.signal` event. No channel carries it: the scheduler wakes
|
|
4914
5092
|
* its readers from the event, the runner lists it in their prompts, and the interface's log shows
|
|
@@ -4943,8 +5121,9 @@ ${DASHBOARD_LINE}
|
|
|
4943
5121
|
}
|
|
4944
5122
|
if (event.type === "turn.started") {
|
|
4945
5123
|
const start = TurnStartPayloadSchema.safeParse(event.payload);
|
|
4946
|
-
if (start.success)
|
|
4947
|
-
starts.set(start.data.turnId, event.ts);
|
|
5124
|
+
if (start.success) {
|
|
5125
|
+
starts.set(start.data.turnId, { ts: event.ts, heartbeat: start.data.heartbeat });
|
|
5126
|
+
}
|
|
4948
5127
|
continue;
|
|
4949
5128
|
}
|
|
4950
5129
|
if (event.type !== "turn.completed" && event.type !== "turn.failed") {
|
|
@@ -4952,17 +5131,19 @@ ${DASHBOARD_LINE}
|
|
|
4952
5131
|
}
|
|
4953
5132
|
const parsed = TurnHistoryPayloadSchema.safeParse(event.payload);
|
|
4954
5133
|
if (parsed.success) {
|
|
4955
|
-
const {
|
|
4956
|
-
const
|
|
4957
|
-
if (turnId !== void 0) {
|
|
4958
|
-
starts.delete(turnId);
|
|
5134
|
+
const { heartbeat: ended, ...turn } = parsed.data;
|
|
5135
|
+
const start = turn.turnId === void 0 ? void 0 : starts.get(turn.turnId);
|
|
5136
|
+
if (turn.turnId !== void 0) {
|
|
5137
|
+
starts.delete(turn.turnId);
|
|
4959
5138
|
}
|
|
5139
|
+
const heartbeat = ended ?? start?.heartbeat;
|
|
4960
5140
|
entries.push({
|
|
4961
5141
|
id: event.id,
|
|
4962
5142
|
ts: event.ts,
|
|
4963
5143
|
outcome: event.type === "turn.failed" ? "failed" : "completed",
|
|
4964
|
-
...
|
|
4965
|
-
...
|
|
5144
|
+
...turn,
|
|
5145
|
+
...heartbeat === void 0 ? {} : { heartbeat },
|
|
5146
|
+
...start === void 0 ? {} : { startedAt: start.ts }
|
|
4966
5147
|
});
|
|
4967
5148
|
}
|
|
4968
5149
|
}
|
|
@@ -4992,14 +5173,31 @@ ${DASHBOARD_LINE}
|
|
|
4992
5173
|
return society;
|
|
4993
5174
|
});
|
|
4994
5175
|
}
|
|
4995
|
-
/**
|
|
5176
|
+
/**
|
|
5177
|
+
* Releases every claim whose lease has expired, but for a holder's that its own cron in the
|
|
5178
|
+
* task's thread keeps (`keptLease`). Called by the scheduler on its tick.
|
|
5179
|
+
*/
|
|
4996
5180
|
async expireLeases() {
|
|
4997
5181
|
return this.mutex.run(async () => {
|
|
4998
5182
|
const expired = [];
|
|
4999
5183
|
const now = this.now();
|
|
5184
|
+
let crons;
|
|
5185
|
+
let runners2;
|
|
5000
5186
|
for (const project of await listDirs(this.paths.projects())) {
|
|
5187
|
+
let place;
|
|
5001
5188
|
for (const task of await this.listTasks(project)) {
|
|
5002
|
-
if (task.status
|
|
5189
|
+
if (task.status !== "claimed") {
|
|
5190
|
+
continue;
|
|
5191
|
+
}
|
|
5192
|
+
crons ??= await this.listCrons();
|
|
5193
|
+
runners2 ??= await this.listRunners();
|
|
5194
|
+
place ??= await this.readProject(project);
|
|
5195
|
+
const kept = latestLease(this.keptLease(task, crons, now), await this.pausedLease(task, runners2, place.runner, now));
|
|
5196
|
+
if (kept !== null) {
|
|
5197
|
+
await this.writeTask(project, { ...task, leaseExpiresAt: kept });
|
|
5198
|
+
continue;
|
|
5199
|
+
}
|
|
5200
|
+
if (this.leaseExpired(task, now)) {
|
|
5003
5201
|
const released = await this.writeTask(project, {
|
|
5004
5202
|
...task,
|
|
5005
5203
|
status: "open",
|
|
@@ -5068,20 +5266,24 @@ ${DASHBOARD_LINE}
|
|
|
5068
5266
|
* general channel, one of its other channels, one of its threads, or, in the society scope, a
|
|
5069
5267
|
* project's channels. The heartbeat asks, so that it wakes a member where its unread
|
|
5070
5268
|
* messages are and nowhere else. Society channels count only for a member woken in the society
|
|
5071
|
-
* scope; a project member reads them at its next turn.
|
|
5269
|
+
* scope; a project member reads them at its next turn. Some posts are in the digest but not
|
|
5072
5270
|
* counted, and are read when the member wakes for something else: a task step's, which wakes
|
|
5073
|
-
* whoever it hands work to
|
|
5074
|
-
*
|
|
5075
|
-
* nothing
|
|
5076
|
-
*
|
|
5271
|
+
* whoever it hands work to; the board's own, operations signals and landing announcements, whose
|
|
5272
|
+
* signals that call for action wake their readers directly; and a knowledge topic's notice, which
|
|
5273
|
+
* asks nothing of anyone. In a task's thread a post counts only for whoever the current stage is
|
|
5274
|
+
* for, its holder or, while nobody holds it, a member who may take it, and for the task's creator
|
|
5275
|
+
* when it mentions the user, so the creator can put a wait on the user in; a post that asks the
|
|
5276
|
+
* reader counts wherever it is. A closed thread counts for nothing: its conversation has ended,
|
|
5277
|
+
* and a turn there could only read its posts and be refused a reply, while a wake queued before
|
|
5278
|
+
* it closed, as for a mention, still runs there.
|
|
5077
5279
|
*/
|
|
5078
5280
|
async unreadByConversation(actor) {
|
|
5079
5281
|
const { cursors, upto } = await this.digestSnapshot(actor.name);
|
|
5080
5282
|
const counts = /* @__PURE__ */ new Map();
|
|
5081
|
-
const
|
|
5283
|
+
const threadCounts = /* @__PURE__ */ new Map();
|
|
5082
5284
|
const unscoped = { name: actor.name, role: actor.role };
|
|
5083
|
-
for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
|
|
5084
|
-
if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
|
|
5285
|
+
for (const { scope, part, message, news, asked } of await this.digestSince(unscoped, cursors, upto)) {
|
|
5286
|
+
if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name || message.notice?.kind === "knowledge") {
|
|
5085
5287
|
continue;
|
|
5086
5288
|
}
|
|
5087
5289
|
const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
|
|
@@ -5090,12 +5292,13 @@ ${DASHBOARD_LINE}
|
|
|
5090
5292
|
}
|
|
5091
5293
|
const thread = threadOfPart(part);
|
|
5092
5294
|
if (thread !== void 0) {
|
|
5093
|
-
let
|
|
5094
|
-
if (
|
|
5095
|
-
|
|
5096
|
-
|
|
5295
|
+
let counted = threadCounts.get(thread);
|
|
5296
|
+
if (counted === void 0) {
|
|
5297
|
+
counted = await this.threadCount(actor, thread);
|
|
5298
|
+
threadCounts.set(thread, counted);
|
|
5097
5299
|
}
|
|
5098
|
-
|
|
5300
|
+
const forReader = counted === "every" || counted !== "none" && asked === true || counted === "asks-user" && message.mentions.includes(USER_NAME);
|
|
5301
|
+
if (!forReader) {
|
|
5099
5302
|
continue;
|
|
5100
5303
|
}
|
|
5101
5304
|
}
|
|
@@ -5248,7 +5451,7 @@ ${DASHBOARD_LINE}
|
|
|
5248
5451
|
include = participates;
|
|
5249
5452
|
}
|
|
5250
5453
|
if (include && message.author !== actor.name) {
|
|
5251
|
-
collected.push({ scope: filedIn, part, message });
|
|
5454
|
+
collected.push({ scope: filedIn, part, message, ...asked ? { asked: true } : {} });
|
|
5252
5455
|
}
|
|
5253
5456
|
}
|
|
5254
5457
|
return collected.toSorted(byId);
|
|
@@ -5525,12 +5728,30 @@ ${current.body}`);
|
|
|
5525
5728
|
throw new BoardError("NOT_FOUND", "this board has no runners to read an image from");
|
|
5526
5729
|
}
|
|
5527
5730
|
const bytes = await this.imageSource(actor, args.path);
|
|
5731
|
+
return this.keepImage(actor, bytes, args.path, path8.basename(args.path));
|
|
5732
|
+
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Keeps an image the user posts from a composer, as its bytes, under the same checks as an image
|
|
5735
|
+
* a turn uploads. `name`, the file's, describes it in the markdown answered; without one the
|
|
5736
|
+
* description is empty, for the user to write.
|
|
5737
|
+
*/
|
|
5738
|
+
async postImage(actor, bytes, name) {
|
|
5739
|
+
this.assertUser(actor, "only the user posts an image's bytes; a turn uploads one with upload_image");
|
|
5740
|
+
const given = name === void 0 ? "" : ImageNameSchema.parse(name);
|
|
5741
|
+
return this.keepImage(actor, bytes, given === "" ? "the image" : given, given);
|
|
5742
|
+
}
|
|
5743
|
+
/**
|
|
5744
|
+
* Keeps `bytes` under a new id when they are a PNG, JPEG, GIF, or WebP of at most
|
|
5745
|
+
* `IMAGE_LIMIT_BYTES`, and answers the markdown that shows them, described by `name`. `what`
|
|
5746
|
+
* names the image in a refusal.
|
|
5747
|
+
*/
|
|
5748
|
+
async keepImage(actor, bytes, what, name) {
|
|
5528
5749
|
const type = imageTypeOf(bytes);
|
|
5529
5750
|
if (type === null) {
|
|
5530
|
-
throw new BoardError("VALIDATION", `${
|
|
5751
|
+
throw new BoardError("VALIDATION", `${what} is not a PNG, JPEG, GIF, or WebP image by its first bytes`);
|
|
5531
5752
|
}
|
|
5532
5753
|
if (bytes.byteLength > IMAGE_LIMIT_BYTES) {
|
|
5533
|
-
throw new BoardError("VALIDATION", `${
|
|
5754
|
+
throw new BoardError("VALIDATION", `${what} is ${bytes.byteLength} bytes, over the ${IMAGE_LIMIT_BYTES} an image may be`);
|
|
5534
5755
|
}
|
|
5535
5756
|
return this.mutex.run(async () => {
|
|
5536
5757
|
const id = this.newId();
|
|
@@ -5540,15 +5761,37 @@ ${current.body}`);
|
|
|
5540
5761
|
type,
|
|
5541
5762
|
size: bytes.byteLength
|
|
5542
5763
|
});
|
|
5543
|
-
const
|
|
5764
|
+
const description = imageDescription(name);
|
|
5544
5765
|
return {
|
|
5545
5766
|
id,
|
|
5546
5767
|
type,
|
|
5547
5768
|
size: bytes.byteLength,
|
|
5548
|
-
markdown: ``
|
|
5549
5770
|
};
|
|
5550
5771
|
});
|
|
5551
5772
|
}
|
|
5773
|
+
/**
|
|
5774
|
+
* Opens an image the board keeps for the calling turn, by its id or its `image:<id>` address:
|
|
5775
|
+
* the runner the turn runs on writes the bytes to a file in a folder of the turn's own in the
|
|
5776
|
+
* system temp folder, removed when the turn ends, and the board answers its path, for the
|
|
5777
|
+
* citizen to open with its own file tools, since a post reaches a turn as markdown and an image
|
|
5778
|
+
* in it as its address. Any image the board keeps may be opened: every post, brief, topic, and
|
|
5779
|
+
* dashboard that could show one is in the projection every turn reads.
|
|
5780
|
+
*/
|
|
5781
|
+
async openImage(actor, input) {
|
|
5782
|
+
const args = VerbInputs.read_image.parse(input);
|
|
5783
|
+
await this.authorize(actor, "read_image");
|
|
5784
|
+
if (actor.scope === void 0) {
|
|
5785
|
+
throw new BoardError("INVALID_STATE", "read_image writes the image on the machine a turn runs on, so only a turn can call it");
|
|
5786
|
+
}
|
|
5787
|
+
if (this.imageSink === null) {
|
|
5788
|
+
throw new BoardError("NOT_FOUND", "this board has no runners to write an image to");
|
|
5789
|
+
}
|
|
5790
|
+
const id = imageIdOf(args.image_id);
|
|
5791
|
+
const { type, bytes } = await this.readImage(id);
|
|
5792
|
+
const file = await this.imageSink(actor, { id, type, bytes });
|
|
5793
|
+
return { id, type, size: bytes.byteLength, path: file };
|
|
5794
|
+
}
|
|
5552
5795
|
/** An uploaded image's bytes and kind, which its first bytes tell. */
|
|
5553
5796
|
async readImage(id) {
|
|
5554
5797
|
const file = this.paths.image(UlidSchema.parse(id));
|
|
@@ -5894,6 +6137,13 @@ ${current.body}`);
|
|
|
5894
6137
|
readImagesWith(source) {
|
|
5895
6138
|
this.imageSource = source;
|
|
5896
6139
|
}
|
|
6140
|
+
/**
|
|
6141
|
+
* Where `read_image` writes an image to: the runner the calling turn runs on, through the server
|
|
6142
|
+
* that hosts the runners. Without one, as in the admin CLI, nothing can be opened.
|
|
6143
|
+
*/
|
|
6144
|
+
openImagesWith(sink) {
|
|
6145
|
+
this.imageSink = sink;
|
|
6146
|
+
}
|
|
5897
6147
|
/**
|
|
5898
6148
|
* Refuses to end conversations while another citizen's turn is in one of them: `cut` picks the
|
|
5899
6149
|
* turns that would lose theirs. The caller's own turn is exempt, since ending a conversation
|
|
@@ -6729,6 +6979,8 @@ ${rationale}`}`);
|
|
|
6729
6979
|
return this.removeCron(actor, VerbInputs.remove_cron.parse(input));
|
|
6730
6980
|
case "upload_image":
|
|
6731
6981
|
return this.uploadImage(actor, VerbInputs.upload_image.parse(input));
|
|
6982
|
+
case "read_image":
|
|
6983
|
+
return this.openImage(actor, VerbInputs.read_image.parse(input));
|
|
6732
6984
|
default:
|
|
6733
6985
|
throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
|
|
6734
6986
|
}
|
|
@@ -6964,6 +7216,7 @@ ${rationale}`}`);
|
|
|
6964
7216
|
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
6965
7217
|
...parsed.place === void 0 ? {} : { place: parsed.place },
|
|
6966
7218
|
trigger: parsed.trigger.kind,
|
|
7219
|
+
...parsed.trigger.heartbeat === void 0 ? {} : { heartbeat: parsed.trigger.heartbeat },
|
|
6967
7220
|
session: parsed.session,
|
|
6968
7221
|
runner: parsed.runner
|
|
6969
7222
|
});
|
|
@@ -6986,6 +7239,11 @@ ${rationale}`}`);
|
|
|
6986
7239
|
for (const id of parsed.crons ?? []) {
|
|
6987
7240
|
await this.recordCronRun(id, parsed, failed);
|
|
6988
7241
|
}
|
|
7242
|
+
if (parsed.exitReason === "completed" || parsed.exitReason === "blocked") {
|
|
7243
|
+
for (const shown of parsed.cronFailures ?? []) {
|
|
7244
|
+
await this.catchUpCron(shown, parsed);
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
6989
7247
|
if (parsed.exitReason === "stopped") {
|
|
6990
7248
|
await this.events.append("turn.stopped", parsed.stoppedBy ?? USER_NAME, {
|
|
6991
7249
|
...parsed.id === void 0 ? {} : { turnId: parsed.id },
|
|
@@ -7003,6 +7261,7 @@ ${rationale}`}`);
|
|
|
7003
7261
|
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
7004
7262
|
...parsed.place === void 0 ? {} : { place: parsed.place },
|
|
7005
7263
|
trigger: parsed.trigger.kind,
|
|
7264
|
+
...parsed.trigger.heartbeat === void 0 ? {} : { heartbeat: parsed.trigger.heartbeat },
|
|
7006
7265
|
session: parsed.session,
|
|
7007
7266
|
exitReason: parsed.exitReason,
|
|
7008
7267
|
costUsd: parsed.costUsd,
|
|
@@ -7126,6 +7385,27 @@ ${rationale}`}`);
|
|
|
7126
7385
|
failures: outcome === "failed" ? cron.failures + 1 : outcome === "completed" ? 0 : cron.failures
|
|
7127
7386
|
});
|
|
7128
7387
|
}
|
|
7388
|
+
/**
|
|
7389
|
+
* Marks a cron's failed fire caught up by a completed turn whose prompt named it, while that
|
|
7390
|
+
* failure is still its latest run. Its schedule and its run of failures stay as they are.
|
|
7391
|
+
*/
|
|
7392
|
+
async catchUpCron(shown, record) {
|
|
7393
|
+
if (record.id === void 0 || !await exists(this.paths.cron(shown.cron))) {
|
|
7394
|
+
return;
|
|
7395
|
+
}
|
|
7396
|
+
const cron = await this.readCron(shown.cron);
|
|
7397
|
+
const failure = cronFailure(cron);
|
|
7398
|
+
if (failure === null || failure.turnId !== shown.turnId) {
|
|
7399
|
+
return;
|
|
7400
|
+
}
|
|
7401
|
+
await this.writeCron({
|
|
7402
|
+
...cron,
|
|
7403
|
+
lastRun: {
|
|
7404
|
+
...failure,
|
|
7405
|
+
caughtUp: { turnId: record.id, at: record.endedAt ?? this.now().toISOString() }
|
|
7406
|
+
}
|
|
7407
|
+
});
|
|
7408
|
+
}
|
|
7129
7409
|
/**
|
|
7130
7410
|
* Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
|
|
7131
7411
|
* (for one from before tasks had threads), a channel archived, or a project archived, which ends
|
|
@@ -8495,7 +8775,7 @@ ${charter.purpose}
|
|
|
8495
8775
|
}
|
|
8496
8776
|
assertMayGate(actor, what) {
|
|
8497
8777
|
if (!PLANNING_ROLES.includes(actor.role)) {
|
|
8498
|
-
throw new BoardError("FORBIDDEN", `only the user, the steward, and the concierge may ${what}; ask one of them in the task's thread`);
|
|
8778
|
+
throw new BoardError("FORBIDDEN", `only the user, the steward, and the concierge may ${what}; ask one of them in the task's thread, with a mention`);
|
|
8499
8779
|
}
|
|
8500
8780
|
}
|
|
8501
8781
|
/** The user holds anything; everyone else must be a project member the current stage admits. */
|
|
@@ -8537,6 +8817,51 @@ ${charter.purpose}
|
|
|
8537
8817
|
leaseEnd(now) {
|
|
8538
8818
|
return new Date(now.getTime() + this.leaseMs).toISOString();
|
|
8539
8819
|
}
|
|
8820
|
+
/**
|
|
8821
|
+
* The lease a holder's own crons in the task's thread keep its claim to, when later than the one
|
|
8822
|
+
* it has, else null. While one of them is pending, the lease is renewed for a full lease once less
|
|
8823
|
+
* than half is left, so it never lapses between fires that may be hours apart; once every one is
|
|
8824
|
+
* paused, ended, or failed at its latest fire with nothing catching it up (`cronStoppedAt`), it
|
|
8825
|
+
* runs a full lease from when the last of them stopped.
|
|
8826
|
+
*/
|
|
8827
|
+
keptLease(task, crons, now) {
|
|
8828
|
+
if (task.leaseExpiresAt === void 0) {
|
|
8829
|
+
return null;
|
|
8830
|
+
}
|
|
8831
|
+
const lease = Date.parse(task.leaseExpiresAt);
|
|
8832
|
+
let pending = false;
|
|
8833
|
+
let stopped = null;
|
|
8834
|
+
for (const cron of crons) {
|
|
8835
|
+
if (cron.agent !== task.claimedBy || !firesInTask(cron, task)) {
|
|
8836
|
+
continue;
|
|
8837
|
+
}
|
|
8838
|
+
const at = cronStoppedAt(cron);
|
|
8839
|
+
if (at === null) {
|
|
8840
|
+
pending = true;
|
|
8841
|
+
} else {
|
|
8842
|
+
stopped = Math.max(stopped ?? 0, Date.parse(at));
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
const kept = pending ? lease - now.getTime() < this.leaseMs / 2 ? now.getTime() + this.leaseMs : lease : stopped === null ? lease : stopped + this.leaseMs;
|
|
8846
|
+
return kept > lease ? new Date(kept).toISOString() : null;
|
|
8847
|
+
}
|
|
8848
|
+
/**
|
|
8849
|
+
* The lease a holder keeps while the user's pause holds its turns in the task's project
|
|
8850
|
+
* (`heldOn`), when later than the one it has, else null. It is renewed for a full lease once less
|
|
8851
|
+
* than half is left, as for a pending cron, so it never lapses while the holder cannot take a
|
|
8852
|
+
* turn there, and after the resume the holder has at least half a lease to come back to it.
|
|
8853
|
+
*/
|
|
8854
|
+
async pausedLease(task, runners2, runner, now) {
|
|
8855
|
+
if (task.claimedBy === void 0 || task.leaseExpiresAt === void 0) {
|
|
8856
|
+
return null;
|
|
8857
|
+
}
|
|
8858
|
+
const holder = await this.readAgent(task.claimedBy).catch(() => null);
|
|
8859
|
+
if (holder === null || !heldOn(runners2, runner, holder.cli)) {
|
|
8860
|
+
return null;
|
|
8861
|
+
}
|
|
8862
|
+
const lease = Date.parse(task.leaseExpiresAt);
|
|
8863
|
+
return lease - now.getTime() < this.leaseMs / 2 ? this.leaseEnd(now) : null;
|
|
8864
|
+
}
|
|
8540
8865
|
leaseExpired(task, now) {
|
|
8541
8866
|
return task.leaseExpiresAt !== void 0 && new Date(task.leaseExpiresAt).getTime() <= now.getTime();
|
|
8542
8867
|
}
|
|
@@ -8649,11 +8974,7 @@ ${charter.purpose}
|
|
|
8649
8974
|
if (task.claimedBy === actor.name || task.createdBy === actor.name || task.stages.some((stage) => stage.agent === actor.name || stage.holders.includes(actor.name))) {
|
|
8650
8975
|
return true;
|
|
8651
8976
|
}
|
|
8652
|
-
|
|
8653
|
-
return false;
|
|
8654
|
-
}
|
|
8655
|
-
const agent = await this.readAgent(actor.name);
|
|
8656
|
-
return agent.memberships.includes(task.project) && mayHoldStage(actor, task);
|
|
8977
|
+
return await this.mayTakeStage(actor, task);
|
|
8657
8978
|
}
|
|
8658
8979
|
if (subject?.kind === "proposal") {
|
|
8659
8980
|
const proposal = await this.readProposal(subject.id);
|
|
@@ -8667,6 +8988,34 @@ ${charter.purpose}
|
|
|
8667
8988
|
}
|
|
8668
8989
|
return false;
|
|
8669
8990
|
}
|
|
8991
|
+
/** Whether a task's current stage waits for a holder and the member may take it. */
|
|
8992
|
+
async mayTakeStage(actor, task) {
|
|
8993
|
+
if (task.status !== "open" || task.completing) {
|
|
8994
|
+
return false;
|
|
8995
|
+
}
|
|
8996
|
+
const agent = await this.readAgent(actor.name);
|
|
8997
|
+
return agent.memberships.includes(task.project) && mayHoldStage(actor, task);
|
|
8998
|
+
}
|
|
8999
|
+
/**
|
|
9000
|
+
* Which of a thread's posts count toward a reader's heartbeat (see `unreadByConversation`). A
|
|
9001
|
+
* thread, or a task, the board cannot find counts as an open topic's would.
|
|
9002
|
+
*/
|
|
9003
|
+
async threadCount(actor, id) {
|
|
9004
|
+
const found = await this.tryFindThread(id);
|
|
9005
|
+
if (found?.thread.state === "closed") {
|
|
9006
|
+
return "none";
|
|
9007
|
+
}
|
|
9008
|
+
const subject = found?.thread.subject;
|
|
9009
|
+
const task = subject?.kind === "task" ? await this.findTask(subject.id).then((location) => location.task, () => null) : null;
|
|
9010
|
+
if (task === null) {
|
|
9011
|
+
return "every";
|
|
9012
|
+
}
|
|
9013
|
+
const stageIsTheirs = task.status === "claimed" ? task.claimedBy === actor.name : await this.mayTakeStage(actor, task);
|
|
9014
|
+
if (stageIsTheirs) {
|
|
9015
|
+
return "every";
|
|
9016
|
+
}
|
|
9017
|
+
return task.createdBy === actor.name ? "asks-user" : "asked";
|
|
9018
|
+
}
|
|
8670
9019
|
async threadScopes() {
|
|
8671
9020
|
const projects = await listDirs(this.paths.projects());
|
|
8672
9021
|
return [this.paths.societyThreads(), ...projects.map((slug) => this.paths.threads(slug))];
|
|
@@ -8771,6 +9120,15 @@ ${charter.purpose}
|
|
|
8771
9120
|
};
|
|
8772
9121
|
|
|
8773
9122
|
// packages/scheduler/dist/wake.js
|
|
9123
|
+
function heartbeatCause(input) {
|
|
9124
|
+
if (input.digestSize > 0) {
|
|
9125
|
+
return "unread";
|
|
9126
|
+
}
|
|
9127
|
+
if (input.claimsHeld > 0) {
|
|
9128
|
+
return "held";
|
|
9129
|
+
}
|
|
9130
|
+
return input.waitingStages > 0 ? "waiting" : null;
|
|
9131
|
+
}
|
|
8774
9132
|
function decideWake(input) {
|
|
8775
9133
|
if (input.paused) {
|
|
8776
9134
|
return { wake: false, reason: "society paused", priority: 0 };
|
|
@@ -8802,7 +9160,7 @@ function decideWake(input) {
|
|
|
8802
9160
|
case "heartbeat":
|
|
8803
9161
|
break;
|
|
8804
9162
|
}
|
|
8805
|
-
if (input
|
|
9163
|
+
if (heartbeatCause(input) === null) {
|
|
8806
9164
|
return { wake: false, reason: "nothing to read, hold, or take", priority: 0 };
|
|
8807
9165
|
}
|
|
8808
9166
|
return {
|
|
@@ -8897,6 +9255,18 @@ var StateSchema = z17.object({
|
|
|
8897
9255
|
lastHeartbeat: z17.record(z17.string(), z17.string()),
|
|
8898
9256
|
/** Waiting stages already signalled, as `task:stage`, so each is logged once while it waits. */
|
|
8899
9257
|
waitingSeen: z17.record(z17.string(), z17.string()).default({}),
|
|
9258
|
+
/**
|
|
9259
|
+
* The last pass at which each waiting stage was held by the user's pause of its runner, as
|
|
9260
|
+
* `task:stage`; its wait counts from there, so a stage that waited through a pause is not
|
|
9261
|
+
* signalled the moment the pause ends.
|
|
9262
|
+
*/
|
|
9263
|
+
waitingHeld: z17.record(z17.string(), z17.string()).default({}),
|
|
9264
|
+
/**
|
|
9265
|
+
* The last operations pass at which each citizen was held by the user's pause of a runner it
|
|
9266
|
+
* works on; its idle time counts from there, so a citizen held through a pause is not signalled
|
|
9267
|
+
* idle the moment the pause ends.
|
|
9268
|
+
*/
|
|
9269
|
+
idleHeld: z17.record(z17.string(), z17.string()).default({}),
|
|
8900
9270
|
/** Queued dispatches survive a restart; the cursor has already moved past the events that made them. */
|
|
8901
9271
|
pending: z17.record(z17.string(), PendingSchema).default({}),
|
|
8902
9272
|
opsReported: z17.record(z17.string(), z17.string()).default({}),
|
|
@@ -9835,10 +10205,14 @@ var Scheduler = class {
|
|
|
9835
10205
|
* A heartbeat per member and scope on the cadence, which then looks at every conversation of the
|
|
9836
10206
|
* scope: the unread messages of its home, of each other channel, and of each thread, a stage the
|
|
9837
10207
|
* member holds, and a stage of an unblocked task waiting that it may take, each in its task's
|
|
9838
|
-
* thread.
|
|
9839
|
-
*
|
|
10208
|
+
* thread. A stage counts for neither while the member has a cron pending in its task's thread
|
|
10209
|
+
* (`cronStoppedAt`: neither paused nor ended, and its latest fire's turn did not fail or was
|
|
10210
|
+
* caught up since), whose fires are its checks there. Each conversation with something wakes on
|
|
10211
|
+
* its own, so a heartbeat never brings one task's business into another's turn, and its trigger
|
|
10212
|
+
* says what it woke for (`heartbeatCause`).
|
|
9840
10213
|
*/
|
|
9841
10214
|
async checkHeartbeats(now) {
|
|
10215
|
+
let pendingCrons;
|
|
9842
10216
|
for (const agent of await this.board.listAgents()) {
|
|
9843
10217
|
if (agent.status !== "active" || agent.cli === null) {
|
|
9844
10218
|
continue;
|
|
@@ -9864,6 +10238,9 @@ var Scheduler = class {
|
|
|
9864
10238
|
this.state.lastHeartbeat[cadence] = iso(now);
|
|
9865
10239
|
unread ??= await this.board.unreadByConversation(actor);
|
|
9866
10240
|
held ??= await this.board.heldClaims(agent.name);
|
|
10241
|
+
pendingCrons ??= (await this.board.listCrons()).filter((cron) => cronStoppedAt(cron) === null);
|
|
10242
|
+
const crons = pendingCrons;
|
|
10243
|
+
const leftToCron = (task) => crons.some((cron) => cron.agent === agent.name && firesInTask(cron, task));
|
|
9867
10244
|
const reasons = /* @__PURE__ */ new Map();
|
|
9868
10245
|
const reasonsIn = (beside) => {
|
|
9869
10246
|
const key = conversationKey2(agent.name, project, beside);
|
|
@@ -9882,13 +10259,13 @@ var Scheduler = class {
|
|
|
9882
10259
|
}
|
|
9883
10260
|
}
|
|
9884
10261
|
for (const task of held) {
|
|
9885
|
-
if (task.project === project) {
|
|
10262
|
+
if (task.project === project && !leftToCron(task)) {
|
|
9886
10263
|
reasonsIn({ thread: { id: task.id, task: true } }).claimsHeld += 1;
|
|
9887
10264
|
}
|
|
9888
10265
|
}
|
|
9889
10266
|
if (project !== SOCIETY_SCOPE) {
|
|
9890
10267
|
for (const task of await this.board.openTasks(project)) {
|
|
9891
|
-
if (mayHoldStage(agent, task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
|
|
10268
|
+
if (mayHoldStage(agent, task) && !leftToCron(task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
|
|
9892
10269
|
reasonsIn({ thread: { id: task.id, task: true } }).waiting += 1;
|
|
9893
10270
|
}
|
|
9894
10271
|
}
|
|
@@ -9897,7 +10274,16 @@ var Scheduler = class {
|
|
|
9897
10274
|
if (this.pending.has(key) || this.running.has(key)) {
|
|
9898
10275
|
continue;
|
|
9899
10276
|
}
|
|
9900
|
-
const
|
|
10277
|
+
const cause = heartbeatCause({
|
|
10278
|
+
digestSize: reason.digestSize,
|
|
10279
|
+
claimsHeld: reason.claimsHeld,
|
|
10280
|
+
waitingStages: reason.waiting
|
|
10281
|
+
});
|
|
10282
|
+
const trigger = TriggerSchema.parse({
|
|
10283
|
+
kind: "heartbeat",
|
|
10284
|
+
reason: "heartbeat",
|
|
10285
|
+
...cause === null ? {} : { heartbeat: cause }
|
|
10286
|
+
});
|
|
9901
10287
|
const decision = decideWake({
|
|
9902
10288
|
trigger,
|
|
9903
10289
|
digestSize: reason.digestSize,
|
|
@@ -10100,7 +10486,9 @@ var Scheduler = class {
|
|
|
10100
10486
|
*/
|
|
10101
10487
|
async checkWaitingStages(now) {
|
|
10102
10488
|
const stillWaiting = /* @__PURE__ */ new Set();
|
|
10489
|
+
const runners2 = await this.board.listRunners();
|
|
10103
10490
|
for (const project of await this.board.listProjects()) {
|
|
10491
|
+
let members;
|
|
10104
10492
|
for (const task of await this.board.openTasks(project.slug)) {
|
|
10105
10493
|
const stage = currentStage(task);
|
|
10106
10494
|
if (stage === void 0) {
|
|
@@ -10111,7 +10499,14 @@ var Scheduler = class {
|
|
|
10111
10499
|
if (this.state.waitingSeen[key] !== void 0) {
|
|
10112
10500
|
continue;
|
|
10113
10501
|
}
|
|
10114
|
-
|
|
10502
|
+
members ??= await this.board.projectMembers(project.slug);
|
|
10503
|
+
const takers = members.filter((member) => member.cli !== null && mayHoldStage(member, task));
|
|
10504
|
+
if (takers.length > 0 && takers.every((taker) => heldOn(runners2, project.runner, taker.cli))) {
|
|
10505
|
+
this.state.waitingHeld[key] = iso(now);
|
|
10506
|
+
continue;
|
|
10507
|
+
}
|
|
10508
|
+
const held = this.state.waitingHeld[key];
|
|
10509
|
+
const age = now - Math.max(Date.parse(task.stageSince), held === void 0 ? 0 : Date.parse(held));
|
|
10115
10510
|
if (age < this.timings.waitingStageMs) {
|
|
10116
10511
|
continue;
|
|
10117
10512
|
}
|
|
@@ -10132,6 +10527,11 @@ var Scheduler = class {
|
|
|
10132
10527
|
});
|
|
10133
10528
|
}
|
|
10134
10529
|
}
|
|
10530
|
+
for (const key of Object.keys(this.state.waitingHeld)) {
|
|
10531
|
+
if (!stillWaiting.has(key)) {
|
|
10532
|
+
delete this.state.waitingHeld[key];
|
|
10533
|
+
}
|
|
10534
|
+
}
|
|
10135
10535
|
for (const key of Object.keys(this.state.waitingSeen)) {
|
|
10136
10536
|
if (!stillWaiting.has(key)) {
|
|
10137
10537
|
delete this.state.waitingSeen[key];
|
|
@@ -10197,7 +10597,8 @@ var Scheduler = class {
|
|
|
10197
10597
|
const roles = (await this.board.listRoles()).filter((role) => role.name !== USER_ROLE);
|
|
10198
10598
|
const runners2 = await this.board.listRunners();
|
|
10199
10599
|
const anywhere = new Set(runners2.filter((runner) => runner.status === "connected").flatMap((runner) => runner.capabilities));
|
|
10200
|
-
|
|
10600
|
+
const projects = await this.board.listProjects();
|
|
10601
|
+
for (const project of projects) {
|
|
10201
10602
|
const slug = project.slug;
|
|
10202
10603
|
const tasks = await this.board.listTasks(slug);
|
|
10203
10604
|
const home = runners2.find((runner) => runner.name === project.runner);
|
|
@@ -10289,7 +10690,13 @@ var Scheduler = class {
|
|
|
10289
10690
|
}
|
|
10290
10691
|
}
|
|
10291
10692
|
const crons = (await this.board.listCrons()).filter((cron) => cron.ended === void 0);
|
|
10292
|
-
|
|
10693
|
+
const agents = await this.board.listAgents();
|
|
10694
|
+
for (const name of Object.keys(this.state.idleHeld)) {
|
|
10695
|
+
if (!agents.some((agent) => agent.name === name && agent.status === "active")) {
|
|
10696
|
+
delete this.state.idleHeld[name];
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
for (const agent of agents) {
|
|
10293
10700
|
if (agent.status !== "active" || agent.cli === null) {
|
|
10294
10701
|
continue;
|
|
10295
10702
|
}
|
|
@@ -10301,7 +10708,12 @@ var Scheduler = class {
|
|
|
10301
10708
|
last = Math.max(last, Date.parse(ended));
|
|
10302
10709
|
}
|
|
10303
10710
|
}
|
|
10304
|
-
const
|
|
10711
|
+
const held = heldOn(runners2, agent.homeRunner, agent.cli) || projects.some((project) => agent.memberships.includes(project.slug) && heldOn(runners2, project.runner, agent.cli));
|
|
10712
|
+
if (held) {
|
|
10713
|
+
this.state.idleHeld[agent.name] = iso(now);
|
|
10714
|
+
}
|
|
10715
|
+
const heldAt = this.state.idleHeld[agent.name];
|
|
10716
|
+
const idle = now - Math.max(last, heldAt === void 0 ? 0 : Date.parse(heldAt));
|
|
10305
10717
|
if (idle >= this.timings.idleMemberMs) {
|
|
10306
10718
|
signals.push({
|
|
10307
10719
|
kind: "idle_member",
|
|
@@ -10654,20 +11066,21 @@ var Scheduler = class {
|
|
|
10654
11066
|
import { randomUUID } from "node:crypto";
|
|
10655
11067
|
|
|
10656
11068
|
// packages/turn-host/dist/render.js
|
|
11069
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
10657
11070
|
var TURN_CONTRACT = [
|
|
10658
11071
|
"- Every turn belongs to one conversation, named at the top of your prompt: your home conversation in a project or the society, for its general channel and for work tied to no other channel and no thread; the conversation of one other channel of the place, a workstream such as a release; outside projects, the conversation of one project's channels, general and its workstreams together, for posts there that ask something of you while you are not a member; or the conversation of one thread you take part in, a task's, a proposal's, or a topic's. Each has its own session and its own turns, which may run while this one does. The prompt carries this conversation only: in a thread, its new messages, or the whole thread when the conversation is new, and in a task's thread the task and its stage; in a channel's, a project's, or at home, that channel's, that project's channels', or general's messages that concern you. Read anything else you need with get_task, search, and the board's files, and leave other channels', other threads', and other projects' work to their own turns.",
|
|
10659
|
-
"- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it.",
|
|
11072
|
+
"- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it, and so does the board while a cron of yours in the task's thread is pending.",
|
|
10660
11073
|
"- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. The branch is checked out in one worktree at a time and kept for the turns of whoever holds the task's stage, so a turn of anyone else there, or one that starts while another turn has the branch, works detached at its tip, which Your workspace then says; its commits go on the branch by a fast-forward when a turn in the task ends with the branch free, and the task's thread is told of commits that no longer build on its tip. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task; nothing lands from it by itself. In a proposal's, a topic's, or a channel's conversation it is a worktree of its own, detached at the project's default branch, which goes when the thread closes or the channel is archived, so work that should last goes on a task's branch. Before each turn the runner fetches the project's remote and brings what holds no work of yours up to the default branch: your own branch when it has no commits of its own, a task's branch nobody has worked on, and a detached worktree with nothing uncommitted; what it leaves where it is, and why, is under Your workspace in the prompt. It never merges or commits for you.",
|
|
10661
11074
|
"- Posts that would wake this conversation may arrive while you work, delivered into the turn under a heading that says so and formatted like the digest. They are part of this turn: act on them now, after the step in hand, or not at all, as with the digest. The user may also stop a turn; the next turn is told.",
|
|
10662
11075
|
"- Silence is allowed. If the digest needs no reply, post nothing.",
|
|
10663
|
-
"- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by sleeping or leaving a process running: a turn's board token ends with the turn, so nothing it leaves behind can act on the board. While you check your own stage's work on a schedule, such as a running job, CI, or a deploy, keep holding the stage: the cron sets when you check, and
|
|
11076
|
+
"- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by sleeping or leaving a process running: a turn's board token ends with the turn, so nothing it leaves behind can act on the board. While you check your own stage's work on a schedule, such as a running job, CI, or a deploy, keep holding the stage: the cron sets when you check, and while it is pending in the task's thread, neither ended nor paused, with its latest fire's turn not failed or a turn of yours there completed since, the board keeps your lease and the heartbeat leaves the stage to it, waking you there only for a post you have not read. Release a stage only to hand it on, or once its task's blocked_by names the tasks it waits on: the stage then wakes nobody until the last of them ends. Any other wait on someone else, such as the user or an outside party, is a stage of its own, not a held stage. The society's crons are under society/crons/ in the board projection.",
|
|
10664
11077
|
"- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. A name in a code span or a code block is no mention, so code keeps its @ as written. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
|
|
10665
|
-
"- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
|
|
10666
|
-
"- To show an image in a post, such as a screenshot or a chart, save it anywhere you can read it, such as a folder from `mktemp -d`, call upload_image with its path, and put the markdown it returns in the post, ``, with what the image shows as its alt text: the board keeps the image, so it shows in any conversation after the file is gone. It takes a PNG, JPEG, GIF, or WebP of at most 8 MB from your worktree, your home, or the system temp folder. An image committed on a task's branch, linked by its path in the task's worktree, still shows in place, and so does one at an https address. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt.",
|
|
11078
|
+
"- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. In a task's thread, only the current stage's holder, or while nobody holds it whoever may take it, wakes for your post, and a post that asks the user also wakes the task's creator; mention anyone else you need, including the concierge for a plan change. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
|
|
11079
|
+
"- To show an image in a post, such as a screenshot or a chart, save it anywhere you can read it, such as a folder from `mktemp -d`, call upload_image with its path, and put the markdown it returns in the post, ``, with what the image shows as its alt text: the board keeps the image, so it shows in any conversation after the file is gone. It takes a PNG, JPEG, GIF, or WebP of at most 8 MB from your worktree, your home, or the system temp folder. An image committed on a task's branch, linked by its path in the task's worktree, still shows in place, and so does one at an https address. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt. To see an image a post, a brief, or a topic shows as `image:<id>`, call read_image with the id: it returns the path of a file of this turn's own that holds the image, removed when the turn ends, which you open with the tool that shows you an image, Read in Claude Code or view_image in Codex.",
|
|
10667
11080
|
"- Route every lesson: about you, your craft, or the user, write it to memory/core.md in your home directory, and keep that file short by moving detail to memory/<topic>.md, your archive; how a codebase behaves (architecture, conventions, gotchas, how to build, test, and verify it) goes in its AGENTS.md or README, committed on a task's branch like any change to the code, or proposed in the task's thread or the project channel when it is outside the task at hand; a durable fact about a project as a place (runners, access, flow, where results live) goes through `write_knowledge` on that project; something the whole project should know now, post it to the project channel, and anything about one task, in the task's thread.",
|
|
10668
11081
|
'- A procedure you have followed twice is a candidate for a skill, which it is when it makes a citizen better at a kind of work on any project: write it to skills/<name>/SKILL.md in your home, frontmatter with `name` and a one-line `description` and then the steps, and your skills index lists it from the next turn. Files the steps use, such as scripts/ and references/, go in the same folder, named in SKILL.md by paths relative to it. Propose it with kind "skill" when more than one citizen would use it and nothing in it is tied to one place; a skill only you use stays yours, however many projects you use it on.',
|
|
10669
11082
|
"- To take a published skill for yourself, install it in a temporary folder and copy it into your home: in a folder from `mktemp -d`, `npx -y skills add <source> --skill <name> -a universal --copy -y` writes `.agents/skills/<name>/` and a lock file there, and you copy that skill folder to skills/<name>/ in your home, named in lowercase letters, digits, and hyphens. Never install one inside a worktree, where it would be committed into the project, or with -g, which writes to the runner's own home, shared by every citizen on it and listed in no skills index.",
|
|
10670
|
-
"- Report memoryUpdated: true in the status object whenever you changed memory/core.md or a skill,
|
|
11083
|
+
"- Report memoryUpdated: true in the status object whenever you changed memory/core.md or a skill. A change to your instructions reaches each of your conversations at its next turn, whose prompt then carries them whole, replacing what that conversation had; a turn already running keeps what it started with.",
|
|
10671
11084
|
"- Other turns of yours may be running at the same time and share your home. Change a file there with your edit tool, which refuses a file changed since you read it, and never rewrite one whole from an earlier read, with a shell redirect or a script, or you erase what another of your turns wrote.",
|
|
10672
11085
|
"- To ask the user something, mention @user where the question belongs: the task's thread for a task, the proposal's thread for a proposal, and for anything else a thread you open on your scope's general channel, titled with the question. The user answers in the same thread, and the answer comes back to that thread's conversation. A proposal waiting on the user is already in front of them and needs no mention.",
|
|
10673
11086
|
"- End every turn with the status object: summary, claims held, what is blocked, and whether the user must decide. If you report that the user must decide, mentioned @user nowhere this turn, and proposed nothing for the user to decide, the runner asks in this turn's thread when it has one, and otherwise opens a thread on your scope's general channel with your summary as the question."
|
|
@@ -10676,9 +11089,9 @@ var PLANNING = [
|
|
|
10676
11089
|
"- A task is a plan of stages between open and done. Hold the current stage with claim_task and finish it with advance_task; the next stage becomes current, and past the last one the task is done.",
|
|
10677
11090
|
"- Name stages by what gets done. Plan the next few steps rather than everything, and reshape with plan_task as the work teaches you.",
|
|
10678
11091
|
"- Assign a stage to a role when anyone in it could do it, to a citizen only when it must be them, and to nobody when anyone in the project could. A citizen named on a stage must be a member of the task's project; add them first. A stage wakes everyone it is open to when it comes up, so the narrower the assignee, the fewer turns it costs.",
|
|
10679
|
-
"- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, the holder of the current stage sends the task back to an earlier stage with update_task, and so may the user, the steward, and the concierge; anyone else asks the concierge in the task's thread.",
|
|
11092
|
+
"- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, the holder of the current stage sends the task back to an earlier stage with update_task, and so may the user, the steward, and the concierge; anyone else asks the concierge in the task's thread, with a mention.",
|
|
10680
11093
|
"- A task that cannot start until other tasks finish names them with update_task blocked_by, and whoever holds its stage then releases it, since the heartbeat still wakes a holder. While any of them is in play, a stage nobody holds wakes nobody, neither when it comes up nor on a heartbeat; once the last has ended, done or abandoned, the stage wakes as if it had just come up, or wakes its holder. Nobody needs to clear the block or mention anyone when they end; change blocked_by only when the plan changes.",
|
|
10681
|
-
"- A gated stage is an independent check: nobody who held an earlier stage of the task may hold it, and only the user, the steward, and the concierge may add, remove, move, reassign, or ungate it. Ask them in the task's thread when a gate should change.",
|
|
11094
|
+
"- A gated stage is an independent check: nobody who held an earlier stage of the task may hold it, and only the user, the steward, and the concierge may add, remove, move, reassign, or ungate it. Ask them in the task's thread, with a mention, when a gate should change.",
|
|
10682
11095
|
"- Commit your work on the task's branch before you advance, and say what you did in the advance note; it is the handover the next holder reads in the task's thread."
|
|
10683
11096
|
].join("\n");
|
|
10684
11097
|
var GOVERNANCE = [
|
|
@@ -10737,6 +11150,10 @@ function renderInstructions(input) {
|
|
|
10737
11150
|
return `${sections.join("\n")}
|
|
10738
11151
|
`;
|
|
10739
11152
|
}
|
|
11153
|
+
function instructionsHash(input) {
|
|
11154
|
+
const { onboarding: _first, ...kept } = input;
|
|
11155
|
+
return createHash3("sha256").update(renderInstructions(kept)).digest("hex");
|
|
11156
|
+
}
|
|
10740
11157
|
|
|
10741
11158
|
// packages/turn-host/dist/prompt.js
|
|
10742
11159
|
var REFLECTION_INTRO = "This turn is for your memory; take no new work, and answer unread messages only where a reply is needed. Read memory/core.md and your recent turn records, then:";
|
|
@@ -10749,6 +11166,7 @@ var REFLECTION_STEPS = [
|
|
|
10749
11166
|
];
|
|
10750
11167
|
var SAVE_WHAT_IS_OPEN = "- This conversation's session ends with this turn, and the next turn here starts a new one that knows only your home's files and the board: write anything still open into memory/core.md, or the archive it points to, before you end.";
|
|
10751
11168
|
var REFLECTION_REPORT = "- Report memoryUpdated: true in the status object when core.md or a skill changed.";
|
|
11169
|
+
var INSTRUCTIONS_CHANGED = "Your instructions changed since this conversation last received them. Between the tags below are the whole of them now: they replace the instructions this conversation began with and any earlier text like this, so where the two differ, follow these, and a rule missing from them no longer applies.";
|
|
10752
11170
|
var MAX_BODY_CHARS = 1500;
|
|
10753
11171
|
var MAX_PROFILE_CHARS = 200;
|
|
10754
11172
|
function clip(text) {
|
|
@@ -10803,12 +11221,19 @@ function quotaPhrase(cli, quota, now) {
|
|
|
10803
11221
|
}
|
|
10804
11222
|
function runnerLine(runner, projects, now) {
|
|
10805
11223
|
const living = projects.filter((project) => project.runner === runner.name).map((project) => project.slug);
|
|
10806
|
-
const state =
|
|
11224
|
+
const state = [
|
|
11225
|
+
runner.status === "connected" ? "connected" : `away${runner.lastSeen === void 0 ? "" : ` since ${runner.lastSeen}`}`,
|
|
11226
|
+
...runner.paused === void 0 ? [] : [`paused by the user since ${runner.paused.at}`]
|
|
11227
|
+
].join(", ");
|
|
11228
|
+
const pauses = runner.clis.flatMap((cli) => {
|
|
11229
|
+
const pause = runner.pausedClis?.[cli];
|
|
11230
|
+
return pause === void 0 ? [] : [`${cli} paused by the user since ${pause.at}`];
|
|
11231
|
+
});
|
|
10807
11232
|
const quotas = runner.clis.flatMap((cli) => {
|
|
10808
11233
|
const quota = runner.quotas?.[cli];
|
|
10809
11234
|
return quota === void 0 ? [] : [quotaPhrase(cli, quota, now)];
|
|
10810
11235
|
});
|
|
10811
|
-
return `- ${runner.name}: ${state}; ${runner.os}; CLIs ${runner.clis.length === 0 ? "none" : runner.clis.join(", ")}; offers ${runner.capabilities.length === 0 ? "nothing beyond its CLIs" : runner.capabilities.join(", ")}; ${living.length === 0 ? "no projects" : `projects ${living.join(", ")}`}${quotas.length === 0 ? "" : `; quota ${quotas.join("; ")}`}`;
|
|
11236
|
+
return `- ${runner.name}: ${state}; ${runner.os}; CLIs ${runner.clis.length === 0 ? "none" : runner.clis.join(", ")}; offers ${runner.capabilities.length === 0 ? "nothing beyond its CLIs" : runner.capabilities.join(", ")}; ${living.length === 0 ? "no projects" : `projects ${living.join(", ")}`}${pauses.map((pause) => `; ${pause}`).join("")}${quotas.length === 0 ? "" : `; quota ${quotas.join("; ")}`}`;
|
|
10812
11237
|
}
|
|
10813
11238
|
function profileLine(profile) {
|
|
10814
11239
|
const line = profile.split("\n").map((entry) => entry.trim()).find((entry) => entry.length > 0 && !entry.startsWith("#"));
|
|
@@ -10845,7 +11270,7 @@ function messageLines(messages, threads) {
|
|
|
10845
11270
|
function cronLines(cron, timeZone) {
|
|
10846
11271
|
const previous = cron.lastRun === void 0 ? "" : ` The turn of its previous fire ${cron.lastRun.outcome === "stopped" ? "was stopped by the user" : cron.lastRun.outcome} at ${cron.lastRun.at}.`;
|
|
10847
11272
|
const next = nextFire(cron, timeZone);
|
|
10848
|
-
const state = isOneTime(cron.schedule) ? " It fired once, as set, and has ended." : cron.ended !== void 0 ? ` ${cron.ended.by} has ended it since it fired (${cron.ended.reason}), so do what it asks only if it still matters.` : next === null ? "" : ` Next it fires at ${next.toISOString()}.`;
|
|
11273
|
+
const state = isOneTime(cron.schedule) ? " It fired once, as set, and has ended." : cron.ended !== void 0 ? ` ${cron.ended.by} has ended it since it fired (${cron.ended.reason}), so do what it asks only if it still matters.` : cron.paused !== void 0 ? ` ${cron.paused.by} has paused it since it fired, so it fires no more until resumed.` : next === null ? "" : ` Next it fires at ${next.toISOString()}.`;
|
|
10849
11274
|
return [
|
|
10850
11275
|
`- "${cron.title}" (cron ${cron.id}), set by ${cron.createdBy} at ${cron.createdAt}, fires ${describeSchedule(cron.schedule, timeZone)}.${previous}${state}`,
|
|
10851
11276
|
"",
|
|
@@ -10899,6 +11324,10 @@ function buildTurnPrompt(input) {
|
|
|
10899
11324
|
if (dispatch.trigger.kind === "user_post") {
|
|
10900
11325
|
lines.push("The user posted. Route it: answer where it was posted, in its thread when it came in one, or create what it needs: a task with a plan whose stages name who does them, a thread, or a project, adding citizens to a project first when they are not members. Stay silent when the user already addressed a citizen and nothing else is needed.");
|
|
10901
11326
|
}
|
|
11327
|
+
const changed = input.changedInstructions ?? null;
|
|
11328
|
+
if (changed !== null) {
|
|
11329
|
+
lines.push("", "## Your instructions changed", "", INSTRUCTIONS_CHANGED, "", "<instructions>", changed.trimEnd(), "</instructions>");
|
|
11330
|
+
}
|
|
10902
11331
|
const conversation = input.conversation ?? null;
|
|
10903
11332
|
lines.push("", "## This conversation", "");
|
|
10904
11333
|
if (conversation === null && dispatch.channel !== void 0) {
|
|
@@ -10947,6 +11376,11 @@ function buildTurnPrompt(input) {
|
|
|
10947
11376
|
"Its CLI could not resume this conversation's session, so the session was dropped and this turn starts a new one: nothing of your earlier turns here carries over but what is on the board and in your home."
|
|
10948
11377
|
] : [], "The worktree may hold uncommitted changes. Run git status before doing anything else.");
|
|
10949
11378
|
}
|
|
11379
|
+
const failed = input.failedCrons ?? [];
|
|
11380
|
+
if (failed.length > 0) {
|
|
11381
|
+
const timeZone = input.clock?.timeZone ?? DEFAULT_TIME_ZONE;
|
|
11382
|
+
lines.push("", "## Cron fires that did not finish", "", failed.length === 1 ? "The turn of this cron's latest fire here failed, so what its note asks may not be done:" : "The turns of these crons' latest fires here failed, so what their notes ask may not be done:", "", ...failed.flatMap((cron) => cronLines(cron, timeZone)));
|
|
11383
|
+
}
|
|
10950
11384
|
if (last?.trigger.kind === "reflection" && last.exitReason === "completed" && last.sessionDropped === true) {
|
|
10951
11385
|
lines.push("", "## Your session started afresh", "", "Your last turn here was a reflection, and this conversation's session ended with it, so this turn starts a new one: what you kept is in memory/core.md and the archive it points to, and what your earlier turns did is on the board, in your posts and the threads and tasks you worked in, which search and the board's files reach.");
|
|
10952
11386
|
}
|
|
@@ -10966,7 +11400,7 @@ function buildTurnPrompt(input) {
|
|
|
10966
11400
|
}
|
|
10967
11401
|
const machines = input.runners;
|
|
10968
11402
|
if (machines !== void 0 && machines !== null) {
|
|
10969
|
-
lines.push("", "## Runners", "", "The machines turns run on. A project lives on the runner that takes its first turn, one that offers every capability the project and that turn's task require, and moves only when the user moves it. A task's required_capabilities must name capabilities exactly as a runner below offers them; a task needing one its project's runner lacks waits. A runner's quota is what each CLI's login there last reported of its plan, in percent of each window; a CLI limited until a time fails its turns there until then.", "");
|
|
11403
|
+
lines.push("", "## Runners", "", "The machines turns run on. A project lives on the runner that takes its first turn, one that offers every capability the project and that turn's task require, and moves only when the user moves it. A task's required_capabilities must name capabilities exactly as a runner below offers them; a task needing one its project's runner lacks waits. A runner's quota is what each CLI's login there last reported of its plan, in percent of each window; a CLI limited until a time fails its turns there until then. A runner, or a CLI on it, paused by the user starts none of the turns it would run until the user resumes it: they wait, and the stages their citizens hold keep their leases.", "");
|
|
10970
11404
|
if (machines.runners.length === 0) {
|
|
10971
11405
|
lines.push("None registered yet.");
|
|
10972
11406
|
}
|
|
@@ -11211,7 +11645,7 @@ var TurnHost = class {
|
|
|
11211
11645
|
worktree: PATH_TOKENS.worktree,
|
|
11212
11646
|
otherChannels: (project?.channels ?? []).map((name) => channelRef(dispatch.project, name)).filter((ref) => !agent.subscriptions.includes(ref))
|
|
11213
11647
|
} : null;
|
|
11214
|
-
const
|
|
11648
|
+
const rendered = {
|
|
11215
11649
|
agentName: agent.name,
|
|
11216
11650
|
roleCharter: await this.board.readAgentRoleBody(agent.name),
|
|
11217
11651
|
memoryCore: await this.board.readMemoryCore(agent.name),
|
|
@@ -11229,7 +11663,11 @@ var TurnHost = class {
|
|
|
11229
11663
|
}))
|
|
11230
11664
|
],
|
|
11231
11665
|
...onboarding === null ? {} : { onboarding }
|
|
11232
|
-
}
|
|
11666
|
+
};
|
|
11667
|
+
const instructions = renderInstructions(rendered);
|
|
11668
|
+
const instructionsNow = instructionsHash(rendered);
|
|
11669
|
+
const instructionsHeld = !newSession && lastTurn?.session === recorded ? lastTurn.sessionInstructionsHash : void 0;
|
|
11670
|
+
const changedInstructions = !newSession && instructionsHeld !== instructionsNow;
|
|
11233
11671
|
const societyView = charter.wakeTriggers.includes(USER_POST_TRIGGER2) ? {
|
|
11234
11672
|
projects: (await this.board.listProjects()).filter((each) => each.archived === void 0),
|
|
11235
11673
|
members: await this.board.listMembers()
|
|
@@ -11270,6 +11708,11 @@ var TurnHost = class {
|
|
|
11270
11708
|
crons.push(cron);
|
|
11271
11709
|
}
|
|
11272
11710
|
}
|
|
11711
|
+
const failedCrons = (await this.board.listCrons()).filter((cron) => cron.agent === agent.name && cron.scope === dispatch.project && conversationPart(cron) === part && cronFailure(cron) !== null && !(dispatch.crons ?? []).includes(cron.id));
|
|
11712
|
+
const cronFailures = failedCrons.flatMap((cron) => {
|
|
11713
|
+
const failure = cronFailure(cron);
|
|
11714
|
+
return failure === null ? [] : [{ cron: cron.id, turnId: failure.turnId }];
|
|
11715
|
+
});
|
|
11273
11716
|
const prompt = buildTurnPrompt({
|
|
11274
11717
|
dispatch,
|
|
11275
11718
|
clock: { now: this.now(), timeZone: society.timezone },
|
|
@@ -11282,6 +11725,8 @@ var TurnHost = class {
|
|
|
11282
11725
|
waitingStages: waiting,
|
|
11283
11726
|
project,
|
|
11284
11727
|
lastTurn,
|
|
11728
|
+
failedCrons,
|
|
11729
|
+
...changedInstructions ? { changedInstructions: instructions } : {},
|
|
11285
11730
|
onboarding,
|
|
11286
11731
|
afreshAfterReflection,
|
|
11287
11732
|
societyView,
|
|
@@ -11297,7 +11742,9 @@ var TurnHost = class {
|
|
|
11297
11742
|
...base,
|
|
11298
11743
|
session: recorded ?? null,
|
|
11299
11744
|
sessionCostUsd: costSoFarUsd,
|
|
11300
|
-
...usageSoFar === void 0 ? {} : { sessionUsage: usageSoFar }
|
|
11745
|
+
...usageSoFar === void 0 ? {} : { sessionUsage: usageSoFar },
|
|
11746
|
+
...instructionsHeld === void 0 ? {} : { sessionInstructionsHash: instructionsHeld },
|
|
11747
|
+
...cronFailures.length === 0 ? {} : { cronFailures }
|
|
11301
11748
|
});
|
|
11302
11749
|
if (record.id === void 0) {
|
|
11303
11750
|
throw new Error("the board gave the turn no id");
|
|
@@ -11317,6 +11764,7 @@ var TurnHost = class {
|
|
|
11317
11764
|
costSoFarUsd,
|
|
11318
11765
|
...usageSoFar === void 0 ? {} : { usageSoFar },
|
|
11319
11766
|
instructions,
|
|
11767
|
+
instructionsHash: instructionsNow,
|
|
11320
11768
|
prompt,
|
|
11321
11769
|
mcp: { url: this.mcpUrl, token },
|
|
11322
11770
|
limits: { timeoutMs: this.turnTimeoutMs, maxTurns: this.maxTurns },
|
|
@@ -11340,6 +11788,7 @@ var TurnHost = class {
|
|
|
11340
11788
|
stopRequestedBy: null,
|
|
11341
11789
|
held: held.map((each) => each.id),
|
|
11342
11790
|
costSoFarUsd,
|
|
11791
|
+
instructionsNow,
|
|
11343
11792
|
resumeRetries,
|
|
11344
11793
|
configuredModel: agent.model ?? null,
|
|
11345
11794
|
transcript: [],
|
|
@@ -11510,7 +11959,10 @@ var TurnHost = class {
|
|
|
11510
11959
|
const resumeRetries = outcome.resumeFailed === "retry" ? turn.resumeRetries + 1 : void 0;
|
|
11511
11960
|
const dropSession = job.session !== null && (outcome.resumeFailed === "lost" || (resumeRetries ?? 0) >= RESUME_RETRIES);
|
|
11512
11961
|
const reflected = turn.afreshAfterReflection && turn.record.trigger.kind === "reflection" && outcome.exitReason === "completed";
|
|
11513
|
-
const { sessionUsage: _started, ...begun } = turn.record;
|
|
11962
|
+
const { sessionUsage: _started, sessionInstructionsHash: heldBefore, ...begun } = turn.record;
|
|
11963
|
+
const promptDelivered = outcome.exitReason === "completed" || outcome.exitReason === "blocked" || outcome.exitReason === "stopped";
|
|
11964
|
+
const instructionsTaken = outcome.exitReason === "stopped" ? outcome.contextTokens !== void 0 : promptDelivered;
|
|
11965
|
+
const instructionsHeld = job.session === null ? outcome.session === null ? void 0 : turn.instructionsNow : instructionsTaken ? turn.instructionsNow : heldBefore;
|
|
11514
11966
|
const finished = {
|
|
11515
11967
|
...begun,
|
|
11516
11968
|
session: outcome.session,
|
|
@@ -11522,6 +11974,7 @@ var TurnHost = class {
|
|
|
11522
11974
|
costUsd: outcome.costUsd,
|
|
11523
11975
|
sessionCostUsd: outcome.sessionCostUsd ?? turn.costSoFarUsd,
|
|
11524
11976
|
...outcome.sessionUsage === void 0 ? {} : { sessionUsage: outcome.sessionUsage },
|
|
11977
|
+
...instructionsHeld === void 0 ? {} : { sessionInstructionsHash: instructionsHeld },
|
|
11525
11978
|
...outcome.contextTokens === void 0 ? {} : { contextTokens: outcome.contextTokens },
|
|
11526
11979
|
toolCalls: turn.transcript.filter((entry) => entry.event.type === "tool_call").length,
|
|
11527
11980
|
model: outcome.model ?? turn.configuredModel,
|
|
@@ -11556,7 +12009,7 @@ var TurnHost = class {
|
|
|
11556
12009
|
} else if (outcome.session !== null && outcome.session !== job.session) {
|
|
11557
12010
|
await this.board.writeSession(job.agent, job.scope, job.cli, outcome.session, turn.runner, part);
|
|
11558
12011
|
}
|
|
11559
|
-
if (
|
|
12012
|
+
if (promptDelivered) {
|
|
11560
12013
|
await this.board.setDigestCursor(job.agent, job.scope, turn.delivered, part);
|
|
11561
12014
|
await this.renewLeases(turn.actor, turn.held);
|
|
11562
12015
|
if (outcome.status?.needsUserDecision === true) {
|
|
@@ -12171,7 +12624,8 @@ var RunnerHub = class {
|
|
|
12171
12624
|
return { refused: `${agent.name} has no CLI binding` };
|
|
12172
12625
|
}
|
|
12173
12626
|
const cli = agent.cli;
|
|
12174
|
-
const
|
|
12627
|
+
const runners2 = await this.board.listRunners();
|
|
12628
|
+
const free = (seat) => seat.send !== null && !seat.unconfirmed && seat.hello.clis.includes(cli) && (seat.hello.slots === null || seat.inUse < seat.hello.slots) && !heldOn(runners2, seat.name, cli);
|
|
12175
12629
|
let chosen;
|
|
12176
12630
|
if (dispatch.project === SOCIETY_SCOPE) {
|
|
12177
12631
|
chosen = await this.pinnedSeat(agent.name, agent.homeRunner, cli, free);
|
|
@@ -12428,11 +12882,7 @@ var RunnerHub = class {
|
|
|
12428
12882
|
move.id = await this.board.openMoveRepo();
|
|
12429
12883
|
const id = move.id;
|
|
12430
12884
|
move.phase = "export";
|
|
12431
|
-
const exported = ProjectExportedSchema.parse(await this.request(source, (request) => ({
|
|
12432
|
-
type: "export",
|
|
12433
|
-
request,
|
|
12434
|
-
export: { move: id, repo }
|
|
12435
|
-
})).catch((error) => {
|
|
12885
|
+
const exported = ProjectExportedSchema.parse(await this.request(source, (request) => ({ type: "export", request, export: { move: id, repo } }), moveTransferMs(MOVE_PACK_LIMIT_BYTES) + this.requestTimeoutMs).catch((error) => {
|
|
12436
12886
|
throw new BoardError("INVALID_STATE", `${from} could not send ${slug}: ${describe(error)}`);
|
|
12437
12887
|
}));
|
|
12438
12888
|
if (!exported.ok) {
|
|
@@ -12442,8 +12892,13 @@ var RunnerHub = class {
|
|
|
12442
12892
|
const imported = ProjectImportedSchema.parse(await this.request(target, (request) => ({
|
|
12443
12893
|
type: "import",
|
|
12444
12894
|
request,
|
|
12445
|
-
import: {
|
|
12446
|
-
|
|
12895
|
+
import: {
|
|
12896
|
+
move: id,
|
|
12897
|
+
repo,
|
|
12898
|
+
pullRequests: project.onDone === "ghpr",
|
|
12899
|
+
bytes: exported.bytes
|
|
12900
|
+
}
|
|
12901
|
+
}), moveTransferMs(exported.bytes) + this.requestTimeoutMs).catch((error) => {
|
|
12447
12902
|
throw new BoardError("INVALID_STATE", `${to} could not take ${slug}: ${describe(error)}`);
|
|
12448
12903
|
}));
|
|
12449
12904
|
move.phase = "recording";
|
|
@@ -12570,18 +13025,8 @@ var RunnerHub = class {
|
|
|
12570
13025
|
* back as a `BoardError` in the runner's words.
|
|
12571
13026
|
*/
|
|
12572
13027
|
async readTurnImage(actor, file) {
|
|
12573
|
-
const
|
|
12574
|
-
|
|
12575
|
-
throw new BoardError("INVALID_STATE", `${actor.name} has no turn running in this conversation to read ${file} from`);
|
|
12576
|
-
}
|
|
12577
|
-
const seat = this.seats.get(turn.runner);
|
|
12578
|
-
if (seat === void 0 || seat.send === null) {
|
|
12579
|
-
throw new RunnerAwayError(`runner ${turn.runner}, where the turn runs, is not connected`);
|
|
12580
|
-
}
|
|
12581
|
-
if (!seat.hello.imageReads) {
|
|
12582
|
-
throw new BoardError("INVALID_STATE", `runner ${seat.name}, where the turn runs, cannot read images yet; upgrade it`);
|
|
12583
|
-
}
|
|
12584
|
-
const image = TurnImageSchema.nullable().parse(await this.request(seat, (request) => ({ type: "image", request, read: { turnId: turn.turnId, path: file } }), FILE_TIMEOUT_MS));
|
|
13028
|
+
const { turnId, seat } = this.turnSeat(actor, "imageReads", `read ${file} from`, "read images");
|
|
13029
|
+
const image = TurnImageSchema.nullable().parse(await this.request(seat, (request) => ({ type: "image", request, read: { turnId, path: file } }), FILE_TIMEOUT_MS));
|
|
12585
13030
|
if (image === null) {
|
|
12586
13031
|
throw new BoardError("INVALID_STATE", `the turn is no longer running on runner ${seat.name}`);
|
|
12587
13032
|
}
|
|
@@ -12590,6 +13035,28 @@ var RunnerHub = class {
|
|
|
12590
13035
|
}
|
|
12591
13036
|
return Buffer.from(image.content, "base64");
|
|
12592
13037
|
}
|
|
13038
|
+
/**
|
|
13039
|
+
* Hands an image the board keeps to the runner a citizen's turn runs on, for `read_image`, which
|
|
13040
|
+
* writes it to a folder of that turn's own in the system temp folder, removed when the turn
|
|
13041
|
+
* ends, and answers the file's path there, with the same refusals as `readTurnImage`.
|
|
13042
|
+
*/
|
|
13043
|
+
async writeTurnImage(actor, image) {
|
|
13044
|
+
const { turnId, seat } = this.turnSeat(actor, "imageWrites", "open an image in", "open images");
|
|
13045
|
+
const file = TurnImageFileSchema.nullable().parse(await this.request(seat, (request) => ({
|
|
13046
|
+
type: "image_write",
|
|
13047
|
+
request,
|
|
13048
|
+
write: {
|
|
13049
|
+
turnId,
|
|
13050
|
+
id: image.id,
|
|
13051
|
+
type: image.type,
|
|
13052
|
+
content: Buffer.from(image.bytes).toString("base64")
|
|
13053
|
+
}
|
|
13054
|
+
}), FILE_TIMEOUT_MS));
|
|
13055
|
+
if (file === null) {
|
|
13056
|
+
throw new BoardError("INVALID_STATE", `the turn is no longer running on runner ${seat.name}`);
|
|
13057
|
+
}
|
|
13058
|
+
return file.path;
|
|
13059
|
+
}
|
|
12593
13060
|
/** The models a CLI offers, asked of a connected runner that has it; none when no runner has it. */
|
|
12594
13061
|
/**
|
|
12595
13062
|
* The connected runner a model list for `cli` comes from: the first of `prefer` that has the CLI,
|
|
@@ -12767,6 +13234,25 @@ var RunnerHub = class {
|
|
|
12767
13234
|
repo: projectRepo(record)
|
|
12768
13235
|
};
|
|
12769
13236
|
}
|
|
13237
|
+
/**
|
|
13238
|
+
* The caller's turn in flight, found by its conversation, and the connected runner it runs on,
|
|
13239
|
+
* which must say `able` in its hello: an image's way to and from the machine a turn runs on.
|
|
13240
|
+
* `where` and `what` say what the caller asked for in a refusal.
|
|
13241
|
+
*/
|
|
13242
|
+
turnSeat(actor, able, where, what) {
|
|
13243
|
+
const turn = this.host.runningTurns().find((each) => each.agent === actor.name && each.scope === actor.scope && each.thread === actor.thread && each.channel === actor.channel && each.place === actor.place);
|
|
13244
|
+
if (turn === void 0) {
|
|
13245
|
+
throw new BoardError("INVALID_STATE", `${actor.name} has no turn running in this conversation to ${where}`);
|
|
13246
|
+
}
|
|
13247
|
+
const seat = this.seats.get(turn.runner);
|
|
13248
|
+
if (seat === void 0 || seat.send === null) {
|
|
13249
|
+
throw new RunnerAwayError(`runner ${turn.runner}, where the turn runs, is not connected`);
|
|
13250
|
+
}
|
|
13251
|
+
if (!seat.hello[able]) {
|
|
13252
|
+
throw new BoardError("INVALID_STATE", `runner ${seat.name}, where the turn runs, cannot ${what} yet; upgrade it`);
|
|
13253
|
+
}
|
|
13254
|
+
return { turnId: turn.turnId, seat };
|
|
13255
|
+
}
|
|
12770
13256
|
assertOwns(runner, turnId) {
|
|
12771
13257
|
const owner = this.host.runnerOf(turnId);
|
|
12772
13258
|
if (owner !== void 0 && owner !== runner) {
|
|
@@ -12899,6 +13385,7 @@ import { bodyLimit } from "hono/body-limit";
|
|
|
12899
13385
|
var ENROLL_BODY_LIMIT_BYTES = 16 * 1024;
|
|
12900
13386
|
var APP_TOKEN_BODY_LIMIT_BYTES = 16 * 1024;
|
|
12901
13387
|
var API_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
13388
|
+
var IMAGE_BODY_LIMIT_BYTES = IMAGE_LIMIT_BYTES;
|
|
12902
13389
|
var RUNNER_BODY_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
12903
13390
|
var RUNNER_ANSWER_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
12904
13391
|
function limitBody(maxSize) {
|
|
@@ -12915,7 +13402,7 @@ function limitBody(maxSize) {
|
|
|
12915
13402
|
}
|
|
12916
13403
|
|
|
12917
13404
|
// apps/server/dist/github-auth.js
|
|
12918
|
-
import { createHash as
|
|
13405
|
+
import { createHash as createHash4, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
|
|
12919
13406
|
import { Hono } from "hono";
|
|
12920
13407
|
import { z as z18 } from "zod";
|
|
12921
13408
|
var SIGN_IN_TTL_MS = 30 * 24 * 36e5;
|
|
@@ -12968,7 +13455,7 @@ function prune(map, now) {
|
|
|
12968
13455
|
}
|
|
12969
13456
|
}
|
|
12970
13457
|
function provesChallenge(verifier, challenge) {
|
|
12971
|
-
const answer = Buffer.from(
|
|
13458
|
+
const answer = Buffer.from(createHash4("sha256").update(verifier).digest("base64url"));
|
|
12972
13459
|
const expected = Buffer.from(challenge);
|
|
12973
13460
|
return answer.length === expected.length && timingSafeEqual(answer, expected);
|
|
12974
13461
|
}
|
|
@@ -13203,9 +13690,109 @@ import { Hono as Hono2 } from "hono";
|
|
|
13203
13690
|
import { streamSSE } from "hono/streaming";
|
|
13204
13691
|
|
|
13205
13692
|
// apps/server/dist/home-git.js
|
|
13693
|
+
import { once } from "node:events";
|
|
13206
13694
|
import { execa as execa3 } from "execa";
|
|
13695
|
+
|
|
13696
|
+
// apps/server/dist/serve.js
|
|
13697
|
+
import { serve } from "@hono/node-server";
|
|
13698
|
+
var REQUEST_TIMEOUT_MS = 3e5;
|
|
13699
|
+
var HEADERS_TIMEOUT_MS = 6e4;
|
|
13700
|
+
var deadlines = /* @__PURE__ */ new WeakMap();
|
|
13701
|
+
function serveBoard(options, listening) {
|
|
13702
|
+
const timeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
13703
|
+
return serve({
|
|
13704
|
+
fetch: (request, env) => {
|
|
13705
|
+
const { incoming, outgoing } = env;
|
|
13706
|
+
if (!incoming.complete) {
|
|
13707
|
+
const timer = setTimeout(() => {
|
|
13708
|
+
if (!incoming.complete) {
|
|
13709
|
+
incoming.socket.destroy();
|
|
13710
|
+
}
|
|
13711
|
+
}, timeoutMs);
|
|
13712
|
+
timer.unref();
|
|
13713
|
+
const lift = () => clearTimeout(timer);
|
|
13714
|
+
deadlines.set(request, lift);
|
|
13715
|
+
outgoing.once("close", lift);
|
|
13716
|
+
}
|
|
13717
|
+
return options.fetch(request, env);
|
|
13718
|
+
},
|
|
13719
|
+
port: options.port,
|
|
13720
|
+
hostname: options.hostname,
|
|
13721
|
+
serverOptions: {
|
|
13722
|
+
requestTimeout: 0,
|
|
13723
|
+
headersTimeout: options.headersTimeoutMs ?? HEADERS_TIMEOUT_MS,
|
|
13724
|
+
...options.connectionsCheckingIntervalMs === void 0 ? {} : { connectionsCheckingInterval: options.connectionsCheckingIntervalMs }
|
|
13725
|
+
}
|
|
13726
|
+
}, listening);
|
|
13727
|
+
}
|
|
13728
|
+
function liftDeadline(request) {
|
|
13729
|
+
deadlines.get(request)?.();
|
|
13730
|
+
deadlines.delete(request);
|
|
13731
|
+
}
|
|
13732
|
+
|
|
13733
|
+
// apps/server/dist/home-git.js
|
|
13207
13734
|
var SMART_PATHS = /* @__PURE__ */ new Set(["/info/refs", "/git-upload-pack", "/git-receive-pack"]);
|
|
13208
13735
|
var MAX_BYTES = MOVE_PACK_LIMIT_BYTES;
|
|
13736
|
+
var IDLE_MS = 5 * 6e4;
|
|
13737
|
+
async function feed(body, stdin, limit2, idleMs) {
|
|
13738
|
+
if (body === null) {
|
|
13739
|
+
stdin.end();
|
|
13740
|
+
return "done";
|
|
13741
|
+
}
|
|
13742
|
+
const reader = body.getReader();
|
|
13743
|
+
const waits = new AbortController();
|
|
13744
|
+
let ended = null;
|
|
13745
|
+
const endedAs = () => ended;
|
|
13746
|
+
const end = (why) => {
|
|
13747
|
+
if (ended === null) {
|
|
13748
|
+
ended = why;
|
|
13749
|
+
waits.abort();
|
|
13750
|
+
if (why === "idle" || why === "cut") {
|
|
13751
|
+
reader.cancel().catch(() => void 0);
|
|
13752
|
+
} else {
|
|
13753
|
+
reader.releaseLock();
|
|
13754
|
+
}
|
|
13755
|
+
}
|
|
13756
|
+
return ended;
|
|
13757
|
+
};
|
|
13758
|
+
const timer = setTimeout(() => end("idle"), idleMs);
|
|
13759
|
+
const stopped = () => {
|
|
13760
|
+
end("stopped");
|
|
13761
|
+
};
|
|
13762
|
+
stdin.on("error", stopped);
|
|
13763
|
+
stdin.once("close", stopped);
|
|
13764
|
+
let total = 0;
|
|
13765
|
+
try {
|
|
13766
|
+
for (; ; ) {
|
|
13767
|
+
const next = await reader.read().catch(() => null);
|
|
13768
|
+
const early = endedAs();
|
|
13769
|
+
if (early !== null) {
|
|
13770
|
+
return early;
|
|
13771
|
+
}
|
|
13772
|
+
if (next === null) {
|
|
13773
|
+
return end("cut");
|
|
13774
|
+
}
|
|
13775
|
+
if (next.done) {
|
|
13776
|
+
stdin.end();
|
|
13777
|
+
return "done";
|
|
13778
|
+
}
|
|
13779
|
+
timer.refresh();
|
|
13780
|
+
total += next.value.byteLength;
|
|
13781
|
+
if (total > limit2) {
|
|
13782
|
+
return end("too large");
|
|
13783
|
+
}
|
|
13784
|
+
if (!stdin.write(next.value)) {
|
|
13785
|
+
const drained = await once(stdin, "drain", { signal: waits.signal }).then(() => true, () => false);
|
|
13786
|
+
if (!drained) {
|
|
13787
|
+
return end("stopped");
|
|
13788
|
+
}
|
|
13789
|
+
}
|
|
13790
|
+
}
|
|
13791
|
+
} finally {
|
|
13792
|
+
clearTimeout(timer);
|
|
13793
|
+
stdin.off("close", stopped);
|
|
13794
|
+
}
|
|
13795
|
+
}
|
|
13209
13796
|
function parseCgi(output) {
|
|
13210
13797
|
const crlf = output.indexOf("\r\n\r\n");
|
|
13211
13798
|
const lf = output.indexOf("\n\n");
|
|
@@ -13233,6 +13820,7 @@ function parseCgi(output) {
|
|
|
13233
13820
|
var HomeGit = class {
|
|
13234
13821
|
pushes = /* @__PURE__ */ new Map();
|
|
13235
13822
|
serve(input) {
|
|
13823
|
+
liftDeadline(input.request);
|
|
13236
13824
|
if (input.rest !== "/git-receive-pack") {
|
|
13237
13825
|
return serveHomeGit(input);
|
|
13238
13826
|
}
|
|
@@ -13247,10 +13835,13 @@ async function serveHomeGit(input) {
|
|
|
13247
13835
|
return new Response("not a smart HTTP path", { status: 404 });
|
|
13248
13836
|
}
|
|
13249
13837
|
const { request } = input;
|
|
13250
|
-
const
|
|
13251
|
-
|
|
13838
|
+
const limit2 = input.limitBytes ?? MAX_BYTES;
|
|
13839
|
+
const length = request.headers.get("content-length");
|
|
13840
|
+
const declared = length !== null && /^\d+$/.test(length) ? length : null;
|
|
13841
|
+
if (declared !== null && Number(declared) > limit2) {
|
|
13252
13842
|
return new Response("pack too large for the board to take", { status: 413 });
|
|
13253
13843
|
}
|
|
13844
|
+
liftDeadline(request);
|
|
13254
13845
|
const url = new URL(request.url);
|
|
13255
13846
|
const env = {
|
|
13256
13847
|
PATH: process.env["PATH"] ?? "/usr/bin:/bin",
|
|
@@ -13261,10 +13852,12 @@ async function serveHomeGit(input) {
|
|
|
13261
13852
|
PATH_INFO: `/${input.repository}${input.rest}`,
|
|
13262
13853
|
QUERY_STRING: url.search.replace(/^\?/, ""),
|
|
13263
13854
|
CONTENT_TYPE: request.headers.get("content-type") ?? "",
|
|
13264
|
-
CONTENT_LENGTH: String(body.length),
|
|
13265
13855
|
REMOTE_USER: input.runner,
|
|
13266
13856
|
REMOTE_ADDR: "127.0.0.1"
|
|
13267
13857
|
};
|
|
13858
|
+
if (declared !== null) {
|
|
13859
|
+
env["CONTENT_LENGTH"] = declared;
|
|
13860
|
+
}
|
|
13268
13861
|
const encoding = request.headers.get("content-encoding");
|
|
13269
13862
|
if (encoding !== null) {
|
|
13270
13863
|
env["HTTP_CONTENT_ENCODING"] = encoding;
|
|
@@ -13273,10 +13866,9 @@ async function serveHomeGit(input) {
|
|
|
13273
13866
|
if (protocol !== null) {
|
|
13274
13867
|
env["HTTP_GIT_PROTOCOL"] = protocol;
|
|
13275
13868
|
}
|
|
13276
|
-
const
|
|
13869
|
+
const backend = execa3("git", ["http-backend"], {
|
|
13277
13870
|
env,
|
|
13278
13871
|
extendEnv: false,
|
|
13279
|
-
input: body,
|
|
13280
13872
|
encoding: "buffer",
|
|
13281
13873
|
// The blank line that ends the headers ends the whole answer when the body is empty, as for
|
|
13282
13874
|
// the 4-byte probe git sends before a pack too large for one post; stripped, it reads as none.
|
|
@@ -13284,6 +13876,15 @@ async function serveHomeGit(input) {
|
|
|
13284
13876
|
reject: false,
|
|
13285
13877
|
maxBuffer: MAX_BYTES
|
|
13286
13878
|
});
|
|
13879
|
+
const fed = await feed(request.body, backend.stdin, limit2, input.idleMs ?? IDLE_MS);
|
|
13880
|
+
if (fed !== "done" && fed !== "stopped") {
|
|
13881
|
+
backend.kill();
|
|
13882
|
+
await backend;
|
|
13883
|
+
return fed === "too large" ? new Response("pack too large for the board to take", { status: 413 }) : new Response(fed === "idle" ? "the request stopped arriving" : "the request was cut", {
|
|
13884
|
+
status: fed === "idle" ? 408 : 400
|
|
13885
|
+
});
|
|
13886
|
+
}
|
|
13887
|
+
const result = await backend;
|
|
13287
13888
|
if (result.exitCode !== 0 && result.stdout.length === 0) {
|
|
13288
13889
|
return new Response(`git http-backend failed: ${Buffer.from(result.stderr).toString("utf8")}`, {
|
|
13289
13890
|
status: 500
|
|
@@ -13566,6 +14167,14 @@ function createApp(deps) {
|
|
|
13566
14167
|
};
|
|
13567
14168
|
const api = new Hono3();
|
|
13568
14169
|
api.use("*", authenticate);
|
|
14170
|
+
api.post("/images", limitBody(IMAGE_BODY_LIMIT_BYTES), async (c) => {
|
|
14171
|
+
if (c.get("actor").role !== USER_ROLE) {
|
|
14172
|
+
throw new BoardError("FORBIDDEN", "only the user posts an image's bytes");
|
|
14173
|
+
}
|
|
14174
|
+
const name = ImageNameSchema.optional().parse(c.req.query("name"));
|
|
14175
|
+
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
14176
|
+
return c.json(await board2.postImage(c.get("actor"), bytes, name));
|
|
14177
|
+
});
|
|
13569
14178
|
api.on(["POST", "PUT", "PATCH", "DELETE"], "*", limitBody(API_BODY_LIMIT_BYTES));
|
|
13570
14179
|
api.get("/me", (c) => {
|
|
13571
14180
|
const signIn = board2.signInOf(bearer2(c) ?? "");
|
|
@@ -13618,6 +14227,21 @@ function createApp(deps) {
|
|
|
13618
14227
|
}
|
|
13619
14228
|
return c.json(await quotas.read(runner, cli));
|
|
13620
14229
|
});
|
|
14230
|
+
for (const [verb, paused] of [
|
|
14231
|
+
["pause", true],
|
|
14232
|
+
["resume", false]
|
|
14233
|
+
]) {
|
|
14234
|
+
for (const path12 of [`/runners/:name/${verb}`, `/runners/:name/${verb}/:cli`]) {
|
|
14235
|
+
api.post(path12, async (c) => {
|
|
14236
|
+
const name = NameSchema.parse(c.req.param("name"));
|
|
14237
|
+
const cli = c.req.param("cli");
|
|
14238
|
+
return c.json(await board2.setRunnerPaused(c.get("actor"), name, {
|
|
14239
|
+
paused,
|
|
14240
|
+
...cli === void 0 ? {} : { cli: CliKindSchema.parse(cli) }
|
|
14241
|
+
}));
|
|
14242
|
+
});
|
|
14243
|
+
}
|
|
14244
|
+
}
|
|
13621
14245
|
api.post("/runners", async (c) => {
|
|
13622
14246
|
const body = RunnerBodySchema.parse(await c.req.json());
|
|
13623
14247
|
return c.json(await board2.addRunner(c.get("actor"), body.name));
|
|
@@ -14194,6 +14818,7 @@ var host = new TurnHost({
|
|
|
14194
14818
|
var runners = new RunnerHub({ board, host, version: VERSION, log });
|
|
14195
14819
|
board.watchTurns(() => host.inFlight());
|
|
14196
14820
|
board.readImagesWith((actor, file) => runners.readTurnImage(actor, file));
|
|
14821
|
+
board.openImagesWith((actor, image) => runners.writeTurnImage(actor, image));
|
|
14197
14822
|
var concurrency = config.concurrency ?? Number.POSITIVE_INFINITY;
|
|
14198
14823
|
var timings = parseTimings(JSON.parse(process.env["STELLARIS_TIMINGS"] ?? "{}"));
|
|
14199
14824
|
var scheduler = new Scheduler({ board, runner: runners, log, concurrency, timings });
|
|
@@ -14227,7 +14852,7 @@ var app = createApp({
|
|
|
14227
14852
|
trustedProxies: config.trustedProxies,
|
|
14228
14853
|
log
|
|
14229
14854
|
});
|
|
14230
|
-
var server =
|
|
14855
|
+
var server = serveBoard({ fetch: app.fetch, port: config.port, hostname: config.host }, (info) => {
|
|
14231
14856
|
log.info({
|
|
14232
14857
|
host: info.address,
|
|
14233
14858
|
port: info.port,
|