cubefarm 0.3.13 → 0.3.14

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.
@@ -0,0 +1 @@
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,o)=>(o=n==null?{}:e(i(n)),c(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));export{s as n,l as r,o as t};
@@ -0,0 +1 @@
1
+ import{Jt as e,Qt as t,Xt as n,Yt as r,Zt as i,cn as a,dn as o,en as s,j as c,nn as l,qt as u,t as d,tn as f}from"./index-BjyRl7GR.js";var p=[];window.__swarmVoice=p;function m(t,n,i){setTimeout(()=>{e(t.id)&&(h=r(h,{message:t,arrived:n}),b())},Math.max(0,n+i-Date.now()))}var h=[],g=null,_=!1;function v(){g?.()}var y=()=>{let e=f();return e.muted?0:o(e.volume)*o(e.voice)};async function b(){if(!_){_=!0;try{for(;;){let{next:e,rest:t}=n(h,Date.now());if(h=t,!e)break;await S(e.message)}}finally{_=!1}}}var x=!1;async function S(e){let{settings:n,voiceKeySet:r}=u.getState(),{provider:i,voiceName:a}=n.voice;if(i===`off`)return;let o={rec:null},l=()=>{o.rec={id:e.id,provider:i,start:performance.now(),end:null,volume:y()},p.push(o.rec),p.length>50&&p.splice(0,p.length-50),u.setState({voiceSpeaking:e.id}),s(!0),o.unduckMusic=c()};try{if(i===`browser`)await T(d(e.text),a,l);else if(r)await C(e.id,l);else throw Error(`no ElevenLabs key is saved`)}catch(n){o.rec||t(),x||(x=!0,console.warn(`[voice] couldn't read message ${e.id} aloud: ${n.message}`))}finally{g=null,o.rec&&(o.rec.end=performance.now(),s(!1),o.unduckMusic?.(),u.setState({voiceSpeaking:null}))}}async function C(e,t){let n=!1;g=()=>void(n=!0);let r=await fetch(`/api/voice/messages/${e}`);if(!r.ok)throw Error(`HTTP ${r.status}`);let a=await r.arrayBuffer();if(n)return;let o=i(),s=l(`voice`);if(!o||!s)throw Error(`audio is unavailable`);let c=await o.ctx.decodeAudioData(a);if(n)return;let u=o.ctx.createBufferSource();u.buffer=c,u.connect(s),await new Promise(e=>{let n=setTimeout(()=>e(),(c.duration+2)*1e3);u.onended=()=>{clearTimeout(n),e()},g=()=>{try{u.stop()}catch{e()}},u.start(),t()}),u.disconnect()}async function w(e){return e.getVoices().length||await new Promise(t=>{let n=setTimeout(t,1e3);e.addEventListener(`voiceschanged`,()=>(clearTimeout(n),t()),{once:!0})}),e.getVoices()}async function T(e,t,n){let r=window.speechSynthesis;if(!r||typeof SpeechSynthesisUtterance>`u`)throw Error(`this browser can't speak`);if(!e)return;let i=new SpeechSynthesisUtterance(e),o=(await w(r)).find(e=>e.name===t);o&&(i.voice=o,i.lang=o.lang),i.volume=y(),await new Promise((t,o)=>{let s=a(()=>f().muted&&d()),c=setTimeout(()=>d(),2e4+e.length*150),l=!1,u=e=>{l||(l=!0,clearTimeout(c),s(),e?o(Error(e)):t())},d=()=>{u(),r.cancel()};g=d,i.onstart=n,i.onend=()=>u(),i.onerror=e=>u(e.error===`interrupted`||e.error===`canceled`?void 0:`speech failed: ${e.error}`),r.speak(i)})}export{m as speakMessage,v as stopSpeaking};
package/dist/index.html CHANGED
@@ -8,8 +8,9 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
10
  <link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
11
- <script type="module" crossorigin src="/assets/index-BljMMlNA.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-qBB5_EtY.css">
11
+ <script type="module" crossorigin src="/assets/index-BjyRl7GR.js"></script>
12
+ <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-CqNx66hk.css">
13
14
  </head>
