@kubitnodes/stellaris 0.3.0 → 0.3.2
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 +122 -4
- package/dist/server/index.js +488 -30
- package/package.json +1 -1
- package/web/dist/assets/ChannelView-CX4cPMWM.js +1 -0
- package/web/dist/assets/CitizenView-8ZU2hK4D.js +2 -0
- package/web/dist/assets/{CitizensView-DB2TlGxn.js → CitizensView-BDenU7WD.js} +1 -1
- package/web/dist/assets/KnowledgeView-IUzeK83J.js +1 -0
- package/web/dist/assets/Markdown-DH59NuID.js +31 -0
- package/web/dist/assets/{MessageItem-BR30twQh.js → MessageItem-BWuFJqdk.js} +1 -1
- package/web/dist/assets/MetricsView-RzP1J0EG.js +1 -0
- package/web/dist/assets/NeedsYouView-DvnPW0tZ.js +1 -0
- package/web/dist/assets/Overview-BYcIErNt.js +1 -0
- package/web/dist/assets/{Pane-BXpuXst6.js → Pane-KNrdAxkc.js} +1 -1
- package/web/dist/assets/{ProjectView-CxUA5nFw.js → ProjectView-CmoGtfJ4.js} +1 -1
- package/web/dist/assets/ProposalView-FjvrD4Fz.js +1 -0
- package/web/dist/assets/{ProposalsView-BscIGUTT.js → ProposalsView-DSiFdLGD.js} +1 -1
- package/web/dist/assets/{RunnersView-BypVHvzI.js → RunnersView-DfduymeY.js} +1 -1
- package/web/dist/assets/{SocietyView-C403e1uR.js → SocietyView-BwfrqQiF.js} +1 -1
- package/web/dist/assets/SubjectThread-B-1_FwBN.js +1 -0
- package/web/dist/assets/TaskFileView-BNR8mQRk.js +1 -0
- package/web/dist/assets/TaskView-qdFvjqvJ.js +1 -0
- package/web/dist/assets/{TasksView-CA02-WIW.js → TasksView-QpcbsOzR.js} +1 -1
- package/web/dist/assets/ThreadCard-ClSg5-Lj.js +1 -0
- package/web/dist/assets/{ThreadForms-Dh9ky_aU.js → ThreadForms-B6Lv_t5p.js} +1 -1
- package/web/dist/assets/{ThreadView-CAdCu-y_.js → ThreadView-5PMRCHd4.js} +1 -1
- package/web/dist/assets/citizen-B9DadtKn.js +1 -0
- package/web/dist/assets/format-DbfHOiKW.js +55 -0
- package/web/dist/assets/index-2BnqwsfR.js +23 -0
- package/web/dist/assets/index-C3ujZpqY.css +2 -0
- package/web/dist/assets/{tasks-DZgkXWwu.js → tasks-Bw07dkQC.js} +1 -1
- package/web/dist/assets/{useStickyScroll-CVTdIoKj.js → useStickyScroll-CRgyaBkv.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/ChannelView-B4rv0TMD.js +0 -1
- package/web/dist/assets/CitizenView-DfBiP1Jw.js +0 -2
- package/web/dist/assets/KnowledgeView-l0I_R5Zd.js +0 -1
- package/web/dist/assets/Markdown-C6mhAmbQ.js +0 -29
- package/web/dist/assets/MetricsView-BZ0WW2Do.js +0 -1
- package/web/dist/assets/NeedsYouView-C6CZNnJn.js +0 -1
- package/web/dist/assets/Overview-D47FfDDO.js +0 -1
- package/web/dist/assets/ProposalView-C0oLBLQD.js +0 -1
- package/web/dist/assets/SubjectThread-irSHRJpB.js +0 -1
- package/web/dist/assets/TaskView-B-4WGLWK.js +0 -1
- package/web/dist/assets/ThreadCard-C4rb-53v.js +0 -1
- package/web/dist/assets/citizen-BWGATcew.js +0 -1
- package/web/dist/assets/format-DZLzLk7S.js +0 -55
- package/web/dist/assets/index-D-wuKd9C.js +0 -21
- package/web/dist/assets/index-HwY3AK-1.css +0 -2
package/dist/server/index.js
CHANGED
|
@@ -703,6 +703,8 @@ var BOARD_EVENT_TYPES = [
|
|
|
703
703
|
"paused.changed",
|
|
704
704
|
"wake.requested",
|
|
705
705
|
"turn.started",
|
|
706
|
+
"turn.steered",
|
|
707
|
+
"turn.stopped",
|
|
706
708
|
"turn.completed",
|
|
707
709
|
"turn.failed",
|
|
708
710
|
"merge.completed",
|
|
@@ -734,7 +736,8 @@ var TurnExitReasonSchema = z5.enum([
|
|
|
734
736
|
"timeout",
|
|
735
737
|
"error",
|
|
736
738
|
"interrupted",
|
|
737
|
-
"blocked"
|
|
739
|
+
"blocked",
|
|
740
|
+
"stopped"
|
|
738
741
|
]);
|
|
739
742
|
var AgentEventSchema = z5.discriminatedUnion("type", [
|
|
740
743
|
z5.object({
|
|
@@ -745,6 +748,16 @@ var AgentEventSchema = z5.discriminatedUnion("type", [
|
|
|
745
748
|
/** The model the CLI reports running, when it reports one. */
|
|
746
749
|
model: z5.string().optional()
|
|
747
750
|
}),
|
|
751
|
+
/**
|
|
752
|
+
* The CLI took a steer into the running turn. Adapters name the steer; the server adds the
|
|
753
|
+
* messages it delivered and the text the CLI was given.
|
|
754
|
+
*/
|
|
755
|
+
z5.object({
|
|
756
|
+
type: z5.literal("steered"),
|
|
757
|
+
steer: z5.string().min(1),
|
|
758
|
+
messages: z5.array(UlidSchema).optional(),
|
|
759
|
+
text: z5.string().optional()
|
|
760
|
+
}),
|
|
748
761
|
z5.object({ type: z5.literal("text"), delta: z5.string() }),
|
|
749
762
|
z5.object({ type: z5.literal("tool_call"), name: z5.string(), input: z5.unknown() }),
|
|
750
763
|
z5.object({
|
|
@@ -799,6 +812,12 @@ var RunnerHelloSchema = z6.object({
|
|
|
799
812
|
clis: z6.array(CliKindSchema),
|
|
800
813
|
/** The CLIs whose adapter can keep a session warm between turns. */
|
|
801
814
|
residentClis: z6.array(CliKindSchema).default([]),
|
|
815
|
+
/** The CLIs whose adapter can take input into a running turn; the server steers only these. */
|
|
816
|
+
steerableClis: z6.array(CliKindSchema).default([]),
|
|
817
|
+
/** The CLIs whose adapter can stop a running turn; the server stops only these. */
|
|
818
|
+
stoppableClis: z6.array(CliKindSchema).default([]),
|
|
819
|
+
/** Whether the runner answers file reads on its projects' branches; one from before never does. */
|
|
820
|
+
branchReads: z6.boolean().default(false),
|
|
802
821
|
capabilities: z6.array(z6.string()).default([]),
|
|
803
822
|
/** Turns the runner runs at once, or null for no limit. */
|
|
804
823
|
slots: z6.number().int().positive().nullable(),
|
|
@@ -902,10 +921,54 @@ var LandRequestSchema = z6.object({
|
|
|
902
921
|
});
|
|
903
922
|
var MergeOutcomeSchema = z6.object({ ok: z6.boolean(), detail: z6.string() });
|
|
904
923
|
var ModelListSchema = z6.array(ModelOptionSchema);
|
|
924
|
+
var BRANCH_FILE_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
925
|
+
var BranchPathSchema = z6.string().max(4096).refine((value) => value === "" || value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".." && !segment.includes("\0")), { message: "a path inside the branch, with no empty, `.`, or `..` segment" });
|
|
926
|
+
var BranchNameSchema = z6.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/);
|
|
927
|
+
var BranchReadSchema = z6.object({
|
|
928
|
+
repo: ProjectRepoSchema,
|
|
929
|
+
branch: BranchNameSchema,
|
|
930
|
+
path: BranchPathSchema
|
|
931
|
+
});
|
|
932
|
+
var BranchCommitSchema = z6.object({
|
|
933
|
+
id: z6.string().min(1),
|
|
934
|
+
at: IsoDateTimeSchema,
|
|
935
|
+
author: z6.string()
|
|
936
|
+
});
|
|
937
|
+
var BranchFileSchema = z6.discriminatedUnion("kind", [
|
|
938
|
+
z6.object({
|
|
939
|
+
kind: z6.literal("file"),
|
|
940
|
+
path: BranchPathSchema,
|
|
941
|
+
commit: BranchCommitSchema,
|
|
942
|
+
size: z6.number().int().nonnegative(),
|
|
943
|
+
content: z6.string().nullable()
|
|
944
|
+
}),
|
|
945
|
+
z6.object({
|
|
946
|
+
kind: z6.literal("dir"),
|
|
947
|
+
path: BranchPathSchema,
|
|
948
|
+
commit: BranchCommitSchema,
|
|
949
|
+
entries: z6.array(z6.object({
|
|
950
|
+
name: z6.string().min(1),
|
|
951
|
+
kind: z6.enum(["file", "dir"]),
|
|
952
|
+
size: z6.number().int().nonnegative().nullable()
|
|
953
|
+
}))
|
|
954
|
+
})
|
|
955
|
+
]);
|
|
956
|
+
var TurnSteerSchema = z6.object({ id: z6.uuid(), text: z6.string().min(1) });
|
|
905
957
|
var RunnerMessageSchema = z6.discriminatedUnion("type", [
|
|
906
958
|
z6.object({ type: z6.literal("turn"), job: TurnJobSchema }),
|
|
907
959
|
z6.object({ type: z6.literal("land"), request: z6.string().min(1), land: LandRequestSchema }),
|
|
908
|
-
z6.object({ type: z6.literal("models"), request: z6.string().min(1), cli: CliKindSchema })
|
|
960
|
+
z6.object({ type: z6.literal("models"), request: z6.string().min(1), cli: CliKindSchema }),
|
|
961
|
+
/** Answered with a `BranchFile`, or null when the branch or the path does not exist. */
|
|
962
|
+
z6.object({ type: z6.literal("file"), request: z6.string().min(1), read: BranchReadSchema }),
|
|
963
|
+
/** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
|
|
964
|
+
z6.object({
|
|
965
|
+
type: z6.literal("steer"),
|
|
966
|
+
request: z6.string().min(1),
|
|
967
|
+
turnId: UlidSchema,
|
|
968
|
+
steer: TurnSteerSchema
|
|
969
|
+
}),
|
|
970
|
+
/** Answered with whether the turn was there to stop; it ends `stopped`. */
|
|
971
|
+
z6.object({ type: z6.literal("stop"), request: z6.string().min(1), turnId: UlidSchema })
|
|
909
972
|
]);
|
|
910
973
|
var RunnerAnswerSchema = z6.discriminatedUnion("ok", [
|
|
911
974
|
z6.object({ ok: z6.literal(true), value: z6.unknown() }),
|
|
@@ -984,7 +1047,19 @@ var TurnRecordSchema = z7.object({
|
|
|
984
1047
|
/** The model the CLI reported for this turn, else the model configured for the agent, else null. */
|
|
985
1048
|
model: z7.string().nullable().default(null),
|
|
986
1049
|
/** The task branch the turn left work on, with its commit, as the runner reported it. */
|
|
987
|
-
work: TurnWorkSchema.optional()
|
|
1050
|
+
work: TurnWorkSchema.optional(),
|
|
1051
|
+
/** Who stopped the turn, for one that ended `stopped`. */
|
|
1052
|
+
stoppedBy: NameSchema.optional()
|
|
1053
|
+
});
|
|
1054
|
+
var RunningTurnSchema = z7.object({
|
|
1055
|
+
turnId: UlidSchema,
|
|
1056
|
+
agent: NameSchema,
|
|
1057
|
+
scope: NameSchema,
|
|
1058
|
+
thread: UlidSchema.optional(),
|
|
1059
|
+
cli: CliKindSchema,
|
|
1060
|
+
channel: ChannelRefSchema.optional(),
|
|
1061
|
+
steerable: z7.boolean(),
|
|
1062
|
+
stoppable: z7.boolean()
|
|
988
1063
|
});
|
|
989
1064
|
var SessionsFileSchema = z7.object({
|
|
990
1065
|
claude: z7.string().optional(),
|
|
@@ -1240,7 +1315,7 @@ var RunnerConfigSchema = z10.object({
|
|
|
1240
1315
|
});
|
|
1241
1316
|
|
|
1242
1317
|
// packages/shared/dist/version.js
|
|
1243
|
-
var STELLARIS_VERSION = "0.3.
|
|
1318
|
+
var STELLARIS_VERSION = "0.3.2";
|
|
1244
1319
|
|
|
1245
1320
|
// packages/board-core/dist/errors.js
|
|
1246
1321
|
var BoardError = class extends Error {
|
|
@@ -1787,10 +1862,12 @@ var TaskPayload = z11.object({
|
|
|
1787
1862
|
from: z11.string().optional()
|
|
1788
1863
|
});
|
|
1789
1864
|
var MessagePayload = z11.object({
|
|
1865
|
+
id: z11.string().optional(),
|
|
1790
1866
|
channel: z11.string(),
|
|
1791
1867
|
thread: z11.string().nullable().optional(),
|
|
1792
1868
|
mentions: z11.array(z11.string()).default([])
|
|
1793
1869
|
});
|
|
1870
|
+
var SteeredPayload = z11.object({ messages: z11.array(z11.string()) });
|
|
1794
1871
|
var SignalPayload = z11.object({
|
|
1795
1872
|
kind: z11.string(),
|
|
1796
1873
|
key: z11.string(),
|
|
@@ -1897,6 +1974,23 @@ function computeMetrics(input) {
|
|
|
1897
1974
|
}
|
|
1898
1975
|
break;
|
|
1899
1976
|
}
|
|
1977
|
+
case "turn.steered": {
|
|
1978
|
+
const steered = SteeredPayload.safeParse(event.payload);
|
|
1979
|
+
if (!steered.success) {
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1982
|
+
const delivered = new Set(steered.data.messages);
|
|
1983
|
+
const at = Date.parse(event.ts);
|
|
1984
|
+
const answered = waiting.filter((mention) => mention.agent === event.actor && mention.message !== null && delivered.has(mention.message));
|
|
1985
|
+
if (answered.length > 0) {
|
|
1986
|
+
latencies.set(event.actor, [
|
|
1987
|
+
...latencies.get(event.actor) ?? [],
|
|
1988
|
+
...answered.map((mention) => at - mention.at)
|
|
1989
|
+
]);
|
|
1990
|
+
waiting = waiting.filter((mention) => !answered.includes(mention));
|
|
1991
|
+
}
|
|
1992
|
+
break;
|
|
1993
|
+
}
|
|
1900
1994
|
case "task.completed": {
|
|
1901
1995
|
const task = TaskPayload.safeParse(event.payload);
|
|
1902
1996
|
if (counted && task.success) {
|
|
@@ -1961,6 +2055,7 @@ function computeMetrics(input) {
|
|
|
1961
2055
|
waiting.push({
|
|
1962
2056
|
agent: name,
|
|
1963
2057
|
at: Date.parse(event.ts),
|
|
2058
|
+
message: message.data.id ?? null,
|
|
1964
2059
|
thread,
|
|
1965
2060
|
scopes: [project, wakeScope(citizen, project)].filter((scope) => scope !== null)
|
|
1966
2061
|
});
|
|
@@ -4773,6 +4868,21 @@ ${rationale}`}`);
|
|
|
4773
4868
|
});
|
|
4774
4869
|
});
|
|
4775
4870
|
}
|
|
4871
|
+
/** Where a conversation's digest resumes: the newest message a turn there was shown. */
|
|
4872
|
+
async digestCursor(agent, scope, thread) {
|
|
4873
|
+
return this.mutex.run(async () => cursorOf(await this.readCursors(agent), scope, thread ?? null));
|
|
4874
|
+
}
|
|
4875
|
+
/** Records that a running turn took posts delivered into it, which wake latency counts to. */
|
|
4876
|
+
async recordSteered(input) {
|
|
4877
|
+
await this.mutex.run(async () => {
|
|
4878
|
+
await this.events.append("turn.steered", input.agent, {
|
|
4879
|
+
turnId: input.turnId,
|
|
4880
|
+
project: input.project,
|
|
4881
|
+
...input.thread === void 0 ? {} : { thread: input.thread },
|
|
4882
|
+
messages: [...input.messages]
|
|
4883
|
+
});
|
|
4884
|
+
});
|
|
4885
|
+
}
|
|
4776
4886
|
/** Where a conversation's session ids and last turn are kept: the scope's home, or a thread's. */
|
|
4777
4887
|
sessionDir(agent, scope, thread) {
|
|
4778
4888
|
return thread === void 0 ? this.paths.agentProject(agent, scope) : this.paths.agentThread(agent, scope, thread);
|
|
@@ -4842,6 +4952,14 @@ ${rationale}`}`);
|
|
|
4842
4952
|
}
|
|
4843
4953
|
await this.writeTurnRecord(parsed);
|
|
4844
4954
|
await this.refreshMember(parsed.agent);
|
|
4955
|
+
if (parsed.exitReason === "stopped") {
|
|
4956
|
+
await this.events.append("turn.stopped", parsed.stoppedBy ?? USER_NAME, {
|
|
4957
|
+
...parsed.id === void 0 ? {} : { turnId: parsed.id },
|
|
4958
|
+
agent: parsed.agent,
|
|
4959
|
+
project: parsed.project,
|
|
4960
|
+
...parsed.thread === void 0 ? {} : { thread: parsed.thread }
|
|
4961
|
+
});
|
|
4962
|
+
}
|
|
4845
4963
|
await this.events.append(failed ? "turn.failed" : "turn.completed", parsed.agent, {
|
|
4846
4964
|
...parsed.id === void 0 ? {} : { turnId: parsed.id },
|
|
4847
4965
|
project: parsed.project,
|
|
@@ -6133,7 +6251,10 @@ var PendingSchema = z13.object({
|
|
|
6133
6251
|
dispatch: TurnDispatchSchema,
|
|
6134
6252
|
readyAt: z13.number(),
|
|
6135
6253
|
stageWakes: z13.array(TriggerSchema).optional(),
|
|
6136
|
-
holder: z13.boolean().optional()
|
|
6254
|
+
holder: z13.boolean().optional(),
|
|
6255
|
+
newestMessage: UlidSchema.optional(),
|
|
6256
|
+
onlyMessages: z13.boolean().optional(),
|
|
6257
|
+
steeredThrough: UlidSchema.optional()
|
|
6137
6258
|
});
|
|
6138
6259
|
var StateSchema = z13.object({
|
|
6139
6260
|
cursor: z13.string().nullable(),
|
|
@@ -6152,6 +6273,16 @@ var StateSchema = z13.object({
|
|
|
6152
6273
|
/** When each member last reflected, or was first seen; the cadence counts from here. */
|
|
6153
6274
|
lastReflection: z13.record(z13.string(), z13.string()).default({})
|
|
6154
6275
|
});
|
|
6276
|
+
function messageOf(trigger) {
|
|
6277
|
+
return trigger.kind === "mention" || trigger.kind === "user_post" ? trigger.messageId : void 0;
|
|
6278
|
+
}
|
|
6279
|
+
function later(a, b) {
|
|
6280
|
+
if (a === void 0)
|
|
6281
|
+
return b;
|
|
6282
|
+
if (b === void 0)
|
|
6283
|
+
return a;
|
|
6284
|
+
return a > b ? a : b;
|
|
6285
|
+
}
|
|
6155
6286
|
var SILENT_LOG = { info() {
|
|
6156
6287
|
}, warn() {
|
|
6157
6288
|
}, error() {
|
|
@@ -6191,6 +6322,8 @@ var Scheduler = class {
|
|
|
6191
6322
|
pending = /* @__PURE__ */ new Map();
|
|
6192
6323
|
/** Turns in flight by session key, each with the lane it holds (see `laneOf`). */
|
|
6193
6324
|
running = /* @__PURE__ */ new Map();
|
|
6325
|
+
/** Steers under way, by session key; a pass does not wait for a runner to answer one. */
|
|
6326
|
+
steering = /* @__PURE__ */ new Map();
|
|
6194
6327
|
/** Tasks whose completion effect waits for the turn that finished their last stage to end. */
|
|
6195
6328
|
completions = /* @__PURE__ */ new Map();
|
|
6196
6329
|
completing = /* @__PURE__ */ new Set();
|
|
@@ -6245,7 +6378,8 @@ var Scheduler = class {
|
|
|
6245
6378
|
await this.passing?.catch(() => void 0);
|
|
6246
6379
|
await Promise.allSettled([
|
|
6247
6380
|
...[...this.running.values()].map((turn) => turn.promise),
|
|
6248
|
-
...this.completing
|
|
6381
|
+
...this.completing,
|
|
6382
|
+
...this.steering.values()
|
|
6249
6383
|
]);
|
|
6250
6384
|
}
|
|
6251
6385
|
/** One pass, unless one is under way already. */
|
|
@@ -6292,6 +6426,7 @@ var Scheduler = class {
|
|
|
6292
6426
|
/** Waits for every running turn and in-flight completion to settle. Used by tests and by stop(). */
|
|
6293
6427
|
async drain() {
|
|
6294
6428
|
await Promise.allSettled([
|
|
6429
|
+
...this.steering.values(),
|
|
6295
6430
|
...[...this.running.values()].map((turn) => turn.promise),
|
|
6296
6431
|
...this.completing
|
|
6297
6432
|
]);
|
|
@@ -6708,6 +6843,7 @@ var Scheduler = class {
|
|
|
6708
6843
|
const key = sessionKey(agent, project, thread?.id);
|
|
6709
6844
|
const readyAt = now + debounce;
|
|
6710
6845
|
const existing = this.pending.get(key);
|
|
6846
|
+
const message = messageOf(trigger);
|
|
6711
6847
|
if (existing === void 0) {
|
|
6712
6848
|
this.pending.set(key, {
|
|
6713
6849
|
dispatch: {
|
|
@@ -6719,10 +6855,13 @@ var Scheduler = class {
|
|
|
6719
6855
|
onboarding: trigger.kind === "onboarding"
|
|
6720
6856
|
},
|
|
6721
6857
|
readyAt,
|
|
6722
|
-
...trigger.kind === "stage" ? { stageWakes: [trigger] } : {}
|
|
6858
|
+
...trigger.kind === "stage" ? { stageWakes: [trigger] } : {},
|
|
6859
|
+
...message === void 0 ? {} : { newestMessage: message },
|
|
6860
|
+
onlyMessages: message !== void 0
|
|
6723
6861
|
});
|
|
6724
6862
|
return;
|
|
6725
6863
|
}
|
|
6864
|
+
const newestMessage = later(existing.newestMessage, message);
|
|
6726
6865
|
const keepNew = decision.priority > existing.dispatch.priority;
|
|
6727
6866
|
const priority = keepNew ? decision.priority : existing.dispatch.priority;
|
|
6728
6867
|
this.pending.set(key, {
|
|
@@ -6736,7 +6875,10 @@ var Scheduler = class {
|
|
|
6736
6875
|
...existing.stageWakes !== void 0 && trigger.kind === "stage" ? {
|
|
6737
6876
|
stageWakes: [...existing.stageWakes, trigger],
|
|
6738
6877
|
...existing.holder === true ? { holder: true } : {}
|
|
6739
|
-
} : {}
|
|
6878
|
+
} : {},
|
|
6879
|
+
...newestMessage === void 0 ? {} : { newestMessage },
|
|
6880
|
+
onlyMessages: existing.onlyMessages === true && message !== void 0,
|
|
6881
|
+
...existing.steeredThrough === void 0 ? {} : { steeredThrough: existing.steeredThrough }
|
|
6740
6882
|
});
|
|
6741
6883
|
}
|
|
6742
6884
|
/** Debounce coalesces chatter: mentions, stage handoffs, and bursts of signals. Everything else runs at once. */
|
|
@@ -7186,7 +7328,51 @@ var Scheduler = class {
|
|
|
7186
7328
|
}
|
|
7187
7329
|
return false;
|
|
7188
7330
|
}
|
|
7331
|
+
/**
|
|
7332
|
+
* Delivers ready message wakes into the turn already running in their conversation, rather than
|
|
7333
|
+
* keeping them for a turn of their own. Each stays queued until that turn ends and is checked
|
|
7334
|
+
* again then, so a steer refused, or left untaken, loses nothing. A pass never waits on a runner.
|
|
7335
|
+
*/
|
|
7336
|
+
steerRunning(now) {
|
|
7337
|
+
const steer = this.runner.steer?.bind(this.runner);
|
|
7338
|
+
if (steer === void 0) {
|
|
7339
|
+
return;
|
|
7340
|
+
}
|
|
7341
|
+
for (const [key, item] of this.pending) {
|
|
7342
|
+
const through = item.newestMessage;
|
|
7343
|
+
if (item.readyAt > now || through === void 0 || !this.running.has(key) || this.steering.has(key) || item.steeredThrough !== void 0 && through <= item.steeredThrough) {
|
|
7344
|
+
continue;
|
|
7345
|
+
}
|
|
7346
|
+
const { dispatch } = item;
|
|
7347
|
+
const attempt = steer({
|
|
7348
|
+
agent: dispatch.agent,
|
|
7349
|
+
project: dispatch.project,
|
|
7350
|
+
...dispatch.thread === void 0 ? {} : { thread: dispatch.thread.id }
|
|
7351
|
+
}).then((outcome) => {
|
|
7352
|
+
const current = this.pending.get(key);
|
|
7353
|
+
if (outcome !== "busy" && current !== void 0) {
|
|
7354
|
+
current.steeredThrough = later(current.steeredThrough, through);
|
|
7355
|
+
}
|
|
7356
|
+
return void 0;
|
|
7357
|
+
}).catch((error) => {
|
|
7358
|
+
this.log.warn({ key, error: String(error) }, "steering failed");
|
|
7359
|
+
}).finally(() => {
|
|
7360
|
+
this.steering.delete(key);
|
|
7361
|
+
});
|
|
7362
|
+
this.steering.set(key, attempt);
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
/** Whether every message that woke a queued turn was read in its conversation, by a turn already. */
|
|
7366
|
+
async alreadyRead(item) {
|
|
7367
|
+
if (item.onlyMessages !== true || item.newestMessage === void 0) {
|
|
7368
|
+
return false;
|
|
7369
|
+
}
|
|
7370
|
+
const { dispatch } = item;
|
|
7371
|
+
const cursor = await this.board.digestCursor(dispatch.agent, dispatch.project, dispatch.thread?.id);
|
|
7372
|
+
return cursor !== null && cursor >= item.newestMessage;
|
|
7373
|
+
}
|
|
7189
7374
|
async dispatchReady(now) {
|
|
7375
|
+
this.steerRunning(now);
|
|
7190
7376
|
const ready = [...this.pending.entries()].filter(([key, item]) => item.readyAt <= now && !this.running.has(key)).toSorted(([, a], [, b]) => b.dispatch.priority - a.dispatch.priority || a.readyAt - b.readyAt);
|
|
7191
7377
|
for (const [key, item] of ready) {
|
|
7192
7378
|
if (this.running.size >= this.concurrency) {
|
|
@@ -7197,6 +7383,10 @@ var Scheduler = class {
|
|
|
7197
7383
|
continue;
|
|
7198
7384
|
}
|
|
7199
7385
|
this.pending.delete(key);
|
|
7386
|
+
if (await this.alreadyRead(item)) {
|
|
7387
|
+
this.log.info({ agent: item.dispatch.agent, project: item.dispatch.project }, "dropping message wake, its conversation has read the message");
|
|
7388
|
+
continue;
|
|
7389
|
+
}
|
|
7200
7390
|
let { dispatch } = item;
|
|
7201
7391
|
if (item.stageWakes !== void 0) {
|
|
7202
7392
|
const current = await this.stillWaiting(dispatch.agent, item.stageWakes, item.holder === true);
|
|
@@ -7258,11 +7448,15 @@ var Scheduler = class {
|
|
|
7258
7448
|
}
|
|
7259
7449
|
};
|
|
7260
7450
|
|
|
7451
|
+
// packages/turn-host/dist/host.js
|
|
7452
|
+
import { randomUUID } from "node:crypto";
|
|
7453
|
+
|
|
7261
7454
|
// packages/turn-host/dist/render.js
|
|
7262
7455
|
var TURN_CONTRACT = [
|
|
7263
7456
|
"- Every turn belongs to one conversation, named at the top of your prompt: your home conversation in a project or the society, for its channels and for work tied to no thread, 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; at home, the channel messages that concern you. Read anything else you need with get_task, search, and the board's files, and leave other threads' and other projects' work to their own turns.",
|
|
7264
7457
|
"- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it.",
|
|
7265
7458
|
"- 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. 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.",
|
|
7459
|
+
"- 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.",
|
|
7266
7460
|
"- Silence is allowed. If the digest needs no reply, post nothing.",
|
|
7267
7461
|
"- 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. @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.",
|
|
7268
7462
|
"- 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 its project's general channel: 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.",
|
|
@@ -7412,6 +7606,26 @@ function rosterLine(member) {
|
|
|
7412
7606
|
].filter((part) => part.length > 0);
|
|
7413
7607
|
return `- ${member.name}: ${parts.join("; ")}. Profile: ${profileLine(member.profile)}`;
|
|
7414
7608
|
}
|
|
7609
|
+
function messageLines(messages, threads) {
|
|
7610
|
+
const lines = [];
|
|
7611
|
+
for (const message of messages) {
|
|
7612
|
+
const thread = message.thread === void 0 ? void 0 : threads?.get(message.thread);
|
|
7613
|
+
const where = message.thread === void 0 ? message.channel : thread === void 0 ? `${message.channel} thread ${message.thread}` : `${message.channel} thread "${thread.title}" (${message.thread})`;
|
|
7614
|
+
const step = message.step === void 0 ? "" : `, who ${stepPhrase(message.step)}`;
|
|
7615
|
+
lines.push(`### [${message.ts}] ${where} from @${message.author}${step} (message ${message.id})`, "", clip(message.body), "");
|
|
7616
|
+
}
|
|
7617
|
+
return lines;
|
|
7618
|
+
}
|
|
7619
|
+
function buildSteerText(messages, threads) {
|
|
7620
|
+
return `${[
|
|
7621
|
+
`## New in this conversation while you work (${messages.length})`,
|
|
7622
|
+
"",
|
|
7623
|
+
"The board delivered these into your turn as they arrived, as it delivers the digest. Act on them now, after the step in hand, or not at all; the turn still ends with one status object covering everything it did.",
|
|
7624
|
+
"",
|
|
7625
|
+
...messageLines(messages, threads)
|
|
7626
|
+
].join("\n").trimEnd()}
|
|
7627
|
+
`;
|
|
7628
|
+
}
|
|
7415
7629
|
function buildTurnPrompt(input) {
|
|
7416
7630
|
const { dispatch } = input;
|
|
7417
7631
|
const lines = [];
|
|
@@ -7464,6 +7678,9 @@ function buildTurnPrompt(input) {
|
|
|
7464
7678
|
lines.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
|
|
7465
7679
|
}
|
|
7466
7680
|
const last = input.lastTurn;
|
|
7681
|
+
if (last?.exitReason === "stopped") {
|
|
7682
|
+
lines.push("", "## The user stopped your previous turn", "", `It was ended on purpose at ${last.endedAt ?? "its end"}, not by a failure. Do not pick its work up again unless this turn asks for it. The worktree may hold uncommitted changes; run git status before doing anything else.`);
|
|
7683
|
+
}
|
|
7467
7684
|
if (last !== null && (last.exitReason === "error" || last.exitReason === "timeout")) {
|
|
7468
7685
|
lines.push("", "## Your previous turn did not finish", "", `It ended with ${last.exitReason}${last.error === null ? "" : `: ${last.error}`}.`, "The worktree may hold uncommitted changes. Run git status before doing anything else.");
|
|
7469
7686
|
}
|
|
@@ -7545,12 +7762,7 @@ function buildTurnPrompt(input) {
|
|
|
7545
7762
|
if (input.messages.length === 0) {
|
|
7546
7763
|
lines.push("Nothing new.");
|
|
7547
7764
|
} else {
|
|
7548
|
-
|
|
7549
|
-
const thread = message.thread === void 0 ? void 0 : input.threads?.get(message.thread);
|
|
7550
|
-
const where = message.thread === void 0 ? message.channel : thread === void 0 ? `${message.channel} thread ${message.thread}` : `${message.channel} thread "${thread.title}" (${message.thread})`;
|
|
7551
|
-
const step = message.step === void 0 ? "" : `, who ${stepPhrase(message.step)}`;
|
|
7552
|
-
lines.push(`### [${message.ts}] ${where} from @${message.author}${step} (message ${message.id})`, "", clip(message.body), "");
|
|
7553
|
-
}
|
|
7765
|
+
lines.push(...messageLines(input.messages, input.threads));
|
|
7554
7766
|
}
|
|
7555
7767
|
lines.push("## What to do", "", "Act on the unread messages and your stages through the board tools: claim_task to hold a stage, advance_task when your part is done, plan_task to reshape what comes next, post_message to talk, and propose for what the society lacks. Silence is allowed when nothing needs a reply. End with the status object.");
|
|
7556
7768
|
return `${lines.join("\n")}
|
|
@@ -7757,7 +7969,10 @@ var TurnHost = class {
|
|
|
7757
7969
|
runner: runner.name,
|
|
7758
7970
|
actor,
|
|
7759
7971
|
record,
|
|
7760
|
-
cursorAfter,
|
|
7972
|
+
delivered: cursorAfter,
|
|
7973
|
+
steers: /* @__PURE__ */ new Map(),
|
|
7974
|
+
outstanding: null,
|
|
7975
|
+
stopRequestedBy: null,
|
|
7761
7976
|
held: held.map((each) => each.id),
|
|
7762
7977
|
taskId,
|
|
7763
7978
|
costSoFarUsd,
|
|
@@ -7779,17 +7994,130 @@ var TurnHost = class {
|
|
|
7779
7994
|
}, "turn starting");
|
|
7780
7995
|
return { job, ended };
|
|
7781
7996
|
}
|
|
7782
|
-
/**
|
|
7783
|
-
|
|
7997
|
+
/**
|
|
7998
|
+
* Steps of a turn in flight, as the runner received them. A steer the CLI took moves what the
|
|
7999
|
+
* turn has been shown past its messages, and its step carries them and the text it delivered.
|
|
8000
|
+
*/
|
|
8001
|
+
async addEvents(turnId, entries) {
|
|
7784
8002
|
const turn = this.open.get(turnId);
|
|
7785
8003
|
if (turn === void 0) {
|
|
7786
8004
|
return;
|
|
7787
8005
|
}
|
|
7788
|
-
for (const
|
|
8006
|
+
for (const received of entries) {
|
|
8007
|
+
let entry = received;
|
|
8008
|
+
const { event } = received;
|
|
8009
|
+
if (event.type === "steered") {
|
|
8010
|
+
const sent = turn.steers.get(event.steer);
|
|
8011
|
+
if (sent !== void 0) {
|
|
8012
|
+
entry = {
|
|
8013
|
+
...received,
|
|
8014
|
+
event: { ...event, messages: [...sent.messages], text: sent.text }
|
|
8015
|
+
};
|
|
8016
|
+
if (turn.delivered === null || sent.through > turn.delivered) {
|
|
8017
|
+
turn.delivered = sent.through;
|
|
8018
|
+
}
|
|
8019
|
+
if (turn.outstanding === event.steer) {
|
|
8020
|
+
turn.outstanding = null;
|
|
8021
|
+
}
|
|
8022
|
+
await this.board.recordSteered({
|
|
8023
|
+
turnId,
|
|
8024
|
+
agent: turn.job.agent,
|
|
8025
|
+
project: turn.job.scope,
|
|
8026
|
+
thread: turn.job.thread,
|
|
8027
|
+
messages: sent.messages
|
|
8028
|
+
}).catch((error) => {
|
|
8029
|
+
this.log.warn({ turnId, error: String(error) }, "could not record a steer");
|
|
8030
|
+
});
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
7789
8033
|
turn.transcript.push(entry);
|
|
7790
8034
|
this.onEvent?.(turn.job.agent, turn.job.scope, entry.event, turn.job.thread);
|
|
7791
8035
|
}
|
|
7792
8036
|
}
|
|
8037
|
+
/** The turn in flight in a conversation, if any. */
|
|
8038
|
+
turnIn(agent, scope, thread) {
|
|
8039
|
+
for (const turn of this.open.values()) {
|
|
8040
|
+
const { job } = turn;
|
|
8041
|
+
if (job.agent === agent && job.scope === scope && job.thread === thread) {
|
|
8042
|
+
return { turnId: job.turnId, runner: turn.runner, cli: job.cli };
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
return null;
|
|
8046
|
+
}
|
|
8047
|
+
/** The turns in flight, for the interface: their conversations and where a mention reaches them. */
|
|
8048
|
+
runningTurns() {
|
|
8049
|
+
return [...this.open.values()].map((turn) => ({
|
|
8050
|
+
turnId: turn.job.turnId,
|
|
8051
|
+
agent: turn.job.agent,
|
|
8052
|
+
scope: turn.job.scope,
|
|
8053
|
+
...turn.job.thread === void 0 ? {} : { thread: turn.job.thread },
|
|
8054
|
+
cli: turn.job.cli,
|
|
8055
|
+
...turn.record.trigger.channel === void 0 ? {} : { channel: turn.record.trigger.channel },
|
|
8056
|
+
runner: turn.runner
|
|
8057
|
+
}));
|
|
8058
|
+
}
|
|
8059
|
+
/**
|
|
8060
|
+
* What a steer into a turn would deliver: the conversation's digest from what the turn has been
|
|
8061
|
+
* shown, rendered as the prompt renders its digest. `busy` while a steer it was sent is not yet
|
|
8062
|
+
* taken or refused, and `nothing` when everything new was in what it was shown.
|
|
8063
|
+
*/
|
|
8064
|
+
async prepareSteer(turnId) {
|
|
8065
|
+
const turn = this.open.get(turnId);
|
|
8066
|
+
if (turn === void 0 || turn.stopRequestedBy !== null) {
|
|
8067
|
+
return "gone";
|
|
8068
|
+
}
|
|
8069
|
+
if (turn.outstanding !== null) {
|
|
8070
|
+
return "busy";
|
|
8071
|
+
}
|
|
8072
|
+
const { job } = turn;
|
|
8073
|
+
const digest = await this.board.readDigest({
|
|
8074
|
+
...turn.actor,
|
|
8075
|
+
scope: job.scope,
|
|
8076
|
+
...job.thread === void 0 ? {} : { thread: job.thread }
|
|
8077
|
+
}, {
|
|
8078
|
+
...turn.delivered === null ? {} : { since_cursor: turn.delivered },
|
|
8079
|
+
advance: false,
|
|
8080
|
+
limit: 50
|
|
8081
|
+
});
|
|
8082
|
+
const last = digest.messages.at(-1);
|
|
8083
|
+
if (!this.open.has(turnId) || turn.outstanding !== null) {
|
|
8084
|
+
return "gone";
|
|
8085
|
+
}
|
|
8086
|
+
if (last === void 0) {
|
|
8087
|
+
return "nothing";
|
|
8088
|
+
}
|
|
8089
|
+
const threads = /* @__PURE__ */ new Map();
|
|
8090
|
+
for (const id of new Set(digest.messages.flatMap((message) => message.thread ?? []))) {
|
|
8091
|
+
const record = await this.board.readThread(id).catch(() => void 0);
|
|
8092
|
+
if (record !== void 0) {
|
|
8093
|
+
threads.set(id, record);
|
|
8094
|
+
}
|
|
8095
|
+
}
|
|
8096
|
+
const steer = { id: randomUUID(), text: buildSteerText(digest.messages, threads) };
|
|
8097
|
+
turn.steers.set(steer.id, {
|
|
8098
|
+
through: last.id,
|
|
8099
|
+
messages: digest.messages.map((message) => message.id),
|
|
8100
|
+
text: steer.text
|
|
8101
|
+
});
|
|
8102
|
+
turn.outstanding = steer.id;
|
|
8103
|
+
return steer;
|
|
8104
|
+
}
|
|
8105
|
+
/** The runner's answer to a steer: one it did not take frees the turn for the next. */
|
|
8106
|
+
steerAnswered(turnId, steerId, accepted) {
|
|
8107
|
+
const turn = this.open.get(turnId);
|
|
8108
|
+
if (turn !== void 0 && !accepted && turn.outstanding === steerId) {
|
|
8109
|
+
turn.outstanding = null;
|
|
8110
|
+
}
|
|
8111
|
+
}
|
|
8112
|
+
/** Marks a turn stopped by `by`; returns false for a turn not in flight. */
|
|
8113
|
+
requestStop(turnId, by) {
|
|
8114
|
+
const turn = this.open.get(turnId);
|
|
8115
|
+
if (turn === void 0) {
|
|
8116
|
+
return false;
|
|
8117
|
+
}
|
|
8118
|
+
turn.stopRequestedBy ??= by;
|
|
8119
|
+
return true;
|
|
8120
|
+
}
|
|
7793
8121
|
/**
|
|
7794
8122
|
* Records a turn's end from its outcome. Returns null for a turn the host does not know, else
|
|
7795
8123
|
* whether the task the turn worked on has ended, so the runner may drop its worktree.
|
|
@@ -7820,10 +8148,11 @@ var TurnHost = class {
|
|
|
7820
8148
|
sessionCostUsd: outcome.sessionCostUsd ?? turn.costSoFarUsd,
|
|
7821
8149
|
toolCalls: turn.transcript.filter((entry) => entry.event.type === "tool_call").length,
|
|
7822
8150
|
model: outcome.model ?? turn.configuredModel,
|
|
7823
|
-
...outcome.work === null ? {} : { work: outcome.work }
|
|
8151
|
+
...outcome.work === null ? {} : { work: outcome.work },
|
|
8152
|
+
...outcome.exitReason === "stopped" ? { stoppedBy: turn.stopRequestedBy ?? USER_NAME } : {}
|
|
7824
8153
|
};
|
|
7825
|
-
if (outcome.exitReason === "completed" || outcome.exitReason === "blocked") {
|
|
7826
|
-
await this.board.setDigestCursor(job.agent, job.scope, turn.
|
|
8154
|
+
if (outcome.exitReason === "completed" || outcome.exitReason === "blocked" || outcome.exitReason === "stopped") {
|
|
8155
|
+
await this.board.setDigestCursor(job.agent, job.scope, turn.delivered, job.thread);
|
|
7827
8156
|
await this.renewLeases(turn.actor, turn.held);
|
|
7828
8157
|
if (outcome.status?.needsUserDecision === true) {
|
|
7829
8158
|
await this.askUser(turn.actor, job.scope, job.thread, turn.record.startedAt, outcome.status.summary);
|
|
@@ -7999,15 +8328,19 @@ var TurnHost = class {
|
|
|
7999
8328
|
};
|
|
8000
8329
|
|
|
8001
8330
|
// packages/turn-host/dist/hub.js
|
|
8002
|
-
import { randomUUID } from "node:crypto";
|
|
8331
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
8003
8332
|
var RunnerProtocolError = class extends Error {
|
|
8004
8333
|
};
|
|
8334
|
+
var RunnerAwayError = class extends Error {
|
|
8335
|
+
};
|
|
8005
8336
|
var SILENT2 = { info() {
|
|
8006
8337
|
}, warn() {
|
|
8007
8338
|
}, error() {
|
|
8008
8339
|
} };
|
|
8009
8340
|
var DEFAULT_GRACE_MS = 2 * 6e4;
|
|
8010
8341
|
var DEFAULT_REQUEST_TIMEOUT_MS = 10 * 6e4;
|
|
8342
|
+
var CONTROL_TIMEOUT_MS = 15e3;
|
|
8343
|
+
var FILE_TIMEOUT_MS = 6e4;
|
|
8011
8344
|
var RunnerHub = class {
|
|
8012
8345
|
board;
|
|
8013
8346
|
host;
|
|
@@ -8120,7 +8453,7 @@ var RunnerHub = class {
|
|
|
8120
8453
|
}
|
|
8121
8454
|
async turnEvents(runner, turnId, entries) {
|
|
8122
8455
|
this.assertOwns(runner, turnId);
|
|
8123
|
-
this.host.addEvents(turnId, entries);
|
|
8456
|
+
await this.host.addEvents(turnId, entries);
|
|
8124
8457
|
}
|
|
8125
8458
|
async turnOutcome(runner, turnId, outcome) {
|
|
8126
8459
|
this.assertOwns(runner, turnId);
|
|
@@ -8151,9 +8484,76 @@ var RunnerHub = class {
|
|
|
8151
8484
|
mayTouchHome(runner, agent) {
|
|
8152
8485
|
return this.host.hasTurnFor(runner, agent);
|
|
8153
8486
|
}
|
|
8487
|
+
/** The turns in flight, and whether each one's runner can steer and stop it. */
|
|
8488
|
+
liveTurns() {
|
|
8489
|
+
return this.host.runningTurns().map(({ runner, ...turn }) => {
|
|
8490
|
+
const seat = this.seats.get(runner);
|
|
8491
|
+
const connected = seat !== void 0 && seat.send !== null;
|
|
8492
|
+
return {
|
|
8493
|
+
...turn,
|
|
8494
|
+
steerable: connected && seat.hello.steerableClis.includes(turn.cli),
|
|
8495
|
+
stoppable: connected && seat.hello.stoppableClis.includes(turn.cli)
|
|
8496
|
+
};
|
|
8497
|
+
});
|
|
8498
|
+
}
|
|
8499
|
+
/**
|
|
8500
|
+
* Stops a turn in flight, as the user asked: its runner interrupts the CLI and the turn ends
|
|
8501
|
+
* `stopped`. False for a turn not in flight or on a runner that cannot stop it.
|
|
8502
|
+
*/
|
|
8503
|
+
async stop(turnId, by) {
|
|
8504
|
+
const runner = this.host.runnerOf(turnId);
|
|
8505
|
+
const seat = runner === void 0 ? void 0 : this.seats.get(runner);
|
|
8506
|
+
const cli = this.host.runningTurns().find((turn) => turn.turnId === turnId)?.cli;
|
|
8507
|
+
if (seat === void 0 || seat.send === null || cli === void 0) {
|
|
8508
|
+
return false;
|
|
8509
|
+
}
|
|
8510
|
+
if (!seat.hello.stoppableClis.includes(cli) || !this.host.requestStop(turnId, by)) {
|
|
8511
|
+
return false;
|
|
8512
|
+
}
|
|
8513
|
+
try {
|
|
8514
|
+
const answered = await this.request(seat, (request) => ({ type: "stop", request, turnId }), CONTROL_TIMEOUT_MS);
|
|
8515
|
+
this.log.info({ turnId, runner: seat.name, by }, "turn stop requested");
|
|
8516
|
+
return answered === true;
|
|
8517
|
+
} catch (error) {
|
|
8518
|
+
this.log.warn({ turnId, error: String(error) }, "could not stop a turn");
|
|
8519
|
+
return false;
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8154
8522
|
// -------------------------------------------------------------------------------------------
|
|
8155
8523
|
// The scheduler's runner
|
|
8156
8524
|
// -------------------------------------------------------------------------------------------
|
|
8525
|
+
/**
|
|
8526
|
+
* Delivers what arrived in a conversation into its turn in flight, when that turn's runner can
|
|
8527
|
+
* steer its CLI. `refused` leaves the wake to a turn of its own once this one ends.
|
|
8528
|
+
*/
|
|
8529
|
+
async steer(conversation) {
|
|
8530
|
+
const turn = this.host.turnIn(conversation.agent, conversation.project, conversation.thread);
|
|
8531
|
+
const seat = turn === null ? void 0 : this.seats.get(turn.runner);
|
|
8532
|
+
if (turn === null || seat === void 0 || seat.send === null) {
|
|
8533
|
+
return "refused";
|
|
8534
|
+
}
|
|
8535
|
+
if (!seat.hello.steerableClis.includes(turn.cli)) {
|
|
8536
|
+
return "refused";
|
|
8537
|
+
}
|
|
8538
|
+
const steer = await this.host.prepareSteer(turn.turnId);
|
|
8539
|
+
if (steer === "busy" || steer === "nothing") {
|
|
8540
|
+
return steer;
|
|
8541
|
+
}
|
|
8542
|
+
if (steer === "gone") {
|
|
8543
|
+
return "refused";
|
|
8544
|
+
}
|
|
8545
|
+
let accepted = false;
|
|
8546
|
+
try {
|
|
8547
|
+
accepted = await this.request(seat, (request) => ({ type: "steer", request, turnId: turn.turnId, steer }), CONTROL_TIMEOUT_MS) === true;
|
|
8548
|
+
} catch (error) {
|
|
8549
|
+
this.log.warn({ turnId: turn.turnId, error: String(error) }, "could not steer a turn");
|
|
8550
|
+
}
|
|
8551
|
+
this.host.steerAnswered(turn.turnId, steer.id, accepted);
|
|
8552
|
+
if (accepted) {
|
|
8553
|
+
this.log.info({ ...conversation, turnId: turn.turnId }, "steered a turn in flight");
|
|
8554
|
+
}
|
|
8555
|
+
return accepted ? "sent" : "refused";
|
|
8556
|
+
}
|
|
8157
8557
|
async assign(dispatch) {
|
|
8158
8558
|
const agent = await this.board.readAgent(dispatch.agent).catch(() => null);
|
|
8159
8559
|
if (agent === null) {
|
|
@@ -8272,6 +8672,37 @@ var RunnerHub = class {
|
|
|
8272
8672
|
});
|
|
8273
8673
|
return "done";
|
|
8274
8674
|
}
|
|
8675
|
+
/**
|
|
8676
|
+
* One path on a branch of a project's repository, read on the runner the project lives on.
|
|
8677
|
+
* NOT_FOUND when the project has no runner yet or the branch or the path does not exist, and
|
|
8678
|
+
* `RunnerAwayError` while its runner is away.
|
|
8679
|
+
*/
|
|
8680
|
+
async readBranch(project, branch, path10) {
|
|
8681
|
+
const record = await this.board.readProject(project);
|
|
8682
|
+
if (record.runner === void 0) {
|
|
8683
|
+
throw new BoardError("NOT_FOUND", `${project} has no repository yet, since no turn has run there`);
|
|
8684
|
+
}
|
|
8685
|
+
const seat = this.seats.get(record.runner);
|
|
8686
|
+
if (seat === void 0 || seat.send === null) {
|
|
8687
|
+
throw new RunnerAwayError(`runner ${record.runner}, where ${project} lives, is not connected`);
|
|
8688
|
+
}
|
|
8689
|
+
if (!seat.hello.branchReads) {
|
|
8690
|
+
throw new BoardError("INVALID_STATE", `runner ${seat.name}, where ${project} lives, cannot read files yet; upgrade it`);
|
|
8691
|
+
}
|
|
8692
|
+
const file = BranchFileSchema.nullable().parse(await this.request(seat, (request) => ({
|
|
8693
|
+
type: "file",
|
|
8694
|
+
request,
|
|
8695
|
+
read: {
|
|
8696
|
+
repo: { slug: record.slug, origin: record.repo, defaultBranch: record.defaultBranch },
|
|
8697
|
+
branch,
|
|
8698
|
+
path: path10
|
|
8699
|
+
}
|
|
8700
|
+
}), FILE_TIMEOUT_MS));
|
|
8701
|
+
if (file === null) {
|
|
8702
|
+
throw new BoardError("NOT_FOUND", `${branch} has no ${path10 === "" ? "commit" : path10}`);
|
|
8703
|
+
}
|
|
8704
|
+
return file;
|
|
8705
|
+
}
|
|
8275
8706
|
/** The models a CLI offers, asked of a connected runner that has it; none when no runner has it. */
|
|
8276
8707
|
async models(cli) {
|
|
8277
8708
|
const seat = this.byLoad().find((each) => each.send !== null && each.hello.clis.includes(cli));
|
|
@@ -8288,7 +8719,7 @@ var RunnerHub = class {
|
|
|
8288
8719
|
if (pending.runner === seat.name) {
|
|
8289
8720
|
this.pending.delete(id);
|
|
8290
8721
|
clearTimeout(pending.timer);
|
|
8291
|
-
pending.reject(new
|
|
8722
|
+
pending.reject(new RunnerAwayError(`runner ${seat.name} disconnected`));
|
|
8292
8723
|
}
|
|
8293
8724
|
}
|
|
8294
8725
|
await this.board.markRunner(seat.name, { status: "disconnected" }).catch((error) => {
|
|
@@ -8354,6 +8785,8 @@ var RunnerHub = class {
|
|
|
8354
8785
|
return {
|
|
8355
8786
|
name: seat.name,
|
|
8356
8787
|
residentClis: seat.hello.residentClis,
|
|
8788
|
+
steerableClis: seat.hello.steerableClis,
|
|
8789
|
+
stoppableClis: seat.hello.stoppableClis,
|
|
8357
8790
|
isWarm: (key) => seat.warm.has(key)
|
|
8358
8791
|
};
|
|
8359
8792
|
}
|
|
@@ -8363,19 +8796,19 @@ var RunnerHub = class {
|
|
|
8363
8796
|
throw new RunnerProtocolError(`turn ${turnId} is not ${runner}'s`);
|
|
8364
8797
|
}
|
|
8365
8798
|
}
|
|
8366
|
-
request(seat, message) {
|
|
8367
|
-
const request =
|
|
8799
|
+
request(seat, message, timeoutMs = this.requestTimeoutMs) {
|
|
8800
|
+
const request = randomUUID2();
|
|
8368
8801
|
return new Promise((resolve, reject) => {
|
|
8369
8802
|
const timer = setTimeout(() => {
|
|
8370
8803
|
this.pending.delete(request);
|
|
8371
|
-
reject(new
|
|
8372
|
-
},
|
|
8804
|
+
reject(new RunnerAwayError(`runner ${seat.name} did not answer in time`));
|
|
8805
|
+
}, timeoutMs);
|
|
8373
8806
|
timer.unref?.();
|
|
8374
8807
|
this.pending.set(request, { runner: seat.name, resolve, reject, timer });
|
|
8375
8808
|
if (seat.send === null || !seat.send(message(request))) {
|
|
8376
8809
|
this.pending.delete(request);
|
|
8377
8810
|
clearTimeout(timer);
|
|
8378
|
-
reject(new
|
|
8811
|
+
reject(new RunnerAwayError(`runner ${seat.name} is not connected`));
|
|
8379
8812
|
}
|
|
8380
8813
|
});
|
|
8381
8814
|
}
|
|
@@ -8939,6 +9372,9 @@ function createApp(deps) {
|
|
|
8939
9372
|
if (error instanceof RunnerProtocolError) {
|
|
8940
9373
|
return c.json({ error: "RUNNER_PROTOCOL", message: error.message }, 409);
|
|
8941
9374
|
}
|
|
9375
|
+
if (error instanceof RunnerAwayError) {
|
|
9376
|
+
return c.json({ error: "RUNNER_AWAY", message: error.message }, 503);
|
|
9377
|
+
}
|
|
8942
9378
|
return c.json({ error: "INTERNAL", message: error.message }, 500);
|
|
8943
9379
|
});
|
|
8944
9380
|
const authenticate = async (c, next) => {
|
|
@@ -9053,6 +9489,15 @@ function createApp(deps) {
|
|
|
9053
9489
|
api.get("/projects/:slug/knowledge", async (c) => c.json(await board2.listKnowledge(c.req.param("slug"))));
|
|
9054
9490
|
api.get("/tasks/:id", async (c) => c.json((await board2.findTask(c.req.param("id"))).task));
|
|
9055
9491
|
api.get("/tasks/:id/thread", async (c) => c.json(await board2.listThread(c.req.param("id"))));
|
|
9492
|
+
const taskFile = async (c, file) => {
|
|
9493
|
+
const { task } = await board2.findTask(UlidSchema.parse(c.req.param("id")));
|
|
9494
|
+
if (deps.runners === void 0) {
|
|
9495
|
+
throw new BoardError("NOT_FOUND", "this board has no runners to read a task's files from");
|
|
9496
|
+
}
|
|
9497
|
+
return c.json(await deps.runners.readBranch(task.project, `task/${task.id}`, BranchPathSchema.parse(file)));
|
|
9498
|
+
};
|
|
9499
|
+
api.get("/tasks/:id/files", (c) => taskFile(c, ""));
|
|
9500
|
+
api.get("/tasks/:id/files/:path{.+}", (c) => taskFile(c, c.req.param("path")));
|
|
9056
9501
|
api.get("/threads", async (c) => c.json(await board2.listThreads()));
|
|
9057
9502
|
api.get("/requests", async (c) => c.json(await board2.listRequests()));
|
|
9058
9503
|
api.get("/threads/:id", async (c) => {
|
|
@@ -9087,8 +9532,21 @@ function createApp(deps) {
|
|
|
9087
9532
|
running: scheduler2?.runningPairs ?? [],
|
|
9088
9533
|
pending: scheduler2?.pendingPairs ?? [],
|
|
9089
9534
|
resident: scheduler2?.residentPairs ?? [],
|
|
9090
|
-
signals: scheduler2?.activeSignals ?? []
|
|
9535
|
+
signals: scheduler2?.activeSignals ?? [],
|
|
9536
|
+
turns: deps.runners?.liveTurns() ?? []
|
|
9091
9537
|
}));
|
|
9538
|
+
api.post("/turns/:turnId/stop", async (c) => {
|
|
9539
|
+
const actor = c.get("actor");
|
|
9540
|
+
if (actor.role !== USER_ROLE) {
|
|
9541
|
+
throw new BoardError("FORBIDDEN", "only the user stops a turn");
|
|
9542
|
+
}
|
|
9543
|
+
const turnId = UlidSchema.parse(c.req.param("turnId"));
|
|
9544
|
+
const stopped = await deps.runners?.stop(turnId, actor.name) ?? false;
|
|
9545
|
+
if (!stopped) {
|
|
9546
|
+
throw new BoardError("NOT_FOUND", `turn ${turnId} is not in flight, or its runner cannot stop it`);
|
|
9547
|
+
}
|
|
9548
|
+
return c.json({ stopped });
|
|
9549
|
+
});
|
|
9092
9550
|
api.post("/pause", async (c) => {
|
|
9093
9551
|
await board2.setPaused(c.get("actor"), true);
|
|
9094
9552
|
return c.json({ paused: true });
|