@galda/cli 0.10.121 → 0.10.122
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/engine/analytics-client.mjs +4 -2
- package/engine/relay-pref.mjs +41 -0
- package/engine/server.mjs +17 -13
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// review_approved / review_dismissed → on the review action
|
|
24
24
|
|
|
25
25
|
import { readAnalyticsPref, resolveAnalyticsEnabled } from './analytics-pref.mjs';
|
|
26
|
-
import { readRelayPref,
|
|
26
|
+
import { readRelayPref, resolveRemoteAccess, relayClientRunning } from './relay-pref.mjs';
|
|
27
27
|
|
|
28
28
|
const BILLING_API_URL = (process.env.MANAGER_BILLING_API_URL ?? '').replace(/\/$/, '');
|
|
29
29
|
|
|
@@ -48,7 +48,9 @@ export function analyticsEnabled() {
|
|
|
48
48
|
return resolveAnalyticsEnabled({
|
|
49
49
|
configured: Boolean(BILLING_API_URL),
|
|
50
50
|
pref: dataDir ? readAnalyticsPref(dataDir) : null,
|
|
51
|
-
relayEnabled: dataDir
|
|
51
|
+
relayEnabled: dataDir
|
|
52
|
+
? resolveRemoteAccess({ relayUrl, pref: readRelayPref(dataDir), relayRunning: relayClientRunning(dataDir) }).enabled
|
|
53
|
+
: undefined,
|
|
52
54
|
});
|
|
53
55
|
}
|
|
54
56
|
|
package/engine/relay-pref.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
|
|
20
20
|
export const RELAY_PREF_FILE = 'relay-pref.json';
|
|
21
|
+
export const RELAY_LOCK_FILE = 'relay-client.lock';
|
|
21
22
|
|
|
22
23
|
export function relayPrefPath(dataDir) {
|
|
23
24
|
return join(dataDir, RELAY_PREF_FILE);
|
|
@@ -55,3 +56,43 @@ export function writeRelayPref(dataDir, enabled) {
|
|
|
55
56
|
writeFileSync(relayPrefPath(dataDir), JSON.stringify({ enabled: value, at: Date.now() }) + '\n');
|
|
56
57
|
return value;
|
|
57
58
|
}
|
|
59
|
+
|
|
60
|
+
// Is remote access actually on, from the server's point of view?
|
|
61
|
+
//
|
|
62
|
+
// `resolveRelayEnabled` above answers the LAUNCHER's question — "should I start a
|
|
63
|
+
// relay?" — from the URL it was configured with. The server cannot use the same
|
|
64
|
+
// answer, because it does not always have that env var: Masa's resident instance
|
|
65
|
+
// is launched by launchd as `node engine/server.mjs`, not through
|
|
66
|
+
// `bin/manager-for-ai.mjs` which is where RELAY_URL gets set. On 2026-08-02 that
|
|
67
|
+
// made the server report `relayEnabled:false` while a relay-client was connected
|
|
68
|
+
// and serving app.galda.app — so Settings said "remote access is off" to someone
|
|
69
|
+
// whose remote access was on, and the analytics gate (analytics-pref.mjs) went
|
|
70
|
+
// quiet on that install for a reason that was not true.
|
|
71
|
+
//
|
|
72
|
+
// A running relay-client is the ground truth: it holds a PID lock in the same
|
|
73
|
+
// DATA_DIR, which is what bin/manager-for-ai.mjs already checks for its
|
|
74
|
+
// duplicate-instance guard. Env is the fallback, not the authority.
|
|
75
|
+
//
|
|
76
|
+
// pref === false → off. The person said so, and that beats any evidence.
|
|
77
|
+
// otherwise → on if a relay is running OR one is configured.
|
|
78
|
+
export function resolveRemoteAccess({ relayUrl, pref, relayRunning } = {}) {
|
|
79
|
+
const configured = Boolean(relayUrl) || Boolean(relayRunning);
|
|
80
|
+
return { configured, enabled: configured && pref !== false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Read the relay-client's PID lock and ask the OS whether that process is alive.
|
|
84
|
+
// `isAlive` is injected so the decision stays testable without spawning anything;
|
|
85
|
+
// callers pass a real probe (see relayClientRunning below).
|
|
86
|
+
export function relayLockIsLive(raw, isAlive) {
|
|
87
|
+
let lock;
|
|
88
|
+
try { lock = JSON.parse(String(raw ?? '')); } catch { return false; }
|
|
89
|
+
const pid = Number(lock?.pid);
|
|
90
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
91
|
+
try { return Boolean(isAlive(pid)); } catch { return false; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function relayClientRunning(dataDir) {
|
|
95
|
+
let raw = '';
|
|
96
|
+
try { raw = readFileSync(join(dataDir, RELAY_LOCK_FILE), 'utf8'); } catch { return false; }
|
|
97
|
+
return relayLockIsLive(raw, (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } });
|
|
98
|
+
}
|
package/engine/server.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { bootBanner, bootDestination, folderLabel, chooseFolderScript, parseChos
|
|
|
24
24
|
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, DEFAULT_PRIORITY, normalizePriority, isAcceptedPriority, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, buildQueueWaits, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, licenseTokenPayload, shouldRefreshLicense, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, REQUIREMENT_EVIDENCE_FILE, parseRequirementEvidenceDocument, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
|
|
25
25
|
import { migrateRequirementModel, validateRequirementModel } from './requirement-model.mjs';
|
|
26
26
|
import { buildManagerVerificationChecks, buildRequirementVerificationEvidence, mergeRequirementVerificationEvidence } from './requirement-verification.mjs';
|
|
27
|
-
import { readRelayPref, writeRelayPref,
|
|
27
|
+
import { readRelayPref, writeRelayPref, resolveRemoteAccess, relayClientRunning } from './relay-pref.mjs';
|
|
28
28
|
import { readAnalyticsPref, writeAnalyticsPref, resolveAnalyticsEnabled } from './analytics-pref.mjs';
|
|
29
29
|
import { createSerialQueue } from './lib.mjs';
|
|
30
30
|
import { wantsPullRequest } from './lib.mjs';
|
|
@@ -4463,6 +4463,16 @@ const server = createServer(async (req, res) => {
|
|
|
4463
4463
|
return;
|
|
4464
4464
|
}
|
|
4465
4465
|
|
|
4466
|
+
// What the server can honestly say about remote access. NOT `RELAY_URL` alone:
|
|
4467
|
+
// launchd starts this process as `node engine/server.mjs`, so the variable
|
|
4468
|
+
// bin/manager-for-ai.mjs sets is simply absent there while a relay-client is
|
|
4469
|
+
// connected and serving app.galda.app. A live relay lock is the ground truth.
|
|
4470
|
+
const remoteAccess = () => resolveRemoteAccess({
|
|
4471
|
+
relayUrl: process.env.RELAY_URL,
|
|
4472
|
+
pref: readRelayPref(DATA_DIR),
|
|
4473
|
+
relayRunning: relayClientRunning(DATA_DIR),
|
|
4474
|
+
});
|
|
4475
|
+
|
|
4466
4476
|
// GET /api/relay-status — is this account's relay slot held by ANOTHER device?
|
|
4467
4477
|
// The relay-client (a separate process) writes relay-status.json when it stands
|
|
4468
4478
|
// down after being evicted (see relay-client.mjs / lib.mjs relayReconnectDecision).
|
|
@@ -4490,11 +4500,8 @@ const server = createServer(async (req, res) => {
|
|
|
4490
4500
|
// relay-takeover.req above.
|
|
4491
4501
|
if (url.pathname === '/api/relay-pref' && req.method === 'GET') {
|
|
4492
4502
|
const pref = readRelayPref(DATA_DIR);
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
enabled: resolveRelayEnabled({ relayUrl: process.env.RELAY_URL, pref }),
|
|
4496
|
-
configured: Boolean(process.env.RELAY_URL),
|
|
4497
|
-
});
|
|
4503
|
+
const ra = remoteAccess();
|
|
4504
|
+
return json(res, 200, { pref, enabled: ra.enabled, configured: ra.configured });
|
|
4498
4505
|
}
|
|
4499
4506
|
if (url.pathname === '/api/relay-pref' && req.method === 'POST') {
|
|
4500
4507
|
let body = '';
|
|
@@ -4508,11 +4515,8 @@ const server = createServer(async (req, res) => {
|
|
|
4508
4515
|
let saved;
|
|
4509
4516
|
try { saved = writeRelayPref(DATA_DIR, input.enabled); }
|
|
4510
4517
|
catch (e) { return json(res, 500, { error: String(e?.message ?? e) }); }
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
enabled: resolveRelayEnabled({ relayUrl: process.env.RELAY_URL, pref: saved }),
|
|
4514
|
-
configured: Boolean(process.env.RELAY_URL),
|
|
4515
|
-
});
|
|
4518
|
+
const ra = resolveRemoteAccess({ relayUrl: process.env.RELAY_URL, pref: saved, relayRunning: relayClientRunning(DATA_DIR) });
|
|
4519
|
+
return json(res, 200, { pref: saved, enabled: ra.enabled, configured: ra.configured });
|
|
4516
4520
|
});
|
|
4517
4521
|
return;
|
|
4518
4522
|
}
|
|
@@ -4524,7 +4528,7 @@ const server = createServer(async (req, res) => {
|
|
|
4524
4528
|
// nothing.
|
|
4525
4529
|
if (url.pathname === '/api/analytics-pref' && req.method === 'GET') {
|
|
4526
4530
|
const pref = readAnalyticsPref(DATA_DIR);
|
|
4527
|
-
const relayEnabled =
|
|
4531
|
+
const relayEnabled = remoteAccess().enabled;
|
|
4528
4532
|
return json(res, 200, {
|
|
4529
4533
|
pref,
|
|
4530
4534
|
enabled: resolveAnalyticsEnabled({ configured: Boolean(BILLING_API_URL), pref, relayEnabled }),
|
|
@@ -4545,7 +4549,7 @@ const server = createServer(async (req, res) => {
|
|
|
4545
4549
|
let saved;
|
|
4546
4550
|
try { saved = writeAnalyticsPref(DATA_DIR, input.enabled); }
|
|
4547
4551
|
catch (e) { return json(res, 500, { error: String(e?.message ?? e) }); }
|
|
4548
|
-
const relayEnabled =
|
|
4552
|
+
const relayEnabled = remoteAccess().enabled;
|
|
4549
4553
|
return json(res, 200, {
|
|
4550
4554
|
pref: saved,
|
|
4551
4555
|
enabled: resolveAnalyticsEnabled({ configured: Boolean(BILLING_API_URL), pref: saved, relayEnabled }),
|
package/package.json
CHANGED