@kubitnodes/stellaris 0.4.0 → 0.4.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.
Files changed (59) hide show
  1. package/dist/cli/index.js +644 -36
  2. package/dist/server/index.js +901 -116
  3. package/package.json +1 -1
  4. package/web/dist/assets/{BackLink-BLiSCuKv.js → BackLink-DNHWkHmh.js} +1 -1
  5. package/web/dist/assets/{Button-D4-iu6aX.js → Button-B6EN7Lh7.js} +1 -1
  6. package/web/dist/assets/{ChannelForms-DjviQfVm.js → ChannelForms-BrcWrkej.js} +1 -1
  7. package/web/dist/assets/ChannelView-D7bT7d9q.js +2 -0
  8. package/web/dist/assets/CitizenView-D6d1KnS6.js +2 -0
  9. package/web/dist/assets/{CitizensView-DXD3qkq1.js → CitizensView-C55zOiYE.js} +1 -1
  10. package/web/dist/assets/{CliIcon-DL4ZwgZv.js → CliIcon-BRq1tnhu.js} +1 -1
  11. package/web/dist/assets/{CronsView-CX_G5D3h.js → CronsView-BTGiqn10.js} +1 -1
  12. package/web/dist/assets/{FileTree-BFa_KVI8.js → FileTree-BDhtTIzT.js} +1 -1
  13. package/web/dist/assets/{FileView-Cd9Ijarf.js → FileView-BkZPGEbO.js} +2 -2
  14. package/web/dist/assets/{KnowledgeView-D_a4pjGV.js → KnowledgeView-BiTS-lyE.js} +1 -1
  15. package/web/dist/assets/{Markdown-DPqvK--4.js → Markdown-DYk-gVwe.js} +4 -4
  16. package/web/dist/assets/{MessageItem-WBTzWwyj.js → MessageItem-WBBIzv48.js} +1 -1
  17. package/web/dist/assets/MetricsView-CYmnG6aQ.js +1 -0
  18. package/web/dist/assets/NeedsYouView-B-5olhZL.js +1 -0
  19. package/web/dist/assets/{Overview-BGUnf6K1.js → Overview-BO_lR1cu.js} +1 -1
  20. package/web/dist/assets/{Pane-C9TbEME4.js → Pane-DFJdAhv9.js} +1 -1
  21. package/web/dist/assets/{Picker-BK48MjLY.js → Picker-CAEREZF9.js} +1 -1
  22. package/web/dist/assets/{ProjectView-sL3aGAC6.js → ProjectView-LU59V2vh.js} +1 -1
  23. package/web/dist/assets/ProposalView-DHemTlkc.js +1 -0
  24. package/web/dist/assets/{ProposalsView-DTGx8NDn.js → ProposalsView-DBIOJAul.js} +1 -1
  25. package/web/dist/assets/{Quota-BkF7pa0u.js → Quota-D6_yyawu.js} +1 -1
  26. package/web/dist/assets/RunnersView-BNKgYuNw.js +1 -0
  27. package/web/dist/assets/{SkillFileList-BUpKUkaG.js → SkillFileList-Ntyv8noL.js} +1 -1
  28. package/web/dist/assets/SkillView-Dez1fvqU.js +1 -0
  29. package/web/dist/assets/{SocietyView-JSdVE50m.js → SocietyView-CbmbQXHi.js} +1 -1
  30. package/web/dist/assets/{StickyList-DafbJutb.js → StickyList-Q0KEWBUl.js} +1 -1
  31. package/web/dist/assets/{SubjectThread-Jyl5YCFL.js → SubjectThread-1PnxxtZc.js} +1 -1
  32. package/web/dist/assets/{TaskView-Jke2pVGT.js → TaskView-CcIlNHBh.js} +2 -2
  33. package/web/dist/assets/{TasksView-zJTIaqEA.js → TasksView-BkTU43PD.js} +1 -1
  34. package/web/dist/assets/{ThreadForms-DwUOktyP.js → ThreadForms-BlOEvY5E.js} +1 -1
  35. package/web/dist/assets/ThreadItem-D5N_Fw1K.js +1 -0
  36. package/web/dist/assets/{ThreadView-CdQpBb7M.js → ThreadView-BaYV77mQ.js} +1 -1
  37. package/web/dist/assets/{citizen-BOn5BBRL.js → citizen-BRg9UIgD.js} +1 -1
  38. package/web/dist/assets/{compose-D-X3BQgY.js → compose-BnOoMY1c.js} +1 -1
  39. package/web/dist/assets/format-hyYD8qd8.js +55 -0
  40. package/web/dist/assets/governance-D4hlG-xw.js +1 -0
  41. package/web/dist/assets/index-5CbC_NpF.js +24 -0
  42. package/web/dist/assets/{index-CA_njyMe.css → index-vxBI4BYp.css} +1 -1
  43. package/web/dist/assets/{mentions-S-r32HKM.js → mentions-Ckktc7nR.js} +1 -1
  44. package/web/dist/assets/{tasks-D_Fy7dtN.js → tasks-CFVtN9TK.js} +1 -1
  45. package/web/dist/assets/useMatch-BCaBBHc2.js +1 -0
  46. package/web/dist/favicon.svg +89 -4
  47. package/web/dist/index.html +9 -9
  48. package/web/dist/assets/ChannelView-B-0H7Ps8.js +0 -2
  49. package/web/dist/assets/CitizenView-gE5GgCBa.js +0 -2
  50. package/web/dist/assets/MetricsView-YUkYixqT.js +0 -1
  51. package/web/dist/assets/NeedsYouView-CE_B9bZQ.js +0 -1
  52. package/web/dist/assets/ProposalView-BhZR4MIR.js +0 -1
  53. package/web/dist/assets/RunnersView-Bl62pXNw.js +0 -1
  54. package/web/dist/assets/SkillView-BQJApSh7.js +0 -1
  55. package/web/dist/assets/ThreadItem-Blx8gSvO.js +0 -1
  56. package/web/dist/assets/format-Dhd2NUB3.js +0 -55
  57. package/web/dist/assets/governance-CWzi7hzX.js +0 -1
  58. package/web/dist/assets/index-iplooQqq.js +0 -24
  59. package/web/dist/assets/useMatch-DZXCHHaG.js +0 -1
package/dist/cli/index.js CHANGED
@@ -370,6 +370,18 @@ function mergeQuota(previous, next) {
370
370
  windows: [...windows.values()]
371
371
  };
372
372
  }
