@kevin5251984/guild 0.2.20 → 0.2.22

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/src/handlers.ts CHANGED
@@ -75,6 +75,8 @@ export type HandlerExtras = {
75
75
  turn?: (input: Parameters<typeof chatReply>[0]) => Promise<ChatReply>;
76
76
  /** Bot ids the client already resolved from @mentions. */
77
77
  mentions?: string[];
78
+ /** Hermes: cron child sessions cannot manage cron. */
79
+ cronRun?: boolean;
78
80
  };
79
81
 
80
82
  export function healthPayload(): HealthResponse {
@@ -522,6 +524,7 @@ export function workspace(store: GuildStore) {
522
524
  startedAt: turn.startedAt || "",
523
525
  thinking: turn.thinking,
524
526
  steps: turn.steps,
527
+ ...(turn.paused ? { paused: true } : {}),
525
528
  },
526
529
  ];
527
530
  });
@@ -913,6 +916,8 @@ export function chatTurnForBot(
913
916
  history,
914
917
  userMessage,
915
918
  dataDir: store.dataDir,
919
+ roomId,
920
+ botId,
916
921
  model: detail.model ?? null,
917
922
  skills: mergeSkillRefs(
918
923
  staffedSkills(store, botId),
@@ -996,6 +1001,7 @@ function publicLiveTurn(live: LiveTurn): LiveTurn {
996
1001
  thinking: live.thinking,
997
1002
  steps: live.steps,
998
1003
  startedAt: live.startedAt,
1004
+ ...(live.paused ? { paused: true } : {}),
999
1005
  };
1000
1006
  }
1001
1007
 
@@ -1085,6 +1091,84 @@ export function abortLiveTurn(
1085
1091
  return { ok: true };
1086
1092
  }
1087
1093
 
1094
+ export function pauseLiveTurn(
1095
+ store: GuildStore,
1096
+ roomId: string,
1097
+ botId?: string,
1098
+ ) {
1099
+ if (!store.getRoom(roomId)) throw new StoreError(404, "room not found");
1100
+ const live = botId
1101
+ ? store.getLiveBotTurn(roomId, botId)
1102
+ : store.getLiveTurn(roomId);
1103
+ if (live?.paused) return { ok: true, paused: true as const };
1104
+ const had = store.pauseTurn(roomId, botId);
1105
+ if (!live && !had) throw new StoreError(409, "no live turn");
1106
+ return { ok: true, paused: true as const };
1107
+ }
1108
+
1109
+ function resumeSteer(live: LiveTurn): string {
1110
+ const lines = [
1111
+ "Paused mid-turn so the user could switch models. Continue from here. Do not redo finished tools unless you need a different result.",
1112
+ ];
1113
+ const thinking = (live.thinking || "").trim();
1114
+ if (thinking) lines.push(`Thinking so far:\n${thinking.slice(0, 6000)}`);
1115
+ const tools = (live.traces || []).filter((tr) => tr.name && tr.name !== "think");
1116
+ if (tools.length) {
1117
+ lines.push(
1118
+ "Tools already run:\n" +
1119
+ tools
1120
+ .slice(-20)
1121
+ .map((tr) => {
1122
+ const bit = String(tr.text || "")
1123
+ .replace(/\s+/g, " ")
1124
+ .trim()
1125
+ .slice(0, 400);
1126
+ return `- ${tr.name}${bit ? `: ${bit}` : ""}`;
1127
+ })
1128
+ .join("\n"),
1129
+ );
1130
+ }
1131
+ return lines.join("\n\n");
1132
+ }
1133
+
1134
+ export async function continueLiveTurn(
1135
+ store: GuildStore,
1136
+ roomId: string,
1137
+ botId: string,
1138
+ env: NodeJS.ProcessEnv = process.env,
1139
+ extras: HandlerExtras = {},
1140
+ ) {
1141
+ if (!store.getRoom(roomId)) throw new StoreError(404, "room not found");
1142
+ const id = botId.trim();
1143
+ if (!id) throw new StoreError(400, "botId is required");
1144
+ const live = store.getLiveBotTurn(roomId, id);
1145
+ if (!live?.paused) throw new StoreError(409, "no paused turn");
1146
+ const messages = store.listMessages(roomId);
1147
+ let userIndex = messages.length - 1;
1148
+ while (userIndex >= 0 && messages[userIndex].author !== "you") userIndex -= 1;
1149
+ if (userIndex < 0) throw new StoreError(400, "no user message to continue");
1150
+ const userMessage = messages[userIndex];
1151
+ const asked = live.asked?.trim() || userMessage.body.trim();
1152
+ if (!asked) throw new StoreError(409, "nothing to continue");
1153
+ const history = messages.slice(0, userIndex).map(toHistoryItem);
1154
+ const parent = parentMessage(messages.slice(0, userIndex), userMessage.replyTo);
1155
+ const room = store.getRoom(roomId);
1156
+ store.setLiveTurn(roomId, { ...live, paused: false });
1157
+ store.pushSteer(roomId, resumeSteer(live), id);
1158
+ const replies = await generateReplies(
1159
+ store,
1160
+ roomId,
1161
+ room?.memberIds ?? [id],
1162
+ { ...userMessage, body: asked },
1163
+ history,
1164
+ id,
1165
+ env,
1166
+ parent,
1167
+ extras,
1168
+ );
1169
+ return { replies };
1170
+ }
1171
+
1088
1172
  function isAbortError(err: unknown): boolean {
1089
1173
  return Boolean(
1090
1174
  err &&
@@ -1169,14 +1253,19 @@ async function generateReplies(
1169
1253
  if (!memberIds.includes(botId)) return;
1170
1254
  if (signal.aborted) return;
1171
1255
  const prev = store.getLiveBotTurn(roomId, botId);
1256
+ if (!prev || prev.paused) return;
1172
1257
  const startedAt = prev?.startedAt || new Date().toISOString();
1173
1258
  store.dropLastFailedReply(roomId, botId);
1174
1259
  store.setLiveTurn(roomId, {
1175
1260
  botId,
1176
1261
  thinking: prev?.thinking || "",
1177
1262
  steps: prev?.steps || [],
1263
+ traces: prev?.traces,
1264
+ asked: turnAsked,
1178
1265
  startedAt,
1266
+ paused: false,
1179
1267
  });
1268
+ const botSignal = store.armBotTurn(roomId, botId, signal);
1180
1269
  let generated;
1181
1270
  try {
1182
1271
  generated = await (extras.turn ?? chatReply)({
@@ -1189,10 +1278,12 @@ async function generateReplies(
1189
1278
  userMessage.body,
1190
1279
  ),
1191
1280
  env,
1192
- signal,
1281
+ signal: botSignal,
1193
1282
  mcpTools,
1283
+ ...(extras.cronRun ? { cronRun: true } : {}),
1194
1284
  onProgress: (update) => {
1195
1285
  const prev = store.getLiveBotTurn(roomId, botId);
1286
+ if (prev?.paused) return;
1196
1287
  const next = toLiveTurn(botId, update);
1197
1288
  const handoff = (prev?.steps || []).find((step) => step.name === "handoff");
1198
1289
  const pendingSteers = store.peekSteers(roomId, botId).map((text) => ({
@@ -1209,14 +1300,16 @@ async function generateReplies(
1209
1300
  );
1210
1301
  store.setLiveTurn(roomId, {
1211
1302
  ...next,
1303
+ asked: prev?.asked || turnAsked,
1212
1304
  startedAt: prev?.startedAt || startedAt,
1305
+ paused: false,
1213
1306
  steps: [...(handoff ? [handoff] : []), ...keptSteer, ...rest].slice(0, 5),
1214
1307
  });
1215
1308
  },
1216
1309
  pullSteers: () => store.drainSteers(roomId, botId),
1217
1310
  });
1218
1311
  } catch (err) {
1219
- if (isAbortError(err) || signal.aborted) return;
1312
+ if (isAbortError(err) || botSignal.aborted || signal.aborted) return;
1220
1313
  throw err;
1221
1314
  }
1222
1315
  const usage = { ...(generated.usage || {}), startedAt };
@@ -1406,11 +1499,15 @@ function plantLiveTurns(
1406
1499
  if (!botId) continue;
1407
1500
  if (!memberIds.includes(botId)) continue;
1408
1501
  store.dropLastFailedReply(roomId, botId);
1502
+ const prev = store.getLiveBotTurn(roomId, botId);
1409
1503
  store.setLiveTurn(roomId, {
1410
1504
  botId,
1411
- thinking: "",
1412
- steps: [],
1413
- startedAt,
1505
+ thinking: prev?.thinking || "",
1506
+ steps: prev?.steps || [],
1507
+ traces: prev?.traces,
1508
+ asked: prev?.asked,
1509
+ startedAt: prev?.startedAt || startedAt,
1510
+ paused: false,
1414
1511
  });
1415
1512
  }
1416
1513
  return startedAt;
package/src/harness.ts CHANGED
@@ -133,6 +133,23 @@ export function pathInsideWorkspace(target: string, workspace: string): boolean
133
133
  return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
134
134
  }
135
135
 
136
+ /** Scratch dirs `workspace_write` may also read / write / run-cwd. Not `$HOME`. */
137
+ export function extraWriteRoots(dataDir?: string): string[] {
138
+ const roots = [resolve("/tmp")];
139
+ const home = dataDir?.trim();
140
+ if (home) roots.push(resolve(home, "cache"));
141
+ return roots;
142
+ }
143
+
144
+ export function allowedWorkspaceWritePath(
145
+ target: string,
146
+ workspace: string,
147
+ dataDir?: string,
148
+ ): boolean {
149
+ if (pathInsideWorkspace(target, workspace)) return true;
150
+ return extraWriteRoots(dataDir).some((root) => pathInsideWorkspace(target, root));
151
+ }
152
+
136
153
  export function mutatingTool(name: string): boolean {
137
154
  return (
138
155
  name === "run" ||
@@ -150,6 +167,7 @@ export function gateTool(
150
167
  input: {
151
168
  sandbox?: Sandbox;
152
169
  workspace?: string;
170
+ dataDir?: string;
153
171
  } = {},
154
172
  ): SandboxRefusal | null {
155
173
  const sandbox = parseSandbox(input.sandbox);
@@ -161,7 +179,8 @@ export function gateTool(
161
179
  name === "list" ||
162
180
  name === "skill" ||
163
181
  name === "spawn" ||
164
- name === "read_spawn"
182
+ name === "read_spawn" ||
183
+ name === "cronjob"
165
184
  ) {
166
185
  return null;
167
186
  }
@@ -187,7 +206,7 @@ export function gateTool(
187
206
  const raw = typeof args.path === "string" ? args.path : "";
188
207
  if (name === "list" && !raw.trim()) return null;
189
208
  const target = resolveToolPath(raw, workspace);
190
- if (!pathInsideWorkspace(target, workspace)) {
209
+ if (!allowedWorkspaceWritePath(target, workspace, input.dataDir)) {
191
210
  return {
192
211
  text: `sandbox=workspace_write refused ${name} outside workspace: ${target}`,
193
212
  isError: true,
@@ -199,7 +218,8 @@ export function gateTool(
199
218
  if (
200
219
  name === "skill" ||
201
220
  name === "spawn" ||
202
- name === "read_spawn"
221
+ name === "read_spawn" ||
222
+ name === "cronjob"
203
223
  ) {
204
224
  return null;
205
225
  }
@@ -208,7 +228,7 @@ export function gateTool(
208
228
  const raw = typeof args.path === "string" ? args.path : "";
209
229
  if (!raw.trim()) return null;
210
230
  const target = resolveToolPath(raw, workspace);
211
- if (!pathInsideWorkspace(target, workspace)) {
231
+ if (!allowedWorkspaceWritePath(target, workspace, input.dataDir)) {
212
232
  return {
213
233
  text: `sandbox=workspace_write refused write outside workspace: ${target}`,
214
234
  isError: true,
@@ -220,7 +240,7 @@ export function gateTool(
220
240
  if (name === "run") {
221
241
  const workdir = typeof args.workdir === "string" ? args.workdir.trim() : "";
222
242
  const cwd = workdir ? resolveToolPath(workdir, workspace) : workspace;
223
- if (!pathInsideWorkspace(cwd, workspace)) {
243
+ if (!allowedWorkspaceWritePath(cwd, workspace, input.dataDir)) {
224
244
  return {
225
245
  text: `sandbox=workspace_write refused run cwd outside workspace: ${cwd}`,
226
246
  isError: true,
package/src/llm.ts CHANGED
@@ -496,7 +496,7 @@ export async function llmComplete(input: {
496
496
  };
497
497
  const file = readModelsFile(input.dataDir);
498
498
  const effort = clampEffort(
499
- file.fast ? "low" : file.reasoning,
499
+ file.fast ? "low" : input.prefer ? input.prefer.reasoning : file.reasoning,
500
500
  reasoningFor(target.providerId, target.model),
501
501
  Boolean(file.fast),
502
502
  );
@@ -0,0 +1,26 @@
1
+ import { Service, type Context } from "cordis";
2
+ import { CRON_TICK_MS } from "../cron-schedule.ts";
3
+ import { executeCronjob, tickCronJobs } from "../cron.ts";
4
+ import { guildEnvOf } from "../start.ts";
5
+
6
+ export class CronService extends Service {
7
+ static inject = ["store", "tools"];
8
+
9
+ constructor(ctx: Context) {
10
+ super(ctx, "cron");
11
+ ctx.tools.register("cronjob", (args, toolCtx) =>
12
+ executeCronjob(this.ctx.store.guild, args, toolCtx),
13
+ );
14
+ const tick = () => {
15
+ void tickCronJobs(this.ctx.store.guild, guildEnvOf(this.ctx));
16
+ };
17
+ const interval = setInterval(tick, CRON_TICK_MS);
18
+ interval.unref();
19
+ ctx.effect(() => () => clearInterval(interval));
20
+ const boot = setTimeout(tick, 1500);
21
+ boot.unref();
22
+ ctx.effect(() => () => clearTimeout(boot));
23
+ }
24
+ }
25
+
26
+ export default CronService;
@@ -257,6 +257,57 @@ body.resizing-sidebar iframe { pointer-events: none; }
257
257
  font-size: 0.72rem;
258
258
  letter-spacing: 0.12em;
259
259
  }
260
+ .nav-sec {
261
+ display: flex;
262
+ flex-direction: column;
263
+ min-height: 0;
264
+ }
265
+ .nav-sec[data-nav-sec="dms"],
266
+ .nav-sec[data-nav-sec="cron"] {
267
+ flex: 1 1 0;
268
+ }
269
+ .nav-sec.folded {
270
+ flex: 0 0 auto;
271
+ }
272
+ .nav-sec.folded .navlist,
273
+ .nav-sec.folded .create-row {
274
+ display: none !important;
275
+ }
276
+ .sec-fold {
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 6px;
280
+ flex: 1;
281
+ min-width: 0;
282
+ margin: 0;
283
+ padding: 0;
284
+ border: 0;
285
+ background: transparent;
286
+ color: inherit;
287
+ font: inherit;
288
+ letter-spacing: inherit;
289
+ font-weight: inherit;
290
+ text-shadow: inherit;
291
+ text-align: left;
292
+ cursor: pointer;
293
+ }
294
+ .sec-fold:hover { color: var(--text); }
295
+ .sec-chev {
296
+ display: inline-block;
297
+ width: 0.9em;
298
+ flex: none;
299
+ font-size: 0.8em;
300
+ line-height: 1;
301
+ transition: transform var(--press-ms) var(--ease-out);
302
+ }
303
+ .nav-sec.folded .sec-chev { transform: rotate(-90deg); }
304
+ .nav-empty {
305
+ list-style: none;
306
+ margin: 0;
307
+ padding: 0.35rem 0.9rem 0.7rem;
308
+ color: var(--muted);
309
+ font-size: 0.78rem;
310
+ }
260
311
  .add-ch {
261
312
  margin: 0;
262
313
  width: 22px;
@@ -367,6 +418,7 @@ body.resizing-sidebar iframe { pointer-events: none; }
367
418
  }
368
419
  @media (prefers-reduced-motion: reduce) {
369
420
  .busy-flash { animation: none; }
421
+ .sec-chev { transition: none; }
370
422
  }
371
423
  .navlist a.nav-row:hover { background: var(--lift); }
372
424
  .navlist a.nav-row.on { background: var(--bubble); }
@@ -648,6 +700,8 @@ body.grok .sidebar .section-h {
648
700
  font-weight: 700;
649
701
  text-shadow: 0 1px 0 #0008;
650
702
  }
703
+ body.grok .sidebar .sec-fold:hover { color: #F4E3A4; }
704
+ body.grok .sidebar .nav-empty { color: #B08960; }
651
705
  body.grok .sidebar .add-ch {
652
706
  color: var(--pin);
653
707
  border-radius: 2px;
@@ -890,7 +944,8 @@ body.grok #channels li.nav-quest:has(a.on) .notice-party .members-stack .avatar,
890
944
  body.grok #channels li.nav-quest:hover .notice-party .members-stack .avatar {
891
945
  border-color: var(--plaque-on);
892
946
  }
893
- body.grok #dms.navlist {
947
+ body.grok #dms.navlist,
948
+ body.grok #cron-nav.navlist {
894
949
  flex-grow: 1;
895
950
  flex-shrink: 1;
896
951
  flex-basis: 0;
@@ -901,25 +956,30 @@ body.grok #dms.navlist {
901
956
  scrollbar-width: thin;
902
957
  scrollbar-color: #8A623C transparent;
903
958
  }
904
- body.grok #dms a.nav-row {
959
+ body.grok #dms a.nav-row,
960
+ body.grok #cron-nav a.nav-row {
905
961
  color: #F0E2C8;
906
962
  border-radius: 2px;
907
963
  border: 1px solid transparent;
908
964
  background: color-mix(in srgb, #16100B 35%, transparent);
909
965
  }
910
- body.grok #dms a.nav-row:hover {
966
+ body.grok #dms a.nav-row:hover,
967
+ body.grok #cron-nav a.nav-row:hover {
911
968
  background: color-mix(in srgb, #EDE6D6 12%, transparent);
912
969
  border-color: color-mix(in srgb, var(--board-edge) 45%, transparent);
913
970
  }
914
- body.grok #dms a.nav-row.on {
971
+ body.grok #dms a.nav-row.on,
972
+ body.grok #cron-nav a.nav-row.on {
915
973
  background: color-mix(in srgb, #EDE6D6 18%, transparent);
916
974
  border-color: color-mix(in srgb, var(--pin) 40%, transparent);
917
975
  }
918
- body.grok #dms .nav-chip {
976
+ body.grok #dms .nav-chip,
977
+ body.grok #cron-nav .nav-chip {
919
978
  color: #E8C48A;
920
979
  background: #16100B;
921
980
  }
922
- body.grok #dms .busy-flash { box-shadow: 0 0 0 2px #16100B; }
981
+ body.grok #dms .busy-flash,
982
+ body.grok #cron-nav .busy-flash { box-shadow: 0 0 0 2px #16100B; }
923
983
  body.grok .sidebar .side-foot {
924
984
  position: absolute;
925
985
  left: 0;
@@ -1578,7 +1638,8 @@ a.invite-btn:hover { text-decoration: none; }
1578
1638
  min-height: 240px;
1579
1639
  height: 360px;
1580
1640
  border: 0;
1581
- background: #fff;
1641
+ background: transparent;
1642
+ color-scheme: dark;
1582
1643
  border-radius: 0 0 12px 12px;
1583
1644
  }
1584
1645
  .assistant-text .md-html-preview[data-view="code"] .md-html-frame { display: none; }
@@ -1894,6 +1955,10 @@ a.invite-btn:hover { text-decoration: none; }
1894
1955
  .turn-actions .turn-stop {
1895
1956
  color: var(--danger);
1896
1957
  }
1958
+ .turn-actions .turn-pause,
1959
+ .turn-actions .turn-continue {
1960
+ color: var(--steel);
1961
+ }
1897
1962
  .turn-actions .turn-steer {
1898
1963
  color: #b4c8e8;
1899
1964
  }
@@ -1907,6 +1972,8 @@ body.grok .turn-actions button:hover {
1907
1972
  color: #f2eadf;
1908
1973
  }
1909
1974
  body.grok .turn-actions .turn-stop { color: #d4a090; }
1975
+ body.grok .turn-actions .turn-pause,
1976
+ body.grok .turn-actions .turn-continue { color: #c4b090; }
1910
1977
  body.grok .turn-actions .turn-steer { color: #c4b090; }
1911
1978
  .turn-status .clock {
1912
1979
  margin-left: 8px;
@@ -2931,6 +2998,38 @@ dialog p { margin: 0 0 0.8rem; color: var(--muted); font-size: 0.9rem; }
2931
2998
  color: var(--muted);
2932
2999
  font-size: 0.8rem;
2933
3000
  }
3001
+ .cron-list {
3002
+ display: flex;
3003
+ flex-direction: column;
3004
+ gap: 8px;
3005
+ max-height: 40vh;
3006
+ overflow: auto;
3007
+ margin: 0 0 1rem;
3008
+ }
3009
+ .cron-row {
3010
+ display: grid;
3011
+ grid-template-columns: 1fr auto;
3012
+ gap: 8px;
3013
+ padding: 8px 10px;
3014
+ border: 1px solid var(--line);
3015
+ border-radius: 12px;
3016
+ background: var(--fill);
3017
+ }
3018
+ .cron-row b { display: block; font-size: 0.92rem; }
3019
+ .cron-row span { color: var(--muted); font-size: 0.78rem; }
3020
+ .cron-row .cron-acts { display: flex; gap: 4px; align-items: center; }
3021
+ .cron-row .cron-acts button {
3022
+ margin: 0;
3023
+ padding: 0.15rem 0.45rem;
3024
+ font-size: 0.72rem;
3025
+ }
3026
+ #cron-dialog label { display: block; margin: 0.6rem 0 0.2rem; font-size: 0.8rem; color: var(--muted); }
3027
+ #cron-dialog input,
3028
+ #cron-dialog select,
3029
+ #cron-dialog textarea {
3030
+ width: 100%;
3031
+ box-sizing: border-box;
3032
+ }
2934
3033
  .dialog-actions { display: flex; justify-content: flex-end; gap: 0.4rem; }
2935
3034
  .dialog-actions button {
2936
3035
  margin: 0;
@@ -3107,13 +3206,23 @@ dialog p { margin: 0 0 0.8rem; color: var(--muted); font-size: 0.9rem; }
3107
3206
  line-height: 1.45;
3108
3207
  }
3109
3208
  .traj-empty { color: #8b8b8b; padding: 1.2rem; }
3209
+ .md-img-link {
3210
+ display: inline-block;
3211
+ max-width: 22rem;
3212
+ margin: 0.45rem 0;
3213
+ line-height: 0;
3214
+ border-radius: 12px;
3215
+ }
3110
3216
  .md-img,
3111
3217
  .row-card.image .md-img {
3112
3218
  display: block;
3113
- max-width: min(100%, 48rem);
3219
+ max-width: 22rem;
3220
+ max-height: min(16rem, 40vh);
3221
+ width: auto;
3114
3222
  height: auto;
3223
+ object-fit: contain;
3115
3224
  border-radius: 12px;
3116
- margin: 0.45rem 0;
3225
+ margin: 0;
3117
3226
  background: #111;
3118
3227
  }
3119
3228
  .row-card.image .term-out {