14
15
  <body>
15
16
  <div id="root"></div>
@@ -661,6 +661,80 @@ function sweepDesks(fullName, keep) {
661
661
  return result;
662
662
  });
663
663
  }
664
+ var TRIM_DIRS = ["node_modules", "dist", "dist-server", "test-results", "playwright-report", ".swarm-home", ".preview-tmp", ".playwright-mcp"];
665
+ async function dirSize(dir, concurrency = 8) {
666
+ let total = 0;
667
+ const queue = [dir];
668
+ let active = 0;
669
+ const visit = async (d) => {
670
+ const entries = await fs2.readdir(d, { withFileTypes: true }).catch(() => []);
671
+ const sizes = await Promise.all(
672
+ entries.map((e) => {
673
+ const p = path2.join(d, e.name);
674
+ if (e.isDirectory()) queue.push(p);
675
+ return e.isFile() ? fs2.lstat(p).then((s2) => s2.size, () => 0) : 0;
676
+ })
677
+ );
678
+ for (const s2 of sizes) total += s2;
679
+ };
680
+ await new Promise((resolve) => {
681
+ const next = () => {
682
+ while (active < concurrency && queue.length) {
683
+ active++;
684
+ void visit(queue.pop()).finally(() => {
685
+ active--;
686
+ next();
687
+ });
688
+ }
689
+ if (active === 0 && queue.length === 0) resolve();
690
+ };
691
+ next();
692
+ });
693
+ return total;
694
+ }
695
+ async function trimTargets(wt) {
696
+ const ignored = (await git(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"], { cwd: wt })).split("\0").filter((p) => p.endsWith("/")).map((p) => p.slice(0, -1)).filter((p) => p.includes("/") ? p.endsWith("/node_modules") : TRIM_DIRS.includes(p));
697
+ const present = [];
698
+ for (const name of TRIM_DIRS) if ((await fs2.lstat(path2.join(wt, name)).catch(() => null))?.isDirectory()) present.push(name);
699
+ const candidates = [.../* @__PURE__ */ new Set([...ignored, ...present])];
700
+ if (candidates.length === 0) return [];
701
+ const tracked = (await git(["ls-files", "-z", "--", ...candidates], { cwd: wt })).split("\0").filter(Boolean);
702
+ return candidates.filter((c) => !tracked.some((f) => f.startsWith(`${c}/`)));
703
+ }
704
+ async function trimDesk(fullName, agentSlug, stillIdle = () => true) {
705
+ const wt = deskDir(fullName, agentSlug);
706
+ const trash = path2.join(repoDir(fullName), "trash");
707
+ for (const name of await fs2.readdir(trash).catch(() => [])) {
708
+ if (name.startsWith(`${agentSlug}-`)) await removeDir(path2.join(trash, name)).catch(() => void 0);
709
+ }
710
+ const bin = path2.join(trash, `${agentSlug}-${Date.now()}`);
711
+ const moved = await withRepoLock(fullName, async () => {
712
+ if (!stillIdle() || !await exists(path2.join(wt, ".git"))) return null;
713
+ const out = { removed: [], skipped: [] };
714
+ const targets = await trimTargets(wt);
715
+ for (const [i, rel2] of targets.entries()) {
716
+ if (!stillIdle()) break;
717
+ try {
718
+ await fs2.mkdir(bin, { recursive: true });
719
+ await fs2.rename(path2.join(wt, rel2), path2.join(bin, `${i}-${path2.basename(rel2)}`));
720
+ out.removed.push(rel2);
721
+ } catch {
722
+ out.skipped.push(rel2);
723
+ }
724
+ }
725
+ return out;
726
+ });
727
+ if (!moved) return null;
728
+ if (moved.removed.length === 0) return { freed: 0, ...moved };
729
+ const size = await dirSize(bin);
730
+ let left = 0;
731
+ try {
732
+ await removeDir(bin);
733
+ } catch {
734
+ left = await dirSize(bin);
735
+ }
736
+ return { freed: size - left, ...moved };
737
+ }
664
738
  var LAUNCHERS = ["node.exe", "cmd.exe", "bash.exe", "sh.exe", "conhost.exe", "python.exe", "npm.exe", "npx.exe", "bun.exe", "deno.exe"];
665
739
  var pathForms = (p) => [.../* @__PURE__ */ new Set([p, p.replaceAll("\\", "/"), p.replaceAll("\\", "\\\\")])];
666
740
  function leftoversInDesk(listing, desk, keep) {
@@ -2925,6 +2999,7 @@ var realBackend = {
2925
2999
  installDeps: (dir, cb) => installDesk(dir, cb),
2926
3000
  removeDesk,
2927
3001
  sweepDesks,
3002
+ trimDesk,
2928
3003
  releaseDesk: (fullName, agentSlug, port) => releaseDesk(fullName, agentSlug, port, officeProcesses()),
2929
3004
  startSession: (opts, cb) => opts.terminal ? startCliSession(opts, cb) : startSession(opts, cb),
2930
3005
  terminals: terminalsAvailable,
@@ -3423,6 +3498,12 @@ Closes #${pr.closesIssues[0] ?? "?"}`,
3423
3498
  },
3424
3499
  removeDesk: async () => void 0,
3425
3500
  sweepDesks: async () => ({ desks: 0, folders: 0, branches: 0, patches: [], skipped: [] }),
3501
+ // A made-up install and build of 300-900 MB, so the setting and the phone message can be seen.
3502
+ trimDesk: async (_fullName, _slug, stillIdle) => {
3503
+ await new Promise((r) => setTimeout(r, 300));
3504
+ if (!stillIdle()) return null;
3505
+ return { freed: Math.round((300 + Math.random() * 600) * 2 ** 20), removed: ["node_modules", "dist"], skipped: [] };
3506
+ },
3426
3507
  releaseDesk: async () => void 0,
3427
3508
  startSession: (opts, cb) => {
3428
3509
  if (++sessionsStarted === USAGE_WARNING_AT) fakeUsageWarning(cb);
@@ -4056,7 +4137,7 @@ ${links.join("\n")}` : "",
4056
4137
  "Workflow:",
4057
4138
  "1. Read the issue and explore the relevant code before changing anything.",
4058
4139
  "2. Implement the change with focused commits and clear messages.",
4059
- "3. Run the project's existing tests, linters and build (if any) and fix what you broke. Install dependencies first if needed.",
4140
+ "3. Run the project's existing tests, linters and build (if any) and fix what you broke. Install dependencies first if needed (e.g. npm install when node_modules is missing).",
4060
4141
  repo.browserTesting ? `4. If the project has a web UI, start its dev server in the background on port ${port} (reserved for you, so you don't collide with teammates), then check your change with the Playwright browser tools (mcp__playwright__browser_navigate, browser_snapshot, browser_click, browser_take_screenshot). Stop the dev server when you're done.` : "4. Verify the behaviour you changed as directly as you can.",
4061
4142
  `5. Push: ${push}`,
4062
4143
  fixing ? "6. Reply with a short summary of what you fixed." : `6. Open a pull request with the GitHub CLI: gh pr create --base ${repo.defaultBranch} --head ${branch} --title "<concise title>" --body "<what changed, how you verified it, assumptions>". The body must contain "Closes #<issue number>".`,
@@ -4085,7 +4166,7 @@ ${repo.qaBrief}`] : [],
4085
4166
  "How to test:",
4086
4167
  "1. Read the PR description and the linked issue, and work out the acceptance criteria.",
4087
4168
  `2. Review the code as a careful reviewer would: git diff origin/${repo.defaultBranch}...HEAD. Look for bugs, unhandled errors and edge cases, security problems, leftover debug code, and new logic without tests.`,
4088
- testStep2 ?? "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).",
4169
+ testStep2 ?? "3. Install dependencies if needed (e.g. npm install when node_modules is missing), then run the project's test suite, linters, type checks and build (whichever exist).",
4089
4170
  repo.browserTesting ? `4. If the project has a UI, start it in the background on port ${port} (reserved for you) and exercise the change in a real browser with the Playwright tools: navigate, click, type, resize to a phone size, try edge cases, and check the console for errors. Take a screenshot with browser_take_screenshot (no filename) of every important state: the screenshots are attached to the PR as evidence. Stop the server afterwards.` : "4. Exercise the changed behaviour directly (run the program, call the API, write a quick script).",
4090
4171
  "5. You may write throwaway scripts to probe behaviour, but do not commit them.",
4091
4172
  "A merge conflict with the default branch is not a fail, nor is a red check unrelated to this change (a flake or an outage: say why): judge the change itself (on a throwaway merge if you need newer work) and pass it if it's good. The office sends conflicting PRs back for a merge fix, then you re-test, and re-runs a failed check before anyone fixes it.",
@@ -4491,7 +4572,7 @@ function checksLine(i) {
4491
4572
  return `GitHub checks right now: ${state}`;
4492
4573
  }
4493
4574
  function testStep(hasChecks) {
4494
- return hasChecks ? "3. GitHub's checks run on this PR, and it merges only once they're green. Read .github/workflows to see what they cover and don't re-run that locally. Run only what you need to exercise the change (for example a build to start the app), anything the checks don't cover, or a failing check to reproduce it. A failing check is a finding: name it." : "3. Install dependencies if needed, then run the project's test suite, linters, type checks and build (whichever exist).";
4575
+ return hasChecks ? "3. GitHub's checks run on this PR, and it merges only once they're green. Read .github/workflows to see what they cover and don't re-run that locally. Run only what you need to exercise the change (for example a build to start the app), anything the checks don't cover, or a failing check to reproduce it. A failing check is a finding: name it." : "3. Install dependencies if needed (e.g. npm install when node_modules is missing), then run the project's test suite, linters, type checks and build (whichever exist).";
4495
4576
  }
4496
4577
  function retest(i) {
4497
4578
  const since = i.lastTestedSha && i.lastTestedSha !== i.headSha ? i.lastTestedSha : null;
@@ -4612,6 +4693,10 @@ var Previews = class {
4612
4693
  logTail: (r?.log ?? []).slice(-LOG_VIEW)
4613
4694
  };
4614
4695
  }
4696
+ /** The floor's preview is being set up or running: its worktree is in use. */
4697
+ active(f) {
4698
+ return ACTIVE.includes(this.runs.get(f.id)?.status ?? "stopped");
4699
+ }
4615
4700
  /** Re-check whether a floor without a command has a package.json to fall back on. */
4616
4701
  async refreshDefault(f) {
4617
4702
  const has = await this.backend.previews.hasDefault(f.fullName).catch(() => true);
@@ -4781,6 +4866,32 @@ function pacingMessage(w, until, sessions, now2) {
4781
4866
  return `\u{1F422} Claude's usage is getting high${what ? ` (${what})` : ""}. Until ${clock(until, now2)} the office finishes open work first and starts at most ${sessions} session${sessions === 1 ? "" : "s"} at a time.`;
4782
4867
  }
4783
4868
 
4869
+ // server/deskTrim.ts
4870
+ var TRIM_SWEEP_MS = 15 * 6e4;
4871
+ var DEFAULT_TRIM_IDLE_MIN = 120;
4872
+ var MAX_TRIM_IDLE_MIN = 7 * 24 * 60;
4873
+ var idleSince = (endedAt, busyAt, bootAt) => Math.max(endedAt ?? bootAt, busyAt ?? 0);
4874
+ function desksToTrim(desks, thresholdMin, now2) {
4875
+ if (!(thresholdMin > 0)) return [];
4876
+ return desks.filter((d) => !d.busy && now2 - d.idleSince > thresholdMin * 6e4 && (d.trimmedAt === null || d.trimmedAt < d.idleSince)).map((d) => d.key);
4877
+ }
4878
+ function clampTrimIdleMin(v) {
4879
+ const n = Math.round(Number(v));
4880
+ return Number.isFinite(n) ? Math.max(0, Math.min(MAX_TRIM_IDLE_MIN, n)) : DEFAULT_TRIM_IDLE_MIN;
4881
+ }
4882
+ function formatBytes(n) {
4883
+ const units = ["bytes", "KB", "MB", "GB", "TB"];
4884
+ let v = n;
4885
+ let i = 0;
4886
+ while (v >= 1024 && i < units.length - 1) {
4887
+ v /= 1024;
4888
+ i++;
4889
+ }
4890
+ if (i === 0) return `${Math.round(n)} bytes`;
4891
+ return `${v < 9.95 ? v.toFixed(1) : Math.round(v)} ${units[i]}`;
4892
+ }
4893
+ var freedMessage = (bytes, desks) => `\u{1F9F9} Freed ${formatBytes(bytes)} from ${desks} idle desk${desks === 1 ? "" : "s"}`;
4894
+
4784
4895
  // server/terminal.ts
4785
4896
  import fs12 from "node:fs/promises";
4786
4897
  import path14 from "node:path";
@@ -5159,6 +5270,7 @@ var Swarm = class {
5159
5270
  tutorialStep: 0,
5160
5271
  autoUpdate: true,
5161
5272
  pacingSessions: DEFAULT_PACING_SESSIONS,
5273
+ trimIdleDesksMin: DEFAULT_TRIM_IDLE_MIN,
5162
5274
  voice: { ...DEFAULT_VOICE }
5163
5275
  },
5164
5276
  repos: [],
@@ -5273,6 +5385,7 @@ var Swarm = class {
5273
5385
  if (!EFFORTS.includes(this.state.settings.defaultEffort)) this.state.settings.defaultEffort = "medium";
5274
5386
  if (this.state.settings.runtime !== "sdk") this.state.settings.runtime = "terminal";
5275
5387
  if (!isCli(this.state.settings.defaultCli)) this.state.settings.defaultCli = "claude";
5388
+ this.state.settings.trimIdleDesksMin = clampTrimIdleMin(this.state.settings.trimIdleDesksMin);
5276
5389
  if (!this.state.settings.defaultModel && this.state.settings.defaultCli === "claude") this.state.settings.defaultModel = DEFAULT_MODEL;
5277
5390
  const old = this.state.settings;
5278
5391
  if (old.maxConcurrent !== void 0) {
@@ -5353,6 +5466,7 @@ var Swarm = class {
5353
5466
  setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.syncRepo(r.id), i * 1500)), SYNC_INTERVAL_MS);
5354
5467
  setInterval(() => this.schedule(), SCHEDULER_INTERVAL_MS);
5355
5468
  setInterval(() => void this.saveTerminals(), TERMINAL_SAVE_MS);
5469
+ setInterval(() => void this.trimIdleDesks(), this.backend.demo ? 6e4 : TRIM_SWEEP_MS);
5356
5470
  setInterval(() => this.state.repos.forEach((r, i) => setTimeout(() => void this.sweepFloor(r.id), i * 1500)), DESK_SWEEP_INTERVAL_MS);
5357
5471
  void this.backend.detectClis().then((clis) => {
5358
5472
  this.clis = clis;
@@ -6888,6 +7002,10 @@ Full report with screenshots: ${rec.commentUrl}` : "",
6888
7002
  if (patch.tutorialStep !== void 0) s2.tutorialStep = Math.max(-1, Math.round(Number(patch.tutorialStep)) || 0);
6889
7003
  if (typeof patch.autoUpdate === "boolean") s2.autoUpdate = patch.autoUpdate;
6890
7004
  if (patch.pacingSessions !== void 0) s2.pacingSessions = clampPacingSessions(patch.pacingSessions);
7005
+ if (patch.trimIdleDesksMin !== void 0) {
7006
+ s2.trimIdleDesksMin = clampTrimIdleMin(patch.trimIdleDesksMin);
7007
+ setTimeout(() => void this.trimIdleDesks(), 1e3);
7008
+ }
6891
7009
  if (patch.voice !== void 0) s2.voice = voiceSettings(s2.voice, patch.voice);
6892
7010
  this.save();
6893
7011
  this.broadcast({ type: "settings", settings: s2 });
@@ -7464,6 +7582,63 @@ ${t}`);
7464
7582
  }
7465
7583
  this.enqueueCeo({ kind: "review", at: Date.now() });
7466
7584
  }
7585
+ // ---------- idle desks ----------
7586
+ bootAt = Date.now();
7587
+ deskWatch = /* @__PURE__ */ new Map();
7588
+ // desk → what the sweeps saw
7589
+ trimming = false;
7590
+ /** An agent's desk is in use: a task (preparing, working, testing or fixing) or a live CLI in their terminal. Hands off a fired agent's. */
7591
+ deskBusy(a) {
7592
+ const rt = this.agentRt.get(a.id);
7593
+ return !rt || !this.state.agents.includes(a) || BUSY.includes(a.status) || !!rt.session || !!rt.terminal?.live;
7594
+ }
7595
+ /**
7596
+ * Free disk space on desks idle longer than settings.trimIdleDesksMin: every agent's desk, and each floor's preview
7597
+ * worktree while no preview runs. One desk at a time, once per idle stretch; one phone message per sweep that freed anything.
7598
+ */
7599
+ async trimIdleDesks() {
7600
+ const min = this.state.settings.trimIdleDesksMin;
7601
+ if (!min || this.trimming) return;
7602
+ this.trimming = true;
7603
+ try {
7604
+ const now2 = Date.now();
7605
+ const desks = [];
7606
+ for (const a of this.state.agents) {
7607
+ const repo = this.state.repos.find((r) => r.id === a.repoId);
7608
+ if (repo) desks.push({ key: a.id, repo, slug: this.agentSlug(a), endedAt: a.endedAt, busy: () => this.deskBusy(a) });
7609
+ }
7610
+ for (const repo of this.state.repos) {
7611
+ desks.push({ key: `preview:${repo.id}`, repo, slug: PREVIEW_SLUG, endedAt: null, busy: () => !this.state.repos.includes(repo) || this.previews.active(repo) });
7612
+ }
7613
+ for (const key of this.deskWatch.keys()) if (!desks.some((d) => d.key === key)) this.deskWatch.delete(key);
7614
+ const seen = desks.map((d) => {
7615
+ const w = this.deskWatch.get(d.key) ?? { busyAt: null, trimmedAt: null };
7616
+ this.deskWatch.set(d.key, w);
7617
+ const busy = d.busy();
7618
+ if (busy) w.busyAt = now2;
7619
+ return { key: d.key, busy, idleSince: idleSince(d.endedAt, w.busyAt, this.bootAt), trimmedAt: w.trimmedAt };
7620
+ });
7621
+ const due = new Set(desksToTrim(seen, min, now2));
7622
+ let freed = 0;
7623
+ let count = 0;
7624
+ for (const d of desks.filter((x) => due.has(x.key))) {
7625
+ const result = await this.backend.trimDesk(d.repo.fullName, d.slug, () => !d.busy()).catch((err) => {
7626
+ console.warn(`could not trim desk ${d.slug} of ${d.repo.fullName}:`, oneLine(err));
7627
+ return void 0;
7628
+ });
7629
+ if (result === void 0) continue;
7630
+ if (!result?.skipped.length) this.deskWatch.get(d.key).trimmedAt = Date.now();
7631
+ if (!result?.freed) continue;
7632
+ freed += result.freed;
7633
+ count++;
7634
+ const locked = result.skipped.length ? `; still locked: ${result.skipped.join(", ")}` : "";
7635
+ console.log(`trimmed idle desk ${d.slug} of ${d.repo.fullName}: freed ${formatBytes(result.freed)} (${result.removed.join(", ")})${locked}`);
7636
+ }
7637
+ if (count) this.postMessage("office", freedMessage(freed, count));
7638
+ } finally {
7639
+ this.trimming = false;
7640
+ }
7641
+ }
7467
7642
  // ---------- the phone ----------
7468
7643
  postMessage(from, text, requestId) {
7469
7644
  const m = { id: this.messageSeq++, from, text: text.trim().slice(0, 6e3), at: Date.now(), ...requestId ? { requestId } : {} };