@kubitnodes/stellaris 0.3.24 → 0.3.26
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 +99 -553
- package/dist/server/index.js +318 -581
- package/package.json +4 -4
- package/web/dist/assets/BackLink-UTiLF1J4.js +1 -0
- package/web/dist/assets/Button-Cp4cNRUX.js +1 -0
- package/web/dist/assets/ChannelForms-DD2B0tey.js +1 -0
- package/web/dist/assets/{ChannelView-BIXouQMX.js → ChannelView-CY3oG9hZ.js} +2 -2
- package/web/dist/assets/CitizenView-BNK5ro-6.js +2 -0
- package/web/dist/assets/{CitizensView-CYFS3f9U.js → CitizensView-WPQ6Jhl3.js} +1 -1
- package/web/dist/assets/{CliIcon-DVX5iLhG.js → CliIcon-C7bwChMP.js} +1 -1
- package/web/dist/assets/CronsView-_pdgwX1I.js +1 -0
- package/web/dist/assets/FileTree-jyDUSu7g.js +1 -0
- package/web/dist/assets/FileView-DcQvlc8V.js +4 -0
- package/web/dist/assets/KnowledgeView-DDl0HQPG.js +1 -0
- package/web/dist/assets/{Markdown--whKJBYV.js → Markdown-DT3V-EVe.js} +7 -7
- package/web/dist/assets/{MessageItem-BvDOeJci.js → MessageItem-agtEugfG.js} +1 -1
- package/web/dist/assets/MetricsView-DiXyYeWP.js +1 -0
- package/web/dist/assets/NeedsYouView-CKmuYan_.js +1 -0
- package/web/dist/assets/Overview-CKcTqsMM.js +1 -0
- package/web/dist/assets/{Pane-DTmByqgA.js → Pane-BaM0TdE6.js} +1 -1
- package/web/dist/assets/{Picker-DsHm4f7s.js → Picker-C2ig-0dT.js} +1 -1
- package/web/dist/assets/ProjectView-T2OHTh5j.js +1 -0
- package/web/dist/assets/ProposalView-B5yjTAcn.js +1 -0
- package/web/dist/assets/ProposalsView-p4KjwV9p.js +1 -0
- package/web/dist/assets/{Quota-D8WDWp5H.js → Quota-CfFGIql9.js} +1 -1
- package/web/dist/assets/RunnersView-DdvwoOur.js +1 -0
- package/web/dist/assets/{SkillFileList-DhdEcFQA.js → SkillFileList-CgrCnuaC.js} +1 -1
- package/web/dist/assets/SkillView-Di5Z9h2L.js +1 -0
- package/web/dist/assets/SocietyView-BDiVxUC5.js +1 -0
- package/web/dist/assets/StickyList-BaV_KZ8x.js +1 -0
- package/web/dist/assets/{SubjectThread-BEXt-tub.js → SubjectThread-DD_ZCCnr.js} +1 -1
- package/web/dist/assets/TaskView-CKcs-MOC.js +3 -0
- package/web/dist/assets/TasksView-oZMjncOU.js +1 -0
- package/web/dist/assets/{ThreadForms-DzoRDeBh.js → ThreadForms-CG5Y926b.js} +1 -1
- package/web/dist/assets/{ThreadItem-CDVppcOZ.js → ThreadItem-D-n2arFA.js} +1 -1
- package/web/dist/assets/{ThreadView-B4F10_j1.js → ThreadView-CT9vd0P4.js} +1 -1
- package/web/dist/assets/{citizen-BPIJzKdn.js → citizen-BiHfph2A.js} +1 -1
- package/web/dist/assets/{compose-BUhbVoI-.js → compose-DY1WL8zB.js} +1 -1
- package/web/dist/assets/format-CENiwkJC.js +55 -0
- package/web/dist/assets/governance-C6ez5MLh.js +1 -0
- package/web/dist/assets/index-CPXALUV6.js +23 -0
- package/web/dist/assets/{index-Bj8MlyAn.css → index-DIa8Dlav.css} +1 -1
- package/web/dist/assets/mentions-BEL5U6ra.js +3 -0
- package/web/dist/assets/{tasks-CTaxXtGL.js → tasks-CAI8Eci7.js} +1 -1
- package/web/dist/assets/useMatch-CINzyDWa.js +1 -0
- package/web/dist/index.html +9 -9
- package/web/dist/assets/BackLink-DKMw_rHu.js +0 -1
- package/web/dist/assets/Button-BYJGqZ3Q.js +0 -1
- package/web/dist/assets/ChannelForms-CkYupEL2.js +0 -1
- package/web/dist/assets/CitizenView-C3AMFixP.js +0 -2
- package/web/dist/assets/CronsView-BGCcO7bH.js +0 -1
- package/web/dist/assets/KnowledgeView-DLLYjd1G.js +0 -1
- package/web/dist/assets/MetricsView-B-KUNOZm.js +0 -1
- package/web/dist/assets/NeedsYouView-Dee_bOKj.js +0 -1
- package/web/dist/assets/Overview-DFiHDFnA.js +0 -1
- package/web/dist/assets/ProjectView-DsRXW-qk.js +0 -1
- package/web/dist/assets/ProposalView-B-4P1S-s.js +0 -1
- package/web/dist/assets/ProposalsView-uEuj-ooV.js +0 -1
- package/web/dist/assets/RunnersView-BB3q65rG.js +0 -1
- package/web/dist/assets/SkillView-vAv7zvtJ.js +0 -1
- package/web/dist/assets/SocietyView-Dv4phSkz.js +0 -1
- package/web/dist/assets/StickyList-C4fC_KJv.js +0 -1
- package/web/dist/assets/TaskFileView-B9sex0tS.js +0 -4
- package/web/dist/assets/TaskView-BCT388kb.js +0 -3
- package/web/dist/assets/TasksView-CuPay34-.js +0 -1
- package/web/dist/assets/format-CvfmDNoY.js +0 -55
- package/web/dist/assets/governance-Bba0OHvJ.js +0 -1
- package/web/dist/assets/index-DD3J11PT.js +0 -23
- package/web/dist/assets/mentions-D838wlfQ.js +0 -3
- package/web/dist/assets/useMatch-B8j0hm1q.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -768,7 +768,6 @@ var ReallocationProposalSchema = z5.object({
|
|
|
768
768
|
changes: RoleChangesSchema,
|
|
769
769
|
description: z5.string().min(1)
|
|
770
770
|
});
|
|
771
|
-
var LegacyReallocationSchema = z5.object({ description: z5.string().min(1) });
|
|
772
771
|
function applyRoleChanges(role, changes) {
|
|
773
772
|
return {
|
|
774
773
|
...role,
|
|
@@ -813,8 +812,7 @@ var ArchiveProposalSchema = z5.object({
|
|
|
813
812
|
});
|
|
814
813
|
var SkillProposalSchema = z5.object({
|
|
815
814
|
name: NameSchema,
|
|
816
|
-
summary: z5.string().min(1).max(200)
|
|
817
|
-
body: z5.string().min(1).optional()
|
|
815
|
+
summary: z5.string().min(1).max(200)
|
|
818
816
|
});
|
|
819
817
|
var ProposalCharterSchemas = {
|
|
820
818
|
role: RoleCharterSchema,
|
|
@@ -891,14 +889,10 @@ function describeCharter(kind, charter) {
|
|
|
891
889
|
}
|
|
892
890
|
case "reallocation": {
|
|
893
891
|
const parsed = ReallocationProposalSchema.safeParse(charter);
|
|
894
|
-
if (parsed.success)
|
|
895
|
-
const { role, changes, description } = parsed.data;
|
|
896
|
-
return `reallocation of role ${role} (${describeRoleChanges(changes)}): ${description}`;
|
|
897
|
-
}
|
|
898
|
-
const legacy = LegacyReallocationSchema.safeParse(charter);
|
|
899
|
-
if (!legacy.success)
|
|
892
|
+
if (!parsed.success)
|
|
900
893
|
break;
|
|
901
|
-
|
|
894
|
+
const { role, changes, description } = parsed.data;
|
|
895
|
+
return `reallocation of role ${role} (${describeRoleChanges(changes)}): ${description}`;
|
|
902
896
|
}
|
|
903
897
|
case "skill": {
|
|
904
898
|
const parsed = SkillProposalSchema.safeParse(charter);
|
|
@@ -1109,12 +1103,10 @@ var VerbInputs = {
|
|
|
1109
1103
|
channel: ChannelRefSchema.optional(),
|
|
1110
1104
|
limit: z7.number().int().positive().max(100).default(20)
|
|
1111
1105
|
}),
|
|
1112
|
-
/** On a
|
|
1106
|
+
/** On a channel with a title; a task's and a proposal's open with them. */
|
|
1113
1107
|
open_thread: z7.object({
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
channel: ChannelRefSchema.optional(),
|
|
1117
|
-
title: z7.string().min(1).max(200).optional()
|
|
1108
|
+
channel: ChannelRefSchema,
|
|
1109
|
+
title: z7.string().min(1).max(200)
|
|
1118
1110
|
}),
|
|
1119
1111
|
close_thread: z7.object({ thread_id: UlidSchema, summary: z7.string().min(1) }),
|
|
1120
1112
|
create_task: z7.object({
|
|
@@ -1628,6 +1620,12 @@ var TurnOutcomeSchema = z9.object({
|
|
|
1628
1620
|
* runner from before never sends it.
|
|
1629
1621
|
*/
|
|
1630
1622
|
resumeFailed: ResumeFailureSchema.optional(),
|
|
1623
|
+
/**
|
|
1624
|
+
* Set when the model service turned the turn away for now, as when it was overloaded or failed
|
|
1625
|
+
* on its side, which the scheduler answers by sending the same wake again within minutes. A
|
|
1626
|
+
* runner from before never sends it.
|
|
1627
|
+
*/
|
|
1628
|
+
transient: z9.boolean().optional(),
|
|
1631
1629
|
/** The model the CLI reported, when it reported one. */
|
|
1632
1630
|
model: z9.string().nullable(),
|
|
1633
1631
|
work: TurnWorkSchema.nullable(),
|
|
@@ -1953,6 +1951,8 @@ var TurnRecordSchema = z10.object({
|
|
|
1953
1951
|
stoppedBy: NameSchema.optional(),
|
|
1954
1952
|
/** Why the CLI could not resume the turn's session, for a turn that failed so. */
|
|
1955
1953
|
resumeFailed: ResumeFailureSchema.optional(),
|
|
1954
|
+
/** Whether the model service turned the turn away for now: overloaded, or failing on its side. */
|
|
1955
|
+
transient: z10.boolean().optional(),
|
|
1956
1956
|
/** How many turns in a row, this one included, could not resume the session for a `retry`. */
|
|
1957
1957
|
resumeRetries: z10.number().int().positive().optional(),
|
|
1958
1958
|
/** Whether the server dropped the conversation's session after this turn, which the next starts anew. */
|
|
@@ -2027,7 +2027,8 @@ var OPS_SIGNAL_KINDS = [
|
|
|
2027
2027
|
"runner",
|
|
2028
2028
|
"home_conflict",
|
|
2029
2029
|
"stuck_workspace",
|
|
2030
|
-
"cron_failing"
|
|
2030
|
+
"cron_failing",
|
|
2031
|
+
"turn_refused"
|
|
2031
2032
|
];
|
|
2032
2033
|
var OpsSignalKindSchema = z11.enum(OPS_SIGNAL_KINDS);
|
|
2033
2034
|
var WAKING_SIGNAL_KINDS = [
|
|
@@ -2051,8 +2052,18 @@ var OpsSignalSchema = z11.object({
|
|
|
2051
2052
|
project: NameSchema.optional(),
|
|
2052
2053
|
agent: NameSchema.optional(),
|
|
2053
2054
|
role: NameSchema.optional(),
|
|
2054
|
-
taskId: UlidSchema.optional()
|
|
2055
|
+
taskId: UlidSchema.optional(),
|
|
2056
|
+
/**
|
|
2057
|
+
* Whether this log of the condition wakes the roles charted for `ops_event`, as its publisher
|
|
2058
|
+
* decided: the operations pass wakes them when a condition is new, and again only at the steps
|
|
2059
|
+
* it names, not at every repeat. Absent, the kind decides.
|
|
2060
|
+
*/
|
|
2061
|
+
wakes: z11.boolean().optional()
|
|
2055
2062
|
});
|
|
2063
|
+
var WAKING = new Set(WAKING_SIGNAL_KINDS);
|
|
2064
|
+
function signalWakes(signal) {
|
|
2065
|
+
return signal.wakes ?? WAKING.has(signal.kind);
|
|
2066
|
+
}
|
|
2056
2067
|
|
|
2057
2068
|
// packages/shared/dist/metrics.js
|
|
2058
2069
|
import { z as z12 } from "zod";
|
|
@@ -2246,7 +2257,7 @@ var RunnerConfigSchema = z13.object({
|
|
|
2246
2257
|
});
|
|
2247
2258
|
|
|
2248
2259
|
// packages/shared/dist/version.js
|
|
2249
|
-
var STELLARIS_VERSION = "0.3.
|
|
2260
|
+
var STELLARIS_VERSION = "0.3.26";
|
|
2250
2261
|
|
|
2251
2262
|
// packages/shared/dist/mentions.js
|
|
2252
2263
|
import { parse, postprocess, preprocess } from "micromark";
|
|
@@ -3577,7 +3588,7 @@ var BoardPaths = class {
|
|
|
3577
3588
|
|
|
3578
3589
|
// packages/board-core/dist/skill-folder.js
|
|
3579
3590
|
import { constants } from "node:fs";
|
|
3580
|
-
import { lstat, open as open2, readdir as readdir3, realpath } from "node:fs/promises";
|
|
3591
|
+
import { lstat, open as open2, readdir as readdir3, readlink, realpath } from "node:fs/promises";
|
|
3581
3592
|
import path6 from "node:path";
|
|
3582
3593
|
var SKILL_TEXT_LIMIT_CHARS = 1e5;
|
|
3583
3594
|
var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
|
|
@@ -3592,6 +3603,13 @@ function inside(root, file) {
|
|
|
3592
3603
|
const relative = path6.relative(root, file);
|
|
3593
3604
|
return relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
|
|
3594
3605
|
}
|
|
3606
|
+
async function handleInside(root, handle) {
|
|
3607
|
+
if (process.platform !== "linux") {
|
|
3608
|
+
return true;
|
|
3609
|
+
}
|
|
3610
|
+
const file = await readlink(`/proc/self/fd/${handle.fd}`).catch(() => null);
|
|
3611
|
+
return file !== null && inside(root, file);
|
|
3612
|
+
}
|
|
3595
3613
|
async function walk(root, prefix, found) {
|
|
3596
3614
|
const entries = await readdir3(path6.join(root, ...prefix === "" ? [] : prefix.split("/")), {
|
|
3597
3615
|
withFileTypes: true
|
|
@@ -3626,6 +3644,9 @@ async function readEntry(root, relative) {
|
|
|
3626
3644
|
return null;
|
|
3627
3645
|
}
|
|
3628
3646
|
try {
|
|
3647
|
+
if (!await handleInside(root, handle)) {
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3629
3650
|
const stats = await handle.stat();
|
|
3630
3651
|
if (!stats.isFile()) {
|
|
3631
3652
|
return null;
|
|
@@ -3696,17 +3717,16 @@ var SignInsSchema = z16.object({
|
|
|
3696
3717
|
signIns: z16.array(z16.object({
|
|
3697
3718
|
tokenHash: z16.string().min(1),
|
|
3698
3719
|
login: z16.string().min(1),
|
|
3699
|
-
/** The account's picture
|
|
3720
|
+
/** The account's picture, when GitHub gave one on its own avatar host. */
|
|
3700
3721
|
avatarUrl: z16.url().optional(),
|
|
3701
3722
|
createdAt: IsoDateTimeSchema,
|
|
3702
3723
|
expiresAt: IsoDateTimeSchema
|
|
3703
3724
|
})).default([])
|
|
3704
3725
|
});
|
|
3705
|
-
var TurnStartPayloadSchema = z16.object({
|
|
3726
|
+
var TurnStartPayloadSchema = z16.object({ turnId: UlidSchema });
|
|
3706
3727
|
var CursorsSchema = z16.object({
|
|
3707
3728
|
digest: z16.string().nullable(),
|
|
3708
3729
|
scopes: z16.record(z16.string(), z16.string()).default({}),
|
|
3709
|
-
threadsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
|
|
3710
3730
|
channelsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
|
|
3711
3731
|
placesFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
|
|
3712
3732
|
/** How far a work role has read news, which goes to whichever of its general conversations reads first. */
|
|
@@ -3715,7 +3735,6 @@ var CursorsSchema = z16.object({
|
|
|
3715
3735
|
var NO_CURSORS = {
|
|
3716
3736
|
digest: null,
|
|
3717
3737
|
scopes: {},
|
|
3718
|
-
threadsFrom: {},
|
|
3719
3738
|
channelsFrom: {},
|
|
3720
3739
|
placesFrom: {}
|
|
3721
3740
|
};
|
|
@@ -3739,10 +3758,6 @@ function placeStart(cursors, scope) {
|
|
|
3739
3758
|
const from = cursors.placesFrom[scope];
|
|
3740
3759
|
return from === void 0 ? cursors.digest : from;
|
|
3741
3760
|
}
|
|
3742
|
-
function threadStart(cursors, scope) {
|
|
3743
|
-
const from = cursors.threadsFrom[scope];
|
|
3744
|
-
return from === void 0 ? cursors.digest : from;
|
|
3745
|
-
}
|
|
3746
3761
|
function newsStart(cursors) {
|
|
3747
3762
|
if (cursors.news !== void 0) {
|
|
3748
3763
|
return cursors.news;
|
|
@@ -3754,7 +3769,7 @@ function cursorOf(cursors, scope, part = null) {
|
|
|
3754
3769
|
if (part === null) {
|
|
3755
3770
|
return cursors.scopes[scope] ?? cursors.digest;
|
|
3756
3771
|
}
|
|
3757
|
-
return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) :
|
|
3772
|
+
return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) : cursors.digest);
|
|
3758
3773
|
}
|
|
3759
3774
|
function byId(a, b) {
|
|
3760
3775
|
return a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0;
|
|
@@ -3775,7 +3790,7 @@ function skillFolderOf(proposal) {
|
|
|
3775
3790
|
return null;
|
|
3776
3791
|
}
|
|
3777
3792
|
const parsed = SkillProposalSchema.safeParse(proposal.charter);
|
|
3778
|
-
return parsed.success
|
|
3793
|
+
return parsed.success ? parsed.data.name : null;
|
|
3779
3794
|
}
|
|
3780
3795
|
function proposedFrom(origin, where) {
|
|
3781
3796
|
return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
|
|
@@ -3790,28 +3805,6 @@ var FRONT_DESK_TRIGGER = "user_post";
|
|
|
3790
3805
|
var OPS_CHANNELS = ["governance"];
|
|
3791
3806
|
var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
|
|
3792
3807
|
var AlignmentsSchema = z16.object({ applied: z16.array(z16.string()).default([]) });
|
|
3793
|
-
var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
|
|
3794
|
-
var THREAD_RECORDS = "threads-as-records";
|
|
3795
|
-
var DIGEST_CURSORS = "digest-replaces-inbox";
|
|
3796
|
-
var TASK_RETURNS = "task-returns-recorded";
|
|
3797
|
-
var OPS_CHANNEL_RETIRED = "ops-channel-retired";
|
|
3798
|
-
var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
|
|
3799
|
-
var THREAD_CONVERSATIONS = "thread-conversations";
|
|
3800
|
-
var ASKS_CHANNEL_OPENED = "asks-channel-opened";
|
|
3801
|
-
var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
|
|
3802
|
-
var CHANNEL_CONVERSATIONS = "channel-conversations";
|
|
3803
|
-
var PLACE_CONVERSATIONS = "place-conversations";
|
|
3804
|
-
var DASHBOARD_UPDATES = "dashboard-updates-granted";
|
|
3805
|
-
var CRON_VERBS = "cron-verbs-granted";
|
|
3806
|
-
var KNOWLEDGE_NOTICES = "knowledge-notices";
|
|
3807
|
-
var IMAGE_UPLOADS = "image-uploads-granted";
|
|
3808
|
-
var KnowledgeEventSchema = z16.object({
|
|
3809
|
-
topic: NameSchema,
|
|
3810
|
-
project: NameSchema.nullable(),
|
|
3811
|
-
replaced: z16.boolean().optional()
|
|
3812
|
-
});
|
|
3813
|
-
var CRON_VERB_NAMES = ["create_cron", "update_cron", "remove_cron"];
|
|
3814
|
-
var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
|
|
3815
3808
|
var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
|
|
3816
3809
|
var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
3817
3810
|
var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
@@ -3831,9 +3824,9 @@ function sentence(summary) {
|
|
|
3831
3824
|
return summary.trim().replace(/\.+$/, "");
|
|
3832
3825
|
}
|
|
3833
3826
|
function skillSummary(data, body) {
|
|
3834
|
-
const
|
|
3835
|
-
if (typeof
|
|
3836
|
-
return
|
|
3827
|
+
const description = data["description"];
|
|
3828
|
+
if (typeof description === "string" && description.trim().length > 0) {
|
|
3829
|
+
return description.trim();
|
|
3837
3830
|
}
|
|
3838
3831
|
return body.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#")) ?? "";
|
|
3839
3832
|
}
|
|
@@ -3986,390 +3979,22 @@ var Board = class _Board {
|
|
|
3986
3979
|
return board2;
|
|
3987
3980
|
}
|
|
3988
3981
|
/**
|
|
3989
|
-
* Applies each one-time alignment an existing society has not had yet
|
|
3990
|
-
*
|
|
3982
|
+
* Applies each one-time alignment an existing society has not had yet, and remembers it, so a
|
|
3983
|
+
* member that later chooses otherwise keeps its choice. A new alignment is a name and a method in
|
|
3984
|
+
* this list; once every board has applied it, both are deleted. None is pending.
|
|
3991
3985
|
*/
|
|
3992
3986
|
async applyAlignments() {
|
|
3987
|
+
const alignments = [];
|
|
3993
3988
|
const file = path7.join(this.paths.state(), "alignments.json");
|
|
3994
3989
|
const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
|
|
3995
|
-
const pending =
|
|
3996
|
-
FOLLOW_DECISIONS,
|
|
3997
|
-
THREAD_RECORDS,
|
|
3998
|
-
DIGEST_CURSORS,
|
|
3999
|
-
TASK_RETURNS,
|
|
4000
|
-
OPS_CHANNEL_RETIRED,
|
|
4001
|
-
DECISIONS_CHANNEL_RETIRED,
|
|
4002
|
-
THREAD_CONVERSATIONS,
|
|
4003
|
-
ASKS_CHANNEL_OPENED,
|
|
4004
|
-
KNOWLEDGE_REMOVAL,
|
|
4005
|
-
CHANNEL_CONVERSATIONS,
|
|
4006
|
-
DASHBOARD_UPDATES,
|
|
4007
|
-
CRON_VERBS,
|
|
4008
|
-
KNOWLEDGE_NOTICES,
|
|
4009
|
-
IMAGE_UPLOADS,
|
|
4010
|
-
PLACE_CONVERSATIONS
|
|
4011
|
-
].filter((name) => !applied.includes(name));
|
|
3990
|
+
const pending = alignments.filter(({ name }) => !applied.includes(name));
|
|
4012
3991
|
if (pending.length === 0) {
|
|
4013
3992
|
return;
|
|
4014
3993
|
}
|
|
4015
|
-
|
|
4016
|
-
await
|
|
4017
|
-
}
|
|
4018
|
-
if (pending.includes(THREAD_RECORDS)) {
|
|
4019
|
-
await this.recordTaskThreads();
|
|
4020
|
-
}
|
|
4021
|
-
if (pending.includes(DIGEST_CURSORS)) {
|
|
4022
|
-
await this.renameInboxCursors();
|
|
4023
|
-
}
|
|
4024
|
-
if (pending.includes(TASK_RETURNS)) {
|
|
4025
|
-
await this.recordTaskReturns();
|
|
4026
|
-
}
|
|
4027
|
-
if (pending.includes(OPS_CHANNEL_RETIRED)) {
|
|
4028
|
-
await this.retireSocietyChannel("ops");
|
|
4029
|
-
}
|
|
4030
|
-
if (pending.includes(DECISIONS_CHANNEL_RETIRED)) {
|
|
4031
|
-
await this.retireSocietyChannel("decisions");
|
|
4032
|
-
}
|
|
4033
|
-
if (pending.includes(THREAD_CONVERSATIONS)) {
|
|
4034
|
-
await this.startThreadCursors();
|
|
4035
|
-
}
|
|
4036
|
-
if (pending.includes(ASKS_CHANNEL_OPENED)) {
|
|
4037
|
-
await this.openAsksChannel();
|
|
4038
|
-
}
|
|
4039
|
-
if (pending.includes(KNOWLEDGE_REMOVAL)) {
|
|
4040
|
-
await this.grantBeside("write_knowledge", "remove_knowledge");
|
|
4041
|
-
}
|
|
4042
|
-
if (pending.includes(CHANNEL_CONVERSATIONS)) {
|
|
4043
|
-
await this.startChannelCursors();
|
|
4044
|
-
}
|
|
4045
|
-
if (pending.includes(DASHBOARD_UPDATES)) {
|
|
4046
|
-
await this.grantBeside("write_knowledge", "update_dashboard");
|
|
4047
|
-
await this.rewordDashboards();
|
|
4048
|
-
}
|
|
4049
|
-
if (pending.includes(CRON_VERBS)) {
|
|
4050
|
-
await this.grantToEveryRole(CRON_VERB_NAMES);
|
|
4051
|
-
}
|
|
4052
|
-
if (pending.includes(KNOWLEDGE_NOTICES)) {
|
|
4053
|
-
await this.noticeKnowledgeNotes();
|
|
4054
|
-
}
|
|
4055
|
-
if (pending.includes(IMAGE_UPLOADS)) {
|
|
4056
|
-
await this.grantBeside("post_message", "upload_image");
|
|
4057
|
-
}
|
|
4058
|
-
if (pending.includes(PLACE_CONVERSATIONS)) {
|
|
4059
|
-
await this.startPlaceCursors();
|
|
4060
|
-
}
|
|
4061
|
-
await writeJson(file, { applied: [...applied, ...pending] });
|
|
4062
|
-
}
|
|
4063
|
-
/**
|
|
4064
|
-
* Gives each knowledge note from before notes carried their notice the notice it would carry
|
|
4065
|
-
* now. The event log, not the note's words, says which post is one: a knowledge verb appends
|
|
4066
|
-
* its event and then its note under the mutex, so a note is the post right after such an event,
|
|
4067
|
-
* by the same actor, at the top of the general channel of the topic's place. A post written to
|
|
4068
|
-
* look like a note follows no such event and is left as it is.
|
|
4069
|
-
*/
|
|
4070
|
-
async noticeKnowledgeNotes() {
|
|
4071
|
-
let before;
|
|
4072
|
-
for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
|
|
4073
|
-
const verb = before;
|
|
4074
|
-
before = event;
|
|
4075
|
-
if (event.type !== "message.posted" || verb === void 0 || verb.actor !== event.actor || verb.type !== "knowledge.written" && verb.type !== "knowledge.removed") {
|
|
4076
|
-
continue;
|
|
4077
|
-
}
|
|
4078
|
-
const knowledge = KnowledgeEventSchema.safeParse(verb.payload);
|
|
4079
|
-
const id = UlidSchema.safeParse(event.payload["id"]);
|
|
4080
|
-
if (!knowledge.success || !id.success || (event.payload["thread"] ?? null) !== null || event.payload["channel"] !== channelRef(knowledge.data.project, "general")) {
|
|
4081
|
-
continue;
|
|
4082
|
-
}
|
|
4083
|
-
const { topic, project, replaced } = knowledge.data;
|
|
4084
|
-
const file = this.paths.messageFile(this.paths.channelDir(channelRef(project, "general")), id.data, event.actor);
|
|
4085
|
-
if (!await exists(file)) {
|
|
4086
|
-
continue;
|
|
4087
|
-
}
|
|
4088
|
-
const { data, body, error } = await readLooseMarkdown(file);
|
|
4089
|
-
if (error !== null || data["notice"] !== void 0) {
|
|
4090
|
-
continue;
|
|
4091
|
-
}
|
|
4092
|
-
const notice = {
|
|
4093
|
-
kind: "knowledge",
|
|
4094
|
-
action: verb.type === "knowledge.removed" ? "removed" : replaced === true ? "updated" : "written",
|
|
4095
|
-
topic,
|
|
4096
|
-
project
|
|
4097
|
-
};
|
|
4098
|
-
await writeMarkdown(file, { ...data, notice }, body);
|
|
4099
|
-
}
|
|
4100
|
-
}
|
|
4101
|
-
/**
|
|
4102
|
-
* Grants `verb` once to every charter granting `beside`, the work roles' charters in practice;
|
|
4103
|
-
* seed roles have it from their seed.
|
|
4104
|
-
*/
|
|
4105
|
-
async grantBeside(beside, verb) {
|
|
4106
|
-
for (const charter of await this.listRoles()) {
|
|
4107
|
-
if (charter.verbs.includes(beside) && !charter.verbs.includes(verb)) {
|
|
4108
|
-
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
|
|
4109
|
-
}
|
|
4110
|
-
}
|
|
4111
|
-
}
|
|
4112
|
-
/** Grants verbs once to every charter but the user's, which holds every verb already. */
|
|
4113
|
-
async grantToEveryRole(verbs) {
|
|
4114
|
-
for (const charter of await this.listRoles()) {
|
|
4115
|
-
const missing = verbs.filter((verb) => !charter.verbs.includes(verb));
|
|
4116
|
-
if (charter.name !== USER_ROLE && missing.length > 0) {
|
|
4117
|
-
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, ...missing] }, { verbsAdded: missing });
|
|
4118
|
-
}
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
/** A dashboard still saying agents may edit its file says how it changes now. */
|
|
4122
|
-
async rewordDashboards() {
|
|
4123
|
-
for (const slug of await listDirs(this.paths.projects())) {
|
|
4124
|
-
const file = this.paths.dashboard(slug);
|
|
4125
|
-
if (!await exists(file)) {
|
|
4126
|
-
continue;
|
|
4127
|
-
}
|
|
4128
|
-
const { data, body } = await readLooseMarkdown(file);
|
|
4129
|
-
if (body.includes(OLD_DASHBOARD_LINE)) {
|
|
4130
|
-
await writeMarkdown(file, data, body.replace(OLD_DASHBOARD_LINE, DASHBOARD_LINE));
|
|
4131
|
-
}
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
/**
|
|
4135
|
-
* A task sent back before tasks recorded it kept no trace outside the event log. A task still
|
|
4136
|
-
* short of the stage its latest `task.moved` came from is in that rework, and records it.
|
|
4137
|
-
*/
|
|
4138
|
-
async recordTaskReturns() {
|
|
4139
|
-
const moves = /* @__PURE__ */ new Map();
|
|
4140
|
-
for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
|
|
4141
|
-
const taskId = event.payload["taskId"];
|
|
4142
|
-
if (event.type === "task.moved" && typeof taskId === "string") {
|
|
4143
|
-
moves.set(taskId, event);
|
|
4144
|
-
}
|
|
4145
|
-
}
|
|
4146
|
-
for (const slug of await listDirs(this.paths.projects())) {
|
|
4147
|
-
for (const task of await this.listTasks(slug)) {
|
|
4148
|
-
const move = moves.get(task.id);
|
|
4149
|
-
const from = move?.payload["from"];
|
|
4150
|
-
if (move === void 0 || typeof from !== "string" || task.returned !== void 0 || task.completing || task.status !== "open" && task.status !== "claimed") {
|
|
4151
|
-
continue;
|
|
4152
|
-
}
|
|
4153
|
-
const back = stageIndex(task, from);
|
|
4154
|
-
if (back > stageIndex(task, task.stage)) {
|
|
4155
|
-
await this.writeTask(slug, {
|
|
4156
|
-
...task,
|
|
4157
|
-
returned: { from, by: move.actor, at: move.ts }
|
|
4158
|
-
});
|
|
4159
|
-
}
|
|
4160
|
-
}
|
|
4161
|
-
}
|
|
4162
|
-
}
|
|
4163
|
-
/**
|
|
4164
|
-
* A society channel the board no longer uses leaves the society's list and every member's
|
|
4165
|
-
* subscriptions; its old posts stay on disk. `ops` carried operations signals, which are only
|
|
4166
|
-
* logged now; `decisions` carried proposals' outcomes, which are posts in their threads now, and
|
|
4167
|
-
* requests to the user, which are mentions of the user wherever the question belongs.
|
|
4168
|
-
*/
|
|
4169
|
-
async retireSocietyChannel(name) {
|
|
4170
|
-
const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
|
|
4171
|
-
if (doc.data.channels.includes(name)) {
|
|
4172
|
-
await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: doc.data.channels.filter((channel) => channel !== name) }, doc.body);
|
|
4173
|
-
}
|
|
4174
|
-
for (const agent of await this.listAgents()) {
|
|
4175
|
-
if (agent.subscriptions.includes(name)) {
|
|
4176
|
-
await this.updateAgent(agent.name, (a) => ({
|
|
4177
|
-
...a,
|
|
4178
|
-
subscriptions: a.subscriptions.filter((ref) => ref !== name)
|
|
4179
|
-
}));
|
|
4180
|
-
await this.refreshMember(agent.name);
|
|
4181
|
-
}
|
|
4182
|
-
}
|
|
4183
|
-
}
|
|
4184
|
-
/**
|
|
4185
|
-
* Asks once opened their threads in general, where closing one posted its summary to every
|
|
4186
|
-
* citizen and woke the steward's heartbeat for it. The society gains the asks channel, and the
|
|
4187
|
-
* front desk follows it.
|
|
4188
|
-
*/
|
|
4189
|
-
async openAsksChannel() {
|
|
4190
|
-
const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
|
|
4191
|
-
if (!doc.data.channels.includes(ASK_CHANNEL)) {
|
|
4192
|
-
await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: [...doc.data.channels, ASK_CHANNEL] }, doc.body);
|
|
4193
|
-
}
|
|
4194
|
-
await ensureDir(this.paths.societyChannel(ASK_CHANNEL));
|
|
4195
|
-
for (const agent of await this.listAgents()) {
|
|
4196
|
-
if (agent.status !== "active" || agent.subscriptions.includes(ASK_CHANNEL)) {
|
|
4197
|
-
continue;
|
|
4198
|
-
}
|
|
4199
|
-
if (!(await this.readRole(agent.role)).wakeTriggers.includes(FRONT_DESK_TRIGGER)) {
|
|
4200
|
-
continue;
|
|
4201
|
-
}
|
|
4202
|
-
await this.updateAgent(agent.name, (a) => ({
|
|
4203
|
-
...a,
|
|
4204
|
-
subscriptions: [...a.subscriptions, ASK_CHANNEL]
|
|
4205
|
-
}));
|
|
4206
|
-
await this.refreshMember(agent.name);
|
|
4207
|
-
await this.events.append("subscription.changed", agent.name, {
|
|
4208
|
-
channel: ASK_CHANNEL,
|
|
4209
|
-
subscribed: true,
|
|
4210
|
-
aligned: true
|
|
4211
|
-
});
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4214
|
-
/** Members that predate `decisions` among the ops channels read proposals without their outcome. */
|
|
4215
|
-
async followDecisions() {
|
|
4216
|
-
if (!(await this.society()).channels.includes("decisions")) {
|
|
4217
|
-
return;
|
|
4218
|
-
}
|
|
4219
|
-
for (const agent of await this.listAgents()) {
|
|
4220
|
-
if (agent.status !== "active" || agent.subscriptions.includes("decisions")) {
|
|
4221
|
-
continue;
|
|
4222
|
-
}
|
|
4223
|
-
if (!(await this.readRole(agent.role)).wakeTriggers.includes(OPS_WAKE_TRIGGER)) {
|
|
4224
|
-
continue;
|
|
4225
|
-
}
|
|
4226
|
-
await this.updateAgent(agent.name, (a) => ({
|
|
4227
|
-
...a,
|
|
4228
|
-
subscriptions: [...a.subscriptions, "decisions"]
|
|
4229
|
-
}));
|
|
4230
|
-
await this.refreshMember(agent.name);
|
|
4231
|
-
await this.events.append("subscription.changed", agent.name, {
|
|
4232
|
-
channel: "decisions",
|
|
4233
|
-
subscribed: true,
|
|
4234
|
-
aligned: true
|
|
4235
|
-
});
|
|
4236
|
-
}
|
|
4237
|
-
}
|
|
4238
|
-
/**
|
|
4239
|
-
* Threads were once a state field on their task. Each becomes a record under the task's project,
|
|
4240
|
-
* on its general channel, closed if its task has ended, and the field leaves the task file.
|
|
4241
|
-
*/
|
|
4242
|
-
async recordTaskThreads() {
|
|
4243
|
-
const opened = /* @__PURE__ */ new Map();
|
|
4244
|
-
const closed = /* @__PURE__ */ new Map();
|
|
4245
|
-
for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
|
|
4246
|
-
const taskId = event.payload["taskId"];
|
|
4247
|
-
if (typeof taskId !== "string") {
|
|
4248
|
-
continue;
|
|
4249
|
-
}
|
|
4250
|
-
if (event.type === "thread.opened") {
|
|
4251
|
-
opened.set(taskId, event);
|
|
4252
|
-
} else if (event.type === "thread.closed") {
|
|
4253
|
-
closed.set(taskId, event);
|
|
4254
|
-
}
|
|
4255
|
-
}
|
|
4256
|
-
for (const slug of await listDirs(this.paths.projects())) {
|
|
4257
|
-
const threads = this.paths.threads(slug);
|
|
4258
|
-
const summaries = /* @__PURE__ */ new Map();
|
|
4259
|
-
for (const message of await this.readMessagesIn(this.paths.projectChannel(slug, "general"), null)) {
|
|
4260
|
-
if (message.task !== void 0) {
|
|
4261
|
-
summaries.set(message.task, message.body.replace(/^Thread closed[^\n]*\n\n/, ""));
|
|
4262
|
-
}
|
|
4263
|
-
}
|
|
4264
|
-
for (const file of await listFiles(this.paths.tasks(slug))) {
|
|
4265
|
-
const raw = await readMarkdown(path7.join(this.paths.tasks(slug), file), z16.record(z16.string(), z16.unknown()));
|
|
4266
|
-
const legacy = raw.data["thread"];
|
|
4267
|
-
if (legacy === void 0) {
|
|
4268
|
-
continue;
|
|
4269
|
-
}
|
|
4270
|
-
const task = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
|
|
4271
|
-
if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task.id))) {
|
|
4272
|
-
const ended = task.status === "done" || task.status === "abandoned";
|
|
4273
|
-
const open3 = legacy === "open" && !ended;
|
|
4274
|
-
const openedBy = opened.get(task.id);
|
|
4275
|
-
const closedBy = closed.get(task.id);
|
|
4276
|
-
await this.writeThread(threads, {
|
|
4277
|
-
id: task.id,
|
|
4278
|
-
channel: channelRef(slug, "general"),
|
|
4279
|
-
title: task.title,
|
|
4280
|
-
subject: { kind: "task", id: task.id },
|
|
4281
|
-
state: open3 ? "open" : "closed",
|
|
4282
|
-
openedBy: openedBy?.actor ?? task.createdBy,
|
|
4283
|
-
openedAt: openedBy?.ts ?? task.createdAt,
|
|
4284
|
-
...open3 ? {} : {
|
|
4285
|
-
closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
|
|
4286
|
-
closedAt: closedBy?.ts ?? this.now().toISOString()
|
|
4287
|
-
},
|
|
4288
|
-
body: summaries.get(task.id) ?? ""
|
|
4289
|
-
});
|
|
4290
|
-
}
|
|
4291
|
-
await this.writeTask(slug, task);
|
|
4292
|
-
}
|
|
4293
|
-
}
|
|
4294
|
-
}
|
|
4295
|
-
/**
|
|
4296
|
-
* Every channel of a scope was once read in its home conversation, so a member had read each up
|
|
4297
|
-
* to its scope's cursor. Each channel beside general starts from that cursor now, as each takes a
|
|
4298
|
-
* conversation of its own.
|
|
4299
|
-
*/
|
|
4300
|
-
async startChannelCursors() {
|
|
4301
|
-
for (const agent of await this.listAgents()) {
|
|
4302
|
-
const file = this.paths.agentCursors(agent.name);
|
|
4303
|
-
if (!await exists(file)) {
|
|
4304
|
-
continue;
|
|
4305
|
-
}
|
|
4306
|
-
const cursors = await readJson(file, CursorsSchema);
|
|
4307
|
-
const scopes = /* @__PURE__ */ new Set([
|
|
4308
|
-
...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
|
|
4309
|
-
...agent.memberships,
|
|
4310
|
-
SOCIETY_SCOPE
|
|
4311
|
-
]);
|
|
4312
|
-
const channelsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
|
|
4313
|
-
await writeJson(file, { ...cursors, channelsFrom });
|
|
4314
|
-
}
|
|
4315
|
-
}
|
|
4316
|
-
/**
|
|
4317
|
-
* A project's channels were once read, from the society scope, in the society's home
|
|
4318
|
-
* conversation, so a member had read each up to that cursor. Each project's conversation in the
|
|
4319
|
-
* society scope starts from it now, as each takes a conversation of its own.
|
|
4320
|
-
*/
|
|
4321
|
-
async startPlaceCursors() {
|
|
4322
|
-
for (const agent of await this.listAgents()) {
|
|
4323
|
-
const file = this.paths.agentCursors(agent.name);
|
|
4324
|
-
if (!await exists(file)) {
|
|
4325
|
-
continue;
|
|
4326
|
-
}
|
|
4327
|
-
const cursors = await readJson(file, CursorsSchema);
|
|
4328
|
-
await writeJson(file, {
|
|
4329
|
-
...cursors,
|
|
4330
|
-
placesFrom: { [SOCIETY_SCOPE]: cursorOf(cursors, SOCIETY_SCOPE) }
|
|
4331
|
-
});
|
|
3994
|
+
for (const { apply } of pending) {
|
|
3995
|
+
await apply();
|
|
4332
3996
|
}
|
|
4333
|
-
|
|
4334
|
-
/**
|
|
4335
|
-
* Threads were once read in the scope's single conversation, so a member had read every thread
|
|
4336
|
-
* up to its scope's cursor. Each scope's threads start from that cursor now, as each thread's
|
|
4337
|
-
* conversation takes a cursor of its own.
|
|
4338
|
-
*/
|
|
4339
|
-
async startThreadCursors() {
|
|
4340
|
-
for (const agent of await this.listAgents()) {
|
|
4341
|
-
const file = this.paths.agentCursors(agent.name);
|
|
4342
|
-
if (!await exists(file)) {
|
|
4343
|
-
continue;
|
|
4344
|
-
}
|
|
4345
|
-
const cursors = await readJson(file, CursorsSchema);
|
|
4346
|
-
const scopes = /* @__PURE__ */ new Set([
|
|
4347
|
-
...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
|
|
4348
|
-
...agent.memberships,
|
|
4349
|
-
SOCIETY_SCOPE
|
|
4350
|
-
]);
|
|
4351
|
-
const threadsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
|
|
4352
|
-
await writeJson(file, { ...cursors, threadsFrom });
|
|
4353
|
-
}
|
|
4354
|
-
}
|
|
4355
|
-
/**
|
|
4356
|
-
* The digest was once called the inbox: each cursor file keyed its cursor `inbox`, and the user
|
|
4357
|
-
* followed every society channel and project general channel to read one. The user takes no
|
|
4358
|
-
* turns, so it follows nothing now.
|
|
4359
|
-
*/
|
|
4360
|
-
async renameInboxCursors() {
|
|
4361
|
-
const Legacy = z16.object({ inbox: z16.string().nullable() });
|
|
4362
|
-
for (const agent of await this.listAgents()) {
|
|
4363
|
-
const file = this.paths.agentCursors(agent.name);
|
|
4364
|
-
if (await exists(file)) {
|
|
4365
|
-
const legacy = Legacy.safeParse(JSON.parse(await readFile4(file, "utf8")));
|
|
4366
|
-
if (legacy.success) {
|
|
4367
|
-
await writeJson(file, { digest: legacy.data.inbox });
|
|
4368
|
-
}
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
|
-
await this.updateAgent(USER_NAME, (user) => ({ ...user, subscriptions: [] }));
|
|
4372
|
-
await this.refreshMember(USER_NAME);
|
|
3997
|
+
await writeJson(file, { applied: [...applied, ...pending.map(({ name }) => name)] });
|
|
4373
3998
|
}
|
|
4374
3999
|
/** Writes every missing seed charter and aligns the existing ones with their seed. */
|
|
4375
4000
|
async ensureSeedRoles() {
|
|
@@ -5173,7 +4798,7 @@ ${DASHBOARD_LINE}
|
|
|
5173
4798
|
if (event.type === "turn.started") {
|
|
5174
4799
|
const start = TurnStartPayloadSchema.safeParse(event.payload);
|
|
5175
4800
|
if (start.success)
|
|
5176
|
-
starts.set(start.data.turnId
|
|
4801
|
+
starts.set(start.data.turnId, event.ts);
|
|
5177
4802
|
continue;
|
|
5178
4803
|
}
|
|
5179
4804
|
if (event.type !== "turn.completed" && event.type !== "turn.failed") {
|
|
@@ -5181,9 +4806,11 @@ ${DASHBOARD_LINE}
|
|
|
5181
4806
|
}
|
|
5182
4807
|
const parsed = TurnHistoryPayloadSchema.safeParse(event.payload);
|
|
5183
4808
|
if (parsed.success) {
|
|
5184
|
-
const
|
|
5185
|
-
const startedAt = starts.get(
|
|
5186
|
-
|
|
4809
|
+
const { turnId } = parsed.data;
|
|
4810
|
+
const startedAt = turnId === void 0 ? void 0 : starts.get(turnId);
|
|
4811
|
+
if (turnId !== void 0) {
|
|
4812
|
+
starts.delete(turnId);
|
|
4813
|
+
}
|
|
5187
4814
|
entries.push({
|
|
5188
4815
|
id: event.id,
|
|
5189
4816
|
ts: event.ts,
|
|
@@ -5418,7 +5045,7 @@ ${DASHBOARD_LINE}
|
|
|
5418
5045
|
const starts = [
|
|
5419
5046
|
...scopes.flatMap((each) => conversation === void 0 ? [
|
|
5420
5047
|
cursorOf(cursors, each),
|
|
5421
|
-
|
|
5048
|
+
cursors.digest,
|
|
5422
5049
|
channelStart(cursors, each),
|
|
5423
5050
|
placeStart(cursors, each)
|
|
5424
5051
|
] : [cursorOf(cursors, each, conversation.part)]),
|
|
@@ -6034,11 +5661,12 @@ ${current.body}`);
|
|
|
6034
5661
|
const args = VerbInputs.open_thread.parse(input);
|
|
6035
5662
|
await this.authorize(actor, "open_thread");
|
|
6036
5663
|
return this.mutex.run(async () => {
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
5664
|
+
await this.assertChannelOpen(args.channel);
|
|
5665
|
+
return this.openThreadUnlocked(actor.name, {
|
|
5666
|
+
id: this.newId(),
|
|
5667
|
+
channel: args.channel,
|
|
5668
|
+
title: args.title
|
|
5669
|
+
});
|
|
6042
5670
|
});
|
|
6043
5671
|
}
|
|
6044
5672
|
/** Writes a new open thread and its event. Callers hold the mutex and chose where it hangs. */
|
|
@@ -6073,28 +5701,13 @@ ${current.body}`);
|
|
|
6073
5701
|
subject: { kind: "proposal", id: proposal.id }
|
|
6074
5702
|
};
|
|
6075
5703
|
}
|
|
6076
|
-
/**
|
|
6077
|
-
|
|
6078
|
-
* threads gets one now, opened as its proposer, and a thread closed before the decision opens
|
|
6079
|
-
* again.
|
|
6080
|
-
*/
|
|
6081
|
-
async proposalThreadUnlocked(by, proposal) {
|
|
5704
|
+
/** The open thread of a proposal being decided, which `propose` opened with it. */
|
|
5705
|
+
async proposalThreadUnlocked(proposal) {
|
|
6082
5706
|
const found = await this.tryFindThread(proposal.id);
|
|
6083
|
-
if (found?.thread.state
|
|
6084
|
-
|
|
6085
|
-
}
|
|
6086
|
-
if (found === null) {
|
|
6087
|
-
return this.openThreadUnlocked(proposal.proposedBy, this.proposalThreadOpening(proposal));
|
|
5707
|
+
if (found?.thread.state !== "open") {
|
|
5708
|
+
throw new BoardError("INVALID_STATE", `proposal ${proposal.id} has no open thread`);
|
|
6088
5709
|
}
|
|
6089
|
-
|
|
6090
|
-
const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
|
|
6091
|
-
await this.events.append("thread.opened", by, {
|
|
6092
|
-
threadId: proposal.id,
|
|
6093
|
-
channel: reopened.channel,
|
|
6094
|
-
subject: reopened.subject ?? null,
|
|
6095
|
-
reopened: true
|
|
6096
|
-
});
|
|
6097
|
-
return reopened;
|
|
5710
|
+
return found.thread;
|
|
6098
5711
|
}
|
|
6099
5712
|
/** Where a task's thread hangs, what it is called, and what it is about. */
|
|
6100
5713
|
taskThreadOpening(task) {
|
|
@@ -6106,79 +5719,21 @@ ${current.body}`);
|
|
|
6106
5719
|
};
|
|
6107
5720
|
}
|
|
6108
5721
|
/**
|
|
6109
|
-
* The open thread of a task in play, for a verb about to post into it
|
|
6110
|
-
*
|
|
6111
|
-
* again; an ended task's thread stays closed.
|
|
5722
|
+
* The open thread of a task in play, for a verb about to post into it, which `createTask` opened
|
|
5723
|
+
* with the task; an ended task's thread closed with it.
|
|
6112
5724
|
*/
|
|
6113
|
-
async taskThreadUnlocked(
|
|
5725
|
+
async taskThreadUnlocked(task) {
|
|
6114
5726
|
const found = await this.tryFindThread(task.id);
|
|
6115
5727
|
if (found?.thread.state === "open") {
|
|
6116
5728
|
return found.thread;
|
|
6117
5729
|
}
|
|
6118
|
-
|
|
6119
|
-
throw new BoardError("INVALID_STATE", `task ${task.id} is ${task.status} and its thread closed with it`);
|
|
6120
|
-
}
|
|
6121
|
-
if (found === null) {
|
|
6122
|
-
return this.openThreadUnlocked(by, this.taskThreadOpening(task));
|
|
6123
|
-
}
|
|
6124
|
-
const { closedBy: _by, closedAt: _at, ...kept } = found.thread;
|
|
6125
|
-
const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
|
|
6126
|
-
await this.events.append("thread.opened", by, {
|
|
6127
|
-
threadId: task.id,
|
|
6128
|
-
channel: reopened.channel,
|
|
6129
|
-
subject: reopened.subject ?? null,
|
|
6130
|
-
reopened: true
|
|
6131
|
-
});
|
|
6132
|
-
return reopened;
|
|
5730
|
+
throw new BoardError("INVALID_STATE", task.status === "done" || task.status === "abandoned" ? `task ${task.id} is ${task.status} and its thread closed with it` : `task ${task.id} has no open thread`);
|
|
6133
5731
|
}
|
|
6134
5732
|
/** Posts what a task verb was told to say into the task's thread, as its author, with the step it recorded. */
|
|
6135
5733
|
async postTaskNote(author, task, body, step) {
|
|
6136
|
-
const thread = await this.taskThreadUnlocked(
|
|
5734
|
+
const thread = await this.taskThreadUnlocked(task);
|
|
6137
5735
|
return this.appendThreadMessage(author, thread.id, body, step === void 0 ? {} : { step });
|
|
6138
5736
|
}
|
|
6139
|
-
/** Where a new thread hangs, what it is called, and what it is about. */
|
|
6140
|
-
async threadToOpen(args) {
|
|
6141
|
-
if (args.task_id !== void 0 && args.proposal_id !== void 0) {
|
|
6142
|
-
throw new BoardError("VALIDATION", "a thread is about one task or one proposal, not both");
|
|
6143
|
-
}
|
|
6144
|
-
if (args.task_id !== void 0) {
|
|
6145
|
-
const { task, project } = await this.findTask(args.task_id);
|
|
6146
|
-
if (task.status === "done" || task.status === "abandoned") {
|
|
6147
|
-
throw new BoardError("INVALID_STATE", `task ${task.id} is ${task.status}`);
|
|
6148
|
-
}
|
|
6149
|
-
const channel = args.channel ?? channelRef(project, task.channel);
|
|
6150
|
-
if (parseChannelRef(channel).project !== project) {
|
|
6151
|
-
throw new BoardError("VALIDATION", `a task's thread hangs off a channel of its project, ${project}`);
|
|
6152
|
-
}
|
|
6153
|
-
await this.assertChannelOpen(channel);
|
|
6154
|
-
return {
|
|
6155
|
-
id: task.id,
|
|
6156
|
-
channel,
|
|
6157
|
-
title: args.title ?? task.title,
|
|
6158
|
-
subject: { kind: "task", id: task.id }
|
|
6159
|
-
};
|
|
6160
|
-
}
|
|
6161
|
-
if (args.proposal_id !== void 0) {
|
|
6162
|
-
const proposal = await this.readProposal(args.proposal_id);
|
|
6163
|
-
if (proposal.status !== "proposed") {
|
|
6164
|
-
throw new BoardError("INVALID_STATE", `proposal ${proposal.id} is already ${proposal.status}`);
|
|
6165
|
-
}
|
|
6166
|
-
const channel = args.channel ?? "governance";
|
|
6167
|
-
await this.assertChannelOpen(channel);
|
|
6168
|
-
const described = `${proposal.kind} proposal: ${describeCharter(proposal.kind, proposal.charter)}`;
|
|
6169
|
-
return {
|
|
6170
|
-
id: proposal.id,
|
|
6171
|
-
channel,
|
|
6172
|
-
title: args.title ?? described.slice(0, 200),
|
|
6173
|
-
subject: { kind: "proposal", id: proposal.id }
|
|
6174
|
-
};
|
|
6175
|
-
}
|
|
6176
|
-
if (args.channel === void 0 || args.title === void 0) {
|
|
6177
|
-
throw new BoardError("VALIDATION", "a thread needs a task_id, a proposal_id, or a channel and a title");
|
|
6178
|
-
}
|
|
6179
|
-
await this.assertChannelOpen(args.channel);
|
|
6180
|
-
return { id: this.newId(), channel: args.channel, title: args.title };
|
|
6181
|
-
}
|
|
6182
5737
|
/**
|
|
6183
5738
|
* Where the board learns which turns are in flight, from the server that hosts them, so it
|
|
6184
5739
|
* refuses to end a conversation under someone else's turn.
|
|
@@ -7312,6 +6867,7 @@ ${rationale}`}`);
|
|
|
7312
6867
|
needsUserDecision: parsed.status?.needsUserDecision ?? false,
|
|
7313
6868
|
error: parsed.error,
|
|
7314
6869
|
...parsed.resumeFailed === void 0 ? {} : { resumeFailed: parsed.resumeFailed },
|
|
6870
|
+
...parsed.transient === true ? { transient: true } : {},
|
|
7315
6871
|
...parsed.sessionDropped === true ? { sessionDropped: true } : {}
|
|
7316
6872
|
});
|
|
7317
6873
|
});
|
|
@@ -8019,15 +7575,9 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
8019
7575
|
case "archive":
|
|
8020
7576
|
await this.validateArchive(ArchiveProposalSchema.parse(charter).project);
|
|
8021
7577
|
return;
|
|
8022
|
-
case "reallocation":
|
|
8023
|
-
|
|
8024
|
-
if (parsed.success) {
|
|
8025
|
-
await this.reallocatedRole(parsed.data);
|
|
8026
|
-
} else {
|
|
8027
|
-
LegacyReallocationSchema.parse(charter);
|
|
8028
|
-
}
|
|
7578
|
+
case "reallocation":
|
|
7579
|
+
await this.reallocatedRole(ReallocationProposalSchema.parse(charter));
|
|
8029
7580
|
return;
|
|
8030
|
-
}
|
|
8031
7581
|
default:
|
|
8032
7582
|
return;
|
|
8033
7583
|
}
|
|
@@ -8087,11 +7637,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
8087
7637
|
return { project: input.project, members };
|
|
8088
7638
|
}
|
|
8089
7639
|
case "reallocation": {
|
|
8090
|
-
const
|
|
8091
|
-
if (!parsed.success) {
|
|
8092
|
-
return void 0;
|
|
8093
|
-
}
|
|
8094
|
-
const { next, changed } = await this.reallocatedRole(parsed.data);
|
|
7640
|
+
const { next, changed } = await this.reallocatedRole(ReallocationProposalSchema.parse(proposal.charter));
|
|
8095
7641
|
await this.writeRoleUnlocked(by, next, meta);
|
|
8096
7642
|
return { role: next.name, changed };
|
|
8097
7643
|
}
|
|
@@ -8101,8 +7647,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
8101
7647
|
}
|
|
8102
7648
|
/**
|
|
8103
7649
|
* Publishes a promoted skill under the society's skills, where every citizen's skills index lists
|
|
8104
|
-
* it: the folder the board took from its proposer's home
|
|
8105
|
-
*
|
|
7650
|
+
* it: the folder the board took from its proposer's home. A replacement is the whole skill, so
|
|
7651
|
+
* files the new version dropped go with the old one.
|
|
8106
7652
|
*/
|
|
8107
7653
|
async promoteSkillUnlocked(by, proposal, skill, meta) {
|
|
8108
7654
|
const dir = this.paths.societySkill(skill.name);
|
|
@@ -8110,16 +7656,12 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
8110
7656
|
const replaced = await exists(file);
|
|
8111
7657
|
const taken = proposal.skillFiles;
|
|
8112
7658
|
const staged = path7.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
|
|
8113
|
-
if (
|
|
7659
|
+
if (taken === void 0 || !await exists(staged)) {
|
|
8114
7660
|
throw new BoardError("INVALID_STATE", `proposal ${proposal.id} holds no skill files`);
|
|
8115
7661
|
}
|
|
8116
7662
|
await rm2(dir, { recursive: true, force: true });
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
} else {
|
|
8120
|
-
await ensureDir(dir);
|
|
8121
|
-
}
|
|
8122
|
-
const body = skill.body ?? (await readLooseMarkdown(file)).body;
|
|
7663
|
+
await cp(this.paths.proposalFiles(proposal.id), dir, { recursive: true });
|
|
7664
|
+
const { body } = await readLooseMarkdown(file);
|
|
8123
7665
|
await writeMarkdown(file, {
|
|
8124
7666
|
name: skill.name,
|
|
8125
7667
|
description: skill.summary,
|
|
@@ -8127,7 +7669,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
8127
7669
|
proposedBy: proposal.proposedBy,
|
|
8128
7670
|
promotedBy: by,
|
|
8129
7671
|
promotedAt: this.now().toISOString(),
|
|
8130
|
-
|
|
7672
|
+
commit: taken.commit
|
|
8131
7673
|
}
|
|
8132
7674
|
}, body);
|
|
8133
7675
|
await this.events.append("skill.promoted", by, {
|
|
@@ -8135,7 +7677,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
8135
7677
|
summary: skill.summary,
|
|
8136
7678
|
proposedBy: proposal.proposedBy,
|
|
8137
7679
|
replaced,
|
|
8138
|
-
|
|
7680
|
+
files: taken.files.length,
|
|
8139
7681
|
...meta
|
|
8140
7682
|
});
|
|
8141
7683
|
await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${skill.name} ${replaced ? "updated in" : "promoted to"} the society: ${sentence(skill.summary)}. Every citizen's skills index now lists it.`);
|
|
@@ -8824,6 +8366,7 @@ ${charter.purpose}
|
|
|
8824
8366
|
await this.validateProvision(filed.kind, filed.charter);
|
|
8825
8367
|
}
|
|
8826
8368
|
this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn) => turn.thread === proposalId);
|
|
8369
|
+
const thread = await this.proposalThreadUnlocked(filed);
|
|
8827
8370
|
if (outcome === "approved" && filed.kind === "archive") {
|
|
8828
8371
|
await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(filed.charter).project);
|
|
8829
8372
|
}
|
|
@@ -8865,7 +8408,6 @@ ${charter.purpose}
|
|
|
8865
8408
|
const verdict = outcome === "approved" ? "Approved" : "Rejected";
|
|
8866
8409
|
const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
|
|
8867
8410
|
const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
|
|
8868
|
-
const thread = await this.proposalThreadUnlocked(actor.name, proposal);
|
|
8869
8411
|
const post = `${verdict}: ${describeCharter(proposal.kind, proposal.charter)}.${why}${result}`;
|
|
8870
8412
|
await this.appendThreadMessage(actor.name, thread.id, actor.name === SYSTEM_ACTOR.name ? withoutMentions(post) : post, { step: { action: outcome } });
|
|
8871
8413
|
await this.endThreadOf(proposalId, actor.name, outcome);
|
|
@@ -9085,12 +8627,14 @@ var DEFAULT_TIMINGS = Object.freeze({
|
|
|
9085
8627
|
leaseSweepMs: 6e4,
|
|
9086
8628
|
opsIntervalMs: 5 * 6e4,
|
|
9087
8629
|
signalRepeatMs: 6 * 36e5,
|
|
8630
|
+
signalWakeMs: 24 * 36e5,
|
|
9088
8631
|
costReportMs: 36e5,
|
|
9089
8632
|
staleThreadMs: 24 * 36e5,
|
|
9090
8633
|
idleMemberMs: 3 * 24 * 36e5,
|
|
9091
8634
|
homeConflictMs: 24 * 36e5,
|
|
9092
8635
|
scaleCooldownMs: 36e5,
|
|
9093
|
-
reflectionMs: 24 * 36e5
|
|
8636
|
+
reflectionMs: 24 * 36e5,
|
|
8637
|
+
transientRetryMs: [6e4, 3 * 6e4, 10 * 6e4]
|
|
9094
8638
|
});
|
|
9095
8639
|
var SchedulerTimingsSchema = z17.object({
|
|
9096
8640
|
pollMs: z17.number().positive(),
|
|
@@ -9101,18 +8645,20 @@ var SchedulerTimingsSchema = z17.object({
|
|
|
9101
8645
|
leaseSweepMs: z17.number().positive(),
|
|
9102
8646
|
opsIntervalMs: z17.number().positive(),
|
|
9103
8647
|
signalRepeatMs: z17.number().positive(),
|
|
8648
|
+
signalWakeMs: z17.number().positive(),
|
|
9104
8649
|
costReportMs: z17.number().positive(),
|
|
9105
8650
|
staleThreadMs: z17.number().positive(),
|
|
9106
8651
|
idleMemberMs: z17.number().positive(),
|
|
9107
8652
|
homeConflictMs: z17.number().positive(),
|
|
9108
8653
|
scaleCooldownMs: z17.number().positive(),
|
|
9109
|
-
reflectionMs: z17.number().positive()
|
|
8654
|
+
reflectionMs: z17.number().positive(),
|
|
8655
|
+
transientRetryMs: z17.array(z17.number().positive())
|
|
9110
8656
|
}).partial();
|
|
9111
8657
|
function parseTimings(input) {
|
|
9112
8658
|
const parsed = SchedulerTimingsSchema.parse(input);
|
|
9113
8659
|
const overrides = {};
|
|
9114
8660
|
for (const [key, value] of Object.entries(parsed)) {
|
|
9115
|
-
if (
|
|
8661
|
+
if (value !== void 0) {
|
|
9116
8662
|
Object.assign(overrides, { [key]: value });
|
|
9117
8663
|
}
|
|
9118
8664
|
}
|
|
@@ -9136,6 +8682,11 @@ var StateSchema = z17.object({
|
|
|
9136
8682
|
/** Queued dispatches survive a restart; the cursor has already moved past the events that made them. */
|
|
9137
8683
|
pending: z17.record(z17.string(), PendingSchema).default({}),
|
|
9138
8684
|
opsReported: z17.record(z17.string(), z17.string()).default({}),
|
|
8685
|
+
/**
|
|
8686
|
+
* When each condition of a waking kind that still holds last woke the readers of signals, and
|
|
8687
|
+
* the level it woke at for the kinds that have one (`wakeLevel`); forgotten with the condition.
|
|
8688
|
+
*/
|
|
8689
|
+
opsWoken: z17.record(z17.string(), z17.object({ at: z17.string(), level: z17.number().optional() })).default({}),
|
|
9139
8690
|
releaseCounts: z17.record(z17.string(), z17.number().int().nonnegative()).default({}),
|
|
9140
8691
|
lastOps: z17.string().nullable().default(null),
|
|
9141
8692
|
lastScaled: z17.record(z17.string(), z17.string()).default({}),
|
|
@@ -9160,6 +8711,22 @@ var SILENT_LOG = { info() {
|
|
|
9160
8711
|
}, error() {
|
|
9161
8712
|
} };
|
|
9162
8713
|
var WAKING_SIGNALS = new Set(WAKING_SIGNAL_KINDS);
|
|
8714
|
+
var DAY_MS = 24 * 36e5;
|
|
8715
|
+
var IDLE_WAKE_STEPS_MS = [7 * DAY_MS, 14 * DAY_MS, 30 * DAY_MS];
|
|
8716
|
+
function wakeLevel(signal) {
|
|
8717
|
+
switch (signal.kind) {
|
|
8718
|
+
case "idle_member":
|
|
8719
|
+
return IDLE_WAKE_STEPS_MS.filter((step) => signal.value >= step).length;
|
|
8720
|
+
case "churn":
|
|
8721
|
+
return signal.value;
|
|
8722
|
+
default:
|
|
8723
|
+
return void 0;
|
|
8724
|
+
}
|
|
8725
|
+
}
|
|
8726
|
+
function wokenAt(signal, now) {
|
|
8727
|
+
const level = wakeLevel(signal);
|
|
8728
|
+
return { at: iso(now), ...level === void 0 ? {} : { level } };
|
|
8729
|
+
}
|
|
9163
8730
|
var HEARTBEAT_TRIGGER = "heartbeat";
|
|
9164
8731
|
var OPS_TRIGGER = "ops_event";
|
|
9165
8732
|
var USER_POST_TRIGGER = "user_post";
|
|
@@ -9216,6 +8783,8 @@ var Scheduler = class {
|
|
|
9216
8783
|
sweeping = /* @__PURE__ */ new Set();
|
|
9217
8784
|
/** Crons whose fire's turn is running; a cron's next time passes while one is. */
|
|
9218
8785
|
cronsInFlight = /* @__PURE__ */ new Set();
|
|
8786
|
+
/** Turns in a row the model service turned away, by session key, which `retryRefused` counts. */
|
|
8787
|
+
refused = /* @__PURE__ */ new Map();
|
|
9219
8788
|
state = StateSchema.parse({
|
|
9220
8789
|
cursor: null,
|
|
9221
8790
|
lastHeartbeat: {}
|
|
@@ -9428,10 +8997,6 @@ var Scheduler = class {
|
|
|
9428
8997
|
await this.wakeCreator(taskId, project, creator, event.actor, now);
|
|
9429
8998
|
}
|
|
9430
8999
|
await this.wakeDependents(taskId, event.actor, now);
|
|
9431
|
-
const threaded = await this.board.readThread(taskId).then(() => true, () => false);
|
|
9432
|
-
if (!threaded) {
|
|
9433
|
-
this.sweep({ kind: "thread", id: taskId });
|
|
9434
|
-
}
|
|
9435
9000
|
return;
|
|
9436
9001
|
}
|
|
9437
9002
|
case "thread.closed": {
|
|
@@ -9591,7 +9156,7 @@ var Scheduler = class {
|
|
|
9591
9156
|
}
|
|
9592
9157
|
case "ops.signal": {
|
|
9593
9158
|
const parsed = OpsSignalSchema.safeParse(payload);
|
|
9594
|
-
if (!parsed.success || !
|
|
9159
|
+
if (!parsed.success || !signalWakes(parsed.data)) {
|
|
9595
9160
|
return;
|
|
9596
9161
|
}
|
|
9597
9162
|
await this.wakeSignalReaders(parsed.data, event.actor, now);
|
|
@@ -10302,25 +9867,56 @@ var Scheduler = class {
|
|
|
10302
9867
|
// ---------------------------------------------------------------------------------------------
|
|
10303
9868
|
// Operations: signals from state, the scaling rule, and the spend report
|
|
10304
9869
|
// ---------------------------------------------------------------------------------------------
|
|
9870
|
+
/**
|
|
9871
|
+
* Logs each condition when it is new and again every `signalRepeatMs` while it holds, and marks
|
|
9872
|
+
* each log with whether it wakes the readers of signals (`wakesNow`), so a repeat only logs.
|
|
9873
|
+
*/
|
|
10305
9874
|
async runOperations(now) {
|
|
10306
9875
|
const signals = await this.collectSignals(now);
|
|
10307
9876
|
const current = new Set(signals.map((signal) => signal.key));
|
|
10308
|
-
for (const
|
|
10309
|
-
|
|
10310
|
-
|
|
9877
|
+
for (const record of [this.state.opsReported, this.state.opsWoken]) {
|
|
9878
|
+
for (const key of Object.keys(record)) {
|
|
9879
|
+
if (!current.has(key)) {
|
|
9880
|
+
delete record[key];
|
|
9881
|
+
}
|
|
10311
9882
|
}
|
|
10312
9883
|
}
|
|
10313
9884
|
for (const signal of signals) {
|
|
10314
9885
|
const reported = this.state.opsReported[signal.key];
|
|
10315
|
-
|
|
9886
|
+
const wakes = this.wakesNow(signal, reported !== void 0, now);
|
|
9887
|
+
if (!wakes && reported !== void 0 && now - Date.parse(reported) < this.timings.signalRepeatMs) {
|
|
10316
9888
|
continue;
|
|
10317
9889
|
}
|
|
10318
|
-
await this.board.publishSignal(signal);
|
|
9890
|
+
await this.board.publishSignal({ ...signal, wakes });
|
|
10319
9891
|
this.state.opsReported[signal.key] = iso(now);
|
|
9892
|
+
if (wakes) {
|
|
9893
|
+
this.state.opsWoken[signal.key] = wokenAt(signal, now);
|
|
9894
|
+
}
|
|
10320
9895
|
}
|
|
10321
9896
|
await this.scaleRoles(now);
|
|
10322
9897
|
await this.reportCost(now);
|
|
10323
9898
|
}
|
|
9899
|
+
/**
|
|
9900
|
+
* Whether a condition wakes the readers of signals at this pass, which only a waking kind does:
|
|
9901
|
+
* when it is new; for an idle member or churn, when it has risen past the level it last woke at
|
|
9902
|
+
* (`wakeLevel`); for the other kinds, once its last wake is `signalWakeMs` old. Checked on every
|
|
9903
|
+
* pass, so a step wakes when it is crossed, between repeats.
|
|
9904
|
+
*/
|
|
9905
|
+
wakesNow(signal, held, now) {
|
|
9906
|
+
if (!WAKING_SIGNALS.has(signal.kind)) {
|
|
9907
|
+
return false;
|
|
9908
|
+
}
|
|
9909
|
+
const last = this.state.opsWoken[signal.key];
|
|
9910
|
+
if (last === void 0) {
|
|
9911
|
+
if (held) {
|
|
9912
|
+
this.state.opsWoken[signal.key] = wokenAt(signal, now);
|
|
9913
|
+
return false;
|
|
9914
|
+
}
|
|
9915
|
+
return true;
|
|
9916
|
+
}
|
|
9917
|
+
const level = wakeLevel(signal);
|
|
9918
|
+
return level === void 0 ? now - Date.parse(last.at) >= this.timings.signalWakeMs : level > (last.level ?? 0);
|
|
9919
|
+
}
|
|
10324
9920
|
/** Every operations condition that holds right now, from counters and timers only. */
|
|
10325
9921
|
async collectSignals(now) {
|
|
10326
9922
|
const signals = [];
|
|
@@ -10637,6 +10233,7 @@ var Scheduler = class {
|
|
|
10637
10233
|
exitReason: record.exitReason,
|
|
10638
10234
|
costUsd: record.costUsd
|
|
10639
10235
|
}, "turn finished");
|
|
10236
|
+
await this.retryRefused(key, { ...item, dispatch }, record);
|
|
10640
10237
|
} catch (error) {
|
|
10641
10238
|
this.log.error({ agent: dispatch.agent, project: dispatch.project, error: String(error) }, "turn crashed");
|
|
10642
10239
|
} finally {
|
|
@@ -10655,6 +10252,48 @@ var Scheduler = class {
|
|
|
10655
10252
|
}
|
|
10656
10253
|
}
|
|
10657
10254
|
}
|
|
10255
|
+
/**
|
|
10256
|
+
* A turn the model service turned away for now, overloaded or failing on its side, is sent again:
|
|
10257
|
+
* the same wake in the same conversation, after the next delay of `transientRetryMs`, with every
|
|
10258
|
+
* check a queued wake gets at dispatch. A wake already queued there, as a heartbeat's or a new
|
|
10259
|
+
* post's, runs the conversation instead. Once the delays are spent the heartbeat takes over, and
|
|
10260
|
+
* a `turn_refused` signal, which wakes nobody, says so. A turn that ends any other way ends the run.
|
|
10261
|
+
*/
|
|
10262
|
+
async retryRefused(key, item, record) {
|
|
10263
|
+
if (record.transient !== true || record.exitReason !== "error") {
|
|
10264
|
+
this.refused.delete(key);
|
|
10265
|
+
return;
|
|
10266
|
+
}
|
|
10267
|
+
const refused = (this.refused.get(key) ?? 0) + 1;
|
|
10268
|
+
this.refused.set(key, refused);
|
|
10269
|
+
const { dispatch } = item;
|
|
10270
|
+
const delay = this.timings.transientRetryMs[refused - 1];
|
|
10271
|
+
if (delay !== void 0) {
|
|
10272
|
+
if (this.pending.has(key)) {
|
|
10273
|
+
return;
|
|
10274
|
+
}
|
|
10275
|
+
const { steeredThrough: _steered, ...retry } = item;
|
|
10276
|
+
this.pending.set(key, { ...retry, readyAt: this.now().getTime() + delay });
|
|
10277
|
+
this.log.info({ agent: dispatch.agent, project: dispatch.project, refused, delayMs: delay }, "the model service turned the turn away, sending its wake again");
|
|
10278
|
+
return;
|
|
10279
|
+
}
|
|
10280
|
+
if (refused !== this.timings.transientRetryMs.length + 1) {
|
|
10281
|
+
return;
|
|
10282
|
+
}
|
|
10283
|
+
const agent = await this.tryReadAgent(dispatch.agent);
|
|
10284
|
+
const where = dispatch.thread !== void 0 ? `thread ${dispatch.thread.id}` : dispatch.channel !== void 0 ? `channel ${dispatch.channel}` : dispatch.place !== void 0 ? `project ${dispatch.place}'s channels` : "its home";
|
|
10285
|
+
await this.board.publishSignal({
|
|
10286
|
+
kind: "turn_refused",
|
|
10287
|
+
key: `turn_refused:${key}`,
|
|
10288
|
+
summary: `${dispatch.agent}'s turns in ${where} were turned away by the model service ${refused} times in a row${record.model === null ? "" : ` on ${record.model}`}, the last with "${record.error ?? "no error"}"; its heartbeat takes over`,
|
|
10289
|
+
value: refused,
|
|
10290
|
+
threshold: refused,
|
|
10291
|
+
agent: dispatch.agent,
|
|
10292
|
+
...agent === null ? {} : { role: agent.role },
|
|
10293
|
+
...dispatch.project === SOCIETY_SCOPE ? {} : { project: dispatch.project },
|
|
10294
|
+
...dispatch.thread?.task === true ? { taskId: dispatch.thread.id } : {}
|
|
10295
|
+
});
|
|
10296
|
+
}
|
|
10658
10297
|
/**
|
|
10659
10298
|
* Whether a ready turn waits for another of its citizen's turns that must run alone: its
|
|
10660
10299
|
* onboarding in the project, queued or running, which comes before any other work of its there;
|
|
@@ -10773,7 +10412,7 @@ var GOVERNANCE = [
|
|
|
10773
10412
|
' - retirement: {"agent", "reason"}',
|
|
10774
10413
|
' - archive: {"project", "reason"}: approval archives the project: its members leave, its open threads close, and nothing more is posted, filed, or joined there, while its files and history stay; refused while a task there is in play',
|
|
10775
10414
|
` - reallocation: {"role", "changes": {"maxReplicas"?, "backlogThreshold"?, "wakeTriggers"?, "verbs"?}, "description"}: approval applies the changes to the existing role, each the field's new value and verbs and wakeTriggers whole lists, for its members from their next turn; refused for a role that does not exist or that has those values already. Only the user decides one that changes verbs or maxReplicas`,
|
|
10776
|
-
' - skill: {"name", "summary"}: the folder skills/<name>/ in your home, SKILL.md and every file beside it, which the board takes as it stands when this turn ends into society/proposals/<id>/, where whoever decides reads it; a proposal whose folder has no SKILL.md then is rejected by the board. Approval publishes it under society/skills/<name>/, where every citizen\'s skills index lists it until the steward or the user removes it with `remove_skill`; ask the steward to remove one that is wrong or superseded. Once yours is approved, delete skills/<name>/ from your home and report memoryUpdated: the board leaves your copy there, and your index would list it beside the published one and follow it as it drifts from what was reviewed. To revise a published skill, copy its folder home, edit it, and propose it again.
|
|
10415
|
+
' - skill: {"name", "summary"}: the folder skills/<name>/ in your home, SKILL.md and every file beside it, which the board takes as it stands when this turn ends into society/proposals/<id>/, where whoever decides reads it; a proposal whose folder has no SKILL.md then is rejected by the board. Approval publishes it under society/skills/<name>/, where every citizen\'s skills index lists it until the steward or the user removes it with `remove_skill`; ask the steward to remove one that is wrong or superseded. Once yours is approved, delete skills/<name>/ from your home and report memoryUpdated: the board leaves your copy there, and your index would list it beside the published one and follow it as it drifts from what was reviewed. To revise a published skill, copy its folder home, edit it, and propose it again.',
|
|
10777
10416
|
"- Projects and membership: a project is a lasting area of work, not one request; `create_project` opens one with its general channel (front desk and user), and `configure_project` sets its completion effect, its display name, or its default branch, never its slug (user, steward, concierge). A workstream within a project or the society, such as a release, gets a channel of its own: `create_channel` opens it, which its opener follows, and announces it in the place's general, where members subscribe when it concerns them, `create_task` with `channel` files the workstream's tasks there, and `archive_channel` ends it once none of its tasks is in play, leaving its history readable (user, steward, concierge; others propose a channel). A project whose work is finished or has moved to another project is archived by proposal, once no task there is in play. `join_project` and `leave_project` move yourself, or another citizen when you are the concierge, the steward, or the user; joining gives the pair a worktree and an onboarding turn. A society role, one that keeps watch over the whole society as the concierge and the steward do, is never in a project: it moves others, never itself.",
|
|
10778
10417
|
"- Prefer scaling an existing role over inventing one, with a reallocation of its replicas, backlog threshold, wakes, or verbs; a new role is justified by work a project needs that no existing role covers. A role that needs a tool the board lacks is an engineering task, not a hiring request.",
|
|
10779
10418
|
"- Operations signals are counters and timers the board logs, listed in the prompt of a role that reads them: stages waiting for a holder, backlog per role, stages waiting on a role nobody fills, churn, stale threads, idle members, missing capabilities, replicas added, spend. They are not posts in any channel; interpreting them is your judgment."
|
|
@@ -11083,7 +10722,7 @@ function buildTurnPrompt(input) {
|
|
|
11083
10722
|
if (signals.length === 0) {
|
|
11084
10723
|
lines.push("None since your last turn here.");
|
|
11085
10724
|
} else {
|
|
11086
|
-
lines.push("Counters and timers the board logged since your last turn here, oldest first. A condition that persists is logged again every few hours; one that clears is not logged.", "");
|
|
10725
|
+
lines.push("Counters and timers the board logged since your last turn here, each condition once at its latest log, oldest first, with the hourly spend reports summed into one line. A condition that persists is logged again every few hours; one that clears is not logged.", "");
|
|
11087
10726
|
for (const { ts, signal } of signals) {
|
|
11088
10727
|
lines.push(signalLine(ts, signal));
|
|
11089
10728
|
}
|
|
@@ -11147,6 +10786,43 @@ function stuckNotice(stuck) {
|
|
|
11147
10786
|
return `${stuck.agent}'s worktree for this task holds commits that \`${stuck.branch}\` lacks and that the runner cannot fast-forward it to, because they no longer build on its tip or another worktree's commits build on it too: ${commits}. Until someone puts them on the branch, it lacks them, and so does what lands from it; any worktree of the project's clone can cherry-pick or merge them by id.`;
|
|
11148
10787
|
}
|
|
11149
10788
|
var MAX_SIGNALS = 30;
|
|
10789
|
+
var SIGNAL_READ = 500;
|
|
10790
|
+
var REPORT_TURNS = /^(\d+) turn\(s\) completed /;
|
|
10791
|
+
function sumSpend(records, partial) {
|
|
10792
|
+
const reports = records.filter((record) => record.signal.kind === "turn_cost");
|
|
10793
|
+
const last = reports.at(-1);
|
|
10794
|
+
if (last === void 0 || reports.length === 1 && !partial) {
|
|
10795
|
+
return [...records];
|
|
10796
|
+
}
|
|
10797
|
+
let cost = 0;
|
|
10798
|
+
let turns2 = 0;
|
|
10799
|
+
let dearest = last;
|
|
10800
|
+
for (const report of reports) {
|
|
10801
|
+
cost += report.signal.value;
|
|
10802
|
+
const counted = REPORT_TURNS.exec(report.signal.summary)?.[1];
|
|
10803
|
+
turns2 = turns2 === null || counted === void 0 ? null : turns2 + Number(counted);
|
|
10804
|
+
if (report.signal.value > dearest.signal.value) {
|
|
10805
|
+
dearest = report;
|
|
10806
|
+
}
|
|
10807
|
+
}
|
|
10808
|
+
const first = reports[0] ?? last;
|
|
10809
|
+
const one = reports.length === 1;
|
|
10810
|
+
const summary = [
|
|
10811
|
+
`${turns2 === null ? "turns" : `${turns2} turn(s)`} completed for $${cost.toFixed(2)} in ${reports.length} hourly report(s) logged ${one ? `at ${last.ts}` : `from ${first.ts} to ${last.ts}`}`,
|
|
10812
|
+
...one ? [] : [`the dearest, logged at ${dearest.ts}, was $${dearest.signal.value.toFixed(2)}`],
|
|
10813
|
+
...partial ? [`the read holds the newest ${SIGNAL_READ} signals, so these are lower bounds`] : []
|
|
10814
|
+
].join("; ");
|
|
10815
|
+
const summed = { ...last, signal: { ...last.signal, summary, value: cost } };
|
|
10816
|
+
return records.flatMap((record) => record === last ? [summed] : record.signal.kind === "turn_cost" ? [] : [record]);
|
|
10817
|
+
}
|
|
10818
|
+
function latestPerKey(records) {
|
|
10819
|
+
const latest = /* @__PURE__ */ new Map();
|
|
10820
|
+
for (const record of records) {
|
|
10821
|
+
latest.delete(record.signal.key);
|
|
10822
|
+
latest.set(record.signal.key, record);
|
|
10823
|
+
}
|
|
10824
|
+
return [...latest.values()];
|
|
10825
|
+
}
|
|
11150
10826
|
var TurnHost = class {
|
|
11151
10827
|
board;
|
|
11152
10828
|
mcpUrl;
|
|
@@ -11284,7 +10960,7 @@ var TurnHost = class {
|
|
|
11284
10960
|
dir: `${PATH_TOKENS.board}/projects/${dispatch.project}/knowledge`,
|
|
11285
10961
|
topics: await this.board.listKnowledge(dispatch.project)
|
|
11286
10962
|
};
|
|
11287
|
-
const signals = part === void 0 && charter.wakeTriggers.includes(OPS_TRIGGER2) ?
|
|
10963
|
+
const signals = part === void 0 && charter.wakeTriggers.includes(OPS_TRIGGER2) ? await this.signalsSince(agent, dispatch.project, lastTurn) : null;
|
|
11288
10964
|
const waiting = task !== null && task.status === "open" && !task.completing && mayHoldStage(actor, task) ? [task] : [];
|
|
11289
10965
|
const threads = /* @__PURE__ */ new Map();
|
|
11290
10966
|
for (const id of new Set(messages.flatMap((message) => message.thread ?? []))) {
|
|
@@ -11559,6 +11235,7 @@ var TurnHost = class {
|
|
|
11559
11235
|
...outcome.work === null ? {} : { work: outcome.work },
|
|
11560
11236
|
...outcome.exitReason === "stopped" ? { stoppedBy: turn.stopRequestedBy ?? USER_NAME } : {},
|
|
11561
11237
|
...outcome.resumeFailed === void 0 ? {} : { resumeFailed: outcome.resumeFailed },
|
|
11238
|
+
...outcome.transient === true ? { transient: true } : {},
|
|
11562
11239
|
...resumeRetries === void 0 ? {} : { resumeRetries },
|
|
11563
11240
|
...dropSession ? { sessionDropped: true } : {}
|
|
11564
11241
|
};
|
|
@@ -11738,6 +11415,17 @@ var TurnHost = class {
|
|
|
11738
11415
|
this.residentTokens.set(slot, token);
|
|
11739
11416
|
return token;
|
|
11740
11417
|
}
|
|
11418
|
+
/**
|
|
11419
|
+
* The signals a reader's prompt lists: those logged for its scope since its last turn there,
|
|
11420
|
+
* the spend reports summed, then each condition once at its latest log, the newest kept.
|
|
11421
|
+
*/
|
|
11422
|
+
async signalsSince(agent, scope, lastTurn) {
|
|
11423
|
+
const read2 = await this.board.listSignals(SIGNAL_READ + 1);
|
|
11424
|
+
const past = read2.length > SIGNAL_READ ? read2[0] : void 0;
|
|
11425
|
+
const partial = past !== void 0 && (lastTurn === null || past.ts > lastTurn.startedAt);
|
|
11426
|
+
const since = read2.slice(-SIGNAL_READ).filter((record) => wakeScope(agent, record.signal.project ?? null) === scope && (lastTurn === null || record.ts > lastTurn.startedAt));
|
|
11427
|
+
return latestPerKey(sumSpend(since, partial)).slice(-MAX_SIGNALS);
|
|
11428
|
+
}
|
|
11741
11429
|
/**
|
|
11742
11430
|
* Where a conversation works: a task's on its branch, which its stage's holder has to itself,
|
|
11743
11431
|
* any other thread's, a channel's, and a project's channels' seen from the society scope in a
|
|
@@ -12625,6 +12313,8 @@ import pino from "pino";
|
|
|
12625
12313
|
// packages/board-mcp/dist/index.js
|
|
12626
12314
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12627
12315
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
12316
|
+
var MAX_TOOL_INPUT_ELEMENTS = 1e3;
|
|
12317
|
+
var MAX_REQUEST_BODY_SIZE = 1024 * 1024;
|
|
12628
12318
|
var BOARD_TOOLS = VERB_NAMES.map((name) => ({
|
|
12629
12319
|
name,
|
|
12630
12320
|
description: VERB_DESCRIPTIONS[name],
|
|
@@ -12647,7 +12337,7 @@ function describeError(error) {
|
|
|
12647
12337
|
return String(error);
|
|
12648
12338
|
}
|
|
12649
12339
|
async function createBoardMcpServer(board2, actor, version) {
|
|
12650
|
-
const server2 = new McpServer({ name: "stellaris-board", version });
|
|
12340
|
+
const server2 = new McpServer({ name: "stellaris-board", version }, { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS });
|
|
12651
12341
|
const charter = await board2.readRole(actor.role);
|
|
12652
12342
|
for (const tool of toolsForRole(charter)) {
|
|
12653
12343
|
server2.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, async (args) => {
|
|
@@ -12668,7 +12358,10 @@ async function createBoardMcpServer(board2, actor, version) {
|
|
|
12668
12358
|
}
|
|
12669
12359
|
async function handleMcpRequest(board2, actor, request, version) {
|
|
12670
12360
|
const server2 = await createBoardMcpServer(board2, actor, version);
|
|
12671
|
-
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
12361
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
12362
|
+
enableJsonResponse: true,
|
|
12363
|
+
maxRequestBodySize: MAX_REQUEST_BODY_SIZE
|
|
12364
|
+
});
|
|
12672
12365
|
await server2.connect(transport);
|
|
12673
12366
|
try {
|
|
12674
12367
|
return await transport.handleRequest(request, {
|
|
@@ -12685,6 +12378,25 @@ import { compress } from "hono/compress";
|
|
|
12685
12378
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
12686
12379
|
import { z as z19, ZodError } from "zod";
|
|
12687
12380
|
|
|
12381
|
+
// apps/server/dist/body-limit.js
|
|
12382
|
+
import { bodyLimit } from "hono/body-limit";
|
|
12383
|
+
var ENROLL_BODY_LIMIT_BYTES = 16 * 1024;
|
|
12384
|
+
var API_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
12385
|
+
var RUNNER_BODY_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
12386
|
+
var RUNNER_ANSWER_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
12387
|
+
function limitBody(maxSize) {
|
|
12388
|
+
return bodyLimit({
|
|
12389
|
+
maxSize,
|
|
12390
|
+
onError: (c) => {
|
|
12391
|
+
c.header("connection", "close");
|
|
12392
|
+
return c.json({
|
|
12393
|
+
error: "PAYLOAD_TOO_LARGE",
|
|
12394
|
+
message: `the request body is over the ${maxSize} bytes this route takes`
|
|
12395
|
+
}, 413);
|
|
12396
|
+
}
|
|
12397
|
+
});
|
|
12398
|
+
}
|
|
12399
|
+
|
|
12688
12400
|
// apps/server/dist/github-auth.js
|
|
12689
12401
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
12690
12402
|
import { Hono } from "hono";
|
|
@@ -12706,9 +12418,21 @@ function githubAvatar(url) {
|
|
|
12706
12418
|
return void 0;
|
|
12707
12419
|
}
|
|
12708
12420
|
}
|
|
12421
|
+
var BOARD_ORIGIN = "https://board.invalid";
|
|
12709
12422
|
function safeNext(next) {
|
|
12710
|
-
|
|
12423
|
+
if (next === void 0 || !next.startsWith("/")) {
|
|
12424
|
+
return "/";
|
|
12425
|
+
}
|
|
12426
|
+
let url;
|
|
12427
|
+
try {
|
|
12428
|
+
url = new URL(next, BOARD_ORIGIN);
|
|
12429
|
+
} catch {
|
|
12430
|
+
return "/";
|
|
12431
|
+
}
|
|
12432
|
+
const path11 = `${url.pathname}${url.search}`;
|
|
12433
|
+
return url.origin === BOARD_ORIGIN && /^\/(?![/\\])/.test(path11) ? path11 : "/";
|
|
12711
12434
|
}
|
|
12435
|
+
var APP_LANDING = "stellaris://signin";
|
|
12712
12436
|
function land(c, next, fragment) {
|
|
12713
12437
|
return c.redirect(`${next}#${fragment}`, 302);
|
|
12714
12438
|
}
|
|
@@ -12729,7 +12453,8 @@ function githubAuthRoutes(options) {
|
|
|
12729
12453
|
}
|
|
12730
12454
|
}
|
|
12731
12455
|
const state = randomBytes2(24).toString("base64url");
|
|
12732
|
-
|
|
12456
|
+
const next = c.req.query("landing") === "app" ? APP_LANDING : safeNext(c.req.query("next"));
|
|
12457
|
+
states.set(state, { next, expiresAt: now() + STATE_TTL_MS });
|
|
12733
12458
|
const authorize = new URL("https://github.com/login/oauth/authorize");
|
|
12734
12459
|
authorize.searchParams.set("client_id", github.clientId);
|
|
12735
12460
|
authorize.searchParams.set("state", state);
|
|
@@ -12992,8 +12717,9 @@ function bearer(c) {
|
|
|
12992
12717
|
function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
12993
12718
|
const routes = new Hono2();
|
|
12994
12719
|
if (enrollments !== void 0) {
|
|
12995
|
-
|
|
12996
|
-
routes.post("/enroll
|
|
12720
|
+
const enrollBody = limitBody(ENROLL_BODY_LIMIT_BYTES);
|
|
12721
|
+
routes.post("/enroll", enrollLimit ?? ((_c, next) => next()), enrollBody, async (c) => c.json(enrollments.open(RunnerEnrollRequestSchema.parse(await c.req.json()))));
|
|
12722
|
+
routes.post("/enroll/poll", enrollBody, async (c) => {
|
|
12997
12723
|
const { deviceCode } = RunnerEnrollPollSchema.parse(await c.req.json());
|
|
12998
12724
|
const status = enrollments.poll(deviceCode);
|
|
12999
12725
|
return status === null ? c.json({ error: "NOT_FOUND", message: "no such enrollment; it may have expired" }, 404) : c.json(status);
|
|
@@ -13008,7 +12734,8 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
13008
12734
|
c.set("runner", runner);
|
|
13009
12735
|
await next();
|
|
13010
12736
|
});
|
|
13011
|
-
|
|
12737
|
+
const body = limitBody(RUNNER_BODY_LIMIT_BYTES);
|
|
12738
|
+
routes.post("/hello", body, async (c) => c.json(await hub.register(c.get("runner"), RunnerHelloSchema.parse(await c.req.json()))));
|
|
13012
12739
|
routes.get("/stream", (c) => {
|
|
13013
12740
|
const runner = c.get("runner");
|
|
13014
12741
|
return streamSSE(c, async (stream) => {
|
|
@@ -13046,7 +12773,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
13046
12773
|
}
|
|
13047
12774
|
});
|
|
13048
12775
|
});
|
|
13049
|
-
routes.post("/turns/:id/events", async (c) => {
|
|
12776
|
+
routes.post("/turns/:id/events", body, async (c) => {
|
|
13050
12777
|
const { entries } = TurnEventsSchema.parse(await c.req.json());
|
|
13051
12778
|
await hub.turnEvents(c.get("runner"), UlidSchema.parse(c.req.param("id")), entries);
|
|
13052
12779
|
return c.json({ ok: true });
|
|
@@ -13055,21 +12782,21 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
13055
12782
|
hub.received(c.get("runner"), UlidSchema.parse(c.req.param("id")));
|
|
13056
12783
|
return c.json({ ok: true });
|
|
13057
12784
|
});
|
|
13058
|
-
routes.post("/turns/:id/outcome", async (c) => c.json(await hub.turnOutcome(c.get("runner"), UlidSchema.parse(c.req.param("id")), TurnOutcomeSchema.parse(await c.req.json()))));
|
|
13059
|
-
routes.post("/requests/:id", async (c) => {
|
|
12785
|
+
routes.post("/turns/:id/outcome", body, async (c) => c.json(await hub.turnOutcome(c.get("runner"), UlidSchema.parse(c.req.param("id")), TurnOutcomeSchema.parse(await c.req.json()))));
|
|
12786
|
+
routes.post("/requests/:id", limitBody(RUNNER_ANSWER_LIMIT_BYTES), async (c) => {
|
|
13060
12787
|
hub.answer(c.get("runner"), c.req.param("id"), RunnerAnswerSchema.parse(await c.req.json()));
|
|
13061
12788
|
return c.json({ ok: true });
|
|
13062
12789
|
});
|
|
13063
|
-
routes.post("/workspaces", async (c) => {
|
|
12790
|
+
routes.post("/workspaces", body, async (c) => {
|
|
13064
12791
|
hub.heldWorkspaces(c.get("runner"), HeldWorkspacesSchema.parse(await c.req.json()).workspaces);
|
|
13065
12792
|
return c.json({ ok: true });
|
|
13066
12793
|
});
|
|
13067
|
-
routes.post("/warm", async (c) => {
|
|
12794
|
+
routes.post("/warm", body, async (c) => {
|
|
13068
12795
|
hub.warmChanged(c.get("runner"), WarmSessionsSchema.parse(await c.req.json()).warm);
|
|
13069
12796
|
return c.json({ ok: true });
|
|
13070
12797
|
});
|
|
13071
12798
|
routes.get("/board/manifest", async (c) => c.json({ files: await board2.boardManifest() }));
|
|
13072
|
-
routes.post("/board/read", async (c) => c.json({ files: await board2.readBoardFiles(FileReadSchema.parse(await c.req.json()).paths) }));
|
|
12799
|
+
routes.post("/board/read", body, async (c) => c.json({ files: await board2.readBoardFiles(FileReadSchema.parse(await c.req.json()).paths) }));
|
|
13073
12800
|
const home = new Hono2();
|
|
13074
12801
|
home.use("*", async (c, next) => {
|
|
13075
12802
|
const agent = NameSchema.parse(c.req.param("agent"));
|
|
@@ -13221,6 +12948,7 @@ function createApp(deps) {
|
|
|
13221
12948
|
};
|
|
13222
12949
|
const api = new Hono3();
|
|
13223
12950
|
api.use("*", authenticate);
|
|
12951
|
+
api.on(["POST", "PUT", "PATCH", "DELETE"], "*", limitBody(API_BODY_LIMIT_BYTES));
|
|
13224
12952
|
api.get("/me", (c) => {
|
|
13225
12953
|
const signIn = board2.signInOf(bearer2(c) ?? "");
|
|
13226
12954
|
return c.json(signIn === null ? c.get("actor") : {
|
|
@@ -13370,14 +13098,23 @@ function createApp(deps) {
|
|
|
13370
13098
|
api.get("/projects/:slug/tasks", async (c) => c.json(await board2.listTasks(c.req.param("slug"))));
|
|
13371
13099
|
api.get("/projects/:slug/dashboard", async (c) => c.json(await board2.readDashboard(c.req.param("slug"))));
|
|
13372
13100
|
api.get("/projects/:slug/knowledge", async (c) => c.json(await board2.listKnowledge(c.req.param("slug"))));
|
|
13101
|
+
const branchFile = async (c, project, branch, file) => {
|
|
13102
|
+
if (deps.runners === void 0) {
|
|
13103
|
+
throw new BoardError("NOT_FOUND", "this board has no runners to read files from");
|
|
13104
|
+
}
|
|
13105
|
+
return c.json(await deps.runners.readBranch(project, branch, BranchPathSchema.parse(file)));
|
|
13106
|
+
};
|
|
13107
|
+
const projectFile = async (c, file) => {
|
|
13108
|
+
const project = await board2.readProject(NameSchema.parse(c.req.param("slug")));
|
|
13109
|
+
return branchFile(c, project.slug, project.defaultBranch, file);
|
|
13110
|
+
};
|
|
13111
|
+
api.get("/projects/:slug/files", (c) => projectFile(c, ""));
|
|
13112
|
+
api.get("/projects/:slug/files/:path{.+}", (c) => projectFile(c, c.req.param("path")));
|
|
13373
13113
|
api.get("/tasks/:id", async (c) => c.json((await board2.findTask(c.req.param("id"))).task));
|
|
13374
13114
|
api.get("/tasks/:id/thread", async (c) => c.json(await board2.listThread(c.req.param("id"))));
|
|
13375
13115
|
const taskFile = async (c, file) => {
|
|
13376
13116
|
const { task } = await board2.findTask(UlidSchema.parse(c.req.param("id")));
|
|
13377
|
-
|
|
13378
|
-
throw new BoardError("NOT_FOUND", "this board has no runners to read a task's files from");
|
|
13379
|
-
}
|
|
13380
|
-
return c.json(await deps.runners.readBranch(task.project, `task/${task.id}`, BranchPathSchema.parse(file)));
|
|
13117
|
+
return branchFile(c, task.project, `task/${task.id}`, file);
|
|
13381
13118
|
};
|
|
13382
13119
|
api.get("/tasks/:id/files", (c) => taskFile(c, ""));
|
|
13383
13120
|
api.get("/tasks/:id/changes", async (c) => {
|