@kubitnodes/stellaris 0.3.0 → 0.3.1
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 +86 -4
- package/dist/server/index.js +403 -27
- package/package.json +1 -1
- package/web/dist/assets/ChannelView-UppsahaH.js +1 -0
- package/web/dist/assets/CitizenView-CvZ5sEqw.js +2 -0
- package/web/dist/assets/{CitizensView-DB2TlGxn.js → CitizensView-D7VztzDL.js} +1 -1
- package/web/dist/assets/{KnowledgeView-l0I_R5Zd.js → KnowledgeView-D2jzcn4S.js} +1 -1
- package/web/dist/assets/{Markdown-C6mhAmbQ.js → Markdown-Dc8gCFZz.js} +4 -4
- package/web/dist/assets/{MessageItem-BR30twQh.js → MessageItem-BcDf-lgD.js} +1 -1
- package/web/dist/assets/MetricsView-lRYbAJSl.js +1 -0
- package/web/dist/assets/NeedsYouView-C-2cU-C5.js +1 -0
- package/web/dist/assets/{Overview-D47FfDDO.js → Overview-lKNZgZg_.js} +1 -1
- package/web/dist/assets/{Pane-BXpuXst6.js → Pane-DKfOLaXj.js} +1 -1
- package/web/dist/assets/{ProjectView-CxUA5nFw.js → ProjectView-CSoD4RFx.js} +1 -1
- package/web/dist/assets/ProposalView-BkcPia8W.js +1 -0
- package/web/dist/assets/{ProposalsView-BscIGUTT.js → ProposalsView-BQucmpBt.js} +1 -1
- package/web/dist/assets/{RunnersView-BypVHvzI.js → RunnersView-FbbPyu3y.js} +1 -1
- package/web/dist/assets/{SocietyView-C403e1uR.js → SocietyView-rpbJ_PcL.js} +1 -1
- package/web/dist/assets/SubjectThread-DQHf-MWF.js +1 -0
- package/web/dist/assets/{TaskView-B-4WGLWK.js → TaskView-6_oFPNkL.js} +1 -1
- package/web/dist/assets/{TasksView-CA02-WIW.js → TasksView-CJkLuWGV.js} +1 -1
- package/web/dist/assets/{ThreadCard-C4rb-53v.js → ThreadCard-Cjwceioc.js} +1 -1
- package/web/dist/assets/ThreadForms-CE9t-rVz.js +1 -0
- package/web/dist/assets/{ThreadView-CAdCu-y_.js → ThreadView-D09wIsYl.js} +1 -1
- package/web/dist/assets/citizen-BPzyDHNO.js +1 -0
- package/web/dist/assets/format-CRfcN5Lw.js +55 -0
- package/web/dist/assets/index-Ch3gW4_Z.js +23 -0
- package/web/dist/assets/index-NUcot4yn.css +2 -0
- package/web/dist/assets/{tasks-DZgkXWwu.js → tasks-DcMvT5ru.js} +1 -1
- package/web/dist/assets/{useStickyScroll-CVTdIoKj.js → useStickyScroll-B3_nksqI.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/MetricsView-BZ0WW2Do.js +0 -1
- package/web/dist/assets/NeedsYouView-C6CZNnJn.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/ThreadForms-Dh9ky_aU.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,10 @@ 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([]),
|
|
802
819
|
capabilities: z6.array(z6.string()).default([]),
|
|
803
820
|
/** Turns the runner runs at once, or null for no limit. */
|
|
804
821
|
slots: z6.number().int().positive().nullable(),
|
|
@@ -902,10 +919,20 @@ var LandRequestSchema = z6.object({
|
|
|
902
919
|
});
|
|
903
920
|
var MergeOutcomeSchema = z6.object({ ok: z6.boolean(), detail: z6.string() });
|
|
904
921
|
var ModelListSchema = z6.array(ModelOptionSchema);
|
|
922
|
+
var TurnSteerSchema = z6.object({ id: z6.uuid(), text: z6.string().min(1) });
|
|
905
923
|
var RunnerMessageSchema = z6.discriminatedUnion("type", [
|
|
906
924
|
z6.object({ type: z6.literal("turn"), job: TurnJobSchema }),
|
|
907
925
|
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 })
|
|
926
|
+
z6.object({ type: z6.literal("models"), request: z6.string().min(1), cli: CliKindSchema }),
|
|
927
|
+
/** Answered with whether the CLI accepted it; that it took it comes as a `steered` event. */
|
|
928
|
+
z6.object({
|
|
929
|
+
type: z6.literal("steer"),
|
|
930
|
+
request: z6.string().min(1),
|
|
931
|
+
turnId: UlidSchema,
|
|
932
|
+
steer: TurnSteerSchema
|
|
933
|
+
}),
|
|
934
|
+
/** Answered with whether the turn was there to stop; it ends `stopped`. */
|
|
935
|
+
z6.object({ type: z6.literal("stop"), request: z6.string().min(1), turnId: UlidSchema })
|
|
909
936
|
]);
|
|
910
937
|
var RunnerAnswerSchema = z6.discriminatedUnion("ok", [
|
|
911
938
|
z6.object({ ok: z6.literal(true), value: z6.unknown() }),
|
|
@@ -984,7 +1011,19 @@ var TurnRecordSchema = z7.object({
|
|
|
984
1011
|
/** The model the CLI reported for this turn, else the model configured for the agent, else null. */
|
|
985
1012
|
model: z7.string().nullable().default(null),
|
|
986
1013
|
/** The task branch the turn left work on, with its commit, as the runner reported it. */
|
|
987
|
-
work: TurnWorkSchema.optional()
|
|
1014
|
+
work: TurnWorkSchema.optional(),
|
|
1015
|
+
/** Who stopped the turn, for one that ended `stopped`. */
|
|
1016
|
+
stoppedBy: NameSchema.optional()
|
|
1017
|
+
});
|
|
1018
|
+
var RunningTurnSchema = z7.object({
|
|
1019
|
+
turnId: UlidSchema,
|
|
1020
|
+
agent: NameSchema,
|
|
1021
|
+
scope: NameSchema,
|
|
1022
|
+
thread: UlidSchema.optional(),
|
|
1023
|
+
cli: CliKindSchema,
|
|
1024
|
+
channel: ChannelRefSchema.optional(),
|
|
1025
|
+
steerable: z7.boolean(),
|
|
1026
|
+
stoppable: z7.boolean()
|
|
988
1027
|
});
|
|
989
1028
|
var SessionsFileSchema = z7.object({
|
|
990
1029
|
claude: z7.string().optional(),
|
|
@@ -1240,7 +1279,7 @@ var RunnerConfigSchema = z10.object({
|
|
|
1240
1279
|
});
|
|
1241
1280
|
|
|
1242
1281
|
// packages/shared/dist/version.js
|
|
1243
|
-
var STELLARIS_VERSION = "0.3.
|
|
1282
|
+
var STELLARIS_VERSION = "0.3.1";
|
|
1244
1283
|
|
|
1245
1284
|
// packages/board-core/dist/errors.js
|
|
1246
1285
|
var BoardError = class extends Error {
|
|
@@ -1787,10 +1826,12 @@ var TaskPayload = z11.object({
|
|
|
1787
1826
|
from: z11.string().optional()
|
|
1788
1827
|
});
|
|
1789
1828
|
var MessagePayload = z11.object({
|
|
1829
|
+
id: z11.string().optional(),
|
|
1790
1830
|
channel: z11.string(),
|
|
1791
1831
|
thread: z11.string().nullable().optional(),
|
|
1792
1832
|
mentions: z11.array(z11.string()).default([])
|
|
1793
1833
|
});
|
|
1834
|
+
var SteeredPayload = z11.object({ messages: z11.array(z11.string()) });
|
|
1794
1835
|
var SignalPayload = z11.object({
|
|
1795
1836
|
kind: z11.string(),
|
|
1796
1837
|
key: z11.string(),
|
|
@@ -1897,6 +1938,23 @@ function computeMetrics(input) {
|
|
|
1897
1938
|
}
|
|
1898
1939
|
break;
|
|
1899
1940
|
}
|
|
1941
|
+
case "turn.steered": {
|
|
1942
|
+
const steered = SteeredPayload.safeParse(event.payload);
|
|
1943
|
+
if (!steered.success) {
|
|
1944
|
+
break;
|
|
1945
|
+
}
|
|
1946
|
+
const delivered = new Set(steered.data.messages);
|
|
1947
|
+
const at = Date.parse(event.ts);
|
|
1948
|
+
const answered = waiting.filter((mention) => mention.agent === event.actor && mention.message !== null && delivered.has(mention.message));
|
|
1949
|
+
if (answered.length > 0) {
|
|
1950
|
+
latencies.set(event.actor, [
|
|
1951
|
+
...latencies.get(event.actor) ?? [],
|
|
1952
|
+
...answered.map((mention) => at - mention.at)
|
|
1953
|
+
]);
|
|
1954
|
+
waiting = waiting.filter((mention) => !answered.includes(mention));
|
|
1955
|
+
}
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1900
1958
|
case "task.completed": {
|
|
1901
1959
|
const task = TaskPayload.safeParse(event.payload);
|
|
1902
1960
|
if (counted && task.success) {
|
|
@@ -1961,6 +2019,7 @@ function computeMetrics(input) {
|
|
|
1961
2019
|
waiting.push({
|
|
1962
2020
|
agent: name,
|
|
1963
2021
|
at: Date.parse(event.ts),
|
|
2022
|
+
message: message.data.id ?? null,
|
|
1964
2023
|
thread,
|
|
1965
2024
|
scopes: [project, wakeScope(citizen, project)].filter((scope) => scope !== null)
|
|
1966
2025
|
});
|
|
@@ -4773,6 +4832,21 @@ ${rationale}`}`);
|
|
|
4773
4832
|
});
|
|
4774
4833
|
});
|
|
4775
4834
|
}
|
|
4835
|
+
/** Where a conversation's digest resumes: the newest message a turn there was shown. */
|
|
4836
|
+
async digestCursor(agent, scope, thread) {
|
|
4837
|
+
return this.mutex.run(async () => cursorOf(await this.readCursors(agent), scope, thread ?? null));
|
|
4838
|
+
}
|
|
4839
|
+
/** Records that a running turn took posts delivered into it, which wake latency counts to. */
|
|
4840
|
+
async recordSteered(input) {
|
|
4841
|
+
await this.mutex.run(async () => {
|
|
4842
|
+
await this.events.append("turn.steered", input.agent, {
|
|
4843
|
+
turnId: input.turnId,
|
|
4844
|
+
project: input.project,
|
|
4845
|
+
...input.thread === void 0 ? {} : { thread: input.thread },
|
|
4846
|
+
messages: [...input.messages]
|
|
4847
|
+
});
|
|
4848
|
+
});
|
|
4849
|
+
}
|
|
4776
4850
|
/** Where a conversation's session ids and last turn are kept: the scope's home, or a thread's. */
|
|
4777
4851
|
sessionDir(agent, scope, thread) {
|
|
4778
4852
|
return thread === void 0 ? this.paths.agentProject(agent, scope) : this.paths.agentThread(agent, scope, thread);
|
|
@@ -4842,6 +4916,14 @@ ${rationale}`}`);
|
|
|
4842
4916
|
}
|
|
4843
4917
|
await this.writeTurnRecord(parsed);
|
|
4844
4918
|
await this.refreshMember(parsed.agent);
|
|
4919
|
+
if (parsed.exitReason === "stopped") {
|
|
4920
|
+
await this.events.append("turn.stopped", parsed.stoppedBy ?? USER_NAME, {
|
|
4921
|
+
...parsed.id === void 0 ? {} : { turnId: parsed.id },
|
|
4922
|
+
agent: parsed.agent,
|
|
4923
|
+
project: parsed.project,
|
|
4924
|
+
...parsed.thread === void 0 ? {} : { thread: parsed.thread }
|
|
4925
|
+
});
|
|
4926
|
+
}
|
|
4845
4927
|
await this.events.append(failed ? "turn.failed" : "turn.completed", parsed.agent, {
|
|
4846
4928
|
...parsed.id === void 0 ? {} : { turnId: parsed.id },
|
|
4847
4929
|
project: parsed.project,
|
|
@@ -6133,7 +6215,10 @@ var PendingSchema = z13.object({
|
|
|
6133
6215
|
dispatch: TurnDispatchSchema,
|
|
6134
6216
|
readyAt: z13.number(),
|
|
6135
6217
|
stageWakes: z13.array(TriggerSchema).optional(),
|
|
6136
|
-
holder: z13.boolean().optional()
|
|
6218
|
+
holder: z13.boolean().optional(),
|
|
6219
|
+
newestMessage: UlidSchema.optional(),
|
|
6220
|
+
onlyMessages: z13.boolean().optional(),
|
|
6221
|
+
steeredThrough: UlidSchema.optional()
|
|
6137
6222
|
});
|
|
6138
6223
|
var StateSchema = z13.object({
|
|
6139
6224
|
cursor: z13.string().nullable(),
|
|
@@ -6152,6 +6237,16 @@ var StateSchema = z13.object({
|
|
|
6152
6237
|
/** When each member last reflected, or was first seen; the cadence counts from here. */
|
|
6153
6238
|
lastReflection: z13.record(z13.string(), z13.string()).default({})
|
|
6154
6239
|
});
|
|
6240
|
+
function messageOf(trigger) {
|
|
6241
|
+
return trigger.kind === "mention" || trigger.kind === "user_post" ? trigger.messageId : void 0;
|
|
6242
|
+
}
|
|
6243
|
+
function later(a, b) {
|
|
6244
|
+
if (a === void 0)
|
|
6245
|
+
return b;
|
|
6246
|
+
if (b === void 0)
|
|
6247
|
+
return a;
|
|
6248
|
+
return a > b ? a : b;
|
|
6249
|
+
}
|
|
6155
6250
|
var SILENT_LOG = { info() {
|
|
6156
6251
|
}, warn() {
|
|
6157
6252
|
}, error() {
|
|
@@ -6191,6 +6286,8 @@ var Scheduler = class {
|
|
|
6191
6286
|
pending = /* @__PURE__ */ new Map();
|
|
6192
6287
|
/** Turns in flight by session key, each with the lane it holds (see `laneOf`). */
|
|
6193
6288
|
running = /* @__PURE__ */ new Map();
|
|
6289
|
+
/** Steers under way, by session key; a pass does not wait for a runner to answer one. */
|
|
6290
|
+
steering = /* @__PURE__ */ new Map();
|
|
6194
6291
|
/** Tasks whose completion effect waits for the turn that finished their last stage to end. */
|
|
6195
6292
|
completions = /* @__PURE__ */ new Map();
|
|
6196
6293
|
completing = /* @__PURE__ */ new Set();
|
|
@@ -6245,7 +6342,8 @@ var Scheduler = class {
|
|
|
6245
6342
|
await this.passing?.catch(() => void 0);
|
|
6246
6343
|
await Promise.allSettled([
|
|
6247
6344
|
...[...this.running.values()].map((turn) => turn.promise),
|
|
6248
|
-
...this.completing
|
|
6345
|
+
...this.completing,
|
|
6346
|
+
...this.steering.values()
|
|
6249
6347
|
]);
|
|
6250
6348
|
}
|
|
6251
6349
|
/** One pass, unless one is under way already. */
|
|
@@ -6292,6 +6390,7 @@ var Scheduler = class {
|
|
|
6292
6390
|
/** Waits for every running turn and in-flight completion to settle. Used by tests and by stop(). */
|
|
6293
6391
|
async drain() {
|
|
6294
6392
|
await Promise.allSettled([
|
|
6393
|
+
...this.steering.values(),
|
|
6295
6394
|
...[...this.running.values()].map((turn) => turn.promise),
|
|
6296
6395
|
...this.completing
|
|
6297
6396
|
]);
|
|
@@ -6708,6 +6807,7 @@ var Scheduler = class {
|
|
|
6708
6807
|
const key = sessionKey(agent, project, thread?.id);
|
|
6709
6808
|
const readyAt = now + debounce;
|
|
6710
6809
|
const existing = this.pending.get(key);
|
|
6810
|
+
const message = messageOf(trigger);
|
|
6711
6811
|
if (existing === void 0) {
|
|
6712
6812
|
this.pending.set(key, {
|
|
6713
6813
|
dispatch: {
|
|
@@ -6719,10 +6819,13 @@ var Scheduler = class {
|
|
|
6719
6819
|
onboarding: trigger.kind === "onboarding"
|
|
6720
6820
|
},
|
|
6721
6821
|
readyAt,
|
|
6722
|
-
...trigger.kind === "stage" ? { stageWakes: [trigger] } : {}
|
|
6822
|
+
...trigger.kind === "stage" ? { stageWakes: [trigger] } : {},
|
|
6823
|
+
...message === void 0 ? {} : { newestMessage: message },
|
|
6824
|
+
onlyMessages: message !== void 0
|
|
6723
6825
|
});
|
|
6724
6826
|
return;
|
|
6725
6827
|
}
|
|
6828
|
+
const newestMessage = later(existing.newestMessage, message);
|
|
6726
6829
|
const keepNew = decision.priority > existing.dispatch.priority;
|
|
6727
6830
|
const priority = keepNew ? decision.priority : existing.dispatch.priority;
|
|
6728
6831
|
this.pending.set(key, {
|
|
@@ -6736,7 +6839,10 @@ var Scheduler = class {
|
|
|
6736
6839
|
...existing.stageWakes !== void 0 && trigger.kind === "stage" ? {
|
|
6737
6840
|
stageWakes: [...existing.stageWakes, trigger],
|
|
6738
6841
|
...existing.holder === true ? { holder: true } : {}
|
|
6739
|
-
} : {}
|
|
6842
|
+
} : {},
|
|
6843
|
+
...newestMessage === void 0 ? {} : { newestMessage },
|
|
6844
|
+
onlyMessages: existing.onlyMessages === true && message !== void 0,
|
|
6845
|
+
...existing.steeredThrough === void 0 ? {} : { steeredThrough: existing.steeredThrough }
|
|
6740
6846
|
});
|
|
6741
6847
|
}
|
|
6742
6848
|
/** Debounce coalesces chatter: mentions, stage handoffs, and bursts of signals. Everything else runs at once. */
|
|
@@ -7186,7 +7292,51 @@ var Scheduler = class {
|
|
|
7186
7292
|
}
|
|
7187
7293
|
return false;
|
|
7188
7294
|
}
|
|
7295
|
+
/**
|
|
7296
|
+
* Delivers ready message wakes into the turn already running in their conversation, rather than
|
|
7297
|
+
* keeping them for a turn of their own. Each stays queued until that turn ends and is checked
|
|
7298
|
+
* again then, so a steer refused, or left untaken, loses nothing. A pass never waits on a runner.
|
|
7299
|
+
*/
|
|
7300
|
+
steerRunning(now) {
|
|
7301
|
+
const steer = this.runner.steer?.bind(this.runner);
|
|
7302
|
+
if (steer === void 0) {
|
|
7303
|
+
return;
|
|
7304
|
+
}
|
|
7305
|
+
for (const [key, item] of this.pending) {
|
|
7306
|
+
const through = item.newestMessage;
|
|
7307
|
+
if (item.readyAt > now || through === void 0 || !this.running.has(key) || this.steering.has(key) || item.steeredThrough !== void 0 && through <= item.steeredThrough) {
|
|
7308
|
+
continue;
|
|
7309
|
+
}
|
|
7310
|
+
const { dispatch } = item;
|
|
7311
|
+
const attempt = steer({
|
|
7312
|
+
agent: dispatch.agent,
|
|
7313
|
+
project: dispatch.project,
|
|
7314
|
+
...dispatch.thread === void 0 ? {} : { thread: dispatch.thread.id }
|
|
7315
|
+
}).then((outcome) => {
|
|
7316
|
+
const current = this.pending.get(key);
|
|
7317
|
+
if (outcome !== "busy" && current !== void 0) {
|
|
7318
|
+
current.steeredThrough = later(current.steeredThrough, through);
|
|
7319
|
+
}
|
|
7320
|
+
return void 0;
|
|
7321
|
+
}).catch((error) => {
|
|
7322
|
+
this.log.warn({ key, error: String(error) }, "steering failed");
|
|
7323
|
+
}).finally(() => {
|
|
7324
|
+
this.steering.delete(key);
|
|
7325
|
+
});
|
|
7326
|
+
this.steering.set(key, attempt);
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
7329
|
+
/** Whether every message that woke a queued turn was read in its conversation, by a turn already. */
|
|
7330
|
+
async alreadyRead(item) {
|
|
7331
|
+
if (item.onlyMessages !== true || item.newestMessage === void 0) {
|
|
7332
|
+
return false;
|
|
7333
|
+
}
|
|
7334
|
+
const { dispatch } = item;
|
|
7335
|
+
const cursor = await this.board.digestCursor(dispatch.agent, dispatch.project, dispatch.thread?.id);
|
|
7336
|
+
return cursor !== null && cursor >= item.newestMessage;
|
|
7337
|
+
}
|
|
7189
7338
|
async dispatchReady(now) {
|
|
7339
|
+
this.steerRunning(now);
|
|
7190
7340
|
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
7341
|
for (const [key, item] of ready) {
|
|
7192
7342
|
if (this.running.size >= this.concurrency) {
|
|
@@ -7197,6 +7347,10 @@ var Scheduler = class {
|
|
|
7197
7347
|
continue;
|
|
7198
7348
|
}
|
|
7199
7349
|
this.pending.delete(key);
|
|
7350
|
+
if (await this.alreadyRead(item)) {
|
|
7351
|
+
this.log.info({ agent: item.dispatch.agent, project: item.dispatch.project }, "dropping message wake, its conversation has read the message");
|
|
7352
|
+
continue;
|
|
7353
|
+
}
|
|
7200
7354
|
let { dispatch } = item;
|
|
7201
7355
|
if (item.stageWakes !== void 0) {
|
|
7202
7356
|
const current = await this.stillWaiting(dispatch.agent, item.stageWakes, item.holder === true);
|
|
@@ -7258,11 +7412,15 @@ var Scheduler = class {
|
|
|
7258
7412
|
}
|
|
7259
7413
|
};
|
|
7260
7414
|
|
|
7415
|
+
// packages/turn-host/dist/host.js
|
|
7416
|
+
import { randomUUID } from "node:crypto";
|
|
7417
|
+
|
|
7261
7418
|
// packages/turn-host/dist/render.js
|
|
7262
7419
|
var TURN_CONTRACT = [
|
|
7263
7420
|
"- 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
7421
|
"- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it.",
|
|
7265
7422
|
"- 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.",
|
|
7423
|
+
"- 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
7424
|
"- Silence is allowed. If the digest needs no reply, post nothing.",
|
|
7267
7425
|
"- 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
7426
|
"- 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 +7570,26 @@ function rosterLine(member) {
|
|
|
7412
7570
|
].filter((part) => part.length > 0);
|
|
7413
7571
|
return `- ${member.name}: ${parts.join("; ")}. Profile: ${profileLine(member.profile)}`;
|
|
7414
7572
|
}
|
|
7573
|
+
function messageLines(messages, threads) {
|
|
7574
|
+
const lines = [];
|
|
7575
|
+
for (const message of messages) {
|
|
7576
|
+
const thread = message.thread === void 0 ? void 0 : threads?.get(message.thread);
|
|
7577
|
+
const where = message.thread === void 0 ? message.channel : thread === void 0 ? `${message.channel} thread ${message.thread}` : `${message.channel} thread "${thread.title}" (${message.thread})`;
|
|
7578
|
+
const step = message.step === void 0 ? "" : `, who ${stepPhrase(message.step)}`;
|
|
7579
|
+
lines.push(`### [${message.ts}] ${where} from @${message.author}${step} (message ${message.id})`, "", clip(message.body), "");
|
|
7580
|
+
}
|
|
7581
|
+
return lines;
|
|
7582
|
+
}
|
|
7583
|
+
function buildSteerText(messages, threads) {
|
|
7584
|
+
return `${[
|
|
7585
|
+
`## New in this conversation while you work (${messages.length})`,
|
|
7586
|
+
"",
|
|
7587
|
+
"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.",
|
|
7588
|
+
"",
|
|
7589
|
+
...messageLines(messages, threads)
|
|
7590
|
+
].join("\n").trimEnd()}
|
|
7591
|
+
`;
|
|
7592
|
+
}
|
|
7415
7593
|
function buildTurnPrompt(input) {
|
|
7416
7594
|
const { dispatch } = input;
|
|
7417
7595
|
const lines = [];
|
|
@@ -7464,6 +7642,9 @@ function buildTurnPrompt(input) {
|
|
|
7464
7642
|
lines.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
|
|
7465
7643
|
}
|
|
7466
7644
|
const last = input.lastTurn;
|
|
7645
|
+
if (last?.exitReason === "stopped") {
|
|
7646
|
+
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.`);
|
|
7647
|
+
}
|
|
7467
7648
|
if (last !== null && (last.exitReason === "error" || last.exitReason === "timeout")) {
|
|
7468
7649
|
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
7650
|
}
|
|
@@ -7545,12 +7726,7 @@ function buildTurnPrompt(input) {
|
|
|
7545
7726
|
if (input.messages.length === 0) {
|
|
7546
7727
|
lines.push("Nothing new.");
|
|
7547
7728
|
} 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
|
-
}
|
|
7729
|
+
lines.push(...messageLines(input.messages, input.threads));
|
|
7554
7730
|
}
|
|
7555
7731
|
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
7732
|
return `${lines.join("\n")}
|
|
@@ -7757,7 +7933,10 @@ var TurnHost = class {
|
|
|
7757
7933
|
runner: runner.name,
|
|
7758
7934
|
actor,
|
|
7759
7935
|
record,
|
|
7760
|
-
cursorAfter,
|
|
7936
|
+
delivered: cursorAfter,
|
|
7937
|
+
steers: /* @__PURE__ */ new Map(),
|
|
7938
|
+
outstanding: null,
|
|
7939
|
+
stopRequestedBy: null,
|
|
7761
7940
|
held: held.map((each) => each.id),
|
|
7762
7941
|
taskId,
|
|
7763
7942
|
costSoFarUsd,
|
|
@@ -7779,17 +7958,130 @@ var TurnHost = class {
|
|
|
7779
7958
|
}, "turn starting");
|
|
7780
7959
|
return { job, ended };
|
|
7781
7960
|
}
|
|
7782
|
-
/**
|
|
7783
|
-
|
|
7961
|
+
/**
|
|
7962
|
+
* Steps of a turn in flight, as the runner received them. A steer the CLI took moves what the
|
|
7963
|
+
* turn has been shown past its messages, and its step carries them and the text it delivered.
|
|
7964
|
+
*/
|
|
7965
|
+
async addEvents(turnId, entries) {
|
|
7784
7966
|
const turn = this.open.get(turnId);
|
|
7785
7967
|
if (turn === void 0) {
|
|
7786
7968
|
return;
|
|
7787
7969
|
}
|
|
7788
|
-
for (const
|
|
7970
|
+
for (const received of entries) {
|
|
7971
|
+
let entry = received;
|
|
7972
|
+
const { event } = received;
|
|
7973
|
+
if (event.type === "steered") {
|
|
7974
|
+
const sent = turn.steers.get(event.steer);
|
|
7975
|
+
if (sent !== void 0) {
|
|
7976
|
+
entry = {
|
|
7977
|
+
...received,
|
|
7978
|
+
event: { ...event, messages: [...sent.messages], text: sent.text }
|
|
7979
|
+
};
|
|
7980
|
+
if (turn.delivered === null || sent.through > turn.delivered) {
|
|
7981
|
+
turn.delivered = sent.through;
|
|
7982
|
+
}
|
|
7983
|
+
if (turn.outstanding === event.steer) {
|
|
7984
|
+
turn.outstanding = null;
|
|
7985
|
+
}
|
|
7986
|
+
await this.board.recordSteered({
|
|
7987
|
+
turnId,
|
|
7988
|
+
agent: turn.job.agent,
|
|
7989
|
+
project: turn.job.scope,
|
|
7990
|
+
thread: turn.job.thread,
|
|
7991
|
+
messages: sent.messages
|
|
7992
|
+
}).catch((error) => {
|
|
7993
|
+
this.log.warn({ turnId, error: String(error) }, "could not record a steer");
|
|
7994
|
+
});
|
|
7995
|
+
}
|
|
7996
|
+
}
|
|
7789
7997
|
turn.transcript.push(entry);
|
|
7790
7998
|
this.onEvent?.(turn.job.agent, turn.job.scope, entry.event, turn.job.thread);
|
|
7791
7999
|
}
|
|
7792
8000
|
}
|
|
8001
|
+
/** The turn in flight in a conversation, if any. */
|
|
8002
|
+
turnIn(agent, scope, thread) {
|
|
8003
|
+
for (const turn of this.open.values()) {
|
|
8004
|
+
const { job } = turn;
|
|
8005
|
+
if (job.agent === agent && job.scope === scope && job.thread === thread) {
|
|
8006
|
+
return { turnId: job.turnId, runner: turn.runner, cli: job.cli };
|
|
8007
|
+
}
|
|
8008
|
+
}
|
|
8009
|
+
return null;
|
|
8010
|
+
}
|
|
8011
|
+
/** The turns in flight, for the interface: their conversations and where a mention reaches them. */
|
|
8012
|
+
runningTurns() {
|
|
8013
|
+
return [...this.open.values()].map((turn) => ({
|
|
8014
|
+
turnId: turn.job.turnId,
|
|
8015
|
+
agent: turn.job.agent,
|
|
8016
|
+
scope: turn.job.scope,
|
|
8017
|
+
...turn.job.thread === void 0 ? {} : { thread: turn.job.thread },
|
|
8018
|
+
cli: turn.job.cli,
|
|
8019
|
+
...turn.record.trigger.channel === void 0 ? {} : { channel: turn.record.trigger.channel },
|
|
8020
|
+
runner: turn.runner
|
|
8021
|
+
}));
|
|
8022
|
+
}
|
|
8023
|
+
/**
|
|
8024
|
+
* What a steer into a turn would deliver: the conversation's digest from what the turn has been
|
|
8025
|
+
* shown, rendered as the prompt renders its digest. `busy` while a steer it was sent is not yet
|
|
8026
|
+
* taken or refused, and `nothing` when everything new was in what it was shown.
|
|
8027
|
+
*/
|
|
8028
|
+
async prepareSteer(turnId) {
|
|
8029
|
+
const turn = this.open.get(turnId);
|
|
8030
|
+
if (turn === void 0 || turn.stopRequestedBy !== null) {
|
|
8031
|
+
return "gone";
|
|
8032
|
+
}
|
|
8033
|
+
if (turn.outstanding !== null) {
|
|
8034
|
+
return "busy";
|
|
8035
|
+
}
|
|
8036
|
+
const { job } = turn;
|
|
8037
|
+
const digest = await this.board.readDigest({
|
|
8038
|
+
...turn.actor,
|
|
8039
|
+
scope: job.scope,
|
|
8040
|
+
...job.thread === void 0 ? {} : { thread: job.thread }
|
|
8041
|
+
}, {
|
|
8042
|
+
...turn.delivered === null ? {} : { since_cursor: turn.delivered },
|
|
8043
|
+
advance: false,
|
|
8044
|
+
limit: 50
|
|
8045
|
+
});
|
|
8046
|
+
const last = digest.messages.at(-1);
|
|
8047
|
+
if (!this.open.has(turnId) || turn.outstanding !== null) {
|
|
8048
|
+
return "gone";
|
|
8049
|
+
}
|
|
8050
|
+
if (last === void 0) {
|
|
8051
|
+
return "nothing";
|
|
8052
|
+
}
|
|
8053
|
+
const threads = /* @__PURE__ */ new Map();
|
|
8054
|
+
for (const id of new Set(digest.messages.flatMap((message) => message.thread ?? []))) {
|
|
8055
|
+
const record = await this.board.readThread(id).catch(() => void 0);
|
|
8056
|
+
if (record !== void 0) {
|
|
8057
|
+
threads.set(id, record);
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
const steer = { id: randomUUID(), text: buildSteerText(digest.messages, threads) };
|
|
8061
|
+
turn.steers.set(steer.id, {
|
|
8062
|
+
through: last.id,
|
|
8063
|
+
messages: digest.messages.map((message) => message.id),
|
|
8064
|
+
text: steer.text
|
|
8065
|
+
});
|
|
8066
|
+
turn.outstanding = steer.id;
|
|
8067
|
+
return steer;
|
|
8068
|
+
}
|
|
8069
|
+
/** The runner's answer to a steer: one it did not take frees the turn for the next. */
|
|
8070
|
+
steerAnswered(turnId, steerId, accepted) {
|
|
8071
|
+
const turn = this.open.get(turnId);
|
|
8072
|
+
if (turn !== void 0 && !accepted && turn.outstanding === steerId) {
|
|
8073
|
+
turn.outstanding = null;
|
|
8074
|
+
}
|
|
8075
|
+
}
|
|
8076
|
+
/** Marks a turn stopped by `by`; returns false for a turn not in flight. */
|
|
8077
|
+
requestStop(turnId, by) {
|
|
8078
|
+
const turn = this.open.get(turnId);
|
|
8079
|
+
if (turn === void 0) {
|
|
8080
|
+
return false;
|
|
8081
|
+
}
|
|
8082
|
+
turn.stopRequestedBy ??= by;
|
|
8083
|
+
return true;
|
|
8084
|
+
}
|
|
7793
8085
|
/**
|
|
7794
8086
|
* Records a turn's end from its outcome. Returns null for a turn the host does not know, else
|
|
7795
8087
|
* whether the task the turn worked on has ended, so the runner may drop its worktree.
|
|
@@ -7820,10 +8112,11 @@ var TurnHost = class {
|
|
|
7820
8112
|
sessionCostUsd: outcome.sessionCostUsd ?? turn.costSoFarUsd,
|
|
7821
8113
|
toolCalls: turn.transcript.filter((entry) => entry.event.type === "tool_call").length,
|
|
7822
8114
|
model: outcome.model ?? turn.configuredModel,
|
|
7823
|
-
...outcome.work === null ? {} : { work: outcome.work }
|
|
8115
|
+
...outcome.work === null ? {} : { work: outcome.work },
|
|
8116
|
+
...outcome.exitReason === "stopped" ? { stoppedBy: turn.stopRequestedBy ?? USER_NAME } : {}
|
|
7824
8117
|
};
|
|
7825
|
-
if (outcome.exitReason === "completed" || outcome.exitReason === "blocked") {
|
|
7826
|
-
await this.board.setDigestCursor(job.agent, job.scope, turn.
|
|
8118
|
+
if (outcome.exitReason === "completed" || outcome.exitReason === "blocked" || outcome.exitReason === "stopped") {
|
|
8119
|
+
await this.board.setDigestCursor(job.agent, job.scope, turn.delivered, job.thread);
|
|
7827
8120
|
await this.renewLeases(turn.actor, turn.held);
|
|
7828
8121
|
if (outcome.status?.needsUserDecision === true) {
|
|
7829
8122
|
await this.askUser(turn.actor, job.scope, job.thread, turn.record.startedAt, outcome.status.summary);
|
|
@@ -7999,7 +8292,7 @@ var TurnHost = class {
|
|
|
7999
8292
|
};
|
|
8000
8293
|
|
|
8001
8294
|
// packages/turn-host/dist/hub.js
|
|
8002
|
-
import { randomUUID } from "node:crypto";
|
|
8295
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
8003
8296
|
var RunnerProtocolError = class extends Error {
|
|
8004
8297
|
};
|
|
8005
8298
|
var SILENT2 = { info() {
|
|
@@ -8008,6 +8301,7 @@ var SILENT2 = { info() {
|
|
|
8008
8301
|
} };
|
|
8009
8302
|
var DEFAULT_GRACE_MS = 2 * 6e4;
|
|
8010
8303
|
var DEFAULT_REQUEST_TIMEOUT_MS = 10 * 6e4;
|
|
8304
|
+
var CONTROL_TIMEOUT_MS = 15e3;
|
|
8011
8305
|
var RunnerHub = class {
|
|
8012
8306
|
board;
|
|
8013
8307
|
host;
|
|
@@ -8120,7 +8414,7 @@ var RunnerHub = class {
|
|
|
8120
8414
|
}
|
|
8121
8415
|
async turnEvents(runner, turnId, entries) {
|
|
8122
8416
|
this.assertOwns(runner, turnId);
|
|
8123
|
-
this.host.addEvents(turnId, entries);
|
|
8417
|
+
await this.host.addEvents(turnId, entries);
|
|
8124
8418
|
}
|
|
8125
8419
|
async turnOutcome(runner, turnId, outcome) {
|
|
8126
8420
|
this.assertOwns(runner, turnId);
|
|
@@ -8151,9 +8445,76 @@ var RunnerHub = class {
|
|
|
8151
8445
|
mayTouchHome(runner, agent) {
|
|
8152
8446
|
return this.host.hasTurnFor(runner, agent);
|
|
8153
8447
|
}
|
|
8448
|
+
/** The turns in flight, and whether each one's runner can steer and stop it. */
|
|
8449
|
+
liveTurns() {
|
|
8450
|
+
return this.host.runningTurns().map(({ runner, ...turn }) => {
|
|
8451
|
+
const seat = this.seats.get(runner);
|
|
8452
|
+
const connected = seat !== void 0 && seat.send !== null;
|
|
8453
|
+
return {
|
|
8454
|
+
...turn,
|
|
8455
|
+
steerable: connected && seat.hello.steerableClis.includes(turn.cli),
|
|
8456
|
+
stoppable: connected && seat.hello.stoppableClis.includes(turn.cli)
|
|
8457
|
+
};
|
|
8458
|
+
});
|
|
8459
|
+
}
|
|
8460
|
+
/**
|
|
8461
|
+
* Stops a turn in flight, as the user asked: its runner interrupts the CLI and the turn ends
|
|
8462
|
+
* `stopped`. False for a turn not in flight or on a runner that cannot stop it.
|
|
8463
|
+
*/
|
|
8464
|
+
async stop(turnId, by) {
|
|
8465
|
+
const runner = this.host.runnerOf(turnId);
|
|
8466
|
+
const seat = runner === void 0 ? void 0 : this.seats.get(runner);
|
|
8467
|
+
const cli = this.host.runningTurns().find((turn) => turn.turnId === turnId)?.cli;
|
|
8468
|
+
if (seat === void 0 || seat.send === null || cli === void 0) {
|
|
8469
|
+
return false;
|
|
8470
|
+
}
|
|
8471
|
+
if (!seat.hello.stoppableClis.includes(cli) || !this.host.requestStop(turnId, by)) {
|
|
8472
|
+
return false;
|
|
8473
|
+
}
|
|
8474
|
+
try {
|
|
8475
|
+
const answered = await this.request(seat, (request) => ({ type: "stop", request, turnId }), CONTROL_TIMEOUT_MS);
|
|
8476
|
+
this.log.info({ turnId, runner: seat.name, by }, "turn stop requested");
|
|
8477
|
+
return answered === true;
|
|
8478
|
+
} catch (error) {
|
|
8479
|
+
this.log.warn({ turnId, error: String(error) }, "could not stop a turn");
|
|
8480
|
+
return false;
|
|
8481
|
+
}
|
|
8482
|
+
}
|
|
8154
8483
|
// -------------------------------------------------------------------------------------------
|
|
8155
8484
|
// The scheduler's runner
|
|
8156
8485
|
// -------------------------------------------------------------------------------------------
|
|
8486
|
+
/**
|
|
8487
|
+
* Delivers what arrived in a conversation into its turn in flight, when that turn's runner can
|
|
8488
|
+
* steer its CLI. `refused` leaves the wake to a turn of its own once this one ends.
|
|
8489
|
+
*/
|
|
8490
|
+
async steer(conversation) {
|
|
8491
|
+
const turn = this.host.turnIn(conversation.agent, conversation.project, conversation.thread);
|
|
8492
|
+
const seat = turn === null ? void 0 : this.seats.get(turn.runner);
|
|
8493
|
+
if (turn === null || seat === void 0 || seat.send === null) {
|
|
8494
|
+
return "refused";
|
|
8495
|
+
}
|
|
8496
|
+
if (!seat.hello.steerableClis.includes(turn.cli)) {
|
|
8497
|
+
return "refused";
|
|
8498
|
+
}
|
|
8499
|
+
const steer = await this.host.prepareSteer(turn.turnId);
|
|
8500
|
+
if (steer === "busy" || steer === "nothing") {
|
|
8501
|
+
return steer;
|
|
8502
|
+
}
|
|
8503
|
+
if (steer === "gone") {
|
|
8504
|
+
return "refused";
|
|
8505
|
+
}
|
|
8506
|
+
let accepted = false;
|
|
8507
|
+
try {
|
|
8508
|
+
accepted = await this.request(seat, (request) => ({ type: "steer", request, turnId: turn.turnId, steer }), CONTROL_TIMEOUT_MS) === true;
|
|
8509
|
+
} catch (error) {
|
|
8510
|
+
this.log.warn({ turnId: turn.turnId, error: String(error) }, "could not steer a turn");
|
|
8511
|
+
}
|
|
8512
|
+
this.host.steerAnswered(turn.turnId, steer.id, accepted);
|
|
8513
|
+
if (accepted) {
|
|
8514
|
+
this.log.info({ ...conversation, turnId: turn.turnId }, "steered a turn in flight");
|
|
8515
|
+
}
|
|
8516
|
+
return accepted ? "sent" : "refused";
|
|
8517
|
+
}
|
|
8157
8518
|
async assign(dispatch) {
|
|
8158
8519
|
const agent = await this.board.readAgent(dispatch.agent).catch(() => null);
|
|
8159
8520
|
if (agent === null) {
|
|
@@ -8354,6 +8715,8 @@ var RunnerHub = class {
|
|
|
8354
8715
|
return {
|
|
8355
8716
|
name: seat.name,
|
|
8356
8717
|
residentClis: seat.hello.residentClis,
|
|
8718
|
+
steerableClis: seat.hello.steerableClis,
|
|
8719
|
+
stoppableClis: seat.hello.stoppableClis,
|
|
8357
8720
|
isWarm: (key) => seat.warm.has(key)
|
|
8358
8721
|
};
|
|
8359
8722
|
}
|
|
@@ -8363,13 +8726,13 @@ var RunnerHub = class {
|
|
|
8363
8726
|
throw new RunnerProtocolError(`turn ${turnId} is not ${runner}'s`);
|
|
8364
8727
|
}
|
|
8365
8728
|
}
|
|
8366
|
-
request(seat, message) {
|
|
8367
|
-
const request =
|
|
8729
|
+
request(seat, message, timeoutMs = this.requestTimeoutMs) {
|
|
8730
|
+
const request = randomUUID2();
|
|
8368
8731
|
return new Promise((resolve, reject) => {
|
|
8369
8732
|
const timer = setTimeout(() => {
|
|
8370
8733
|
this.pending.delete(request);
|
|
8371
8734
|
reject(new Error(`runner ${seat.name} did not answer in time`));
|
|
8372
|
-
},
|
|
8735
|
+
}, timeoutMs);
|
|
8373
8736
|
timer.unref?.();
|
|
8374
8737
|
this.pending.set(request, { runner: seat.name, resolve, reject, timer });
|
|
8375
8738
|
if (seat.send === null || !seat.send(message(request))) {
|
|
@@ -9087,8 +9450,21 @@ function createApp(deps) {
|
|
|
9087
9450
|
running: scheduler2?.runningPairs ?? [],
|
|
9088
9451
|
pending: scheduler2?.pendingPairs ?? [],
|
|
9089
9452
|
resident: scheduler2?.residentPairs ?? [],
|
|
9090
|
-
signals: scheduler2?.activeSignals ?? []
|
|
9453
|
+
signals: scheduler2?.activeSignals ?? [],
|
|
9454
|
+
turns: deps.runners?.liveTurns() ?? []
|
|
9091
9455
|
}));
|
|
9456
|
+
api.post("/turns/:turnId/stop", async (c) => {
|
|
9457
|
+
const actor = c.get("actor");
|
|
9458
|
+
if (actor.role !== USER_ROLE) {
|
|
9459
|
+
throw new BoardError("FORBIDDEN", "only the user stops a turn");
|
|
9460
|
+
}
|
|
9461
|
+
const turnId = UlidSchema.parse(c.req.param("turnId"));
|
|
9462
|
+
const stopped = await deps.runners?.stop(turnId, actor.name) ?? false;
|
|
9463
|
+
if (!stopped) {
|
|
9464
|
+
throw new BoardError("NOT_FOUND", `turn ${turnId} is not in flight, or its runner cannot stop it`);
|
|
9465
|
+
}
|
|
9466
|
+
return c.json({ stopped });
|
|
9467
|
+
});
|
|
9092
9468
|
api.post("/pause", async (c) => {
|
|
9093
9469
|
await board2.setPaused(c.get("actor"), true);
|
|
9094
9470
|
return c.json({ paused: true });
|