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.
- package/dist/assets/ToyWorld-pH_SiIaW.js +2 -0
- package/dist/assets/index-BjyRl7GR.js +4714 -0
- package/dist/assets/{index-qBB5_EtY.css → index-CqNx66hk.css} +1 -1
- package/dist/assets/rapier-DNlHxnYv.js +1 -0
- package/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/assets/voiceMessages-DuJL7za2.js +1 -0
- package/dist/index.html +3 -2
- package/dist-server/index.js +178 -3
- package/dist-server/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/assets/ToyWorld-aw61IQYN.js +0 -2
- package/dist/assets/index-BljMMlNA.js +0 -4711
- package/dist/assets/rapier-3MlT3vHU.js +0 -1
|
@@ -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-
|
|
12
|
-
<link rel="
|
|
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>
|
package/dist-server/index.js
CHANGED
|
@@ -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 } : {} };
|