@galda/cli 0.10.52 → 0.10.54
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/app/index.html +5 -3
- package/engine/server.mjs +34 -12
- package/engine/worker-availability.mjs +8 -0
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -6894,16 +6894,17 @@ function renderFsTodo(){
|
|
|
6894
6894
|
// (only a reply task, which Doing excludes) — it used to vanish from the lane (Masa 2026-07-24).
|
|
6895
6895
|
// Show it in Doing as a goal row, and stop its old stopped task from lingering under "Needs you".
|
|
6896
6896
|
const runningTaskGoalIds = new Set(running.filter((t) => !t.reply).map((t) => t.goalId));
|
|
6897
|
-
const
|
|
6897
|
+
const runningReplyGoalIds = new Set(list.filter((t) => t.reply && t.status === 'running').map((t) => t.goalId));
|
|
6898
|
+
const resumedGoalIds = new Set(runningReplyGoalIds);
|
|
6898
6899
|
const doingGoals = pgoals.filter((g) => g.status === 'running' && g.startedAt
|
|
6899
|
-
&& !runningTaskGoalIds.has(g.id) && !pausedGoalIds.has(g.id));
|
|
6900
|
+
&& runningReplyGoalIds.has(g.id) && !runningTaskGoalIds.has(g.id) && !pausedGoalIds.has(g.id));
|
|
6900
6901
|
// A reply that was OVER the parallel cap (engine PR #342): the goal is 'running'
|
|
6901
6902
|
// but startedAt is cleared, and its only pending work is a QUEUED reply task —
|
|
6902
6903
|
// which Doing needs startedAt for, and Next-up excludes (!t.reply). With no
|
|
6903
6904
|
// bucket it vanished. Surface these "parked" goals at the TOP of To Do ("最上部",
|
|
6904
6905
|
// Masa 2026-07-24) as Queued rows — not "Working on it" (nothing runs yet). They
|
|
6905
6906
|
// auto-promote to Doing the moment a slot frees and the engine sets startedAt.
|
|
6906
|
-
const parkedReplyGoals = pgoals.filter((g) => g.status === 'running'
|
|
6907
|
+
const parkedReplyGoals = pgoals.filter((g) => g.status === 'running'
|
|
6907
6908
|
&& !pausedGoalIds.has(g.id) && !runningTaskGoalIds.has(g.id)
|
|
6908
6909
|
&& list.some((t) => t.goalId === g.id && t.reply && t.status === 'queued')
|
|
6909
6910
|
&& !list.some((t) => t.goalId === g.id && !t.reply && ['running', 'queued'].includes(t.status)));
|
|
@@ -7695,6 +7696,7 @@ const AUTH_REASON_TEXT = {
|
|
|
7695
7696
|
'logged-out': 'Not signed in to claude. Run `claude` in a terminal and /login, then relaunch the manager from that terminal — tasks are paused until then.',
|
|
7696
7697
|
'bad-credentials': 'Worker sign-in failed. Check ANTHROPIC_API_KEY, or run `claude` and /login, then relaunch the manager from that terminal.',
|
|
7697
7698
|
'probe-failed': 'Couldn’t verify claude sign-in. Ensure the `claude` CLI is installed and signed in, then relaunch the manager.',
|
|
7699
|
+
'rate-limited': 'Claude has reached its usage limit. Claude tasks are queued until access resets; Codex tasks can continue now.',
|
|
7698
7700
|
};
|
|
7699
7701
|
function renderAuthBanner(auth){
|
|
7700
7702
|
const bar = $('errbar');
|
package/engine/server.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { createSerialQueue } from './lib.mjs';
|
|
|
26
26
|
import { collectProjectRules } from './lib.mjs';
|
|
27
27
|
import { FALLBACK_CODEX_MODELS, loadCodexModels } from './codex-models.mjs';
|
|
28
28
|
import { CLAUDE_MODELS } from './claude-models.mjs';
|
|
29
|
+
import { classifyWorkerPreflight, workerCanStart } from './worker-availability.mjs';
|
|
29
30
|
import { openPR } from './pr.mjs';
|
|
30
31
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
31
32
|
|
|
@@ -1101,6 +1102,7 @@ function rebuildQueuesAfterAdopt() {
|
|
|
1101
1102
|
for (const p of projects) queues.set(p.id, { running: new Set(), waiting: [] });
|
|
1102
1103
|
for (const p of projects) {
|
|
1103
1104
|
const pending = tasks.filter((t) => t.projectId === p.id && t.status === 'queued').sort((a, b) => a.num - b.num);
|
|
1105
|
+
reconcileQueuedGoalStarts(p.id);
|
|
1104
1106
|
queues.get(p.id).waiting.push(...pending);
|
|
1105
1107
|
pump(p.id);
|
|
1106
1108
|
for (const g of goals.filter((g) => g.projectId === p.id && g.status === 'stacked')) {
|
|
@@ -1110,6 +1112,17 @@ function rebuildQueuesAfterAdopt() {
|
|
|
1110
1112
|
}
|
|
1111
1113
|
}
|
|
1112
1114
|
|
|
1115
|
+
function reconcileQueuedGoalStarts(projectId) {
|
|
1116
|
+
const projectTasks = tasks.filter((task) => task.projectId === projectId);
|
|
1117
|
+
for (const goal of goals.filter((item) => item.projectId === projectId && item.status === 'running' && item.startedAt)) {
|
|
1118
|
+
const goalTasks = projectTasks.filter((task) => task.goalId === goal.id);
|
|
1119
|
+
if (goalTasks.some((task) => task.status === 'queued') && !goalTasks.some((task) => task.status === 'running')) {
|
|
1120
|
+
goal.startedAt = null;
|
|
1121
|
+
saveGoal(goal);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1113
1126
|
const ACTIVITY_LIMIT = 1000;
|
|
1114
1127
|
function activityLine(line) {
|
|
1115
1128
|
return String(line ?? '')
|
|
@@ -1470,9 +1483,9 @@ function startNextStacked(projectId) {
|
|
|
1470
1483
|
// standalone `claude` (runClaude), inheriting the launching shell's login. If
|
|
1471
1484
|
// that shell has no usable credentials, every task fails late with "Not logged
|
|
1472
1485
|
// in". We probe once at boot and hold the queue + raise a banner instead of
|
|
1473
|
-
// failing task after task.
|
|
1474
|
-
//
|
|
1475
|
-
let workerAuth = { ok:
|
|
1486
|
+
// failing task after task. Claude waits for the short probe; Codex is gated
|
|
1487
|
+
// independently and may start immediately.
|
|
1488
|
+
let workerAuth = { ok: null, reason: null, checkedAt: null };
|
|
1476
1489
|
const publicAuth = () => ({ ok: workerAuth.ok, reason: workerAuth.reason });
|
|
1477
1490
|
|
|
1478
1491
|
// Boot preflight: one cheap Read-only `claude` call; classify the result (pure,
|
|
@@ -1481,14 +1494,18 @@ const publicAuth = () => ({ ok: workerAuth.ok, reason: workerAuth.reason });
|
|
|
1481
1494
|
async function probeWorkerAuth() {
|
|
1482
1495
|
try {
|
|
1483
1496
|
const out = await runClaude({ prompt: 'Reply with the single word: ok', cwd: ROOT, tools: 'Read', model: 'haiku' });
|
|
1484
|
-
workerAuth = {
|
|
1497
|
+
workerAuth = {
|
|
1498
|
+
...classifyWorkerPreflight(out, { classifyAuthProbe, isRateLimited }),
|
|
1499
|
+
checkedAt: Date.now(),
|
|
1500
|
+
};
|
|
1485
1501
|
} catch {
|
|
1486
1502
|
workerAuth = { ok: false, reason: 'probe-failed', checkedAt: Date.now() };
|
|
1487
1503
|
}
|
|
1488
1504
|
console.log(workerAuth.ok
|
|
1489
1505
|
? '[manager] worker auth preflight OK'
|
|
1490
|
-
: `[manager]
|
|
1506
|
+
: `[manager] Claude preflight unavailable (${workerAuth.reason}) — holding Claude tasks; Codex may continue.`);
|
|
1491
1507
|
send({ ev: 'auth', auth: publicAuth() });
|
|
1508
|
+
if (workerAuth.ok) for (const project of projects) pump(project.id);
|
|
1492
1509
|
return workerAuth;
|
|
1493
1510
|
}
|
|
1494
1511
|
|
|
@@ -1654,10 +1671,8 @@ async function authorVerify(task, goal, project) {
|
|
|
1654
1671
|
// `lastStartAt` check in pump() below.
|
|
1655
1672
|
const slowModeLastStart = new Map(); // projectId -> ms epoch of the last task start
|
|
1656
1673
|
function pump(projectId) {
|
|
1657
|
-
//
|
|
1658
|
-
//
|
|
1659
|
-
// with "Not logged in". Cleared automatically once a probe succeeds.
|
|
1660
|
-
if (!workerAuth.ok) return;
|
|
1674
|
+
// A Claude sign-in or usage hold must not stop an installed Codex worker.
|
|
1675
|
+
// Claude work stays queued until a later probe succeeds.
|
|
1661
1676
|
// Project paused by the user (Doing列の一時停止 / chat) → hold this project's
|
|
1662
1677
|
// queue exactly like the auth hold above. Resume flips the flag and pumps.
|
|
1663
1678
|
if (isProjectPaused(projectId)) return;
|
|
@@ -1684,7 +1699,8 @@ function pump(projectId) {
|
|
|
1684
1699
|
if (wait > 0) { setTimeout(() => pump(projectId), wait); return; }
|
|
1685
1700
|
}
|
|
1686
1701
|
const runningGoalIds = [...q.running].map((t) => t.goalId);
|
|
1687
|
-
const
|
|
1702
|
+
const eligible = q.waiting.filter((task) => workerCanStart({ agent: task.agent, auth: workerAuth }));
|
|
1703
|
+
const toStart = nextRunnableTasks(eligible, runningGoalIds, slow.enabled ? 1 : slots);
|
|
1688
1704
|
for (const task of toStart) {
|
|
1689
1705
|
const i = q.waiting.indexOf(task);
|
|
1690
1706
|
if (i !== -1) q.waiting.splice(i, 1);
|
|
@@ -1981,7 +1997,9 @@ function queueReplyTask(goal, text, { from = 'manager', via = 'composer' } = {})
|
|
|
1981
1997
|
const projectCap = slow.enabled ? Math.min(PROJECT_MAX_PARALLEL, slow.maxParallel) : PROJECT_MAX_PARALLEL;
|
|
1982
1998
|
const slots = Math.min(projectCap - q.running.size, GLOBAL_MAX_PARALLEL - globalRunningCount());
|
|
1983
1999
|
const runningGoalIds = [...q.running].map((t) => t.goalId);
|
|
1984
|
-
const
|
|
2000
|
+
const eligible = q.waiting.filter((candidate) => workerCanStart({ agent: candidate.agent, auth: workerAuth }));
|
|
2001
|
+
const startsNow = workerCanStart({ agent: task.agent, auth: workerAuth })
|
|
2002
|
+
&& nextRunnableTasks(eligible, runningGoalIds, slow.enabled ? 1 : slots).some((candidate) => candidate.id === task.id);
|
|
1985
2003
|
goal.startedAt = startsNow ? new Date().toISOString() : null;
|
|
1986
2004
|
saveGoal(goal);
|
|
1987
2005
|
}
|
|
@@ -4913,7 +4931,10 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
4913
4931
|
spawn('open', ['-g', openUrl], { stdio: 'ignore' }).unref();
|
|
4914
4932
|
}
|
|
4915
4933
|
console.log(`[manager] projects: ${projects.map((p) => `${p.id}=${p.dir}`).join(' ')}`);
|
|
4916
|
-
probeWorkerAuth().catch(() => {}); // hold
|
|
4934
|
+
probeWorkerAuth().catch(() => {}); // hold Claude tasks + banner if Claude is unavailable
|
|
4935
|
+
setInterval(() => {
|
|
4936
|
+
if (!workerAuth.ok) probeWorkerAuth().catch(() => {});
|
|
4937
|
+
}, 5 * 60_000).unref();
|
|
4917
4938
|
syncAllExternal().catch(() => {});
|
|
4918
4939
|
syncGoalMerges().catch(() => {});
|
|
4919
4940
|
setInterval(() => {
|
|
@@ -4930,6 +4951,7 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
4930
4951
|
for (const p of projects) {
|
|
4931
4952
|
const pending = tasks.filter((t) => t.projectId === p.id && t.status === 'queued').sort((a, b) => a.num - b.num);
|
|
4932
4953
|
const q = queues.get(p.id);
|
|
4954
|
+
reconcileQueuedGoalStarts(p.id);
|
|
4933
4955
|
q.waiting.push(...pending);
|
|
4934
4956
|
if (pending.length) console.log(`[manager] ${p.id}: re-queued ${pending.length} pending task(s) after restart`);
|
|
4935
4957
|
pump(p.id);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function classifyWorkerPreflight({ result = '', code = 0 } = {}, { classifyAuthProbe, isRateLimited }) {
|
|
2
|
+
if (isRateLimited(result)) return { ok: false, reason: 'rate-limited' };
|
|
3
|
+
return classifyAuthProbe({ result, code });
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function workerCanStart({ agent, auth } = {}) {
|
|
7
|
+
return auth?.ok === true || agent === 'codex';
|
|
8
|
+
}
|
package/package.json
CHANGED