373
+ var UNKNOWN_LIMIT_HOLD_MS = 30 * 6e4;
374
+ function stopsTurns(quota) {
375
+ return quota.status === "limited" && !(quota.source === "read" && quota.extraUsage?.enabled === true);
376
+ }
377
+ function limitHold(quota, now) {
378
+ if (!stopsTurns(quota)) {
379
+ return null;
380
+ }
381
+ const known = quota.limitedUntil !== null;
382
+ const until = quota.limitedUntil ?? new Date(Date.parse(quota.observedAt) + UNKNOWN_LIMIT_HOLD_MS).toISOString();
383
+ return Date.parse(until) > now.getTime() ? { until, known } : null;
384
+ }
373
385
  function readings(quota) {
374
386
  const windows = quota.windows.map((window) => `${window.key}=${window.usedPercent === null ? "?" : Math.round(window.usedPercent)}@${window.resetsAt === null ? "?" : Math.round(Date.parse(window.resetsAt) / 6e4)}`).toSorted();
375
387
  return windows.join(",");
@@ -699,12 +711,16 @@ var RunnerSchema = z5.object({
699
711
  function runnerPause(runner2, cli) {
700
712
  return runner2.paused ?? runner2.pausedClis?.[cli];
701
713
  }
702
- function heldOn(runners, runner2, cli) {
714
+ function runnerLimitHold(runner2, cli, now) {
715
+ const quota = runner2.quotas?.[cli];
716
+ return quota === void 0 ? null : limitHold(quota, now);
717
+ }
718
+ function heldOn(runners, runner2, cli, now) {
703
719
  if (runner2 === void 0 || cli === null) {
704
720
  return false;
705
721
  }
706
722
  const record = runners.find((each) => each.name === runner2);
707
- return record !== void 0 && runnerPause(record, cli) !== void 0;
723
+ return record !== void 0 && (runnerPause(record, cli) !== void 0 || runnerLimitHold(record, cli, now) !== null);
708
724
  }
709
725
  var ProposalKindSchema = z5.enum([
710
726
  "role",
@@ -1024,6 +1040,12 @@ import { z as z7 } from "zod";
1024
1040
  import { z as z6 } from "zod";
1025
1041
  var IMAGE_LIMIT_BYTES = 8 * 1024 * 1024;
1026
1042
  var ImageTypeSchema = z6.enum(["png", "jpeg", "gif", "webp"]);
1043
+ var IMAGE_KIND_NAMES = {
1044
+ png: "PNG",
1045
+ jpeg: "JPEG",
1046
+ gif: "GIF",
1047
+ webp: "WebP"
1048
+ };
1027
1049
  function startsWith(bytes, prefix, at = 0) {
1028
1050
  for (let index = 0; index < prefix.length; index += 1) {
1029
1051
  if (bytes[at + index] !== prefix[index]) {
@@ -1445,7 +1467,11 @@ var RunnerHelloSchema = z9.object({
1445
1467
  gitIdentityMissing: z9.array(GitIdentityPartSchema).optional(),
1446
1468
  /** Turns the runner runs at once, or null for no limit. */
1447
1469
  slots: z9.number().int().positive().nullable(),
1448
- /** Turns the runner is still running, when it registers again; the server fails the rest of its turns. */
1470
+ /**
1471
+ * Turns the runner is still running, when it registers again: the server fails the rest of its
1472
+ * turns, and has any of these it has ended stopped once the stream opens, when the runner lists
1473
+ * a CLI it can stop.
1474
+ */
1449
1475
  turns: z9.array(UlidSchema).default([])
1450
1476
  });
1451
1477
  var RunnerWelcomeSchema = z9.object({ name: NameSchema, version: z9.string() });
@@ -1600,9 +1626,9 @@ var TurnOutcomeSchema = z9.object({
1600
1626
  */
1601
1627
  resumeFailed: ResumeFailureSchema.optional(),
1602
1628
  /**
1603
- * Set when the model service turned the turn away for now, as when it was overloaded or failed
1604
- * on its side, which the scheduler answers by sending the same wake again within minutes. A
1605
- * runner from before never sends it.
1629
+ * Set when the turn failed for now: the model service turned it away or cut it off, or the runner
1630
+ * could not reach the board before the CLI started. The scheduler answers it by sending the same
1631
+ * wake again within minutes. A runner from before never sends it.
1606
1632
  */
1607
1633
  transient: z9.boolean().optional(),
1608
1634
  /** The model the CLI reported, when it reported one. */
@@ -1993,13 +2019,18 @@ var TurnRecordSchema = z10.object({
1993
2019
  stoppedBy: NameSchema.optional(),
1994
2020
  /** Why the CLI could not resume the turn's session, for a turn that failed so. */
1995
2021
  resumeFailed: ResumeFailureSchema.optional(),
1996
- /** Whether the model service turned the turn away for now: overloaded, or failing on its side. */
2022
+ /**
2023
+ * Whether the turn failed for now, which the same wake may get past in a minute: the model service
2024
+ * turned it away or cut it off, the runner could not reach the board before the CLI started, or
2025
+ * the job never reached the runner.
2026
+ */
1997
2027
  transient: z10.boolean().optional(),
1998
2028
  /** How many turns in a row, this one included, could not resume the session for a `retry`. */
1999
2029
  resumeRetries: z10.number().int().positive().optional(),
2000
2030
  /**
2001
2031
  * Whether the server dropped the conversation's session after this turn, which the next starts
2002
- * anew: one its CLI could not resume, or a society role's home after a reflection completed.
2032
+ * anew: one its CLI could not resume, or a society role's or a project member's home after a
2033
+ * reflection completed.
2003
2034
  */
2004
2035
  sessionDropped: z10.boolean().optional(),
2005
2036
  /** The crons whose fire started the turn, which record how it ended. */
@@ -2244,6 +2275,11 @@ var ServerConfigSchema = z13.object({
2244
2275
  dataDir: z13.string().min(1),
2245
2276
  host: z13.string().min(1),
2246
2277
  port: z13.number().int().positive().max(65535),
2278
+ /**
2279
+ * A second port, on the same host, that serves Prometheus's counters at `/metrics` and nothing
2280
+ * else, or null for none: the main port's `/metrics` is the interface's page.
2281
+ */
2282
+ metricsPort: z13.number().int().positive().max(65535).nullable(),
2247
2283
  /**
2248
2284
  * An address every runner's agents reach this server at, for example https://board.example, or
2249
2285
  * null for each runner to use the address it reaches the server at itself.
@@ -2284,7 +2320,7 @@ var RunnerConfigSchema = z13.object({
2284
2320
  });
2285
2321
 
2286
2322
  // packages/shared/dist/version.js
2287
- var STELLARIS_VERSION = "0.4.0";
2323
+ var STELLARIS_VERSION = "0.4.2";
2288
2324
 
2289
2325
  // packages/shared/dist/mentions.js
2290
2326
  import { parse, postprocess, preprocess } from "micromark";
@@ -2976,6 +3012,451 @@ function filesOf(lines, status) {
2976
3012
  return [...files.values()];
2977
3013
  }
2978
3014
 
3015
+ // packages/shared/dist/image-metadata.js
3016
+ function withoutMetadata(bytes) {
3017
+ switch (imageTypeOf(bytes)) {
3018
+ case "jpeg":
3019
+ return jpegWithoutMetadata(bytes);
3020
+ case "png":
3021
+ return pngWithoutMetadata(bytes);
3022
+ case "gif":
3023
+ return gifWithoutMetadata(bytes);
3024
+ case "webp":
3025
+ return webpWithoutMetadata(bytes);
3026
+ default:
3027
+ return null;
3028
+ }
3029
+ }
3030
+ function exifOrientation(block) {
3031
+ const tiff = startsWith2(block, EXIF_HEADER) ? block.subarray(EXIF_HEADER.length) : block;
3032
+ let little;
3033
+ if (tiff[0] === 73 && tiff[1] === 73) {
3034
+ little = true;
3035
+ } else if (tiff[0] === 77 && tiff[1] === 77) {
3036
+ little = false;
3037
+ } else {
3038
+ return 1;
3039
+ }
3040
+ const number = (offset, size) => offset >= 0 && size <= tiff.length && offset <= tiff.length - size ? numberAt(tiff, offset, size, little) : null;
3041
+ const directory = number(4, 4);
3042
+ const count = directory === null ? null : number(directory, 2);
3043
+ if (number(2, 2) !== 42 || directory === null || count === null) {
3044
+ return 1;
3045
+ }
3046
+ for (let entry = directory + 2; entry < directory + 2 + count * 12; entry += 12) {
3047
+ const tag = number(entry, 2);
3048
+ if (tag === null) {
3049
+ return 1;
3050
+ }
3051
+ if (tag === 274) {
3052
+ const value = number(entry + 8, 2);
3053
+ if (number(entry + 2, 2) !== 3 || number(entry + 4, 4) !== 1 || value === null || value < 1 || value > 8) {
3054
+ return 1;
3055
+ }
3056
+ return value;
3057
+ }
3058
+ }
3059
+ return 1;
3060
+ }
3061
+ function orientationOnly(orientation) {
3062
+ const block = new Uint8Array([
3063
+ 77,
3064
+ 77,
3065
+ 0,
3066
+ 42,
3067
+ 0,
3068
+ 0,
3069
+ 0,
3070
+ 8,
3071
+ 0,
3072
+ 1,
3073
+ 1,
3074
+ 18,
3075
+ 0,
3076
+ 3,
3077
+ 0,
3078
+ 0,
3079
+ 0,
3080
+ 1,
3081
+ 0,
3082
+ 0,
3083
+ 0,
3084
+ 0,
3085
+ 0,
3086
+ 0,
3087
+ 0,
3088
+ 0
3089
+ ]);
3090
+ block[19] = orientation;
3091
+ return block;
3092
+ }
3093
+ function crc32(bytes) {
3094
+ let crc = 4294967295;
3095
+ for (const byte of bytes) {
3096
+ crc = (crcTable()[(crc ^ byte) & 255] ?? 0) ^ crc >>> 8;
3097
+ }
3098
+ return (crc ^ 4294967295) >>> 0;
3099
+ }
3100
+ var crcEntries;
3101
+ function crcTable() {
3102
+ if (crcEntries === void 0) {
3103
+ crcEntries = new Uint32Array(256);
3104
+ for (let index = 0; index < 256; index += 1) {
3105
+ let crc = index;
3106
+ for (let bit = 0; bit < 8; bit += 1) {
3107
+ crc = crc & 1 ? 3988292384 ^ crc >>> 1 : crc >>> 1;
3108
+ }
3109
+ crcEntries[index] = crc >>> 0;
3110
+ }
3111
+ }
3112
+ return crcEntries;
3113
+ }
3114
+ var ascii2 = (text) => new TextEncoder().encode(text);
3115
+ var EXIF_HEADER = Uint8Array.of(69, 120, 105, 102, 0, 0);
3116
+ function startsWith2(bytes, prefix, at = 0) {
3117
+ if (bytes.length - at < prefix.length) {
3118
+ return false;
3119
+ }
3120
+ for (let index = 0; index < prefix.length; index += 1) {
3121
+ if (bytes[at + index] !== prefix[index]) {
3122
+ return false;
3123
+ }
3124
+ }
3125
+ return true;
3126
+ }
3127
+ function typeAt(bytes, at) {
3128
+ return String.fromCharCode(bytes[at] ?? 0, bytes[at + 1] ?? 0, bytes[at + 2] ?? 0, bytes[at + 3] ?? 0);
3129
+ }
3130
+ function numberAt(bytes, at, size, little = false) {
3131
+ let value = 0;
3132
+ for (let index = 0; index < size; index += 1) {
3133
+ value = value * 256 + (bytes[at + (little ? size - 1 - index : index)] ?? 0);
3134
+ }
3135
+ return value;
3136
+ }
3137
+ function bigEndian(value, size) {
3138
+ return Array.from({ length: size }, (_, index) => value >>> (size - 1 - index) * 8 & 255);
3139
+ }
3140
+ function littleEndian(value, size) {
3141
+ return Array.from({ length: size }, (_, index) => value >>> index * 8 & 255);
3142
+ }
3143
+ var Kept = class {
3144
+ buffer;
3145
+ length = 0;
3146
+ constructor(capacity) {
3147
+ this.buffer = new Uint8Array(Math.max(capacity, 64));
3148
+ }
3149
+ add(piece) {
3150
+ if (this.length + piece.length > this.buffer.length) {
3151
+ const grown = new Uint8Array(Math.max(this.buffer.length * 2, this.length + piece.length));
3152
+ grown.set(this.buffer.subarray(0, this.length));
3153
+ this.buffer = grown;
3154
+ }
3155
+ this.buffer.set(piece, this.length);
3156
+ this.length += piece.length;
3157
+ }
3158
+ /** Writes `piece` over what was kept from `at` on, as a size once what it measures is kept. */
3159
+ set(at, piece) {
3160
+ this.buffer.set(piece, at);
3161
+ }
3162
+ get bytes() {
3163
+ return this.buffer.subarray(0, this.length);
3164
+ }
3165
+ };
3166
+ function jpegWithoutMetadata(bytes) {
3167
+ if (bytes[0] !== 255 || bytes[1] !== 216) {
3168
+ return null;
3169
+ }
3170
+ const kept = new Kept(bytes.length);
3171
+ kept.add([255, 216]);
3172
+ let at = 2;
3173
+ let orientationKept = false;
3174
+ for (; ; ) {
3175
+ if (at + 1 >= bytes.length || bytes[at] !== 255) {
3176
+ return null;
3177
+ }
3178
+ const marker = bytes[at + 1] ?? 0;
3179
+ if (marker === 255) {
3180
+ at += 1;
3181
+ continue;
3182
+ }
3183
+ if (marker === 0 || marker >= 2 && marker <= 191 || marker === 216 || marker === 217) {
3184
+ return null;
3185
+ }
3186
+ if (marker === 1 || marker >= 208 && marker <= 215) {
3187
+ kept.add([255, marker]);
3188
+ at += 2;
3189
+ continue;
3190
+ }
3191
+ if (at + 4 > bytes.length) {
3192
+ return null;
3193
+ }
3194
+ const length = numberAt(bytes, at + 2, 2);
3195
+ const end = at + 2 + length;
3196
+ if (length < 2 || end > bytes.length) {
3197
+ return null;
3198
+ }
3199
+ const payload = bytes.subarray(at + 4, end);
3200
+ if (marker === 224) {
3201
+ if (startsWith2(payload, JFIF) && payload.length >= 14) {
3202
+ kept.add([255, 224, 0, 16]);
3203
+ kept.add(payload.subarray(0, 12));
3204
+ kept.add([0, 0]);
3205
+ }
3206
+ } else if (marker === 225) {
3207
+ if (startsWith2(payload, EXIF_HEADER) && !orientationKept) {
3208
+ orientationKept = true;
3209
+ const orientation = exifOrientation(payload.subarray(EXIF_HEADER.length));
3210
+ if (orientation !== 1) {
3211
+ const exif = orientationOnly(orientation);
3212
+ kept.add([255, 225, ...bigEndian(EXIF_HEADER.length + exif.length + 2, 2)]);
3213
+ kept.add(EXIF_HEADER);
3214
+ kept.add(exif);
3215
+ }
3216
+ }
3217
+ } else if (marker === 226 && startsWith2(payload, ICC_PROFILE) || marker === 238 && startsWith2(payload, ADOBE) || // A frame, its tables, a scan, and their kin, JPEG-LS's frame and parameters among them.
3218
+ marker >= 192 && marker <= 207 && marker !== 200 || marker >= 218 && marker <= 223 || marker === 247 || marker === 248) {
3219
+ kept.add(bytes.subarray(at, end));
3220
+ }
3221
+ at = end;
3222
+ if (marker !== 218) {
3223
+ continue;
3224
+ }
3225
+ const scan = at;
3226
+ for (; ; ) {
3227
+ const next = bytes.indexOf(255, at);
3228
+ if (next === -1 || next + 1 >= bytes.length) {
3229
+ return null;
3230
+ }
3231
+ at = next;
3232
+ const after = bytes[at + 1] ?? 0;
3233
+ if (after !== 0 && after !== 255 && !(after >= 208 && after <= 215)) {
3234
+ break;
3235
+ }
3236
+ at += 1;
3237
+ }
3238
+ kept.add(bytes.subarray(scan, at));
3239
+ if (bytes[at + 1] === 217) {
3240
+ kept.add([255, 217]);
3241
+ return kept.bytes;
3242
+ }
3243
+ }
3244
+ }
3245
+ var JFIF = Uint8Array.of(74, 70, 73, 70, 0);
3246
+ var ICC_PROFILE = ascii2("ICC_PROFILE\0");
3247
+ var ADOBE = ascii2("Adobe");
3248
+ var PNG_DRAWING_CHUNKS = /* @__PURE__ */ new Set([
3249
+ "IHDR",
3250
+ "PLTE",
3251
+ "IDAT",
3252
+ "IEND",
3253
+ "tRNS",
3254
+ "cHRM",
3255
+ "gAMA",
3256
+ "iCCP",
3257
+ "sBIT",
3258
+ "sRGB",
3259
+ "cICP",
3260
+ "mDCV",
3261
+ "cLLI",
3262
+ "bKGD",
3263
+ "hIST",
3264
+ "pHYs",
3265
+ "sPLT",
3266
+ "acTL",
3267
+ "fcTL",
3268
+ "fdAT"
3269
+ ]);
3270
+ function pngWithoutMetadata(bytes) {
3271
+ const kept = new Kept(bytes.length);
3272
+ kept.add(bytes.subarray(0, 8));
3273
+ let at = 8;
3274
+ let orientationKept = false;
3275
+ while (at + 12 <= bytes.length) {
3276
+ const length = numberAt(bytes, at, 4);
3277
+ const type = typeAt(bytes, at + 4);
3278
+ if (length > bytes.length - at - 12) {
3279
+ return null;
3280
+ }
3281
+ const end = at + 12 + length;
3282
+ if (PNG_DRAWING_CHUNKS.has(type)) {
3283
+ kept.add(bytes.subarray(at, end));
3284
+ } else if (type === "eXIf" && !orientationKept) {
3285
+ orientationKept = true;
3286
+ const orientation = exifOrientation(bytes.subarray(at + 8, at + 8 + length));
3287
+ if (orientation !== 1) {
3288
+ const data = new Uint8Array([...ascii2("eXIf"), ...orientationOnly(orientation)]);
3289
+ kept.add(bigEndian(data.length - 4, 4));
3290
+ kept.add(data);
3291
+ kept.add(bigEndian(crc32(data), 4));
3292
+ }
3293
+ }
3294
+ if (type === "IEND") {
3295
+ return kept.bytes;
3296
+ }
3297
+ at = end;
3298
+ }
3299
+ return null;
3300
+ }
3301
+ var GIF_PLAYING_APPLICATIONS = [
3302
+ "NETSCAPE2.0",
3303
+ "ANIMEXTS1.0",
3304
+ "ICCRGBG1012"
3305
+ ].map(ascii2);
3306
+ function gifColourTable(packed) {
3307
+ return packed & 128 ? 3 << (packed & 7) + 1 : 0;
3308
+ }
3309
+ function gifWithoutMetadata(bytes) {
3310
+ const subBlocksEnd = (start) => {
3311
+ let at2 = start;
3312
+ while (at2 < bytes.length) {
3313
+ const size = bytes[at2] ?? 0;
3314
+ if (size === 0) {
3315
+ return at2 + 1;
3316
+ }
3317
+ at2 += 1 + size;
3318
+ }
3319
+ return null;
3320
+ };
3321
+ if (bytes.length < 13) {
3322
+ return null;
3323
+ }
3324
+ let at = 13 + gifColourTable(bytes[10] ?? 0);
3325
+ if (at > bytes.length) {
3326
+ return null;
3327
+ }
3328
+ const kept = new Kept(bytes.length);
3329
+ kept.add(bytes.subarray(0, at));
3330
+ while (at < bytes.length) {
3331
+ switch (bytes[at]) {
3332
+ case 44: {
3333
+ if (at + 10 > bytes.length) {
3334
+ return null;
3335
+ }
3336
+ const data = at + 10 + gifColourTable(bytes[at + 9] ?? 0);
3337
+ const end = data < bytes.length ? subBlocksEnd(data + 1) : null;
3338
+ if (end === null) {
3339
+ return null;
3340
+ }
3341
+ kept.add(bytes.subarray(at, end));
3342
+ at = end;
3343
+ break;
3344
+ }
3345
+ case 33: {
3346
+ const end = at + 2 < bytes.length ? subBlocksEnd(at + 2) : null;
3347
+ if (end === null) {
3348
+ return null;
3349
+ }
3350
+ const label = bytes[at + 1];
3351
+ const plays = label === 255 && bytes[at + 2] === 11 && end >= at + 14 && GIF_PLAYING_APPLICATIONS.some((name) => startsWith2(bytes, name, at + 3));
3352
+ if (label === 249 || label === 1 || plays) {
3353
+ kept.add(bytes.subarray(at, end));
3354
+ }
3355
+ at = end;
3356
+ break;
3357
+ }
3358
+ case 59:
3359
+ kept.add([59]);
3360
+ return kept.bytes;
3361
+ default:
3362
+ return null;
3363
+ }
3364
+ }
3365
+ return null;
3366
+ }
3367
+ var WEBP_DRAWING_CHUNKS = /* @__PURE__ */ new Set([
3368
+ "VP8X",
3369
+ "ICCP",
3370
+ "ANIM",
3371
+ "ANMF",
3372
+ "ALPH",
3373
+ "VP8 ",
3374
+ "VP8L"
3375
+ ]);
3376
+ function webpWithoutMetadata(bytes) {
3377
+ if (bytes.length < 12) {
3378
+ return null;
3379
+ }
3380
+ const end = 8 + numberAt(bytes, 4, 4, true);
3381
+ const chunks = end >= 12 && end <= bytes.length ? webpChunks(bytes.subarray(12, end)) : null;
3382
+ if (chunks === null || chunks.length === 0) {
3383
+ return null;
3384
+ }
3385
+ const kept = new Kept(chunks.length + 12);
3386
+ kept.add(ascii2("RIFF"));
3387
+ kept.add(littleEndian(chunks.length + 4, 4));
3388
+ kept.add(ascii2("WEBP"));
3389
+ kept.add(chunks);
3390
+ const image = kept.bytes;
3391
+ if (image.length > 20 && typeAt(image, 12) === "VP8X" && numberAt(image, 16, 4, true) > 0) {
3392
+ const exif = webpChunkTypes(image.subarray(12)).includes("EXIF");
3393
+ image[20] = (image[20] ?? 0) & ~12 | (exif ? 8 : 0);
3394
+ }
3395
+ return image;
3396
+ }
3397
+ function webpChunks(chunks) {
3398
+ const kept = new Kept(chunks.length);
3399
+ const frames = [];
3400
+ let end = chunks.length;
3401
+ let at = 0;
3402
+ let orientationKept = false;
3403
+ for (; ; ) {
3404
+ if (at >= end) {
3405
+ const frame = frames.pop();
3406
+ if (frame === void 0) {
3407
+ return kept.bytes;
3408
+ }
3409
+ kept.set(frame.size, littleEndian(kept.length - frame.size - 4, 4));
3410
+ at = frame.resume;
3411
+ end = frames.at(-1)?.end ?? chunks.length;
3412
+ continue;
3413
+ }
3414
+ if (end - at < 8) {
3415
+ return null;
3416
+ }
3417
+ const type = typeAt(chunks, at);
3418
+ const size = numberAt(chunks, at + 4, 4, true);
3419
+ if (size > end - at - 8) {
3420
+ return null;
3421
+ }
3422
+ const data = at + 8;
3423
+ at = Math.min(data + size + size % 2, end);
3424
+ if (type === "ANMF") {
3425
+ if (size < 16) {
3426
+ return null;
3427
+ }
3428
+ frames.push({ end: data + size, resume: at, size: kept.length + 4 });
3429
+ kept.add(chunks.subarray(data - 8, data + 16));
3430
+ at = data + 16;
3431
+ end = data + size;
3432
+ } else if (WEBP_DRAWING_CHUNKS.has(type)) {
3433
+ kept.add(chunks.subarray(data - 8, data + size));
3434
+ if (size % 2 === 1) {
3435
+ kept.add([0]);
3436
+ }
3437
+ } else if (type === "EXIF" && frames.length === 0 && !orientationKept) {
3438
+ orientationKept = true;
3439
+ const turned = exifOrientation(chunks.subarray(data, data + size));
3440
+ if (turned !== 1) {
3441
+ const exif = orientationOnly(turned);
3442
+ kept.add(ascii2(type));
3443
+ kept.add(littleEndian(exif.length, 4));
3444
+ kept.add(exif);
3445
+ }
3446
+ }
3447
+ }
3448
+ }
3449
+ function webpChunkTypes(chunks) {
3450
+ const types = [];
3451
+ let at = 0;
3452
+ while (chunks.length - at >= 8) {
3453
+ types.push(typeAt(chunks, at));
3454
+ const size = numberAt(chunks, at + 4, 4, true);
3455
+ at += 8 + size + size % 2;
3456
+ }
3457
+ return types;
3458
+ }
3459
+
2979
3460
  // packages/board-core/dist/moves.js
2980
3461
  import { mkdir as mkdir4, rm as rm2 } from "node:fs/promises";
2981
3462
  import path5 from "node:path";
@@ -3418,6 +3899,72 @@ function tokenUse(events, window, now) {
3418
3899
  };
3419
3900
  }
3420
3901
 
3902
+ // packages/board-core/dist/turn-counters.js
3903
+ var TOKEN_KINDS = ["input", "output", "cache_read", "cache_write"];
3904
+ var USAGE_FIELD = {
3905
+ input: "inputTokens",
3906
+ output: "outputTokens",
3907
+ cache_read: "cacheReadTokens",
3908
+ cache_write: "cacheWriteTokens"
3909
+ };
3910
+ var UNKNOWN_LABEL = "unknown";
3911
+ function byKey(entries) {
3912
+ return [...entries].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, series]) => series);
3913
+ }
3914
+ var TurnCounters = class {
3915
+ cliOf;
3916
+ tokens = /* @__PURE__ */ new Map();
3917
+ turns = /* @__PURE__ */ new Map();
3918
+ /**
3919
+ * `cliOf` names a citizen's CLI for an end logged before ends named theirs: a turn runs on the
3920
+ * CLI its citizen's record names, which nothing changes after the citizen is added.
3921
+ */
3922
+ constructor(cliOf) {
3923
+ this.cliOf = cliOf;
3924
+ }
3925
+ /** Counts a turn's end, `turn.completed` or `turn.failed`, and passes over any other event. */
3926
+ add(event) {
3927
+ if (event.type !== "turn.completed" && event.type !== "turn.failed") {
3928
+ return;
3929
+ }
3930
+ const turn2 = TurnHistoryPayloadSchema.safeParse(event.payload);
3931
+ if (!turn2.success) {
3932
+ return;
3933
+ }
3934
+ const agent2 = event.actor;
3935
+ const outcome = event.type === "turn.failed" ? "failed" : "completed";
3936
+ const trigger = turn2.data.trigger;
3937
+ const counted = JSON.stringify([agent2, outcome, trigger]);
3938
+ this.turns.set(counted, {
3939
+ agent: agent2,
3940
+ outcome,
3941
+ trigger,
3942
+ turns: (this.turns.get(counted)?.turns ?? 0) + 1
3943
+ });
3944
+ const usage = turn2.data.usage;
3945
+ if (usage === void 0) {
3946
+ return;
3947
+ }
3948
+ const named = CliKindSchema.safeParse(event.payload["cli"]);
3949
+ const cli = (named.success ? named.data : this.cliOf(agent2)) ?? UNKNOWN_LABEL;
3950
+ const model = turn2.data.model ?? UNKNOWN_LABEL;
3951
+ for (const kind of TOKEN_KINDS) {
3952
+ const key = JSON.stringify([agent2, cli, model, kind]);
3953
+ this.tokens.set(key, {
3954
+ agent: agent2,
3955
+ cli,
3956
+ model,
3957
+ kind,
3958
+ tokens: (this.tokens.get(key)?.tokens ?? 0) + usage[USAGE_FIELD[kind]]
3959
+ });
3960
+ }
3961
+ }
3962
+ /** The counts so far, in an order that holds from one read to the next. */
3963
+ read() {
3964
+ return { tokens: byKey(this.tokens), turns: byKey(this.turns) };
3965
+ }
3966
+ };
3967
+
3421
3968
  // packages/board-core/dist/mutex.js
3422
3969
  var Mutex = class {
3423
3970
  tail = Promise.resolve();
@@ -3777,6 +4324,7 @@ function hashToken(token) {
3777
4324
  // packages/board-core/dist/board.js
3778
4325
  var SYSTEM_ACTOR = { name: "board", role: USER_ROLE };
3779
4326
  var CLOSING_ATTEMPTS = 3;
4327
+ var CHANNEL_POST_WAIT_MS = 24 * 36e5;
3780
4328
  function ownTurn(actor, turn2) {
3781
4329
  return turn2.agent === actor.name && turn2.scope === actor.scope && turn2.thread === actor.thread && turn2.channel === actor.channel && turn2.place === actor.place;
3782
4330
  }
@@ -3900,8 +4448,10 @@ function plain(value) {
3900
4448
  return "";
3901
4449
  return JSON.stringify(value);
3902
4450
  }
3903
- function sentence(summary) {
3904
- return summary.trim().replace(/\.+$/, "");
4451
+ var SENTENCE_END = /[.…?!]["'”’»)\]}*_]*$/u;
4452
+ function asSentence(text) {
4453
+ const trimmed = text.trim();
4454
+ return SENTENCE_END.test(trimmed) ? trimmed : `${trimmed}.`;
3905
4455
  }
3906
4456
  function skillSummary(data, body) {
3907
4457
  const description = data["description"];
@@ -5089,10 +5639,13 @@ ${DASHBOARD_LINE}
5089
5639
  * when it mentions the user, so the creator can put a wait on the user in; a post that asks the
5090
5640
  * reader counts wherever it is. A closed thread counts for nothing: its conversation has ended,
5091
5641
  * and a turn there could only read its posts and be refused a reply, while a wake queued before
5092
- * it closed, as for a mention, still runs there.
5642
+ * it closed, as for a mention, still runs there. A top-level post in a project's channel, general
5643
+ * or a workstream's, counts only for a reader it asks until it is `channelPostWaitMs` old, and
5644
+ * then for every reader who has not read it, so a turn there for anything else reads it sooner.
5093
5645
  */
5094
- async unreadByConversation(actor) {
5646
+ async unreadByConversation(actor, options = {}) {
5095
5647
  const { cursors, upto } = await this.digestSnapshot(actor.name);
5648
+ const waited = this.now().getTime() - (options.channelPostWaitMs ?? CHANNEL_POST_WAIT_MS);
5096
5649
  const counts = /* @__PURE__ */ new Map();
5097
5650
  const threadCounts = /* @__PURE__ */ new Map();
5098
5651
  const unscoped = { name: actor.name, role: actor.role };
@@ -5100,11 +5653,15 @@ ${DASHBOARD_LINE}
5100
5653
  if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name || message.notice?.kind === "knowledge") {
5101
5654
  continue;
5102
5655
  }
5103
- const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
5656
+ const project2 = parseChannelRef(message.channel).project;
5657
+ const posted = project2 ?? SOCIETY_SCOPE;
5104
5658
  if (posted !== scope && scope !== SOCIETY_SCOPE) {
5105
5659
  continue;
5106
5660
  }
5107
5661
  const thread2 = threadOfPart(part);
5662
+ if (thread2 === void 0 && project2 !== null && asked !== true && Date.parse(message.ts) > waited) {
5663
+ continue;
5664
+ }
5108
5665
  if (thread2 !== void 0) {
5109
5666
  let counted = threadCounts.get(thread2);
5110
5667
  if (counted === void 0) {
@@ -5485,7 +6042,7 @@ ${current.body}`);
5485
6042
  name: args.name,
5486
6043
  reason: args.reason
5487
6044
  });
5488
- await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${args.name} removed from the society by ${actor.name}: ${sentence(withoutMentions(args.reason))}. Every citizen's skills index stops listing it from its next turn.`);
6045
+ await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${args.name} removed from the society by ${actor.name}: ${asSentence(withoutMentions(args.reason))} Every citizen's skills index stops listing it from its next turn.`);
5489
6046
  return removed;
5490
6047
  });
5491
6048
  }
@@ -5528,9 +6085,9 @@ ${current.body}`);
5528
6085
  /**
5529
6086
  * Keeps an image a citizen's turn names by its path, read on the runner the turn runs on, which
5530
6087
  * refuses a file outside the turn's worktree, the citizen's home, and the system temp folder.
5531
- * Only the bytes of a PNG, JPEG, GIF, or WebP of at most `IMAGE_LIMIT_BYTES` are kept, checked
5532
- * here again whatever the runner said, under a new id outside the projection, so no runner
5533
- * mirrors them and no prompt carries them: a post shows the image by its `image:` address.
6088
+ * Only a PNG, JPEG, GIF, or WebP of at most `IMAGE_LIMIT_BYTES` is kept, checked here again
6089
+ * whatever the runner said, without its metadata, under a new id outside the projection, so no
6090
+ * runner mirrors it and no prompt carries it: a post shows the image by its `image:` address.
5534
6091
  */
5535
6092
  async uploadImage(actor, input) {
5536
6093
  const args = VerbInputs.upload_image.parse(input);
@@ -5556,8 +6113,13 @@ ${current.body}`);
5556
6113
  }
5557
6114
  /**
5558
6115
  * Keeps `bytes` under a new id when they are a PNG, JPEG, GIF, or WebP of at most
5559
- * `IMAGE_LIMIT_BYTES`, and answers the markdown that shows them, described by `name`. `what`
5560
- * names the image in a refusal.
6116
+ * `IMAGE_LIMIT_BYTES` that reads to its end, without its metadata (`withoutMetadata`), and
6117
+ * answers the markdown that shows it, described by `name`. `what` names the image in a refusal.
6118
+ * The limit is for the bytes as they came, as the route's body cap measures them, and bounds
6119
+ * what is read; what is kept is checked against it too, since an orientation written smaller
6120
+ * than the board writes it grows by a few bytes. The runner, the web's composer, and the app
6121
+ * strip an image before they send it, and stripping it again changes nothing, but an older
6122
+ * runner, page, or app sends one as it was.
5561
6123
  */
5562
6124
  async keepImage(actor, bytes, what, name) {
5563
6125
  const type = imageTypeOf(bytes);
@@ -5567,19 +6129,26 @@ ${current.body}`);
5567
6129
  if (bytes.byteLength > IMAGE_LIMIT_BYTES) {
5568
6130
  throw new BoardError("VALIDATION", `${what} is ${bytes.byteLength} bytes, over the ${IMAGE_LIMIT_BYTES} an image may be`);
5569
6131
  }
6132
+ const kept = withoutMetadata(bytes);
6133
+ if (kept === null) {
6134
+ throw new BoardError("VALIDATION", `${what} does not read to its end as a ${IMAGE_KIND_NAMES[type]}, so its metadata cannot be told from its picture; save it again and send that copy`);
6135
+ }
6136
+ if (kept.byteLength > IMAGE_LIMIT_BYTES) {
6137
+ throw new BoardError("VALIDATION", `${what} is ${kept.byteLength} bytes without its metadata, over the ${IMAGE_LIMIT_BYTES} an image may be`);
6138
+ }
5570
6139
  return this.mutex.run(async () => {
5571
6140
  const id = this.newId();
5572
- await writeFileAtomic(this.paths.image(id), bytes);
6141
+ await writeFileAtomic(this.paths.image(id), kept);
5573
6142
  await this.events.append("image.uploaded", actor.name, {
5574
6143
  id,
5575
6144
  type,
5576
- size: bytes.byteLength
6145
+ size: kept.byteLength
5577
6146
  });
5578
6147
  const description = imageDescription(name);
5579
6148
  return {
5580
6149
  id,
5581
6150
  type,
5582
- size: bytes.byteLength,
6151
+ size: kept.byteLength,
5583
6152
  markdown: `![${description}](${IMAGE_SCHEME}${id})`
5584
6153
  };
5585
6154
  });
@@ -6555,7 +7124,7 @@ ${args.summary}`);
6555
7124
  });
6556
7125
  const thread2 = await this.openThreadUnlocked(actor.name, this.proposalThreadOpening(proposal2));
6557
7126
  const rationale = args.rationale.trim();
6558
- await this.appendThreadMessage(actor.name, thread2.id, `Proposal ${proposal2.id}: ${describeCharter(proposal2.kind, proposal2.charter)}.${rationale.length === 0 ? "" : `
7127
+ await this.appendThreadMessage(actor.name, thread2.id, `Proposal ${proposal2.id}: ${asSentence(describeCharter(proposal2.kind, proposal2.charter))}${rationale.length === 0 ? "" : `
6559
7128
 
6560
7129
  ${rationale}`}`);
6561
7130
  return proposal2;
@@ -6880,6 +7449,39 @@ ${rationale}`}`);
6880
7449
  const events = await this.events.readSince(null, Number.MAX_SAFE_INTEGER);
6881
7450
  return tokenUse(events, window, this.now());
6882
7451
  }
7452
+ /**
7453
+ * Every finished turn counted for Prometheus: seeded once from the whole event log, then moved
7454
+ * by each turn's end this process appends, so reading the counts never reads the log. The server
7455
+ * logs every turn's end itself; one another process appended would count from the next seed.
7456
+ */
7457
+ async countTurns() {
7458
+ const clis = /* @__PURE__ */ new Map();
7459
+ const counters = new TurnCounters((agent2) => clis.get(agent2) ?? null);
7460
+ let waiting = [];
7461
+ this.events.onAppend((event) => {
7462
+ if (waiting === null) {
7463
+ counters.add(event);
7464
+ } else {
7465
+ waiting.push(event);
7466
+ }
7467
+ });
7468
+ const events = await this.events.readSince(null, Number.MAX_SAFE_INTEGER);
7469
+ for (const agent2 of await this.listAgents()) {
7470
+ clis.set(agent2.name, agent2.cli);
7471
+ }
7472
+ const seen = /* @__PURE__ */ new Set();
7473
+ for (const event of events) {
7474
+ seen.add(event.id);
7475
+ counters.add(event);
7476
+ }
7477
+ for (const event of waiting) {
7478
+ if (!seen.has(event.id)) {
7479
+ counters.add(event);
7480
+ }
7481
+ }
7482
+ waiting = null;
7483
+ return counters;
7484
+ }
6883
7485
  /** The board actions a finished turn took, from its transcript; null when it kept none. */
6884
7486
  async turnActions(agent2, turnId) {
6885
7487
  const known = this.actionCounts.get(turnId);
@@ -7077,6 +7679,7 @@ ${rationale}`}`);
7077
7679
  trigger: parsed.trigger.kind,
7078
7680
  ...parsed.trigger.heartbeat === void 0 ? {} : { heartbeat: parsed.trigger.heartbeat },
7079
7681
  session: parsed.session,
7682
+ cli: parsed.cli,
7080
7683
  exitReason: parsed.exitReason,
7081
7684
  costUsd: parsed.costUsd,
7082
7685
  ...parsed.usage === null ? {} : { usage: parsed.usage },
@@ -7259,7 +7862,7 @@ ${rationale}`}`);
7259
7862
  const ts = this.now().toISOString();
7260
7863
  const landed = current.onDone !== "none";
7261
7864
  if (landed) {
7262
- await this.postTaskNote(actor.name, current, input.ok ? `${sentence(input.detail)}.` : `Landing failed: ${sentence(input.detail)}. The task waits at its last stage for its participants to reshape the plan.`, input.ok ? { action: "landed", stage: current.stage, to: null } : { action: "reopened", stage: current.stage, to: current.stage });
7865
+ await this.postTaskNote(actor.name, current, input.ok ? asSentence(input.detail) : `Landing failed: ${asSentence(input.detail)} The task waits at its last stage for its participants to reshape the plan.`, input.ok ? { action: "landed", stage: current.stage, to: null } : { action: "reopened", stage: current.stage, to: current.stage });
7263
7866
  }
7264
7867
  if (input.ok) {
7265
7868
  const task3 = await this.writeEndedTask(actor.name, location.project, {
@@ -7279,7 +7882,7 @@ ${rationale}`}`);
7279
7882
  await this.refreshMember(name);
7280
7883
  }
7281
7884
  if (landed) {
7282
- await this.appendMessage(actor.name, channelRef(location.project, task3.channel), `Task ${task3.id} "${task3.title}" is done: ${sentence(input.detail)}.`);
7885
+ await this.appendMessage(actor.name, channelRef(location.project, task3.channel), `Task ${task3.id} "${task3.title}" is done: ${asSentence(input.detail)}`);
7283
7886
  }
7284
7887
  return task3;
7285
7888
  }
@@ -7990,7 +8593,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7990
8593
  files: taken.files.length,
7991
8594
  ...meta
7992
8595
  });
7993
- await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${skill2.name} ${replaced ? "updated in" : "promoted to"} the society: ${sentence(skill2.summary)}. Every citizen's skills index now lists it.`);
8596
+ await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${skill2.name} ${replaced ? "updated in" : "promoted to"} the society: ${asSentence(skill2.summary)} Every citizen's skills index now lists it.`);
7994
8597
  return replaced;
7995
8598
  }
7996
8599
  /**
@@ -8237,7 +8840,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
8237
8840
  });
8238
8841
  const purpose = withoutMentions(input.purpose);
8239
8842
  await this.appendMessage(SYSTEM_ACTOR.name, ref, `Channel ${ref} opened: ${purpose}`);
8240
- await this.appendMessage(SYSTEM_ACTOR.name, channelRef(input.project, "general"), `Channel ${ref} opened by ${by}: ${purpose}. Subscribe to it to follow its posts; its tasks and mentions reach whoever they concern either way.`);
8843
+ await this.appendMessage(SYSTEM_ACTOR.name, channelRef(input.project, "general"), `Channel ${ref} opened by ${by}: ${asSentence(purpose)} Subscribe to it to follow its posts; its tasks and mentions reach whoever they concern either way.`);
8241
8844
  return ref;
8242
8845
  }
8243
8846
  /**
@@ -8660,17 +9263,18 @@ ${charter.purpose}
8660
9263
  return kept > lease ? new Date(kept).toISOString() : null;
8661
9264
  }
8662
9265
  /**
8663
- * The lease a holder keeps while the user's pause holds its turns in the task's project
8664
- * (`heldOn`), when later than the one it has, else null. It is renewed for a full lease once less
8665
- * than half is left, as for a pending cron, so it never lapses while the holder cannot take a
8666
- * turn there, and after the resume the holder has at least half a lease to come back to it.
9266
+ * The lease a holder keeps while the user's pause, or its CLI's limit, holds its turns in the
9267
+ * task's project (`heldOn`), when later than the one it has, else null. It is renewed for a full
9268
+ * lease once less than half is left, as for a pending cron, so it never lapses while the holder
9269
+ * cannot take a turn there, and after the resume or the reset the holder has at least half a
9270
+ * lease to come back to it.
8667
9271
  */
8668
9272
  async pausedLease(task2, runners, runner2, now) {
8669
9273
  if (task2.claimedBy === void 0 || task2.leaseExpiresAt === void 0) {
8670
9274
  return null;
8671
9275
  }
8672
9276
  const holder = await this.readAgent(task2.claimedBy).catch(() => null);
8673
- if (holder === null || !heldOn(runners, runner2, holder.cli)) {
9277
+ if (holder === null || !heldOn(runners, runner2, holder.cli, now)) {
8674
9278
  return null;
8675
9279
  }
8676
9280
  const lease = Date.parse(task2.leaseExpiresAt);
@@ -8761,9 +9365,9 @@ ${charter.purpose}
8761
9365
  });
8762
9366
  }
8763
9367
  const verdict = outcome === "approved" ? "Approved" : "Rejected";
8764
- const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
9368
+ const why = reason === void 0 ? "" : ` Reason: ${asSentence(reason)}`;
8765
9369
  const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
8766
- const post = `${verdict}: ${describeCharter(proposal2.kind, proposal2.charter)}.${why}${result}`;
9370
+ const post = `${verdict}: ${asSentence(describeCharter(proposal2.kind, proposal2.charter))}${why}${result}`;
8767
9371
  await this.appendThreadMessage(actor.name, thread2.id, actor.name === SYSTEM_ACTOR.name ? withoutMentions(post) : post, { step: { action: outcome } });
8768
9372
  await this.endThreadOf(proposalId, actor.name, outcome);
8769
9373
  return decision;
@@ -9040,7 +9644,11 @@ runner.command("add <name>").description("Register a runner and print its token
9040
9644
  runner.command("list").description("List runners and what they offer").action(async () => {
9041
9645
  const board = await open3();
9042
9646
  const runners = await board.listRunners();
9043
- print(runners, () => runners.length === 0 ? "No runners yet; add one with `runner add <name>`." : runners.map((each) => `${each.name} ${each.status}${each.paused === void 0 ? "" : " paused"} ${each.os}${each.version === void 0 ? "" : ` v${each.version}`} ${each.clis.join(",") || "no CLIs yet"}${each.capabilities.length === 0 ? "" : ` ${each.capabilities.join(",")}`}${Object.keys(each.pausedClis ?? {}).map((cli) => ` ${cli} paused`).join("")}`).join("\n"));
9647
+ const now = /* @__PURE__ */ new Date();
9648
+ print(runners, () => runners.length === 0 ? "No runners yet; add one with `runner add <name>`." : runners.map((each) => `${each.name} ${each.status}${each.paused === void 0 ? "" : " paused"} ${each.os}${each.version === void 0 ? "" : ` v${each.version}`} ${each.clis.join(",") || "no CLIs yet"}${each.capabilities.length === 0 ? "" : ` ${each.capabilities.join(",")}`}${Object.keys(each.pausedClis ?? {}).map((cli) => ` ${cli} paused`).join("")}${each.clis.map((cli) => {
9649
+ const hold = runnerLimitHold(each, cli, now);
9650
+ return hold === null ? "" : ` ${cli} held until ${hold.until}`;
9651
+ }).join("")}`).join("\n"));
9044
9652
  });
9045
9653
  for (const [verb, paused] of [
9046
9654
  ["pause", true],